You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bookkeeper.apache.org by si...@apache.org on 2018/11/09 05:50:06 UTC

[bookkeeper] branch branch-4.8 updated: [TOOLS] add cookie related commands

This is an automated email from the ASF dual-hosted git repository.

sijie pushed a commit to branch branch-4.8
in repository https://gitbox.apache.org/repos/asf/bookkeeper.git


The following commit(s) were added to refs/heads/branch-4.8 by this push:
     new 2bce677  [TOOLS] add cookie related commands
2bce677 is described below

commit 2bce677e61e797b3898f026f462445e77671ac80
Author: Sijie Guo <gu...@gmail.com>
AuthorDate: Thu Nov 8 21:47:12 2018 -0800

    [TOOLS] add cookie related commands
    
    Descriptions of the changes in this PR:
    
    *Motivation*
    
    In some use cases, you need cookie related tools to create/delete/update/get cookie when handling production issues.
    Currently bookkeeper doesn't provide such commands.
    
    *Changes*
    
    Add cookie related commands
    
    - create
    - delete
    - get
    - update
    - generate
    
    Reviewers: Enrico Olivelli <eo...@gmail.com>, Jia Zhai <None>, Matteo Merli <mm...@apache.org>
    
    This closes #1794 from sijie/add_cookie_commands
    
    (cherry picked from commit e31e844505c59a9c119c6896a7f789db8f432dc2)
    Signed-off-by: Sijie Guo <si...@apache.org>
---
 .../apache/bookkeeper/bookie/BookieException.java  |  23 +++
 .../org/apache/bookkeeper/bookie/BookieShell.java  |  44 ++++-
 .../java/org/apache/bookkeeper/bookie/Cookie.java  |   7 +-
 .../bookkeeper/discover/ZKRegistrationManager.java |   5 +
 .../tools/cli/commands/cookie/CookieCommand.java   | 124 +++++++++++++
 .../cli/commands/cookie/CreateCookieCommand.java   | 102 +++++++++++
 .../cli/commands/cookie/DeleteCookieCommand.java   |  91 ++++++++++
 .../cli/commands/cookie/GenerateCookieCommand.java | 126 +++++++++++++
 .../cli/commands/cookie/GetCookieCommand.java      | 101 +++++++++++
 .../cli/commands/cookie/UpdateCookieCommand.java   | 102 +++++++++++
 .../tools/cli/commands/cookie/package-info.java    |  23 +++
 .../tools/cli/helpers/BookieShellCommand.java      |  62 +++++++
 .../tools/cli/helpers/ClientCommand.java           |   2 +-
 .../tools/cli/commands/CookieCommandGroup.java     |  54 ++++++
 ....apache.bookkeeper.tools.framework.CommandGroup |   1 +
 .../commands/cookie/CreateCookieCommandTest.java   | 180 +++++++++++++++++++
 .../commands/cookie/DeleteCookieCommandTest.java   | 135 ++++++++++++++
 .../commands/cookie/GenerateCookieCommandTest.java | 198 +++++++++++++++++++++
 .../cli/commands/cookie/GetCookieCommandTest.java  | 146 +++++++++++++++
 .../commands/cookie/UpdateCookieCommandTest.java   | 180 +++++++++++++++++++
 .../tools/cli/helpers/BookieShellCommandTest.java  |  65 +++++++
 .../tools/cli/helpers/CookieCommandTestBase.java   |  91 ++++++++++
 .../apache/bookkeeper/tools/common/BKCommand.java  |  14 ++
 23 files changed, 1869 insertions(+), 7 deletions(-)

diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BookieException.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BookieException.java
index 3d84148..83002ce 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BookieException.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BookieException.java
@@ -63,6 +63,8 @@ public abstract class BookieException extends Exception {
             return new DiskPartitionDuplicationException();
         case Code.CookieNotFoundException:
             return new CookieNotFoundException();
+        case Code.CookieExistsException:
+            return new CookieExistException();
         case Code.MetadataStoreException:
             return new MetadataStoreException();
         case Code.UnknownBookieIdException:
@@ -88,6 +90,7 @@ public abstract class BookieException extends Exception {
         int MetadataStoreException = -106;
         int UnknownBookieIdException = -107;
         int OperationRejectedException = -108;
+        int CookieExistsException = -109;
     }
 
     public int getCode() {
@@ -118,6 +121,9 @@ public abstract class BookieException extends Exception {
         case Code.CookieNotFoundException:
             err = "Cookie not found";
             break;
+        case Code.CookieExistsException:
+            err = "Cookie already exists";
+            break;
         case Code.MetadataStoreException:
             err = "Error performing metadata operations";
             break;
@@ -232,6 +238,23 @@ public abstract class BookieException extends Exception {
     }
 
     /**
+     * Signal that cookie already exists when creating a new cookie.
+     */
+    public static class CookieExistException extends BookieException {
+        public CookieExistException() {
+            this("");
+        }
+
+        public CookieExistException(String reason) {
+            super(Code.CookieExistsException, reason);
+        }
+
+        public CookieExistException(Throwable cause) {
+            super(Code.CookieExistsException, cause);
+        }
+    }
+
+    /**
      * Signals that an exception occurs on upgrading a bookie.
      */
     public static class UpgradeException extends BookieException {
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BookieShell.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BookieShell.java
index 1b18055..2666007 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BookieShell.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BookieShell.java
@@ -90,6 +90,7 @@ import org.apache.bookkeeper.client.LedgerEntry;
 import org.apache.bookkeeper.client.LedgerHandle;
 import org.apache.bookkeeper.client.LedgerMetadata;
 import org.apache.bookkeeper.client.UpdateLedgerOp;
+import org.apache.bookkeeper.common.annotation.InterfaceAudience.Private;
 import org.apache.bookkeeper.common.util.OrderedExecutor;
 import org.apache.bookkeeper.conf.ClientConfiguration;
 import org.apache.bookkeeper.conf.ServerConfiguration;
@@ -111,6 +112,11 @@ import org.apache.bookkeeper.stats.NullStatsLogger;
 import org.apache.bookkeeper.tools.cli.commands.bookie.LastMarkCommand;
 import org.apache.bookkeeper.tools.cli.commands.bookies.ListBookiesCommand;
 import org.apache.bookkeeper.tools.cli.commands.client.SimpleTestCommand;
+import org.apache.bookkeeper.tools.cli.commands.cookie.CreateCookieCommand;
+import org.apache.bookkeeper.tools.cli.commands.cookie.DeleteCookieCommand;
+import org.apache.bookkeeper.tools.cli.commands.cookie.GenerateCookieCommand;
+import org.apache.bookkeeper.tools.cli.commands.cookie.GetCookieCommand;
+import org.apache.bookkeeper.tools.cli.commands.cookie.UpdateCookieCommand;
 import org.apache.bookkeeper.tools.framework.CliFlags;
 import org.apache.bookkeeper.util.BookKeeperConstants;
 import org.apache.bookkeeper.util.DiskChecker;
@@ -186,6 +192,14 @@ public class BookieShell implements Tool {
     static final String CMD_CONVERT_TO_DB_STORAGE = "convert-to-db-storage";
     static final String CMD_CONVERT_TO_INTERLEAVED_STORAGE = "convert-to-interleaved-storage";
     static final String CMD_REBUILD_DB_LEDGER_LOCATIONS_INDEX = "rebuild-db-ledger-locations-index";
+
+    // cookie commands
+    static final String CMD_CREATE_COOKIE = "cookie_create";
+    static final String CMD_DELETE_COOKIE = "cookie_delete";
+    static final String CMD_UPDATE_COOKIE = "cookie_update";
+    static final String CMD_GET_COOKIE = "cookie_get";
+    static final String CMD_GENERATE_COOKIE = "cookie_generate";
+
     static final String CMD_HELP = "help";
 
     final ServerConfiguration bkConf = new ServerConfiguration();
@@ -209,9 +223,15 @@ public class BookieShell implements Tool {
         this.entryFormatter = entryFormatter;
     }
 
-    interface Command {
+    /**
+     * BookieShell command.
+     */
+    @Private
+    public interface Command {
         int runCmd(String[] args) throws Exception;
 
+        String description();
+
         void printUsage();
     }
 
@@ -230,6 +250,11 @@ public class BookieShell implements Tool {
             this.cmdName = cmdName;
         }
 
+        public String description() {
+            // we used the string returned by `getUsage` as description in showing the list of commands
+            return getUsage();
+        }
+
         @Override
         public int runCmd(String[] args) throws Exception {
             try {
@@ -2814,7 +2839,7 @@ public class BookieShell implements Tool {
         }
     }
 
-    final Map<String, MyCommand> commands = new HashMap<String, MyCommand>();
+    final Map<String, Command> commands = new HashMap<>();
 
     {
         commands.put(CMD_METAFORMAT, new MetaFormatCmd());
@@ -2850,6 +2875,17 @@ public class BookieShell implements Tool {
         commands.put(CMD_HELP, new HelpCmd());
         commands.put(CMD_LOSTBOOKIERECOVERYDELAY, new LostBookieRecoveryDelayCmd());
         commands.put(CMD_TRIGGERAUDIT, new TriggerAuditCmd());
+        // cookie related commands
+        commands.put(CMD_CREATE_COOKIE,
+            new CreateCookieCommand().asShellCommand(CMD_CREATE_COOKIE, bkConf));
+        commands.put(CMD_DELETE_COOKIE,
+            new DeleteCookieCommand().asShellCommand(CMD_DELETE_COOKIE, bkConf));
+        commands.put(CMD_UPDATE_COOKIE,
+            new UpdateCookieCommand().asShellCommand(CMD_UPDATE_COOKIE, bkConf));
+        commands.put(CMD_GET_COOKIE,
+            new GetCookieCommand().asShellCommand(CMD_GET_COOKIE, bkConf));
+        commands.put(CMD_GENERATE_COOKIE,
+            new GenerateCookieCommand().asShellCommand(CMD_GENERATE_COOKIE, bkConf));
     }
 
     @Override
@@ -2871,8 +2907,8 @@ public class BookieShell implements Tool {
                 + "[-entryformat <hex/string>] [-conf configuration] <command>");
         System.err.println("where command is one of:");
         List<String> commandNames = new ArrayList<String>();
-        for (MyCommand c : commands.values()) {
-            commandNames.add("       " + c.getUsage());
+        for (Command c : commands.values()) {
+            commandNames.add("       " + c.description());
         }
         Collections.sort(commandNames);
         for (String s : commandNames) {
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/Cookie.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/Cookie.java
index 4a9f6f6..7d4175c 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/Cookie.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/Cookie.java
@@ -212,8 +212,11 @@ public class Cookie {
 
     public void writeToDirectory(File directory) throws IOException {
         File versionFile = new File(directory,
-                BookKeeperConstants.VERSION_FILENAME);
+            BookKeeperConstants.VERSION_FILENAME);
+        writeToFile(versionFile);
+    }
 
+    public void writeToFile (File versionFile) throws IOException {
         FileOutputStream fos = new FileOutputStream(versionFile);
         BufferedWriter bw = null;
         try {
@@ -386,7 +389,7 @@ public class Cookie {
      * Cookie builder.
      */
     public static class Builder {
-        private int layoutVersion = 0;
+        private int layoutVersion = CURRENT_COOKIE_LAYOUT_VERSION;
         private String bookieHost = null;
         private String journalDirs = null;
         private String ledgerDirs = null;
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/discover/ZKRegistrationManager.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/discover/ZKRegistrationManager.java
index eab9e4f..a2a3e7c 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/discover/ZKRegistrationManager.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/discover/ZKRegistrationManager.java
@@ -37,6 +37,7 @@ import java.util.function.Function;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.bookkeeper.bookie.BookieException;
 import org.apache.bookkeeper.bookie.BookieException.BookieIllegalOpException;
+import org.apache.bookkeeper.bookie.BookieException.CookieExistException;
 import org.apache.bookkeeper.bookie.BookieException.CookieNotFoundException;
 import org.apache.bookkeeper.bookie.BookieException.MetadataStoreException;
 import org.apache.bookkeeper.client.BKException;
@@ -327,6 +328,10 @@ public class ZKRegistrationManager implements RegistrationManager {
         } catch (InterruptedException ie) {
             Thread.currentThread().interrupt();
             throw new MetadataStoreException("Interrupted writing cookie for bookie " + bookieId, ie);
+        } catch (NoNodeException nne) {
+            throw new CookieNotFoundException(bookieId);
+        } catch (NodeExistsException nee) {
+            throw new CookieExistException(bookieId);
         } catch (KeeperException e) {
             throw new MetadataStoreException("Failed to write cookie for bookie " + bookieId);
         }
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/CookieCommand.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/CookieCommand.java
new file mode 100644
index 0000000..fe2c531
--- /dev/null
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/CookieCommand.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.commands.cookie;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import java.io.IOException;
+import java.net.UnknownHostException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Paths;
+import java.util.concurrent.ExecutionException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.bookkeeper.common.net.ServiceURI;
+import org.apache.bookkeeper.conf.ServerConfiguration;
+import org.apache.bookkeeper.discover.RegistrationManager;
+import org.apache.bookkeeper.meta.MetadataDrivers;
+import org.apache.bookkeeper.meta.exceptions.MetadataException;
+import org.apache.bookkeeper.net.BookieSocketAddress;
+import org.apache.bookkeeper.tools.cli.helpers.BookieShellCommand;
+import org.apache.bookkeeper.tools.common.BKCommand;
+import org.apache.bookkeeper.tools.common.BKFlags;
+import org.apache.bookkeeper.tools.framework.CliFlags;
+import org.apache.bookkeeper.tools.framework.CliSpec;
+import org.apache.commons.configuration.CompositeConfiguration;
+
+/**
+ * This is a mixin for cookie related commands to extends.
+ */
+@Slf4j
+abstract class CookieCommand<CookieFlagsT extends CliFlags>
+    extends BKCommand<CookieFlagsT> {
+
+    protected CookieCommand(CliSpec<CookieFlagsT> spec) {
+        super(spec);
+    }
+
+    @Override
+    protected boolean apply(ServiceURI serviceURI,
+                            CompositeConfiguration conf,
+                            BKFlags globalFlags,
+                            CookieFlagsT cmdFlags) {
+        ServerConfiguration serverConf = new ServerConfiguration();
+        serverConf.loadConf(conf);
+
+        if (null != serviceURI) {
+            serverConf.setMetadataServiceUri(serviceURI.getUri().toString());
+        }
+
+        try {
+            return MetadataDrivers.runFunctionWithRegistrationManager(serverConf, registrationManager -> {
+                try {
+                    apply(registrationManager, cmdFlags);
+                    return true;
+                } catch (Exception e) {
+                    throw new UncheckedExecutionException(e);
+                }
+            });
+        } catch (MetadataException | ExecutionException | UncheckedExecutionException e) {
+            Throwable cause = e;
+            if (!(e instanceof MetadataException) && null != e.getCause()) {
+                cause = e.getCause();
+            }
+            spec.console().println("Failed to process cookie command '" + name() + "'");
+            cause.printStackTrace(spec.console());
+            return false;
+        }
+    }
+
+    protected String getBookieId(CookieFlagsT cmdFlags) throws UnknownHostException {
+        checkArgument(
+            cmdFlags.arguments.size() == 1,
+            "No bookie id or more bookie ids is specified");
+
+        String bookieId = cmdFlags.arguments.get(0);
+        try {
+            new BookieSocketAddress(bookieId);
+        } catch (UnknownHostException nhe) {
+            spec.console()
+                .println("Invalid bookie id '"
+                    + bookieId + "'is used to create cookie."
+                    + " Bookie id should be in the format of '<hostname>:<port>'");
+            throw nhe;
+        }
+        return bookieId;
+    }
+
+    protected byte[] readCookieDataFromFile(String cookieFile) throws IOException {
+        try {
+            return Files.readAllBytes(Paths.get(cookieFile));
+        } catch (NoSuchFileException nfe) {
+            spec.console()
+                .println("Cookie file '" + cookieFile + "' doesn't exist.");
+            throw nfe;
+        }
+    }
+
+
+    protected abstract void apply(RegistrationManager rm, CookieFlagsT cmdFlags)
+        throws Exception;
+
+    public org.apache.bookkeeper.bookie.BookieShell.Command asShellCommand(String shellCmdName,
+                                                                           CompositeConfiguration conf) {
+        return new BookieShellCommand<>(shellCmdName, this, conf);
+    }
+}
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/CreateCookieCommand.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/CreateCookieCommand.java
new file mode 100644
index 0000000..430fed9
--- /dev/null
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/CreateCookieCommand.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.commands.cookie;
+
+import com.beust.jcommander.Parameter;
+import java.io.PrintStream;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.bookkeeper.bookie.BookieException;
+import org.apache.bookkeeper.bookie.BookieException.CookieExistException;
+import org.apache.bookkeeper.discover.RegistrationManager;
+import org.apache.bookkeeper.tools.cli.commands.cookie.CreateCookieCommand.Flags;
+import org.apache.bookkeeper.tools.framework.CliFlags;
+import org.apache.bookkeeper.tools.framework.CliSpec;
+import org.apache.bookkeeper.versioning.Version;
+import org.apache.bookkeeper.versioning.Versioned;
+
+/**
+ * A command that create cookie.
+ */
+@Slf4j
+public class CreateCookieCommand extends CookieCommand<Flags> {
+
+    private static final String NAME = "create";
+    private static final String DESC = "Create a cookie for a given bookie";
+
+    /**
+     * Flags to create a cookie for a given bookie.
+     */
+    @Accessors(fluent = true)
+    @Setter
+    public static class Flags extends CliFlags {
+
+        @Parameter(
+            names = { "-cf", "--cookie-file" },
+            description = "The file to be uploaded as cookie",
+            required = true)
+        private String cookieFile;
+
+    }
+
+    public CreateCookieCommand() {
+        this(new Flags());
+    }
+
+    protected CreateCookieCommand(PrintStream console) {
+        this(new Flags(), console);
+    }
+
+    public CreateCookieCommand(Flags flags) {
+        this(flags, System.out);
+    }
+
+    private CreateCookieCommand(Flags flags, PrintStream console) {
+        super(CliSpec.<Flags>newBuilder()
+            .withName(NAME)
+            .withDescription(DESC)
+            .withFlags(flags)
+            .withConsole(console)
+            .withArgumentsUsage("<bookie-id>")
+            .build());
+    }
+
+    @Override
+    protected void apply(RegistrationManager rm, Flags cmdFlags) throws Exception {
+        String bookieId = getBookieId(cmdFlags);
+
+        byte[] data = readCookieDataFromFile(cmdFlags.cookieFile);
+        Versioned<byte[]> cookie = new Versioned<>(data, Version.NEW);
+        try {
+            rm.writeCookie(bookieId, cookie);
+        } catch (CookieExistException cee) {
+            spec.console()
+                .println("Cookie already exist for bookie '" + bookieId + "'");
+            throw cee;
+        } catch (BookieException be) {
+            spec.console()
+                .println("Exception on creating cookie for bookie '" + bookieId + "'");
+            be.printStackTrace(spec.console());
+            throw be;
+        }
+    }
+
+}
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/DeleteCookieCommand.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/DeleteCookieCommand.java
new file mode 100644
index 0000000..4c42615
--- /dev/null
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/DeleteCookieCommand.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.commands.cookie;
+
+import java.io.PrintStream;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.bookkeeper.bookie.BookieException;
+import org.apache.bookkeeper.bookie.BookieException.CookieNotFoundException;
+import org.apache.bookkeeper.discover.RegistrationManager;
+import org.apache.bookkeeper.tools.cli.commands.cookie.DeleteCookieCommand.Flags;
+import org.apache.bookkeeper.tools.framework.CliFlags;
+import org.apache.bookkeeper.tools.framework.CliSpec;
+import org.apache.bookkeeper.versioning.LongVersion;
+
+/**
+ * A command that deletes cookie.
+ */
+@Slf4j
+public class DeleteCookieCommand extends CookieCommand<Flags> {
+
+    private static final String NAME = "delete";
+    private static final String DESC = "Delete a cookie for a given bookie";
+
+    /**
+     * Flags to delete a cookie for a given bookie.
+     */
+    @Accessors(fluent = true)
+    @Setter
+    public static class Flags extends CliFlags {
+    }
+
+    public DeleteCookieCommand() {
+        this(new Flags());
+    }
+
+    DeleteCookieCommand(PrintStream console) {
+        this(new Flags(), console);
+    }
+
+    public DeleteCookieCommand(Flags flags) {
+        this(flags, System.out);
+    }
+
+    private DeleteCookieCommand(Flags flags, PrintStream console) {
+        super(CliSpec.<Flags>newBuilder()
+            .withName(NAME)
+            .withDescription(DESC)
+            .withFlags(flags)
+            .withConsole(console)
+            .withArgumentsUsage("<bookie-id>")
+            .build());
+    }
+
+    @Override
+    protected void apply(RegistrationManager rm, Flags cmdFlags) throws Exception {
+        String bookieId = getBookieId(cmdFlags);
+
+        try {
+            rm.removeCookie(bookieId, new LongVersion(-1));
+        } catch (CookieNotFoundException cee) {
+            spec.console()
+                .println("Cookie not found for bookie '" + bookieId + "'");
+            throw cee;
+        } catch (BookieException be) {
+            spec.console()
+                .println("Exception on deleting cookie for bookie '" + bookieId + "'");
+            be.printStackTrace(spec.console());
+            throw be;
+        }
+    }
+
+}
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/GenerateCookieCommand.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/GenerateCookieCommand.java
new file mode 100644
index 0000000..daa3c43
--- /dev/null
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/GenerateCookieCommand.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.commands.cookie;
+
+import com.beust.jcommander.Parameter;
+import java.io.File;
+import java.io.PrintStream;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.bookkeeper.bookie.Cookie;
+import org.apache.bookkeeper.bookie.Cookie.Builder;
+import org.apache.bookkeeper.discover.RegistrationManager;
+import org.apache.bookkeeper.tools.cli.commands.cookie.GenerateCookieCommand.Flags;
+import org.apache.bookkeeper.tools.framework.CliFlags;
+import org.apache.bookkeeper.tools.framework.CliSpec;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * A command that generate cookie.
+ */
+@Slf4j
+public class GenerateCookieCommand extends CookieCommand<Flags> {
+
+    private static final String NAME = "generate";
+    private static final String DESC = "Generate a cookie for a given bookie";
+
+    /**
+     * Flags to generate a cookie for a given bookie.
+     */
+    @Accessors(fluent = true)
+    @Setter
+    public static class Flags extends CliFlags {
+
+        @Parameter(
+            names = { "-j", "--journal-dirs" },
+            description = "The journal directories used by this bookie",
+            required = true)
+        private String journalDirs;
+
+        @Parameter(
+            names = { "-l", "--ledger-dirs" },
+            description = "The ledger directories used by this bookie",
+            required = true)
+        private String ledgerDirs;
+
+        @Parameter(
+            names = { "-i", "--instance-id" },
+            description = "The instance id of the cluster that this bookie belongs to."
+                + " If omitted, it will used the instance id of the cluster that this cli connects to.")
+        private String instanceId = null;
+
+        @Parameter(
+            names = { "-o", "--output-file" },
+            description = "The output file to save the generated cookie.",
+            required = true)
+        private String outputFile;
+
+    }
+
+    public GenerateCookieCommand() {
+        this(new Flags());
+    }
+
+    GenerateCookieCommand(PrintStream console) {
+        this(new Flags(), console);
+    }
+
+    public GenerateCookieCommand(Flags flags) {
+        this(flags, System.out);
+    }
+
+    private GenerateCookieCommand(Flags flags, PrintStream console) {
+        super(CliSpec.<Flags>newBuilder()
+            .withName(NAME)
+            .withDescription(DESC)
+            .withFlags(flags)
+            .withConsole(console)
+            .withArgumentsUsage("<bookie-id>")
+            .build());
+    }
+
+    @Override
+    protected void apply(RegistrationManager rm, Flags cmdFlags) throws Exception {
+        String bookieId = getBookieId(cmdFlags);
+
+        String instanceId;
+        if (null == cmdFlags.instanceId) {
+            instanceId = rm.getClusterInstanceId();
+        } else {
+            instanceId = cmdFlags.instanceId;
+        }
+
+        Builder builder = Cookie.newBuilder();
+        builder.setBookieHost(bookieId);
+        if (StringUtils.isEmpty(instanceId)) {
+            builder.setInstanceId(null);
+        } else {
+            builder.setInstanceId(instanceId);
+        }
+        builder.setJournalDirs(cmdFlags.journalDirs);
+        builder.setLedgerDirs(cmdFlags.ledgerDirs);
+
+        Cookie cookie = builder.build();
+        cookie.writeToFile(new File(cmdFlags.outputFile));
+        spec.console().println("Successfully saved the generated cookie to " + cmdFlags.outputFile);
+    }
+
+}
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/GetCookieCommand.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/GetCookieCommand.java
new file mode 100644
index 0000000..76f5f7c
--- /dev/null
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/GetCookieCommand.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.commands.cookie;
+
+import java.io.PrintStream;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.bookkeeper.bookie.BookieException;
+import org.apache.bookkeeper.bookie.BookieException.CookieNotFoundException;
+import org.apache.bookkeeper.bookie.Cookie;
+import org.apache.bookkeeper.discover.RegistrationManager;
+import org.apache.bookkeeper.net.BookieSocketAddress;
+import org.apache.bookkeeper.tools.cli.commands.cookie.GetCookieCommand.Flags;
+import org.apache.bookkeeper.tools.framework.CliFlags;
+import org.apache.bookkeeper.tools.framework.CliSpec;
+import org.apache.bookkeeper.versioning.Versioned;
+
+/**
+ * A command that deletes cookie.
+ */
+@Slf4j
+public class GetCookieCommand extends CookieCommand<Flags> {
+
+    private static final String NAME = "get";
+    private static final String DESC = "Retrieve a cookie for a given bookie";
+
+    /**
+     * Flags to delete a cookie for a given bookie.
+     */
+    @Accessors(fluent = true)
+    @Setter
+    public static class Flags extends CliFlags {
+    }
+
+    public GetCookieCommand() {
+        this(new Flags());
+    }
+
+    GetCookieCommand(PrintStream console) {
+        this(new Flags(), console);
+    }
+
+    public GetCookieCommand(Flags flags) {
+        this(flags, System.out);
+    }
+
+    private GetCookieCommand(Flags flags, PrintStream console) {
+        super(CliSpec.<Flags>newBuilder()
+            .withName(NAME)
+            .withDescription(DESC)
+            .withFlags(flags)
+            .withConsole(console)
+            .withArgumentsUsage("<bookie-id>")
+            .build());
+    }
+
+    @Override
+    protected void apply(RegistrationManager rm, Flags cmdFlags) throws Exception {
+        String bookieId = getBookieId(cmdFlags);
+
+        try {
+            Versioned<Cookie> cookie = Cookie.readFromRegistrationManager(
+                rm, new BookieSocketAddress(bookieId)
+            );
+            spec.console().println("Cookie for bookie '" + bookieId + "' is:");
+            spec.console().println("---");
+            spec.console().println(
+                cookie.getValue()
+            );
+            spec.console().println("---");
+        } catch (CookieNotFoundException cee) {
+            spec.console()
+                .println("Cookie not found for bookie '" + bookieId + "'");
+            throw cee;
+        } catch (BookieException be) {
+            spec.console()
+                .println("Exception on getting cookie for bookie '" + bookieId + "'");
+            be.printStackTrace(spec.console());
+            throw be;
+        }
+    }
+
+}
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/UpdateCookieCommand.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/UpdateCookieCommand.java
new file mode 100644
index 0000000..77e5f05
--- /dev/null
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/UpdateCookieCommand.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.commands.cookie;
+
+import com.beust.jcommander.Parameter;
+import java.io.PrintStream;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.bookkeeper.bookie.BookieException;
+import org.apache.bookkeeper.bookie.BookieException.CookieNotFoundException;
+import org.apache.bookkeeper.discover.RegistrationManager;
+import org.apache.bookkeeper.tools.cli.commands.cookie.UpdateCookieCommand.Flags;
+import org.apache.bookkeeper.tools.framework.CliFlags;
+import org.apache.bookkeeper.tools.framework.CliSpec;
+import org.apache.bookkeeper.versioning.LongVersion;
+import org.apache.bookkeeper.versioning.Versioned;
+
+/**
+ * A command that updates cookie.
+ */
+@Slf4j
+public class UpdateCookieCommand extends CookieCommand<Flags> {
+
+    private static final String NAME = "update";
+    private static final String DESC = "Update a cookie for a given bookie";
+
+    /**
+     * Flags to create a cookie for a given bookie.
+     */
+    @Accessors(fluent = true)
+    @Setter
+    public static class Flags extends CliFlags {
+
+        @Parameter(
+            names = { "-cf", "--cookie-file" },
+            description = "The file to be uploaded as cookie",
+            required = true)
+        private String cookieFile;
+
+    }
+
+    public UpdateCookieCommand() {
+        this(new Flags());
+    }
+
+    UpdateCookieCommand(PrintStream console) {
+        this(new Flags(), console);
+    }
+
+    public UpdateCookieCommand(Flags flags) {
+        this(flags, System.out);
+    }
+
+    private UpdateCookieCommand(Flags flags, PrintStream console) {
+        super(CliSpec.<Flags>newBuilder()
+            .withName(NAME)
+            .withDescription(DESC)
+            .withFlags(flags)
+            .withConsole(console)
+            .withArgumentsUsage("<bookie-id>")
+            .build());
+    }
+
+    @Override
+    protected void apply(RegistrationManager rm, Flags cmdFlags) throws Exception {
+        String bookieId = getBookieId(cmdFlags);
+
+        byte[] data = readCookieDataFromFile(cmdFlags.cookieFile);
+        Versioned<byte[]> cookie = new Versioned<>(data, new LongVersion(-1L));
+        try {
+            rm.writeCookie(bookieId, cookie);
+        } catch (CookieNotFoundException cnfe) {
+            spec.console()
+                .println("Cookie not found for bookie '" + bookieId + "' to update");
+            throw cnfe;
+        } catch (BookieException be) {
+            spec.console()
+                .println("Exception on updating cookie for bookie '" + bookieId + "'");
+            be.printStackTrace(spec.console());
+            throw be;
+        }
+    }
+
+}
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/package-info.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/package-info.java
new file mode 100644
index 0000000..248c49a
--- /dev/null
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/cookie/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Cookie related cli commands.
+ */
+package org.apache.bookkeeper.tools.cli.commands.cookie;
\ No newline at end of file
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/helpers/BookieShellCommand.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/helpers/BookieShellCommand.java
new file mode 100644
index 0000000..4010e60
--- /dev/null
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/helpers/BookieShellCommand.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.helpers;
+
+import org.apache.bookkeeper.bookie.BookieShell.Command;
+import org.apache.bookkeeper.tools.common.BKCommand;
+import org.apache.bookkeeper.tools.framework.CliFlags;
+import org.apache.commons.configuration.CompositeConfiguration;
+
+/**
+ * This is a util class that converts new cli command to old shell command.
+ */
+public class BookieShellCommand<CliFlagsT extends CliFlags> implements Command {
+
+    protected final String shellCmdName;
+    protected final BKCommand<CliFlagsT> bkCmd;
+    protected final CompositeConfiguration conf;
+
+    public BookieShellCommand(String shellCmdName,
+                              BKCommand<CliFlagsT> bkCmd,
+                              CompositeConfiguration conf) {
+        this.shellCmdName = shellCmdName;
+        this.bkCmd = bkCmd;
+        this.conf = conf;
+    }
+
+    @Override
+    public int runCmd(String[] args) throws Exception {
+        return bkCmd.apply(
+            shellCmdName,
+            conf,
+            args
+        );
+    }
+
+    @Override
+    public String description() {
+        return shellCmdName + " [options]";
+    }
+
+    @Override
+    public void printUsage() {
+        bkCmd.usage();
+    }
+}
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/helpers/ClientCommand.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/helpers/ClientCommand.java
index ae807df..45cb40e 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/helpers/ClientCommand.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/helpers/ClientCommand.java
@@ -66,7 +66,7 @@ public abstract class ClientCommand<ClientFlagsT extends CliFlags> extends BKCom
             run(bk, cmdFlags);
             return true;
         } catch (Exception e) {
-            log.error("Faild to process command '{}'", name(), e);
+            log.error("Failed to process command '{}'", name(), e);
             return false;
         }
     }
diff --git a/tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/CookieCommandGroup.java b/tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/CookieCommandGroup.java
new file mode 100644
index 0000000..3dded4e
--- /dev/null
+++ b/tools/all/src/main/java/org/apache/bookkeeper/tools/cli/commands/CookieCommandGroup.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.commands;
+
+import org.apache.bookkeeper.tools.cli.BKCtl;
+import org.apache.bookkeeper.tools.cli.commands.cookie.CreateCookieCommand;
+import org.apache.bookkeeper.tools.cli.commands.cookie.DeleteCookieCommand;
+import org.apache.bookkeeper.tools.cli.commands.cookie.GenerateCookieCommand;
+import org.apache.bookkeeper.tools.cli.commands.cookie.GetCookieCommand;
+import org.apache.bookkeeper.tools.cli.commands.cookie.UpdateCookieCommand;
+import org.apache.bookkeeper.tools.common.BKFlags;
+import org.apache.bookkeeper.tools.framework.CliCommandGroup;
+import org.apache.bookkeeper.tools.framework.CliSpec;
+
+/**
+ * Commands that operates cookies.
+ */
+public class CookieCommandGroup extends CliCommandGroup<BKFlags> {
+
+    private static final String NAME = "cookie";
+    private static final String DESC = "Commands on operating cookies";
+
+    private static final CliSpec<BKFlags> spec = CliSpec.<BKFlags>newBuilder()
+        .withName(NAME)
+        .withDescription(DESC)
+        .withParent(BKCtl.NAME)
+        .addCommand(new CreateCookieCommand())
+        .addCommand(new DeleteCookieCommand())
+        .addCommand(new GetCookieCommand())
+        .addCommand(new UpdateCookieCommand())
+        .addCommand(new GenerateCookieCommand())
+        .build();
+
+    public CookieCommandGroup() {
+        super(spec);
+    }
+}
diff --git a/tools/all/src/main/resources/META-INF/services/org.apache.bookkeeper.tools.framework.CommandGroup b/tools/all/src/main/resources/META-INF/services/org.apache.bookkeeper.tools.framework.CommandGroup
index 44fc194..0147eca 100644
--- a/tools/all/src/main/resources/META-INF/services/org.apache.bookkeeper.tools.framework.CommandGroup
+++ b/tools/all/src/main/resources/META-INF/services/org.apache.bookkeeper.tools.framework.CommandGroup
@@ -18,6 +18,7 @@
 
 org.apache.bookkeeper.tools.cli.commands.BookieCommandGroup
 org.apache.bookkeeper.tools.cli.commands.BookiesCommandGroup
+org.apache.bookkeeper.tools.cli.commands.CookieCommandGroup
 org.apache.bookkeeper.tools.cli.commands.LedgerCommandGroup
 org.apache.bookkeeper.stream.cli.ClusterCommandGroup
 org.apache.bookkeeper.stream.cli.NamespaceCommandGroup
diff --git a/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/CreateCookieCommandTest.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/CreateCookieCommandTest.java
new file mode 100644
index 0000000..fcfae0b
--- /dev/null
+++ b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/CreateCookieCommandTest.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.commands.cookie;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import org.apache.bookkeeper.bookie.BookieException.CookieExistException;
+import org.apache.bookkeeper.bookie.BookieException.OperationRejectedException;
+import org.apache.bookkeeper.meta.MetadataDrivers;
+import org.apache.bookkeeper.tools.cli.helpers.CookieCommandTestBase;
+import org.apache.bookkeeper.tools.common.BKFlags;
+import org.apache.bookkeeper.versioning.Versioned;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+/**
+ * Unit test {@link CreateCookieCommand}.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ MetadataDrivers.class })
+public class CreateCookieCommandTest extends CookieCommandTestBase {
+
+    @Rule
+    public final TemporaryFolder testFolder = new TemporaryFolder();
+
+    private final ByteArrayOutputStream output = new ByteArrayOutputStream();
+    private final PrintStream console = new PrintStream(output);
+
+    private boolean runCommand(String[] args) {
+        CreateCookieCommand createCmd = new CreateCookieCommand(console);
+        BKFlags bkFlags = new BKFlags();
+        bkFlags.serviceUri = "zk://127.0.0.1";
+        return createCmd.apply(bkFlags, args);
+    }
+
+    private String getConsoleOutput() {
+        return new String(output.toByteArray(), UTF_8);
+    }
+
+    /**
+     * Run a command without providing bookie id.
+     */
+    @Test
+    public void testMissingBookieId() {
+        assertFalse(runCommand(new String[] {}));
+        String consoleOutput = getConsoleOutput();
+        assertBookieIdMissing(consoleOutput);
+    }
+
+    private void assertPrintUsage(String consoleOutput) {
+        assertPrintUsage(consoleOutput, "create [flags] <bookie-id>");
+    }
+
+    /**
+     * Run a command without cookie file.
+     */
+    @Test
+    public void testMissingCookieFileOption() {
+        assertFalse(runCommand(new String[] { BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertOptionMissing(consoleOutput, "-cf, --cookie-file");
+        assertPrintUsage(consoleOutput);
+    }
+
+    /**
+     * Run a command with invalid bookie id.
+     */
+    @Test
+    public void testInvalidBookieId() {
+        assertFalse(runCommand(new String[] { "-cf", "test-cookie-file", INVALID_BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertInvalidBookieId(consoleOutput, INVALID_BOOKIE_ID);
+    }
+
+    /**
+     * Run a command with a non-existent cookie file.
+     */
+    @Test
+    public void testCreateCookieFromNonExistentCookieFile() {
+        String file = "/path/to/non-existent-cookie-file";
+        assertFalse(runCommand(new String[] { "-cf", file, BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertCookieFileNotExists(consoleOutput, file);
+    }
+
+    /**
+     * A successful run.
+     */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testCreateCookieFromExistentCookieFile() throws Exception {
+        File file = testFolder.newFile("test-cookie-file");
+        byte[] content = "test-create-cookie".getBytes(UTF_8);
+        Files.write(Paths.get(file.toURI()), content);
+        String fileName = file.getPath();
+        assertTrue(runCommand(new String[] { "-cf", fileName, BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertTrue(consoleOutput, consoleOutput.isEmpty());
+        verify(rm, times(1)).writeCookie(eq(BOOKIE_ID), any(Versioned.class));
+    }
+
+    /**
+     * Run a command to create cookie on an existing cookie.
+     */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testCreateAlreadyExistedCookie() throws Exception {
+        doThrow(new CookieExistException())
+            .when(rm).writeCookie(eq(BOOKIE_ID), any(Versioned.class));
+
+        File file = testFolder.newFile("test-cookie-file");
+        byte[] content = "test-create-cookie".getBytes(UTF_8);
+        Files.write(Paths.get(file.toURI()), content);
+        String fileName = file.getPath();
+        assertFalse(runCommand(new String[] { "-cf", fileName, BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains("Cookie already exist for bookie '" + BOOKIE_ID + "'"));
+        verify(rm, times(1)).writeCookie(eq(BOOKIE_ID), any(Versioned.class));
+    }
+
+    /**
+     * Run a command to create cookie when exception is thrown.
+     */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testCreateCookieException() throws Exception {
+        doThrow(new OperationRejectedException())
+            .when(rm).writeCookie(eq(BOOKIE_ID), any(Versioned.class));
+
+        File file = testFolder.newFile("test-cookie-file");
+        byte[] content = "test-create-cookie".getBytes(UTF_8);
+        Files.write(Paths.get(file.toURI()), content);
+        String fileName = file.getPath();
+        assertFalse(runCommand(new String[] { "-cf", fileName, BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains("Exception on creating cookie for bookie '" + BOOKIE_ID + "'"));
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains(OperationRejectedException.class.getName()));
+        verify(rm, times(1)).writeCookie(eq(BOOKIE_ID), any(Versioned.class));
+    }
+
+}
diff --git a/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/DeleteCookieCommandTest.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/DeleteCookieCommandTest.java
new file mode 100644
index 0000000..01ad58d
--- /dev/null
+++ b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/DeleteCookieCommandTest.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.commands.cookie;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import org.apache.bookkeeper.bookie.BookieException.CookieNotFoundException;
+import org.apache.bookkeeper.bookie.BookieException.OperationRejectedException;
+import org.apache.bookkeeper.meta.MetadataDrivers;
+import org.apache.bookkeeper.tools.cli.helpers.CookieCommandTestBase;
+import org.apache.bookkeeper.tools.common.BKFlags;
+import org.apache.bookkeeper.versioning.LongVersion;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+/**
+ * Unit test {@link DeleteCookieCommand}.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ MetadataDrivers.class })
+public class DeleteCookieCommandTest extends CookieCommandTestBase {
+
+    @Rule
+    public final TemporaryFolder testFolder = new TemporaryFolder();
+
+    private final ByteArrayOutputStream output = new ByteArrayOutputStream();
+    private final PrintStream console = new PrintStream(output);
+
+    private boolean runCommand(String[] args) {
+        DeleteCookieCommand deleteCmd = new DeleteCookieCommand(console);
+        BKFlags bkFlags = new BKFlags();
+        bkFlags.serviceUri = "zk://127.0.0.1";
+        return deleteCmd.apply(bkFlags, args);
+    }
+
+    private String getConsoleOutput() {
+        return new String(output.toByteArray(), UTF_8);
+    }
+
+    /**
+     * Run a command without providing bookie id.
+     */
+    @Test
+    public void testMissingBookieId() {
+        assertFalse(runCommand(new String[] {}));
+        String consoleOutput = getConsoleOutput();
+        assertBookieIdMissing(consoleOutput);
+    }
+
+    /**
+     * Run a command with invalid bookie id.
+     */
+    @Test
+    public void testInvalidBookieId() {
+        assertFalse(runCommand(new String[] { INVALID_BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertInvalidBookieId(consoleOutput, INVALID_BOOKIE_ID);
+    }
+
+    /**
+     * A successful run.
+     */
+    @Test
+    public void testDeleteCookieFromExistentCookieFile() throws Exception {
+        assertTrue(runCommand(new String[] { BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertTrue(consoleOutput, consoleOutput.isEmpty());
+        verify(rm, times(1)).removeCookie(eq(BOOKIE_ID), eq(new LongVersion(-1L)));
+    }
+
+    /**
+     * Run a command to delete cookie on an non-existent cookie.
+     */
+    @Test
+    public void testDeleteNonExistedCookie() throws Exception {
+        doThrow(new CookieNotFoundException())
+            .when(rm).removeCookie(eq(BOOKIE_ID), eq(new LongVersion(-1L)));
+
+        assertFalse(runCommand(new String[] { BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains("Cookie not found for bookie '" + BOOKIE_ID + "'"));
+        verify(rm, times(1)).removeCookie(eq(BOOKIE_ID), eq(new LongVersion(-1L)));
+    }
+
+    /**
+     * Run a command to delete cookie when exception is thrown.
+     */
+    @Test
+    public void testDeleteCookieException() throws Exception {
+        doThrow(new OperationRejectedException())
+            .when(rm).removeCookie(eq(BOOKIE_ID), eq(new LongVersion(-1L)));
+
+        assertFalse(runCommand(new String[] { BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains("Exception on deleting cookie for bookie '" + BOOKIE_ID + "'"));
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains(OperationRejectedException.class.getName()));
+        verify(rm, times(1)).removeCookie(eq(BOOKIE_ID), eq(new LongVersion(-1L)));
+    }
+
+}
diff --git a/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/GenerateCookieCommandTest.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/GenerateCookieCommandTest.java
new file mode 100644
index 0000000..6e27294
--- /dev/null
+++ b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/GenerateCookieCommandTest.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.commands.cookie;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import org.apache.bookkeeper.bookie.Cookie;
+import org.apache.bookkeeper.meta.MetadataDrivers;
+import org.apache.bookkeeper.tools.cli.helpers.CookieCommandTestBase;
+import org.apache.bookkeeper.tools.common.BKFlags;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+/**
+ * Unit test {@link GetCookieCommand}.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ MetadataDrivers.class })
+public class GenerateCookieCommandTest extends CookieCommandTestBase {
+
+    @Rule
+    public final TemporaryFolder testFolder = new TemporaryFolder();
+
+    private final ByteArrayOutputStream output = new ByteArrayOutputStream();
+    private final PrintStream console = new PrintStream(output);
+
+    private boolean runCommand(String[] args) {
+        GenerateCookieCommand getCmd = new GenerateCookieCommand(console);
+        BKFlags bkFlags = new BKFlags();
+        bkFlags.serviceUri = "zk://127.0.0.1";
+        return getCmd.apply(bkFlags, args);
+    }
+
+    private String getConsoleOutput() {
+        return new String(output.toByteArray(), UTF_8);
+    }
+
+    /**
+     * Run a command without providing bookie id.
+     */
+    @Test
+    public void testMissingBookieId() {
+        assertFalse(runCommand(new String[] {}));
+        String consoleOutput = getConsoleOutput();
+        assertBookieIdMissing(consoleOutput);
+    }
+
+    /**
+     * Run a command with invalid bookie id.
+     */
+    @Test
+    public void testInvalidBookieId() {
+        assertFalse(runCommand(new String[] {
+            "-j", "/path/to/journal",
+            "-l", "/path/to/ledgers",
+            "-o", "/path/to/cookie-file",
+            INVALID_BOOKIE_ID
+        }));
+        String consoleOutput = getConsoleOutput();
+        assertInvalidBookieId(consoleOutput, INVALID_BOOKIE_ID);
+    }
+
+    /**
+     * Run a command without journal dirs.
+     */
+    @Test
+    public void testMissingJournalDir() {
+        assertFalse(runCommand(new String[] { "-l", "/path/to/ledgers", "-o", "/path/to/cookie-file", BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertOptionMissing(consoleOutput, "-j, --journal-dirs");
+    }
+
+    /**
+     * Run a command without ledger dirs.
+     */
+    @Test
+    public void testMissingLedgerDirs() {
+        assertFalse(runCommand(new String[] { "-j", "/path/to/journal", "-o", "/path/to/cookie-file", BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertOptionMissing(consoleOutput, "-l, --ledger-dirs");
+    }
+
+    /**
+     * Run a command without output file.
+     */
+    @Test
+    public void testMissingOutputFile() {
+        assertFalse(runCommand(new String[] { "-j", "/path/to/journal", "-l", "/path/to/ledgers", BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertOptionMissing(consoleOutput, "-o, --output-file");
+    }
+
+    /**
+     * A successful run without instance id.
+     */
+    @Test
+    public void testGenerateCookieWithoutInstanceId() throws Exception {
+        File cookieFile = testFolder.newFile("cookie-without-instance-id");
+        String journalDir = "/path/to/journal";
+        String ledgersDir = "/path/to/ledgers";
+        String instanceId = "test-instance-id";
+
+        Cookie cookie = Cookie.newBuilder()
+            .setBookieHost(BOOKIE_ID)
+            .setInstanceId(instanceId)
+            .setJournalDirs(journalDir)
+            .setLedgerDirs(ledgersDir)
+            .build();
+
+        when(rm.getClusterInstanceId()).thenReturn(instanceId);
+        assertTrue(
+            getConsoleOutput(),
+            runCommand(new String[] {
+                "-l", ledgersDir,
+                "-j", journalDir,
+                "-o", cookieFile.getPath(),
+                BOOKIE_ID
+            }));
+        String consoleOutput = getConsoleOutput();
+        assertTrue(consoleOutput, consoleOutput.contains(
+            "Successfully saved the generated cookie to " + cookieFile.getPath()
+        ));
+        verify(rm, times(1)).getClusterInstanceId();
+
+        byte[] data = Files.readAllBytes(Paths.get(cookieFile.getPath()));
+        assertArrayEquals(cookie.toString().getBytes(UTF_8), data);
+    }
+
+    /**
+     * A successful run with instance id.
+     */
+    @Test
+    public void testGenerateCookieWithInstanceId() throws Exception {
+        File cookieFile = testFolder.newFile("cookie-with-instance-id");
+        String journalDir = "/path/to/journal";
+        String ledgersDir = "/path/to/ledgers";
+        String instanceId = "test-instance-id";
+
+        Cookie cookie = Cookie.newBuilder()
+            .setBookieHost(BOOKIE_ID)
+            .setInstanceId(instanceId)
+            .setJournalDirs(journalDir)
+            .setLedgerDirs(ledgersDir)
+            .build();
+
+        when(rm.getClusterInstanceId()).thenReturn(instanceId);
+        assertTrue(
+            getConsoleOutput(),
+            runCommand(new String[] {
+                "-l", ledgersDir,
+                "-j", journalDir,
+                "-o", cookieFile.getPath(),
+                "-i", instanceId,
+                BOOKIE_ID
+            }));
+        String consoleOutput = getConsoleOutput();
+        assertTrue(consoleOutput, consoleOutput.contains(
+            "Successfully saved the generated cookie to " + cookieFile.getPath()
+        ));
+        verify(rm, times(0)).getClusterInstanceId();
+
+        byte[] data = Files.readAllBytes(Paths.get(cookieFile.getPath()));
+        assertArrayEquals(cookie.toString().getBytes(UTF_8), data);
+    }
+
+}
diff --git a/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/GetCookieCommandTest.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/GetCookieCommandTest.java
new file mode 100644
index 0000000..ef45a05
--- /dev/null
+++ b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/GetCookieCommandTest.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.commands.cookie;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import org.apache.bookkeeper.bookie.BookieException.CookieNotFoundException;
+import org.apache.bookkeeper.bookie.BookieException.OperationRejectedException;
+import org.apache.bookkeeper.bookie.Cookie;
+import org.apache.bookkeeper.meta.MetadataDrivers;
+import org.apache.bookkeeper.tools.cli.helpers.CookieCommandTestBase;
+import org.apache.bookkeeper.tools.common.BKFlags;
+import org.apache.bookkeeper.versioning.LongVersion;
+import org.apache.bookkeeper.versioning.Versioned;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+/**
+ * Unit test {@link GetCookieCommand}.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ MetadataDrivers.class })
+public class GetCookieCommandTest extends CookieCommandTestBase {
+
+    @Rule
+    public final TemporaryFolder testFolder = new TemporaryFolder();
+
+    private final ByteArrayOutputStream output = new ByteArrayOutputStream();
+    private final PrintStream console = new PrintStream(output);
+
+    private boolean runCommand(String[] args) {
+        GetCookieCommand getCmd = new GetCookieCommand(console);
+        BKFlags bkFlags = new BKFlags();
+        bkFlags.serviceUri = "zk://127.0.0.1";
+        return getCmd.apply(bkFlags, args);
+    }
+
+    private String getConsoleOutput() {
+        return new String(output.toByteArray(), UTF_8);
+    }
+
+    /**
+     * Run a command without providing bookie id.
+     */
+    @Test
+    public void testMissingBookieId() {
+        assertFalse(runCommand(new String[] {}));
+        String consoleOutput = getConsoleOutput();
+        assertBookieIdMissing(consoleOutput);
+    }
+
+    /**
+     * Run a command with invalid bookie id.
+     */
+    @Test
+    public void testInvalidBookieId() {
+        assertFalse(runCommand(new String[] { INVALID_BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertInvalidBookieId(consoleOutput, INVALID_BOOKIE_ID);
+    }
+
+    /**
+     * A successful run.
+     */
+    @Test
+    public void testGetCookieFromExistentCookieFile() throws Exception {
+        Cookie cookie = Cookie.newBuilder()
+            .setBookieHost(BOOKIE_ID)
+            .setInstanceId("test-instance-id")
+            .setJournalDirs("/path/to/journal/dir")
+            .setLedgerDirs("/path/to/ledger/dirs")
+            .build();
+        when(rm.readCookie(eq(BOOKIE_ID)))
+            .thenReturn(new Versioned<>(cookie.toString().getBytes(UTF_8), new LongVersion(-1L)));
+        assertTrue(runCommand(new String[] { BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertTrue(consoleOutput, consoleOutput.contains(cookie.toString()));
+        verify(rm, times(1)).readCookie(eq(BOOKIE_ID));
+    }
+
+    /**
+     * Run a command to get cookie on an non-existent cookie.
+     */
+    @Test
+    public void testGetNonExistedCookie() throws Exception {
+        doThrow(new CookieNotFoundException())
+            .when(rm).readCookie(eq(BOOKIE_ID));
+
+        assertFalse(runCommand(new String[] { BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains("Cookie not found for bookie '" + BOOKIE_ID + "'"));
+        verify(rm, times(1)).readCookie(eq(BOOKIE_ID));
+    }
+
+    /**
+     * Run a command to get cookie when exception is thrown.
+     */
+    @Test
+    public void testGetCookieException() throws Exception {
+        doThrow(new OperationRejectedException())
+            .when(rm).readCookie(eq(BOOKIE_ID));
+
+        assertFalse(runCommand(new String[] { BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains("Exception on getting cookie for bookie '" + BOOKIE_ID + "'"));
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains(OperationRejectedException.class.getName()));
+        verify(rm, times(1)).readCookie(eq(BOOKIE_ID));
+    }
+
+}
diff --git a/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/UpdateCookieCommandTest.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/UpdateCookieCommandTest.java
new file mode 100644
index 0000000..308cd2a
--- /dev/null
+++ b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/commands/cookie/UpdateCookieCommandTest.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.commands.cookie;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import org.apache.bookkeeper.bookie.BookieException.CookieNotFoundException;
+import org.apache.bookkeeper.bookie.BookieException.OperationRejectedException;
+import org.apache.bookkeeper.meta.MetadataDrivers;
+import org.apache.bookkeeper.tools.cli.helpers.CookieCommandTestBase;
+import org.apache.bookkeeper.tools.common.BKFlags;
+import org.apache.bookkeeper.versioning.Versioned;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+/**
+ * Unit test {@link UpdateCookieCommand}.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ MetadataDrivers.class })
+public class UpdateCookieCommandTest extends CookieCommandTestBase {
+
+    @Rule
+    public final TemporaryFolder testFolder = new TemporaryFolder();
+
+    private final ByteArrayOutputStream output = new ByteArrayOutputStream();
+    private final PrintStream console = new PrintStream(output);
+
+    private boolean runCommand(String[] args) {
+        UpdateCookieCommand updateCmd = new UpdateCookieCommand(console);
+        BKFlags bkFlags = new BKFlags();
+        bkFlags.serviceUri = "zk://127.0.0.1";
+        return updateCmd.apply(bkFlags, args);
+    }
+
+    private String getConsoleOutput() {
+        return new String(output.toByteArray(), UTF_8);
+    }
+
+    /**
+     * Run a command without providing bookie id.
+     */
+    @Test
+    public void testMissingBookieId() {
+        assertFalse(runCommand(new String[] {}));
+        String consoleOutput = getConsoleOutput();
+        assertBookieIdMissing(consoleOutput);
+    }
+
+    private void assertPrintUsage(String consoleOutput) {
+        assertPrintUsage(consoleOutput, "update [flags] <bookie-id>");
+    }
+
+    /**
+     * Run a command without cookie file.
+     */
+    @Test
+    public void testMissingCookieFileOption() {
+        assertFalse(runCommand(new String[] { BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertOptionMissing(consoleOutput, "-cf, --cookie-file");
+        assertPrintUsage(consoleOutput);
+    }
+
+    /**
+     * Run a command with invalid bookie id.
+     */
+    @Test
+    public void testInvalidBookieId() {
+        assertFalse(runCommand(new String[] { "-cf", "test-cookie-file", INVALID_BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertInvalidBookieId(consoleOutput, INVALID_BOOKIE_ID);
+    }
+
+    /**
+     * Run a command with a non-existent cookie file.
+     */
+    @Test
+    public void testUpdateCookieFromNonExistentCookieFile() {
+        String file = "/path/to/non-existent-cookie-file";
+        assertFalse(runCommand(new String[] { "-cf", file, BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertCookieFileNotExists(consoleOutput, file);
+    }
+
+    /**
+     * A successful run.
+     */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testUpdateCookieFromExistentCookieFile() throws Exception {
+        File file = testFolder.newFile("test-cookie-file");
+        byte[] content = "test-update-cookie".getBytes(UTF_8);
+        Files.write(Paths.get(file.toURI()), content);
+        String fileName = file.getPath();
+        assertTrue(runCommand(new String[] { "-cf", fileName, BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertTrue(consoleOutput, consoleOutput.isEmpty());
+        verify(rm, times(1)).writeCookie(eq(BOOKIE_ID), any(Versioned.class));
+    }
+
+    /**
+     * Run a command to update cookie on an non-existent cookie.
+     */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testUpdateNonExistedCookie() throws Exception {
+        doThrow(new CookieNotFoundException())
+            .when(rm).writeCookie(eq(BOOKIE_ID), any(Versioned.class));
+
+        File file = testFolder.newFile("test-cookie-file");
+        byte[] content = "test-update-cookie".getBytes(UTF_8);
+        Files.write(Paths.get(file.toURI()), content);
+        String fileName = file.getPath();
+        assertFalse(runCommand(new String[] { "-cf", fileName, BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains("Cookie not found for bookie '" + BOOKIE_ID + "'"));
+        verify(rm, times(1)).writeCookie(eq(BOOKIE_ID), any(Versioned.class));
+    }
+
+    /**
+     * Run a command to update cookie when exception is thrown.
+     */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testUpdateCookieException() throws Exception {
+        doThrow(new OperationRejectedException())
+            .when(rm).writeCookie(eq(BOOKIE_ID), any(Versioned.class));
+
+        File file = testFolder.newFile("test-cookie-file");
+        byte[] content = "test-update-cookie".getBytes(UTF_8);
+        Files.write(Paths.get(file.toURI()), content);
+        String fileName = file.getPath();
+        assertFalse(runCommand(new String[] { "-cf", fileName, BOOKIE_ID }));
+        String consoleOutput = getConsoleOutput();
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains("Exception on updating cookie for bookie '" + BOOKIE_ID + "'"));
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains(OperationRejectedException.class.getName()));
+        verify(rm, times(1)).writeCookie(eq(BOOKIE_ID), any(Versioned.class));
+    }
+
+}
diff --git a/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/BookieShellCommandTest.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/BookieShellCommandTest.java
new file mode 100644
index 0000000..9597cde
--- /dev/null
+++ b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/BookieShellCommandTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.helpers;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.apache.bookkeeper.tools.common.BKCommand;
+import org.apache.bookkeeper.tools.framework.CliFlags;
+import org.apache.commons.configuration.CompositeConfiguration;
+import org.junit.Test;
+
+/**
+ * Unit test {@link BookieShellCommand}.
+ */
+public class BookieShellCommandTest {
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testShellCommand() throws Exception {
+        BKCommand<CliFlags> command = mock(BKCommand.class);
+        String shellCommandName = "test-shell-command";
+        CompositeConfiguration conf = new CompositeConfiguration();
+        BookieShellCommand<CliFlags> shellCommand = new BookieShellCommand<>(
+            shellCommandName,
+            command,
+            conf);
+
+        // test `description`
+        assertEquals(
+            shellCommandName + " [options]",
+            shellCommand.description());
+
+        // test `printUsage`
+        shellCommand.printUsage();
+        verify(command, times(1)).usage();
+
+        // test `runCmd`
+        String[] args = new String[] { "arg-1", "arg-2" };
+        shellCommand.runCmd(args);
+        verify(command, times(1))
+            .apply(same(shellCommandName), same(conf), same(args));
+    }
+
+}
diff --git a/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/CookieCommandTestBase.java b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/CookieCommandTestBase.java
new file mode 100644
index 0000000..f6f66b6
--- /dev/null
+++ b/tools/all/src/test/java/org/apache/bookkeeper/tools/cli/helpers/CookieCommandTestBase.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.tools.cli.helpers;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+
+import java.util.function.Function;
+import org.apache.bookkeeper.conf.ServerConfiguration;
+import org.apache.bookkeeper.discover.RegistrationManager;
+import org.apache.bookkeeper.meta.MetadataDrivers;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+/**
+ * A test base for testing cookie commands.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ MetadataDrivers.class })
+public class CookieCommandTestBase extends CommandTestBase {
+
+    protected static final String INVALID_BOOKIE_ID = "127.0.0.1";
+    protected static final String BOOKIE_ID = "127.0.0.1:3181";
+
+    protected RegistrationManager rm;
+
+    @Before
+    public void setup() throws Exception {
+        PowerMockito.mockStatic(MetadataDrivers.class);
+        this.rm = mock(RegistrationManager.class);
+        PowerMockito.doAnswer(invocationOnMock -> {
+            Function<RegistrationManager, ?> func = invocationOnMock.getArgument(1);
+            func.apply(rm);
+            return true;
+        }).when(MetadataDrivers.class, "runFunctionWithRegistrationManager",
+            any(ServerConfiguration.class), any(Function.class));
+    }
+
+    protected void assertBookieIdMissing(String consoleOutput) {
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains("No bookie id or more bookie ids is specified")
+        );
+    }
+
+    protected void assertInvalidBookieId(String consoleOutput, String bookieId) {
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains("Invalid bookie id '" + bookieId + "'"));
+    }
+
+    protected void assertOptionMissing(String consoleOutput, String option) {
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains("The following option is required: " + option));
+    }
+
+    protected void assertPrintUsage(String consoleOutput, String usage) {
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains("Usage:  " + usage));
+    }
+
+    protected void assertCookieFileNotExists(String consoleOutput, String cookieFile) {
+        assertTrue(
+            consoleOutput,
+            consoleOutput.contains("Cookie file '" + cookieFile + "' doesn't exist."));
+    }
+
+}
diff --git a/tools/framework/src/main/java/org/apache/bookkeeper/tools/common/BKCommand.java b/tools/framework/src/main/java/org/apache/bookkeeper/tools/common/BKCommand.java
index bf591ab..35b2cbd 100644
--- a/tools/framework/src/main/java/org/apache/bookkeeper/tools/common/BKCommand.java
+++ b/tools/framework/src/main/java/org/apache/bookkeeper/tools/common/BKCommand.java
@@ -23,6 +23,7 @@ import java.net.MalformedURLException;
 import java.net.URL;
 import java.nio.file.Paths;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.bookkeeper.common.annotation.InterfaceAudience.Private;
 import org.apache.bookkeeper.common.net.ServiceURI;
 import org.apache.bookkeeper.tools.framework.Cli;
 import org.apache.bookkeeper.tools.framework.CliCommand;
@@ -50,6 +51,19 @@ public abstract class BKCommand<CommandFlagsT extends CliFlags> extends CliComma
         return 0 == Cli.runCli(newSpec, args);
     }
 
+    /**
+     * Made this as public for allowing old bookie shell use new cli command.
+     * This should be removed once we get rid of the old bookie shell.
+     */
+    @Private
+    public int apply(String commandName, CompositeConfiguration conf, String[] args) {
+        CliSpec<CommandFlagsT> newSpec = CliSpec.newBuilder(spec)
+            .withName(commandName)
+            .withRunFunc(cmdFlags -> apply(null, conf, new BKFlags(), cmdFlags))
+            .build();
+        return Cli.runCli(newSpec, args);
+    }
+
     protected boolean apply(BKFlags bkFlags, CommandFlagsT cmdFlags) {
         ServiceURI serviceURI = null;