sijie closed pull request #1794: [tools] add cookie related commands
URL: https://github.com/apache/bookkeeper/pull/1794
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

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 3d84148d1c..83002ce252 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 static BookieException create(int code) {
             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 static BookieException create(int code) {
         int MetadataStoreException = -106;
         int UnknownBookieIdException = -107;
         int OperationRejectedException = -108;
+        int CookieExistsException = -109;
     }
 
     public int getCode() {
@@ -118,6 +121,9 @@ public String getMessage(int code) {
         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;
@@ -231,6 +237,23 @@ public CookieNotFoundException(Throwable cause) {
         }
     }
 
+    /**
+     * 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.
      */
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 18ce8c4392..e0e79c2de1 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
@@ -92,6 +92,7 @@
 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;
@@ -115,6 +116,11 @@
 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;
@@ -191,6 +197,14 @@
     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";
     static final String CMD_REGENERATE_INTERLEAVED_STORAGE_INDEX_FILE = 
"regenerate-interleaved-storage-index-file";
+
+    // 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();
@@ -214,9 +228,15 @@ public BookieShell(LedgerIdFormatter ledgeridFormatter, 
EntryFormatter entryForm
         this.entryFormatter = entryFormatter;
     }
 
-    interface Command {
+    /**
+     * BookieShell command.
+     */
+    @Private
+    public interface Command {
         int runCmd(String[] args) throws Exception;
 
+        String description();
+
         void printUsage();
     }
 
@@ -235,6 +255,11 @@ public BookieShell(LedgerIdFormatter ledgeridFormatter, 
EntryFormatter entryForm
             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 {
@@ -2881,7 +2906,7 @@ int runCmd(CommandLine cmdLine) throws Exception {
         }
     }
 
-    final Map<String, MyCommand> commands = new HashMap<String, MyCommand>();
+    final Map<String, Command> commands = new HashMap<>();
 
     {
         commands.put(CMD_METAFORMAT, new MetaFormatCmd());
@@ -2918,6 +2943,17 @@ int runCmd(CommandLine cmdLine) throws Exception {
         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
@@ -2939,8 +2975,8 @@ private void printShellUsage() {
                 + "[-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 4a9f6f6c79..7d4175cd6f 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 @@ private static Builder parse(BufferedReader reader) throws 
IOException {
 
     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 boolean isBookieHostCreatedFromIp() throws 
IOException {
      * 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 eab9e4f186..a2a3e7c62a 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 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 void writeCookie(String bookieId,
         } 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 0000000000..fe2c5318df
--- /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 0000000000..430fed9dce
--- /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 0000000000..4c42615e8b
--- /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 0000000000..daa3c435aa
--- /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 0000000000..76f5f7c009
--- /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 0000000000..77e5f05857
--- /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 0000000000..248c49ad7a
--- /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 0000000000..4010e607eb
--- /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 ae807dfb65..45cb40e43a 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 @@ protected boolean apply(ClientConfiguration conf,
             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 0000000000..3dded4e5cb
--- /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 44fc194f59..0147eca5e6 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 0000000000..fcfae0be77
--- /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 0000000000..01ad58d42b
--- /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 0000000000..6e2729450c
--- /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 0000000000..ef45a05d35
--- /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 0000000000..308cd2aee1
--- /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 0000000000..9597cdeef2
--- /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 0000000000..f6f66b6e2f
--- /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 bf591abf44..35b2cbd9b5 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.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 Boolean apply(BKFlags globalFlags, String[] args) {
         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;
 


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


With regards,
Apache Git Services

Reply via email to