org.apache.bookkeeper.bookie.BookieShell.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.bookkeeper.bookie.BookieShell.java

Source

/**
 * 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.bookie;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.bookkeeper.meta.MetadataDrivers.runFunctionWithLedgerManagerFactory;
import static org.apache.bookkeeper.meta.MetadataDrivers.runFunctionWithMetadataBookieDriver;
import static org.apache.bookkeeper.meta.MetadataDrivers.runFunctionWithRegistrationManager;
import static org.apache.bookkeeper.tools.cli.helpers.CommandHelpers.getBookieSocketAddrStringRepresentation;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.UncheckedExecutionException;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.concurrent.DefaultThreadFactory;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Formatter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.LongStream;
import org.apache.bookkeeper.bookie.BookieException.CookieNotFoundException;
import org.apache.bookkeeper.bookie.BookieException.InvalidCookieException;
import org.apache.bookkeeper.bookie.CheckpointSource.Checkpoint;
import org.apache.bookkeeper.bookie.EntryLogger.EntryLogScanner;
import org.apache.bookkeeper.bookie.Journal.JournalScanner;
import org.apache.bookkeeper.bookie.storage.ldb.DbLedgerStorage;
import org.apache.bookkeeper.bookie.storage.ldb.LocationsIndexRebuildOp;
import org.apache.bookkeeper.client.BKException;
import org.apache.bookkeeper.client.BKException.MetaStoreException;
import org.apache.bookkeeper.client.BookKeeper;
import org.apache.bookkeeper.client.BookKeeper.DigestType;
import org.apache.bookkeeper.client.BookKeeperAdmin;
import org.apache.bookkeeper.client.LedgerEntry;
import org.apache.bookkeeper.client.LedgerHandle;
import org.apache.bookkeeper.client.UpdateLedgerOp;
import org.apache.bookkeeper.client.api.LedgerMetadata;
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;
import org.apache.bookkeeper.discover.RegistrationManager;
import org.apache.bookkeeper.meta.LedgerManager;
import org.apache.bookkeeper.meta.LedgerMetadataSerDe;
import org.apache.bookkeeper.meta.LedgerUnderreplicationManager;
import org.apache.bookkeeper.meta.UnderreplicatedLedger;
import org.apache.bookkeeper.meta.zk.ZKMetadataDriverBase;
import org.apache.bookkeeper.net.BookieSocketAddress;
import org.apache.bookkeeper.proto.BookieClient;
import org.apache.bookkeeper.proto.BookieClientImpl;
import org.apache.bookkeeper.proto.BookieProtocol;
import org.apache.bookkeeper.proto.BookkeeperInternalCallbacks.Processor;
import org.apache.bookkeeper.replication.AuditorElector;
import org.apache.bookkeeper.replication.ReplicationException;
import org.apache.bookkeeper.replication.ReplicationException.CompatibilityException;
import org.apache.bookkeeper.replication.ReplicationException.UnavailableException;
import org.apache.bookkeeper.stats.NullStatsLogger;
import org.apache.bookkeeper.tools.cli.commands.bookie.FormatCommand;
import org.apache.bookkeeper.tools.cli.commands.bookie.InitCommand;
import org.apache.bookkeeper.tools.cli.commands.bookie.LastMarkCommand;
import org.apache.bookkeeper.tools.cli.commands.bookies.InfoCommand;
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;
import org.apache.bookkeeper.util.EntryFormatter;
import org.apache.bookkeeper.util.IOUtils;
import org.apache.bookkeeper.util.LedgerIdFormatter;
import org.apache.bookkeeper.util.MathUtils;
import org.apache.bookkeeper.util.Tool;
import org.apache.bookkeeper.versioning.Version;
import org.apache.bookkeeper.versioning.Versioned;
import org.apache.bookkeeper.zookeeper.ZooKeeperClient;
import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.MissingArgumentException;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.configuration.CompositeConfiguration;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.mutable.MutableBoolean;
import org.apache.commons.lang.mutable.MutableLong;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.zookeeper.AsyncCallback;
import org.apache.zookeeper.AsyncCallback.VoidCallback;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Bookie Shell is to provide utilities for users to administer a bookkeeper cluster.
 */
public class BookieShell implements Tool {

    static final Logger LOG = LoggerFactory.getLogger(BookieShell.class);

    static final String CONF_OPT = "conf";
    static final String ENTRY_FORMATTER_OPT = "entryformat";
    static final String LEDGERID_FORMATTER_OPT = "ledgeridformat";

    static final String CMD_METAFORMAT = "metaformat";
    static final String CMD_INITBOOKIE = "initbookie";
    static final String CMD_INITNEWCLUSTER = "initnewcluster";
    static final String CMD_NUKEEXISTINGCLUSTER = "nukeexistingcluster";
    static final String CMD_BOOKIEFORMAT = "bookieformat";
    static final String CMD_RECOVER = "recover";
    static final String CMD_LEDGER = "ledger";
    static final String CMD_READ_LEDGER_ENTRIES = "readledger";
    static final String CMD_LISTLEDGERS = "listledgers";
    static final String CMD_LEDGERMETADATA = "ledgermetadata";
    static final String CMD_LISTUNDERREPLICATED = "listunderreplicated";
    static final String CMD_WHOISAUDITOR = "whoisauditor";
    static final String CMD_WHATISINSTANCEID = "whatisinstanceid";
    static final String CMD_SIMPLETEST = "simpletest";
    static final String CMD_BOOKIESANITYTEST = "bookiesanity";
    static final String CMD_READLOG = "readlog";
    static final String CMD_READLOGMETADATA = "readlogmetadata";
    static final String CMD_READJOURNAL = "readjournal";
    static final String CMD_LASTMARK = "lastmark";
    static final String CMD_AUTORECOVERY = "autorecovery";
    static final String CMD_LISTBOOKIES = "listbookies";
    static final String CMD_LISTFILESONDISC = "listfilesondisc";
    static final String CMD_UPDATECOOKIE = "updatecookie";
    static final String CMD_UPDATELEDGER = "updateledgers";
    static final String CMD_DELETELEDGER = "deleteledger";
    static final String CMD_BOOKIEINFO = "bookieinfo";
    static final String CMD_DECOMMISSIONBOOKIE = "decommissionbookie";
    static final String CMD_LOSTBOOKIERECOVERYDELAY = "lostbookierecoverydelay";
    static final String CMD_TRIGGERAUDIT = "triggeraudit";
    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";
    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";
    static final String CMD_LOCALCONSISTENCYCHECK = "localconsistencycheck";

    final ServerConfiguration bkConf = new ServerConfiguration();
    File[] indexDirectories;
    File[] ledgerDirectories;
    File[] journalDirectories;

    EntryLogger entryLogger = null;
    List<Journal> journals = null;
    EntryFormatter entryFormatter;
    LedgerIdFormatter ledgerIdFormatter;

    int pageSize;
    int entriesPerPage;

    public BookieShell() {
    }

    public BookieShell(LedgerIdFormatter ledgeridFormatter, EntryFormatter entryFormatter) {
        this.ledgerIdFormatter = ledgeridFormatter;
        this.entryFormatter = entryFormatter;
    }

    /**
     * BookieShell command.
     */
    @Private
    public interface Command {
        int runCmd(String[] args) throws Exception;

        String description();

        void printUsage();
    }

    void printInfoLine(String s) {
        System.out.println(s);
    }

    void printErrorLine(String s) {
        System.err.println(s);
    }

    abstract class MyCommand implements Command {
        abstract Options getOptions();

        abstract String getDescription();

        abstract String getUsage();

        abstract int runCmd(CommandLine cmdLine) throws Exception;

        String cmdName;

        MyCommand(String cmdName) {
            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 {
                BasicParser parser = new BasicParser();
                CommandLine cmdLine = parser.parse(getOptions(), args);
                return runCmd(cmdLine);
            } catch (ParseException e) {
                LOG.error("Error parsing command line arguments : ", e);
                printUsage();
                return -1;
            }
        }

        @Override
        public void printUsage() {
            HelpFormatter hf = new HelpFormatter();
            System.err.println(cmdName + ": " + getDescription());
            hf.printHelp(getUsage(), getOptions());
        }
    }

    /**
     * Format the bookkeeper metadata present in zookeeper.
     */
    class MetaFormatCmd extends MyCommand {
        Options opts = new Options();

        MetaFormatCmd() {
            super(CMD_METAFORMAT);
            opts.addOption("n", "nonInteractive", false, "Whether to confirm if old data exists..?");
            opts.addOption("f", "force", false, "If [nonInteractive] is specified, then whether"
                    + " to force delete the old data without prompt.");
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Format bookkeeper metadata in zookeeper.";
        }

        @Override
        String getUsage() {
            return "metaformat   [-nonInteractive] [-force]";
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            boolean interactive = (!cmdLine.hasOption("n"));
            boolean force = cmdLine.hasOption("f");

            boolean result = BookKeeperAdmin.format(bkConf, interactive, force);
            return (result) ? 0 : 1;
        }
    }

    /**
     * Intializes new cluster by creating required znodes for the cluster. If
     * ledgersrootpath is already existing then it will error out. If for any
     * reason it errors out while creating znodes for the cluster, then before
     * running initnewcluster again, try nuking existing cluster by running
     * nukeexistingcluster. This is required because ledgersrootpath znode would
     * be created after verifying that it doesn't exist, hence during next retry
     * of initnewcluster it would complain saying that ledgersrootpath is
     * already existing.
     */
    class InitNewCluster extends MyCommand {
        Options opts = new Options();

        InitNewCluster() {
            super(CMD_INITNEWCLUSTER);
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Initializes a new bookkeeper cluster. If initnewcluster fails then try nuking "
                    + "existing cluster by running nukeexistingcluster before running initnewcluster again";
        }

        @Override
        String getUsage() {
            return "initnewcluster";
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            boolean result = BookKeeperAdmin.initNewCluster(bkConf);
            return (result) ? 0 : 1;
        }
    }

    /**
     * Nuke bookkeeper metadata of existing cluster in zookeeper.
     */
    class NukeExistingCluster extends MyCommand {
        Options opts = new Options();

        NukeExistingCluster() {
            super(CMD_NUKEEXISTINGCLUSTER);
            opts.addOption("p", "zkledgersrootpath", true, "zookeeper ledgers rootpath");
            opts.addOption("i", "instanceid", true, "instanceid");
            opts.addOption("f", "force", false, "If instanceid is not specified, "
                    + "then whether to force nuke the metadata without validating instanceid");
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Nuke bookkeeper cluster by deleting metadata";
        }

        @Override
        String getUsage() {
            return "nukeexistingcluster -zkledgersrootpath <zkledgersrootpath> [-instanceid <instanceid> | -force]";
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            boolean force = cmdLine.hasOption("f");
            String zkledgersrootpath = cmdLine.getOptionValue("zkledgersrootpath");
            String instanceid = cmdLine.getOptionValue("instanceid");

            /*
             * for NukeExistingCluster command 'zkledgersrootpath' should be provided and either force option or
             * instanceid should be provided.
             */
            if ((zkledgersrootpath == null) || (force == (instanceid != null))) {
                LOG.error("zkledgersrootpath should be specified and either force option "
                        + "or instanceid should be specified (but not both)");
                printUsage();
                return -1;
            }

            boolean result = BookKeeperAdmin.nukeExistingCluster(bkConf, zkledgersrootpath, instanceid, force);
            return (result) ? 0 : 1;
        }
    }

    /**
     * Formats the local data present in current bookie server.
     */
    class BookieFormatCmd extends MyCommand {
        Options opts = new Options();

        public BookieFormatCmd() {
            super(CMD_BOOKIEFORMAT);
            opts.addOption("n", "nonInteractive", false, "Whether to confirm if old data exists..?");
            opts.addOption("f", "force", false, "If [nonInteractive] is specified, then whether"
                    + " to force delete the old data without prompt..?");
            opts.addOption("d", "deleteCookie", false, "Delete its cookie on metadata store");
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Format the current server contents.";
        }

        @Override
        String getUsage() {
            return "bookieformat [-nonInteractive] [-force] [-deleteCookie]";
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            boolean interactive = (!cmdLine.hasOption("n"));
            boolean force = cmdLine.hasOption("f");
            boolean deletecookie = cmdLine.hasOption("d");

            FormatCommand.Flags flags = new FormatCommand.Flags().nonInteractive(interactive).force(force)
                    .deleteCookie(deletecookie);
            FormatCommand command = new FormatCommand(flags);
            boolean result = command.apply(bkConf, flags);
            return (result) ? 0 : 1;
        }
    }

    /**
     * Initializes bookie, by making sure that the journalDir, ledgerDirs and
     * indexDirs are empty and there is no registered Bookie with this BookieId.
     */
    class InitBookieCmd extends MyCommand {
        Options opts = new Options();

        public InitBookieCmd() {
            super(CMD_INITBOOKIE);
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Initialize new Bookie";
        }

        @Override
        String getUsage() {
            return CMD_INITBOOKIE;
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            ServerConfiguration conf = new ServerConfiguration(bkConf);
            InitCommand initCommand = new InitCommand();
            boolean result = initCommand.apply(conf, new CliFlags());
            return (result) ? 0 : 1;
        }
    }

    /**
     * Recover command for ledger data recovery for failed bookie.
     */
    class RecoverCmd extends MyCommand {
        Options opts = new Options();

        public RecoverCmd() {
            super(CMD_RECOVER);
            opts.addOption("q", "query", false, "Query the ledgers that contain given bookies");
            opts.addOption("dr", "dryrun", false, "Printing the recovery plan w/o doing actual recovery");
            opts.addOption("f", "force", false, "Force recovery without confirmation");
            opts.addOption("l", "ledger", true, "Recover a specific ledger");
            opts.addOption("sk", "skipOpenLedgers", false, "Skip recovering open ledgers");
            opts.addOption("d", "deleteCookie", false, "Delete cookie node for the bookie.");
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Recover the ledger data for failed bookie.";
        }

        @Override
        String getUsage() {
            return "recover [-deleteCookie] <bookieSrc[:bookieSrc]>";
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            String[] args = cmdLine.getArgs();
            if (args.length < 1) {
                throw new MissingArgumentException("'bookieSrc' argument required");
            }
            if (args.length > 1) {
                System.err.println("The provided bookie dest " + args[1] + " will be ignored!");
            }
            boolean query = cmdLine.hasOption("q");
            boolean dryrun = cmdLine.hasOption("dr");
            boolean force = cmdLine.hasOption("f");
            boolean skipOpenLedgers = cmdLine.hasOption("sk");
            boolean removeCookies = !dryrun && cmdLine.hasOption("d");

            Long ledgerId = getOptionLedgerIdValue(cmdLine, "ledger", -1);

            // Get bookies list
            final String[] bookieStrs = args[0].split(",");
            final Set<BookieSocketAddress> bookieAddrs = new HashSet<>();
            for (String bookieStr : bookieStrs) {
                final String bookieStrParts[] = bookieStr.split(":");
                if (bookieStrParts.length != 2) {
                    System.err.println(
                            "BookieSrcs has invalid bookie address format (host:port expected) : " + bookieStr);
                    return -1;
                }
                bookieAddrs.add(new BookieSocketAddress(bookieStrParts[0], Integer.parseInt(bookieStrParts[1])));
            }

            if (!force) {
                System.err.println("Bookies : " + bookieAddrs);
                if (!IOUtils.confirmPrompt("Are you sure to recover them : (Y/N)")) {
                    System.err.println("Give up!");
                    return -1;
                }
            }

            LOG.info("Constructing admin");
            ClientConfiguration adminConf = new ClientConfiguration(bkConf);
            BookKeeperAdmin admin = new BookKeeperAdmin(adminConf);
            LOG.info("Construct admin : {}", admin);
            try {
                if (query) {
                    return bkQuery(admin, bookieAddrs);
                }
                if (-1 != ledgerId) {
                    return bkRecoveryLedger(admin, ledgerId, bookieAddrs, dryrun, skipOpenLedgers, removeCookies);
                }
                return bkRecovery(admin, bookieAddrs, dryrun, skipOpenLedgers, removeCookies);
            } finally {
                admin.close();
            }
        }

        private int bkQuery(BookKeeperAdmin bkAdmin, Set<BookieSocketAddress> bookieAddrs)
                throws InterruptedException, BKException {
            SortedMap<Long, LedgerMetadata> ledgersContainBookies = bkAdmin.getLedgersContainBookies(bookieAddrs);
            System.err.println("NOTE: Bookies in inspection list are marked with '*'.");
            for (Map.Entry<Long, LedgerMetadata> ledger : ledgersContainBookies.entrySet()) {
                System.out.println("ledger " + ledger.getKey() + " : " + ledger.getValue().getState());
                Map<Long, Integer> numBookiesToReplacePerEnsemble = inspectLedger(ledger.getValue(), bookieAddrs);
                System.out.print("summary: [");
                for (Map.Entry<Long, Integer> entry : numBookiesToReplacePerEnsemble.entrySet()) {
                    System.out.print(entry.getKey() + "=" + entry.getValue() + ", ");
                }
                System.out.println("]");
                System.out.println();
            }
            System.err.println("Done");
            return 0;
        }

        private Map<Long, Integer> inspectLedger(LedgerMetadata metadata,
                Set<BookieSocketAddress> bookiesToInspect) {
            Map<Long, Integer> numBookiesToReplacePerEnsemble = new TreeMap<Long, Integer>();
            for (Map.Entry<Long, ? extends List<BookieSocketAddress>> ensemble : metadata.getAllEnsembles()
                    .entrySet()) {
                List<BookieSocketAddress> bookieList = ensemble.getValue();
                System.out.print(ensemble.getKey() + ":\t");
                int numBookiesToReplace = 0;
                for (BookieSocketAddress bookie : bookieList) {
                    System.out.print(bookie);
                    if (bookiesToInspect.contains(bookie)) {
                        System.out.print("*");
                        ++numBookiesToReplace;
                    } else {
                        System.out.print(" ");
                    }
                    System.out.print(" ");
                }
                System.out.println();
                numBookiesToReplacePerEnsemble.put(ensemble.getKey(), numBookiesToReplace);
            }
            return numBookiesToReplacePerEnsemble;
        }

        private int bkRecoveryLedger(BookKeeperAdmin bkAdmin, long lid, Set<BookieSocketAddress> bookieAddrs,
                boolean dryrun, boolean skipOpenLedgers, boolean removeCookies)
                throws InterruptedException, BKException, KeeperException {
            bkAdmin.recoverBookieData(lid, bookieAddrs, dryrun, skipOpenLedgers);
            if (removeCookies) {
                deleteCookies(bkAdmin.getConf(), bookieAddrs);
            }
            return 0;
        }

        private int bkRecovery(BookKeeperAdmin bkAdmin, Set<BookieSocketAddress> bookieAddrs, boolean dryrun,
                boolean skipOpenLedgers, boolean removeCookies)
                throws InterruptedException, BKException, KeeperException {
            bkAdmin.recoverBookieData(bookieAddrs, dryrun, skipOpenLedgers);
            if (removeCookies) {
                deleteCookies(bkAdmin.getConf(), bookieAddrs);
            }
            return 0;
        }

        private void deleteCookies(ClientConfiguration conf, Set<BookieSocketAddress> bookieAddrs)
                throws BKException {
            ServerConfiguration serverConf = new ServerConfiguration(conf);
            try {
                runFunctionWithRegistrationManager(serverConf, rm -> {
                    try {
                        for (BookieSocketAddress addr : bookieAddrs) {
                            deleteCookie(rm, addr);
                        }
                    } catch (Exception e) {
                        throw new UncheckedExecutionException(e);
                    }
                    return null;
                });
            } catch (Exception e) {
                Throwable cause = e;
                if (e instanceof UncheckedExecutionException) {
                    cause = e.getCause();
                }
                if (cause instanceof BKException) {
                    throw (BKException) cause;
                } else {
                    BKException bke = new MetaStoreException();
                    bke.initCause(bke);
                    throw bke;
                }
            }
        }

        private void deleteCookie(RegistrationManager rm, BookieSocketAddress bookieSrc) throws BookieException {
            try {
                Versioned<Cookie> cookie = Cookie.readFromRegistrationManager(rm, bookieSrc);
                cookie.getValue().deleteFromRegistrationManager(rm, bookieSrc, cookie.getVersion());
            } catch (CookieNotFoundException nne) {
                LOG.warn("No cookie to remove for {} : ", bookieSrc, nne);
            }
        }

    }

    /**
     * Ledger Command Handles ledger related operations.
     */
    class LedgerCmd extends MyCommand {
        Options lOpts = new Options();

        LedgerCmd() {
            super(CMD_LEDGER);
            lOpts.addOption("m", "meta", false, "Print meta information");
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {
            String[] leftArgs = cmdLine.getArgs();
            if (leftArgs.length <= 0) {
                printErrorLine("ERROR: missing ledger id");
                printUsage();
                return -1;
            }

            boolean printMeta = false;
            if (cmdLine.hasOption("m")) {
                printMeta = true;
            }
            long ledgerId;
            try {
                ledgerId = ledgerIdFormatter.readLedgerId(leftArgs[0]);
            } catch (IllegalArgumentException iae) {
                printErrorLine("ERROR: invalid ledger id " + leftArgs[0]);
                printUsage();
                return -1;
            }

            if (bkConf.getLedgerStorageClass().equals(DbLedgerStorage.class.getName())) {
                // dump ledger info
                try {
                    DbLedgerStorage.readLedgerIndexEntries(ledgerId, bkConf,
                            (currentEntry, entryLogId, position) -> printInfoLine("entry " + currentEntry
                                    + "\t:\t(log: " + entryLogId + ", pos: " + position + ")"));
                } catch (IOException e) {
                    System.err.printf("ERROR: initializing dbLedgerStorage %s", e.getMessage());
                    return -1;
                }
            } else if ((bkConf.getLedgerStorageClass().equals(SortedLedgerStorage.class.getName())
                    || bkConf.getLedgerStorageClass().equals(InterleavedLedgerStorage.class.getName()))) {
                ServerConfiguration conf = new ServerConfiguration(bkConf);
                InterleavedLedgerStorage interleavedStorage = new InterleavedLedgerStorage();
                Bookie.mountLedgerStorageOffline(conf, interleavedStorage);

                if (printMeta) {
                    // print meta
                    printInfoLine("===== LEDGER: " + ledgerIdFormatter.formatLedgerId(ledgerId) + " =====");
                    LedgerCache.LedgerIndexMetadata meta = interleavedStorage.readLedgerIndexMetadata(ledgerId);
                    printInfoLine("master key  : " + meta.getMasterKeyHex());

                    long size = meta.size;
                    if (size % 8 == 0) {
                        printInfoLine("size        : " + size);
                    } else {
                        printInfoLine(
                                "size : " + size + " (not aligned with 8, may be corrupted or under flushing now)");
                    }

                    printInfoLine("entries     : " + (size / 8));
                    printInfoLine("isFenced    : " + meta.fenced);
                }

                try {
                    // dump ledger info
                    printInfoLine("===== LEDGER: " + ledgerIdFormatter.formatLedgerId(ledgerId) + " =====");
                    for (LedgerCache.PageEntries page : interleavedStorage.getIndexEntries(ledgerId)) {
                        final MutableLong curEntry = new MutableLong(page.getFirstEntry());
                        try (LedgerEntryPage lep = page.getLEP()) {
                            lep.getEntries((entry, offset) -> {
                                while (curEntry.longValue() < entry) {
                                    printInfoLine("entry " + curEntry + "\t:\tN/A");
                                    curEntry.increment();
                                }
                                long entryLogId = offset >> 32L;
                                long pos = offset & 0xffffffffL;
                                printInfoLine(
                                        "entry " + curEntry + "\t:\t(log:" + entryLogId + ", pos: " + pos + ")");
                                curEntry.increment();
                                return true;
                            });
                        } catch (IOException ie) {
                            printInfoLine("Failed to read index page @ " + page.getFirstEntry()
                                    + ", the index file may be corrupted : " + ie.getMessage());
                            return 1;
                        }

                        while (curEntry.longValue() < page.getLastEntry()) {
                            printInfoLine("entry " + curEntry + "\t:\tN/A");
                            curEntry.increment();
                        }
                    }
                } catch (IOException ie) {
                    LOG.error("Failed to read index page");
                    return 1;
                }
            }

            return 0;
        }

        @Override
        String getDescription() {
            return "Dump ledger index entries into readable format.";
        }

        @Override
        String getUsage() {
            return "ledger       [-m] <ledger_id>";
        }

        @Override
        Options getOptions() {
            return lOpts;
        }
    }

    /**
     * Command for reading ledger entries.
     */
    class ReadLedgerEntriesCmd extends MyCommand {
        Options lOpts = new Options();

        ReadLedgerEntriesCmd() {
            super(CMD_READ_LEDGER_ENTRIES);
            lOpts.addOption("m", "msg", false, "Print message body");
            lOpts.addOption("l", "ledgerid", true, "Ledger ID");
            lOpts.addOption("fe", "firstentryid", true, "First EntryID");
            lOpts.addOption("le", "lastentryid", true, "Last EntryID");
            lOpts.addOption("r", "force-recovery", false, "Ensure the ledger is properly closed before reading");
            lOpts.addOption("b", "bookie", true, "Only read from a specific bookie");
        }

        @Override
        Options getOptions() {
            return lOpts;
        }

        @Override
        String getDescription() {
            return "Read a range of entries from a ledger.";
        }

        @Override
        String getUsage() {
            return "readledger  [-bookie <address:port>]  [-msg] -ledgerid <ledgerid> "
                    + "[-firstentryid <firstentryid> [-lastentryid <lastentryid>]] " + "[-force-recovery]";
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            final long ledgerId = getOptionLedgerIdValue(cmdLine, "ledgerid", -1);
            if (ledgerId == -1) {
                System.err.println("Must specify a ledger id");
                return -1;
            }

            final long firstEntry = getOptionLongValue(cmdLine, "firstentryid", 0);
            long lastEntry = getOptionLongValue(cmdLine, "lastentryid", -1);

            boolean printMsg = cmdLine.hasOption("m");
            boolean forceRecovery = cmdLine.hasOption("r");
            final BookieSocketAddress bookie;
            if (cmdLine.hasOption("b")) {
                // A particular bookie was specified
                bookie = new BookieSocketAddress(cmdLine.getOptionValue("b"));
            } else {
                bookie = null;
            }

            ClientConfiguration conf = new ClientConfiguration();
            conf.addConfiguration(bkConf);

            try (BookKeeperAdmin bk = new BookKeeperAdmin(conf)) {
                if (forceRecovery) {
                    // Force the opening of the ledger to trigger recovery
                    try (LedgerHandle lh = bk.openLedger(ledgerId)) {
                        if (lastEntry == -1 || lastEntry > lh.getLastAddConfirmed()) {
                            lastEntry = lh.getLastAddConfirmed();
                        }
                    }
                }

                if (bookie == null) {
                    // No bookie was specified, use normal bk client
                    Iterator<LedgerEntry> entries = bk.readEntries(ledgerId, firstEntry, lastEntry).iterator();
                    while (entries.hasNext()) {
                        LedgerEntry entry = entries.next();
                        formatEntry(entry, printMsg);
                    }
                } else {
                    // Use BookieClient to target a specific bookie
                    EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
                    OrderedExecutor executor = OrderedExecutor.newBuilder().numThreads(1)
                            .name("BookieClientScheduler").build();

                    ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(
                            new DefaultThreadFactory("BookKeeperClientSchedulerPool"));

                    BookieClient bookieClient = new BookieClientImpl(conf, eventLoopGroup,
                            UnpooledByteBufAllocator.DEFAULT, executor, scheduler, NullStatsLogger.INSTANCE);

                    LongStream.range(firstEntry, lastEntry).forEach(entryId -> {
                        CompletableFuture<Void> future = new CompletableFuture<>();

                        bookieClient.readEntry(bookie, ledgerId, entryId,
                                (rc, ledgerId1, entryId1, buffer, ctx) -> {
                                    if (rc != BKException.Code.OK) {
                                        LOG.error("Failed to read entry {} -- {}", entryId1,
                                                BKException.getMessage(rc));
                                        future.completeExceptionally(BKException.create(rc));
                                        return;
                                    }

                                    System.out.println("--------- Lid=" + ledgerIdFormatter.formatLedgerId(ledgerId)
                                            + ", Eid=" + entryId + " ---------");
                                    if (printMsg) {
                                        System.out.println("Data: " + ByteBufUtil.prettyHexDump(buffer));
                                    }

                                    future.complete(null);
                                }, null, BookieProtocol.FLAG_NONE);

                        try {
                            future.get();
                        } catch (Exception e) {
                            LOG.error("Error future.get while reading entries from ledger {}", ledgerId, e);
                        }
                    });

                    eventLoopGroup.shutdownGracefully();
                    executor.shutdown();
                    bookieClient.close();
                }
            }

            return 0;
        }

    }

    /**
     * Command for listing underreplicated ledgers.
     */
    class ListUnderreplicatedCmd extends MyCommand {
        Options opts = new Options();

        public ListUnderreplicatedCmd() {
            super(CMD_LISTUNDERREPLICATED);
            opts.addOption("missingreplica", true, "Bookie Id of missing replica");
            opts.addOption("excludingmissingreplica", true, "Bookie Id of missing replica to ignore");
            opts.addOption("printmissingreplica", false, "Whether to print missingreplicas list?");
            opts.addOption("printreplicationworkerid", false, "Whether to print replicationworkerid?");
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "List ledgers marked as underreplicated, with optional options to specify missingreplica"
                    + " (BookieId) and to exclude missingreplica.";
        }

        @Override
        String getUsage() {
            return "listunderreplicated [[-missingreplica <bookieaddress>]"
                    + " [-excludingmissingreplica <bookieaddress>]] [-printmissingreplica] [-printreplicationworkerid]";
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {

            final String includingBookieId = cmdLine.getOptionValue("missingreplica");
            final String excludingBookieId = cmdLine.getOptionValue("excludingmissingreplica");
            final boolean printMissingReplica = cmdLine.hasOption("printmissingreplica");
            final boolean printReplicationWorkerId = cmdLine.hasOption("printreplicationworkerid");

            final Predicate<List<String>> predicate;
            if (!StringUtils.isBlank(includingBookieId) && !StringUtils.isBlank(excludingBookieId)) {
                predicate = replicasList -> (replicasList.contains(includingBookieId)
                        && !replicasList.contains(excludingBookieId));
            } else if (!StringUtils.isBlank(includingBookieId)) {
                predicate = replicasList -> replicasList.contains(includingBookieId);
            } else if (!StringUtils.isBlank(excludingBookieId)) {
                predicate = replicasList -> !replicasList.contains(excludingBookieId);
            } else {
                predicate = null;
            }

            runFunctionWithLedgerManagerFactory(bkConf, mFactory -> {
                LedgerUnderreplicationManager underreplicationManager;
                try {
                    underreplicationManager = mFactory.newLedgerUnderreplicationManager();
                } catch (KeeperException | CompatibilityException e) {
                    throw new UncheckedExecutionException("Failed to new ledger underreplicated manager", e);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new UncheckedExecutionException("Interrupted on newing ledger underreplicated manager",
                            e);
                }
                Iterator<UnderreplicatedLedger> iter = underreplicationManager.listLedgersToRereplicate(predicate);
                while (iter.hasNext()) {
                    UnderreplicatedLedger underreplicatedLedger = iter.next();
                    long urLedgerId = underreplicatedLedger.getLedgerId();
                    System.out.println(ledgerIdFormatter.formatLedgerId(urLedgerId));
                    long ctime = underreplicatedLedger.getCtime();
                    if (ctime != UnderreplicatedLedger.UNASSIGNED_CTIME) {
                        System.out.println("\tCtime : " + ctime);
                    }
                    if (printMissingReplica) {
                        underreplicatedLedger.getReplicaList().forEach((missingReplica) -> {
                            System.out.println("\tMissingReplica : " + missingReplica);
                        });
                    }
                    if (printReplicationWorkerId) {
                        try {
                            String replicationWorkerId = underreplicationManager
                                    .getReplicationWorkerIdRereplicatingLedger(urLedgerId);
                            if (replicationWorkerId != null) {
                                System.out.println("\tReplicationWorkerId : " + replicationWorkerId);
                            }
                        } catch (UnavailableException e) {
                            LOG.error("Failed to get ReplicationWorkerId rereplicating ledger {} -- {}", urLedgerId,
                                    e.getMessage());
                        }
                    }
                }
                return null;
            });

            return 0;
        }
    }

    static final int LIST_BATCH_SIZE = 1000;

    /**
     * Command to list all ledgers in the cluster.
     */
    class ListLedgersCmd extends MyCommand {
        Options lOpts = new Options();

        ListLedgersCmd() {
            super(CMD_LISTLEDGERS);
            lOpts.addOption("m", "meta", false, "Print metadata");
            lOpts.addOption("bookieid", true, "List ledgers residing in this bookie");
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {
            final boolean printMeta = cmdLine.hasOption("m");
            final String bookieidToBePartOfEnsemble = cmdLine.getOptionValue("bookieid");
            final BookieSocketAddress bookieAddress = StringUtils.isBlank(bookieidToBePartOfEnsemble) ? null
                    : new BookieSocketAddress(bookieidToBePartOfEnsemble);

            runFunctionWithLedgerManagerFactory(bkConf, mFactory -> {
                try (LedgerManager ledgerManager = mFactory.newLedgerManager()) {

                    final AtomicInteger returnCode = new AtomicInteger(BKException.Code.OK);
                    final CountDownLatch processDone = new CountDownLatch(1);

                    Processor<Long> ledgerProcessor = new Processor<Long>() {
                        @Override
                        public void process(Long ledgerId, VoidCallback cb) {
                            if (!printMeta && (bookieAddress == null)) {
                                printLedgerMetadata(ledgerId, null, false);
                                cb.processResult(BKException.Code.OK, null, null);
                            } else {
                                ledgerManager.readLedgerMetadata(ledgerId).whenComplete((metadata, exception) -> {
                                    if (exception == null) {
                                        if ((bookieAddress == null)
                                                || BookKeeperAdmin.areEntriesOfLedgerStoredInTheBookie(ledgerId,
                                                        bookieAddress, metadata.getValue())) {
                                            /*
                                             * the print method has to be in
                                             * synchronized scope, otherwise
                                             * output of printLedgerMetadata
                                             * could interleave since this
                                             * callback for different
                                             * ledgers can happen in
                                             * different threads.
                                             */
                                            synchronized (BookieShell.this) {
                                                printLedgerMetadata(ledgerId, metadata.getValue(), printMeta);
                                            }
                                        }
                                        cb.processResult(BKException.Code.OK, null, null);
                                    } else if (BKException.getExceptionCode(
                                            exception) == BKException.Code.NoSuchLedgerExistsException) {
                                        cb.processResult(BKException.Code.OK, null, null);
                                    } else {
                                        LOG.error("Unable to read the ledger: {} information", ledgerId);
                                        cb.processResult(BKException.getExceptionCode(exception), null, null);
                                    }
                                });
                            }
                        }
                    };

                    ledgerManager.asyncProcessLedgers(ledgerProcessor, new AsyncCallback.VoidCallback() {
                        @Override
                        public void processResult(int rc, String s, Object obj) {
                            returnCode.set(rc);
                            processDone.countDown();
                        }
                    }, null, BKException.Code.OK, BKException.Code.ReadException);

                    processDone.await();
                    if (returnCode.get() != BKException.Code.OK) {
                        LOG.error("Received error return value while processing ledgers: {}", returnCode.get());
                        throw BKException.create(returnCode.get());
                    }

                } catch (Exception ioe) {
                    LOG.error("Received Exception while processing ledgers", ioe);
                    throw new UncheckedExecutionException(ioe);
                }
                return null;
            });

            return 0;
        }

        @Override
        String getDescription() {
            return "List all ledgers on the cluster (this may take a long time).";
        }

        @Override
        String getUsage() {
            return "listledgers  [-meta] [-bookieid <bookieaddress>]";
        }

        @Override
        Options getOptions() {
            return lOpts;
        }
    }

    void printLedgerMetadata(long ledgerId, LedgerMetadata md, boolean printMeta) {
        System.out.println("ledgerID: " + ledgerIdFormatter.formatLedgerId(ledgerId));
        if (printMeta) {
            System.out.println(md.toString());
        }
    }

    /**
     * Print the metadata for a ledger.
     */
    class LedgerMetadataCmd extends MyCommand {
        Options lOpts = new Options();
        LedgerMetadataSerDe serDe = new LedgerMetadataSerDe();

        LedgerMetadataCmd() {
            super(CMD_LEDGERMETADATA);
            lOpts.addOption("l", "ledgerid", true, "Ledger ID");
            lOpts.addOption("dumptofile", true, "Dump metadata for ledger, to a file");
            lOpts.addOption("restorefromfile", true, "Restore metadata for ledger, from a file");
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {
            final long lid = getOptionLedgerIdValue(cmdLine, "ledgerid", -1);
            if (lid == -1) {
                System.err.println("Must specify a ledger id");
                return -1;
            }

            if (cmdLine.hasOption("dumptofile") && cmdLine.hasOption("restorefromfile")) {
                System.err.println("Only one of --dumptofile and --restorefromfile can be specified");
                return -2;
            }
            runFunctionWithLedgerManagerFactory(bkConf, mFactory -> {
                try (LedgerManager m = mFactory.newLedgerManager()) {
                    if (cmdLine.hasOption("dumptofile")) {
                        Versioned<LedgerMetadata> md = m.readLedgerMetadata(lid).join();
                        Files.write(FileSystems.getDefault().getPath(cmdLine.getOptionValue("dumptofile")),
                                serDe.serialize(md.getValue()));
                    } else if (cmdLine.hasOption("restorefromfile")) {
                        byte[] serialized = Files.readAllBytes(
                                FileSystems.getDefault().getPath(cmdLine.getOptionValue("restorefromfile")));
                        LedgerMetadata md = serDe.parseConfig(serialized, Optional.empty());
                        m.createLedgerMetadata(lid, md).join();
                    } else {
                        printLedgerMetadata(lid, m.readLedgerMetadata(lid).get().getValue(), true);
                    }
                } catch (Exception e) {
                    throw new UncheckedExecutionException(e);
                }
                return null;
            });

            return 0;
        }

        @Override
        String getDescription() {
            return "Print the metadata for a ledger, or optionally dump to a file.";
        }

        @Override
        String getUsage() {
            return "ledgermetadata -ledgerid <ledgerid> [--dump-to-file FILENAME|--restore-from-file FILENAME]";
        }

        @Override
        Options getOptions() {
            return lOpts;
        }
    }

    /**
     * Check local storage for inconsistencies.
     */
    class LocalConsistencyCheck extends MyCommand {
        Options lOpts = new Options();

        LocalConsistencyCheck() {
            super(CMD_LOCALCONSISTENCYCHECK);
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {
            LOG.info("=== Performing local consistency check ===");
            ServerConfiguration conf = new ServerConfiguration(bkConf);
            LedgerStorage ledgerStorage = Bookie.mountLedgerStorageOffline(conf, null);
            List<LedgerStorage.DetectedInconsistency> errors = ledgerStorage
                    .localConsistencyCheck(java.util.Optional.empty());
            if (errors.size() > 0) {
                LOG.info("=== Check returned errors: ===");
                for (LedgerStorage.DetectedInconsistency error : errors) {
                    LOG.error("Ledger {}, entry {}: ", error.getLedgerId(), error.getEntryId(),
                            error.getException());
                }
                return 1;
            } else {
                LOG.info("=== Check passed ===");
                return 0;
            }
        }

        @Override
        String getDescription() {
            return "Validate Ledger Storage internal metadata";
        }

        @Override
        String getUsage() {
            return "localconsistencycheck";
        }

        @Override
        Options getOptions() {
            return lOpts;
        }
    }

    /**
     * Simple test to create a ledger and write to it.
     */
    class SimpleTestCmd extends MyCommand {
        Options lOpts = new Options();

        SimpleTestCmd() {
            super(CMD_SIMPLETEST);
            lOpts.addOption("e", "ensemble", true, "Ensemble size (default 3)");
            lOpts.addOption("w", "writeQuorum", true, "Write quorum size (default 2)");
            lOpts.addOption("a", "ackQuorum", true, "Ack quorum size (default 2)");
            lOpts.addOption("n", "numEntries", true, "Entries to write (default 1000)");
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {
            int ensemble = getOptionIntValue(cmdLine, "ensemble", 3);
            int writeQuorum = getOptionIntValue(cmdLine, "writeQuorum", 2);
            int ackQuorum = getOptionIntValue(cmdLine, "ackQuorum", 2);
            int numEntries = getOptionIntValue(cmdLine, "numEntries", 1000);

            SimpleTestCommand.Flags flags = new SimpleTestCommand.Flags().ensembleSize(ensemble)
                    .writeQuorumSize(writeQuorum).ackQuorumSize(ackQuorum).numEntries(numEntries);

            SimpleTestCommand command = new SimpleTestCommand(flags);

            command.apply(bkConf, flags);
            return 0;
        }

        @Override
        String getDescription() {
            return "Simple test to create a ledger and write entries to it.";
        }

        @Override
        String getUsage() {
            return "simpletest   [-ensemble N] [-writeQuorum N] [-ackQuorum N] [-numEntries N]";
        }

        @Override
        Options getOptions() {
            return lOpts;
        }
    }

    /**
     * Command to run a bookie sanity test.
     */
    class BookieSanityTestCmd extends MyCommand {
        Options lOpts = new Options();

        BookieSanityTestCmd() {
            super(CMD_BOOKIESANITYTEST);
            lOpts.addOption("e", "entries", true, "Total entries to be added for the test (default 10)");
            lOpts.addOption("t", "timeout", true, "Timeout for write/read operations in seconds (default 1)");
        }

        @Override
        Options getOptions() {
            return lOpts;
        }

        @Override
        String getDescription() {
            return "Sanity test for local bookie. Create ledger and write/reads entries on local bookie.";
        }

        @Override
        String getUsage() {
            return "bookiesanity [-entries N] [-timeout N]";
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            int numberOfEntries = getOptionIntValue(cmdLine, "entries", 10);
            int timeoutSecs = getOptionIntValue(cmdLine, "timeout", 1);

            ClientConfiguration conf = new ClientConfiguration();
            conf.addConfiguration(bkConf);
            conf.setEnsemblePlacementPolicy(LocalBookieEnsemblePlacementPolicy.class);
            conf.setAddEntryTimeout(timeoutSecs);
            conf.setReadEntryTimeout(timeoutSecs);

            BookKeeper bk = new BookKeeper(conf);
            LedgerHandle lh = null;
            try {
                lh = bk.createLedger(1, 1, DigestType.MAC, new byte[0]);
                LOG.info("Created ledger {}", lh.getId());

                for (int i = 0; i < numberOfEntries; i++) {
                    String content = "entry-" + i;
                    lh.addEntry(content.getBytes(UTF_8));
                }

                LOG.info("Written {} entries in ledger {}", numberOfEntries, lh.getId());

                // Reopen the ledger and read entries
                lh = bk.openLedger(lh.getId(), DigestType.MAC, new byte[0]);
                if (lh.getLastAddConfirmed() != (numberOfEntries - 1)) {
                    throw new Exception("Invalid last entry found on ledger. expecting: " + (numberOfEntries - 1)
                            + " -- found: " + lh.getLastAddConfirmed());
                }

                Enumeration<LedgerEntry> entries = lh.readEntries(0, numberOfEntries - 1);
                int i = 0;
                while (entries.hasMoreElements()) {
                    LedgerEntry entry = entries.nextElement();
                    String actualMsg = new String(entry.getEntry(), UTF_8);
                    String expectedMsg = "entry-" + (i++);
                    if (!expectedMsg.equals(actualMsg)) {
                        throw new Exception("Failed validation of received message - Expected: " + expectedMsg
                                + ", Actual: " + actualMsg);
                    }
                }

                LOG.info("Read {} entries from ledger {}", entries, lh.getId());
            } catch (Exception e) {
                LOG.warn("Error in bookie sanity test", e);
                return -1;
            } finally {
                if (lh != null) {
                    bk.deleteLedger(lh.getId());
                    LOG.info("Deleted ledger {}", lh.getId());
                }

                bk.close();
            }

            LOG.info("Bookie sanity test succeeded");
            return 0;
        }
    }

    /**
     * Command to read entry log files.
     */
    class ReadLogCmd extends MyCommand {
        Options rlOpts = new Options();

        ReadLogCmd() {
            super(CMD_READLOG);
            rlOpts.addOption("m", "msg", false, "Print message body");
            rlOpts.addOption("l", "ledgerid", true, "Ledger ID");
            rlOpts.addOption("e", "entryid", true, "Entry ID");
            rlOpts.addOption("sp", "startpos", true, "Start Position");
            rlOpts.addOption("ep", "endpos", true, "End Position");
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {
            String[] leftArgs = cmdLine.getArgs();
            if (leftArgs.length <= 0) {
                System.err.println("ERROR: missing entry log id or entry log file name");
                printUsage();
                return -1;
            }

            boolean printMsg = false;
            if (cmdLine.hasOption("m")) {
                printMsg = true;
            }
            long logId;
            try {
                logId = Long.parseLong(leftArgs[0]);
            } catch (NumberFormatException nfe) {
                // not a entry log id
                File f = new File(leftArgs[0]);
                String name = f.getName();
                if (!name.endsWith(".log")) {
                    // not a log file
                    System.err.println("ERROR: invalid entry log file name " + leftArgs[0]);
                    printUsage();
                    return -1;
                }
                String idString = name.split("\\.")[0];
                logId = Long.parseLong(idString, 16);
            }

            final long lId = getOptionLedgerIdValue(cmdLine, "ledgerid", -1);
            final long eId = getOptionLongValue(cmdLine, "entryid", -1);
            final long startpos = getOptionLongValue(cmdLine, "startpos", -1);
            final long endpos = getOptionLongValue(cmdLine, "endpos", -1);

            // scan entry log
            if (startpos != -1) {
                if ((endpos != -1) && (endpos < startpos)) {
                    System.err.println(
                            "ERROR: StartPosition of the range should be lesser than or equal to EndPosition");
                    return -1;
                }
                scanEntryLogForPositionRange(logId, startpos, endpos, printMsg);
            } else if (lId != -1) {
                scanEntryLogForSpecificEntry(logId, lId, eId, printMsg);
            } else {
                scanEntryLog(logId, printMsg);
            }

            return 0;
        }

        @Override
        String getDescription() {
            return "Scan an entry file and format the entries into readable format.";
        }

        @Override
        String getUsage() {
            return "readlog      [-msg] <entry_log_id | entry_log_file_name> [-ledgerid <ledgerid> "
                    + "[-entryid <entryid>]] [-startpos <startEntryLogBytePos> [-endpos <endEntryLogBytePos>]]";
        }

        @Override
        Options getOptions() {
            return rlOpts;
        }
    }

    /**
     * Command to print metadata of entrylog.
     */
    class ReadLogMetadataCmd extends MyCommand {
        Options rlOpts = new Options();

        ReadLogMetadataCmd() {
            super(CMD_READLOGMETADATA);
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {
            String[] leftArgs = cmdLine.getArgs();
            if (leftArgs.length <= 0) {
                LOG.error("ERROR: missing entry log id or entry log file name");
                printUsage();
                return -1;
            }

            long logId;
            try {
                logId = Long.parseLong(leftArgs[0]);
            } catch (NumberFormatException nfe) {
                // not a entry log id
                File f = new File(leftArgs[0]);
                String name = f.getName();
                if (!name.endsWith(".log")) {
                    // not a log file
                    LOG.error("ERROR: invalid entry log file name " + leftArgs[0]);
                    printUsage();
                    return -1;
                }
                String idString = name.split("\\.")[0];
                logId = Long.parseLong(idString, 16);
            }

            printEntryLogMetadata(logId);

            return 0;
        }

        @Override
        String getDescription() {
            return "Prints entrylog's metadata";
        }

        @Override
        String getUsage() {
            return "readlogmetadata <entry_log_id | entry_log_file_name>";
        }

        @Override
        Options getOptions() {
            return rlOpts;
        }
    }

    /**
     * Command to read journal files.
     */
    class ReadJournalCmd extends MyCommand {
        Options rjOpts = new Options();

        ReadJournalCmd() {
            super(CMD_READJOURNAL);
            rjOpts.addOption("dir", false, "Journal directory (needed if more than one journal configured)");
            rjOpts.addOption("m", "msg", false, "Print message body");
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {
            String[] leftArgs = cmdLine.getArgs();
            if (leftArgs.length <= 0) {
                System.err.println("ERROR: missing journal id or journal file name");
                printUsage();
                return -1;
            }

            boolean printMsg = false;
            if (cmdLine.hasOption("m")) {
                printMsg = true;
            }

            Journal journal = null;
            if (getJournals().size() > 1) {
                if (!cmdLine.hasOption("dir")) {
                    System.err.println("ERROR: invalid or missing journal directory");
                    printUsage();
                    return -1;
                }

                File journalDirectory = new File(cmdLine.getOptionValue("dir"));
                for (Journal j : getJournals()) {
                    if (j.getJournalDirectory().equals(journalDirectory)) {
                        journal = j;
                        break;
                    }
                }

                if (journal == null) {
                    System.err.println("ERROR: journal directory not found");
                    printUsage();
                    return -1;
                }
            } else {
                journal = getJournals().get(0);
            }

            long journalId;
            try {
                journalId = Long.parseLong(leftArgs[0]);
            } catch (NumberFormatException nfe) {
                // not a journal id
                File f = new File(leftArgs[0]);
                String name = f.getName();
                if (!name.endsWith(".txn")) {
                    // not a journal file
                    System.err.println("ERROR: invalid journal file name " + leftArgs[0]);
                    printUsage();
                    return -1;
                }
                String idString = name.split("\\.")[0];
                journalId = Long.parseLong(idString, 16);
            }
            // scan journal
            scanJournal(journal, journalId, printMsg);
            return 0;
        }

        @Override
        String getDescription() {
            return "Scan a journal file and format the entries into readable format.";
        }

        @Override
        String getUsage() {
            return "readjournal [-dir] [-msg] <journal_id | journal_file_name>";
        }

        @Override
        Options getOptions() {
            return rjOpts;
        }
    }

    /**
     * Command to print last log mark.
     */
    class LastMarkCmd extends MyCommand {
        LastMarkCmd() {
            super(CMD_LASTMARK);
        }

        @Override
        public int runCmd(CommandLine c) throws Exception {
            LastMarkCommand command = new LastMarkCommand();
            command.apply(bkConf, new CliFlags());
            return 0;
        }

        @Override
        String getDescription() {
            return "Print last log marker.";
        }

        @Override
        String getUsage() {
            return "lastmark";
        }

        @Override
        Options getOptions() {
            return new Options();
        }
    }

    /**
     * List available bookies.
     */
    class ListBookiesCmd extends MyCommand {
        Options opts = new Options();

        ListBookiesCmd() {
            super(CMD_LISTBOOKIES);
            opts.addOption("rw", "readwrite", false, "Print readwrite bookies");
            opts.addOption("ro", "readonly", false, "Print readonly bookies");
            // @deprecated 'rw'/'ro' option print both hostname and ip, so this option is not needed anymore
            opts.addOption("h", "hostnames", false, "Also print hostname of the bookie");
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {
            boolean readwrite = cmdLine.hasOption("rw");
            boolean readonly = cmdLine.hasOption("ro");

            if ((!readwrite && !readonly) || (readwrite && readonly)) {
                LOG.error("One and only one of -readwrite and -readonly must be specified");
                printUsage();
                return 1;
            }

            ListBookiesCommand.Flags flags = new ListBookiesCommand.Flags().readwrite(readwrite).readonly(readonly);

            ListBookiesCommand command = new ListBookiesCommand(flags);

            command.apply(bkConf, flags);
            return 0;
        }

        @Override
        String getDescription() {
            return "List the bookies, which are running as either readwrite or readonly mode.";
        }

        @Override
        String getUsage() {
            return "listbookies  [-readwrite|-readonly] [-hostnames]";
        }

        @Override
        Options getOptions() {
            return opts;
        }
    }

    class ListDiskFilesCmd extends MyCommand {
        Options opts = new Options();

        ListDiskFilesCmd() {
            super(CMD_LISTFILESONDISC);
            opts.addOption("txn", "journal", false, "Print list of Journal Files");
            opts.addOption("log", "entrylog", false, "Print list of EntryLog Files");
            opts.addOption("idx", "index", false, "Print list of Index files");
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {

            boolean journal = cmdLine.hasOption("txn");
            boolean entrylog = cmdLine.hasOption("log");
            boolean index = cmdLine.hasOption("idx");
            boolean all = false;

            if (!journal && !entrylog && !index && !all) {
                all = true;
            }

            if (all || journal) {
                File[] journalDirs = bkConf.getJournalDirs();
                List<File> journalFiles = listFilesAndSort(journalDirs, "txn");
                System.out.println("--------- Printing the list of Journal Files ---------");
                for (File journalFile : journalFiles) {
                    System.out.println(journalFile.getCanonicalPath());
                }
                System.out.println();
            }
            if (all || entrylog) {
                File[] ledgerDirs = bkConf.getLedgerDirs();
                List<File> ledgerFiles = listFilesAndSort(ledgerDirs, "log");
                System.out.println("--------- Printing the list of EntryLog/Ledger Files ---------");
                for (File ledgerFile : ledgerFiles) {
                    System.out.println(ledgerFile.getCanonicalPath());
                }
                System.out.println();
            }
            if (all || index) {
                File[] indexDirs = (bkConf.getIndexDirs() == null) ? bkConf.getLedgerDirs() : bkConf.getIndexDirs();
                List<File> indexFiles = listFilesAndSort(indexDirs, "idx");
                System.out.println("--------- Printing the list of Index Files ---------");
                for (File indexFile : indexFiles) {
                    System.out.println(indexFile.getCanonicalPath());
                }
            }
            return 0;
        }

        @Override
        String getDescription() {
            return "List the files in JournalDirectory/LedgerDirectories/IndexDirectories.";
        }

        @Override
        String getUsage() {
            return "listfilesondisc  [-journal|-entrylog|-index]";
        }

        @Override
        Options getOptions() {
            return opts;
        }
    }

    /**
     * Command to print help message.
     */
    class HelpCmd extends MyCommand {
        HelpCmd() {
            super(CMD_HELP);
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {
            String[] args = cmdLine.getArgs();
            if (args.length == 0) {
                printShellUsage();
                return 0;
            }
            String cmdName = args[0];
            Command cmd = commands.get(cmdName);
            if (null == cmd) {
                System.err.println("Unknown command " + cmdName);
                printShellUsage();
                return -1;
            }
            cmd.printUsage();
            return 0;
        }

        @Override
        String getDescription() {
            return "Describe the usage of this program or its subcommands.";
        }

        @Override
        String getUsage() {
            return "help         [COMMAND]";
        }

        @Override
        Options getOptions() {
            return new Options();
        }
    }

    /**
     * Command for administration of autorecovery.
     */
    class AutoRecoveryCmd extends MyCommand {
        Options opts = new Options();

        public AutoRecoveryCmd() {
            super(CMD_AUTORECOVERY);
            opts.addOption("e", "enable", false, "Enable auto recovery of underreplicated ledgers");
            opts.addOption("d", "disable", false, "Disable auto recovery of underreplicated ledgers");
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Enable or disable autorecovery in the cluster.";
        }

        @Override
        String getUsage() {
            return "autorecovery [-enable|-disable]";
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            boolean disable = cmdLine.hasOption("d");
            boolean enable = cmdLine.hasOption("e");

            if (enable && disable) {
                LOG.error("Only one of -enable and -disable can be specified");
                printUsage();
                return 1;
            }

            runFunctionWithLedgerManagerFactory(bkConf, mFactory -> {
                try {
                    try (LedgerUnderreplicationManager underreplicationManager = mFactory
                            .newLedgerUnderreplicationManager()) {
                        if (!enable && !disable) {
                            boolean enabled = underreplicationManager.isLedgerReplicationEnabled();
                            System.out.println("Autorecovery is " + (enabled ? "enabled." : "disabled."));
                        } else if (enable) {
                            if (underreplicationManager.isLedgerReplicationEnabled()) {
                                LOG.warn("Autorecovery already enabled. Doing nothing");
                            } else {
                                LOG.info("Enabling autorecovery");
                                underreplicationManager.enableLedgerReplication();
                            }
                        } else {
                            if (!underreplicationManager.isLedgerReplicationEnabled()) {
                                LOG.warn("Autorecovery already disabled. Doing nothing");
                            } else {
                                LOG.info("Disabling autorecovery");
                                underreplicationManager.disableLedgerReplication();
                            }
                        }
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new UncheckedExecutionException(e);
                } catch (KeeperException | ReplicationException e) {
                    throw new UncheckedExecutionException(e);
                }
                return null;
            });

            return 0;
        }
    }

    /**
     * Setter and Getter for LostBookieRecoveryDelay value (in seconds) in metadata store.
     */
    class LostBookieRecoveryDelayCmd extends MyCommand {
        Options opts = new Options();

        public LostBookieRecoveryDelayCmd() {
            super(CMD_LOSTBOOKIERECOVERYDELAY);
            opts.addOption("g", "get", false, "Get LostBookieRecoveryDelay value (in seconds)");
            opts.addOption("s", "set", true, "Set LostBookieRecoveryDelay value (in seconds)");
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Setter and Getter for LostBookieRecoveryDelay value (in seconds) in metadata store.";
        }

        @Override
        String getUsage() {
            return "lostbookierecoverydelay [-get|-set <value>]";
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            boolean getter = cmdLine.hasOption("g");
            boolean setter = cmdLine.hasOption("s");

            if ((!getter && !setter) || (getter && setter)) {
                LOG.error("One and only one of -get and -set must be specified");
                printUsage();
                return 1;
            }
            ClientConfiguration adminConf = new ClientConfiguration(bkConf);
            BookKeeperAdmin admin = new BookKeeperAdmin(adminConf);
            try {
                if (getter) {
                    int lostBookieRecoveryDelay = admin.getLostBookieRecoveryDelay();
                    LOG.info("LostBookieRecoveryDelay value in ZK: {}", String.valueOf(lostBookieRecoveryDelay));
                } else {
                    int lostBookieRecoveryDelay = Integer.parseInt(cmdLine.getOptionValue("set"));
                    admin.setLostBookieRecoveryDelay(lostBookieRecoveryDelay);
                    LOG.info("Successfully set LostBookieRecoveryDelay value in ZK: {}",
                            String.valueOf(lostBookieRecoveryDelay));
                }
            } finally {
                if (admin != null) {
                    admin.close();
                }
            }
            return 0;
        }
    }

    /**
     * Print which node has the auditor lock.
     */
    class WhoIsAuditorCmd extends MyCommand {
        Options opts = new Options();

        public WhoIsAuditorCmd() {
            super(CMD_WHOISAUDITOR);
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Print the node which holds the auditor lock.";
        }

        @Override
        String getUsage() {
            return "whoisauditor";
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            ZooKeeper zk = null;
            try {
                String metadataServiceUri = bkConf.getMetadataServiceUri();
                String zkServers = ZKMetadataDriverBase.getZKServersFromServiceUri(URI.create(metadataServiceUri));
                zk = ZooKeeperClient.newBuilder().connectString(zkServers).sessionTimeoutMs(bkConf.getZkTimeout())
                        .build();
                BookieSocketAddress bookieId = AuditorElector.getCurrentAuditor(bkConf, zk);
                if (bookieId == null) {
                    LOG.info("No auditor elected");
                    return -1;
                }
                LOG.info("Auditor: " + getBookieSocketAddrStringRepresentation(bookieId));
            } finally {
                if (zk != null) {
                    zk.close();
                }
            }

            return 0;
        }
    }

    /**
     * Prints the instanceid of the cluster.
     */
    class WhatIsInstanceId extends MyCommand {
        Options opts = new Options();

        public WhatIsInstanceId() {
            super(CMD_WHATISINSTANCEID);
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Print the instanceid of the cluster";
        }

        @Override
        String getUsage() {
            return "whatisinstanceid";
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            runFunctionWithRegistrationManager(bkConf, rm -> {
                String readInstanceId = null;
                try {
                    readInstanceId = rm.getClusterInstanceId();
                } catch (BookieException e) {
                    throw new UncheckedExecutionException(e);
                }
                LOG.info("Metadata Service Uri: {} InstanceId: {}", bkConf.getMetadataServiceUriUnchecked(),
                        readInstanceId);
                return null;
            });
            return 0;
        }
    }

    /**
     * Update cookie command.
     */
    class UpdateCookieCmd extends MyCommand {
        Options opts = new Options();
        private static final String BOOKIEID = "bookieId";
        private static final String EXPANDSTORAGE = "expandstorage";
        private static final String LIST = "list";
        private static final String DELETE = "delete";
        private static final String HOSTNAME = "hostname";
        private static final String IP = "ip";
        private static final String FORCE = "force";

        UpdateCookieCmd() {
            super(CMD_UPDATECOOKIE);
            opts.addOption("b", BOOKIEID, true, "Bookie Id");
            opts.addOption("e", EXPANDSTORAGE, false, "Expand Storage");
            opts.addOption("l", LIST, false, "List paths of all the cookies present locally and on zookkeeper");
            @SuppressWarnings("static-access")
            Option deleteOption = OptionBuilder.withLongOpt(DELETE).hasOptionalArgs(1)
                    .withDescription("Delete cookie both locally and in ZooKeeper").create("d");
            opts.addOption(deleteOption);
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Command to update cookie" + "bookieId - Update bookie id in cookie\n"
                    + "expandstorage - Add new empty ledger/index directories."
                    + " Update the directories info in the conf file before running the command\n"
                    + "list - list the local cookie files path and ZK cookiePath "
                    + "delete - Delete cookies locally and in zookeeper";
        }

        @Override
        String getUsage() {
            return "updatecookie [-bookieId <hostname|ip>] [-expandstorage] [-list] [-delete <force>]";
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            int retValue = -1;
            Option[] options = cmdLine.getOptions();
            if (options.length != 1) {
                LOG.error("Invalid command!");
                this.printUsage();
                return -1;
            }
            Option thisCommandOption = options[0];
            if (thisCommandOption.getLongOpt().equals(BOOKIEID)) {
                final String bookieId = cmdLine.getOptionValue(BOOKIEID);
                if (StringUtils.isBlank(bookieId)) {
                    LOG.error("Invalid argument list!");
                    this.printUsage();
                    return -1;
                }
                if (!StringUtils.equals(bookieId, HOSTNAME) && !StringUtils.equals(bookieId, IP)) {
                    LOG.error("Invalid option value:" + bookieId);
                    this.printUsage();
                    return -1;
                }
                boolean useHostName = getOptionalValue(bookieId, HOSTNAME);
                if (!bkConf.getUseHostNameAsBookieID() && useHostName) {
                    LOG.error("Expects config useHostNameAsBookieID=true as the option value passed is 'hostname'");
                    return -1;
                } else if (bkConf.getUseHostNameAsBookieID() && !useHostName) {
                    LOG.error(
                            "Expects configuration useHostNameAsBookieID=false as the option value passed is 'ip'");
                    return -1;
                }
                retValue = updateBookieIdInCookie(bookieId, useHostName);
            } else if (thisCommandOption.getLongOpt().equals(EXPANDSTORAGE)) {
                bkConf.setAllowStorageExpansion(true);
                return expandStorage();
            } else if (thisCommandOption.getLongOpt().equals(LIST)) {
                return listOrDeleteCookies(false, false);
            } else if (thisCommandOption.getLongOpt().equals(DELETE)) {
                boolean force = false;
                String optionValue = thisCommandOption.getValue();
                if (!StringUtils.isEmpty(optionValue) && optionValue.equals(FORCE)) {
                    force = true;
                }
                return listOrDeleteCookies(true, force);
            } else {
                LOG.error("Invalid command!");
                this.printUsage();
                return -1;
            }
            return retValue;
        }

        private int updateBookieIdInCookie(final String bookieId, final boolean useHostname) throws Exception {
            return runFunctionWithRegistrationManager(bkConf, rm -> {
                try {
                    ServerConfiguration conf = new ServerConfiguration(bkConf);
                    String newBookieId = Bookie.getBookieAddress(conf).toString();
                    // read oldcookie
                    Versioned<Cookie> oldCookie = null;
                    try {
                        conf.setUseHostNameAsBookieID(!useHostname);
                        oldCookie = Cookie.readFromRegistrationManager(rm, conf);
                    } catch (CookieNotFoundException nne) {
                        LOG.error(
                                "Either cookie already updated with UseHostNameAsBookieID={} or no cookie exists!",
                                useHostname, nne);
                        return -1;
                    }
                    Cookie newCookie = Cookie.newBuilder(oldCookie.getValue()).setBookieHost(newBookieId).build();
                    boolean hasCookieUpdatedInDirs = verifyCookie(newCookie, journalDirectories[0]);
                    for (File dir : ledgerDirectories) {
                        hasCookieUpdatedInDirs &= verifyCookie(newCookie, dir);
                    }
                    if (indexDirectories != ledgerDirectories) {
                        for (File dir : indexDirectories) {
                            hasCookieUpdatedInDirs &= verifyCookie(newCookie, dir);
                        }
                    }

                    if (hasCookieUpdatedInDirs) {
                        try {
                            conf.setUseHostNameAsBookieID(useHostname);
                            Cookie.readFromRegistrationManager(rm, conf);
                            // since newcookie exists, just do cleanup of oldcookie and return
                            conf.setUseHostNameAsBookieID(!useHostname);
                            oldCookie.getValue().deleteFromRegistrationManager(rm, conf, oldCookie.getVersion());
                            return 0;
                        } catch (CookieNotFoundException nne) {
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("Ignoring, cookie will be written to zookeeper");
                            }
                        }
                    } else {
                        // writes newcookie to local dirs
                        for (File journalDirectory : journalDirectories) {
                            newCookie.writeToDirectory(journalDirectory);
                            LOG.info("Updated cookie file present in journalDirectory {}", journalDirectory);
                        }
                        for (File dir : ledgerDirectories) {
                            newCookie.writeToDirectory(dir);
                        }
                        LOG.info("Updated cookie file present in ledgerDirectories {}", ledgerDirectories);
                        if (ledgerDirectories != indexDirectories) {
                            for (File dir : indexDirectories) {
                                newCookie.writeToDirectory(dir);
                            }
                            LOG.info("Updated cookie file present in indexDirectories {}", indexDirectories);
                        }
                    }
                    // writes newcookie to zookeeper
                    conf.setUseHostNameAsBookieID(useHostname);
                    newCookie.writeToRegistrationManager(rm, conf, Version.NEW);

                    // delete oldcookie
                    conf.setUseHostNameAsBookieID(!useHostname);
                    oldCookie.getValue().deleteFromRegistrationManager(rm, conf, oldCookie.getVersion());
                    return 0;
                } catch (IOException | BookieException ioe) {
                    LOG.error("IOException during cookie updation!", ioe);
                    return -1;
                }
            });
        }

        private int expandStorage() throws Exception {
            return runFunctionWithMetadataBookieDriver(bkConf, driver -> {
                List<File> allLedgerDirs = Lists.newArrayList();
                allLedgerDirs.addAll(Arrays.asList(ledgerDirectories));
                if (indexDirectories != ledgerDirectories) {
                    allLedgerDirs.addAll(Arrays.asList(indexDirectories));
                }

                try {
                    Bookie.checkEnvironmentWithStorageExpansion(bkConf, driver, Arrays.asList(journalDirectories),
                            allLedgerDirs);
                    return 0;
                } catch (BookieException e) {
                    LOG.error("Exception while updating cookie for storage expansion", e);
                    return -1;
                }
            });
        }

        private boolean verifyCookie(Cookie oldCookie, File dir) throws IOException {
            try {
                Cookie cookie = Cookie.readFromDirectory(dir);
                cookie.verify(oldCookie);
            } catch (InvalidCookieException e) {
                return false;
            }
            return true;
        }

        private int listOrDeleteCookies(boolean delete, boolean force) throws Exception {
            BookieSocketAddress bookieAddress = Bookie.getBookieAddress(bkConf);
            File[] journalDirs = bkConf.getJournalDirs();
            File[] ledgerDirs = bkConf.getLedgerDirs();
            File[] indexDirs = bkConf.getIndexDirs();
            File[] allDirs = ArrayUtils.addAll(journalDirs, ledgerDirs);
            if (indexDirs != null) {
                allDirs = ArrayUtils.addAll(allDirs, indexDirs);
            }

            File[] allCurDirs = Bookie.getCurrentDirectories(allDirs);
            List<File> allVersionFiles = new LinkedList<File>();
            File versionFile;
            for (File curDir : allCurDirs) {
                versionFile = new File(curDir, BookKeeperConstants.VERSION_FILENAME);
                if (versionFile.exists()) {
                    allVersionFiles.add(versionFile);
                }
            }

            if (!allVersionFiles.isEmpty()) {
                if (delete) {
                    boolean confirm = force;
                    if (!confirm) {
                        confirm = IOUtils.confirmPrompt("Are you sure you want to delete Cookies locally?");
                    }
                    if (confirm) {
                        for (File verFile : allVersionFiles) {
                            if (!verFile.delete()) {
                                LOG.error(
                                        "Failed to delete Local cookie file {}. So aborting deletecookie of Bookie: {}",
                                        verFile, bookieAddress);
                                return -1;
                            }
                        }
                        LOG.info("Deleted Local Cookies of Bookie: {}", bookieAddress);
                    } else {
                        LOG.info("Skipping deleting local Cookies of Bookie: {}", bookieAddress);
                    }
                } else {
                    LOG.info("Listing local Cookie Files of Bookie: {}", bookieAddress);
                    for (File verFile : allVersionFiles) {
                        LOG.info(verFile.getCanonicalPath());
                    }
                }
            } else {
                LOG.info("No local cookies for Bookie: {}", bookieAddress);
            }

            return runFunctionWithRegistrationManager(bkConf, rm -> {
                try {
                    Versioned<Cookie> cookie = null;
                    try {
                        cookie = Cookie.readFromRegistrationManager(rm, bookieAddress);
                    } catch (CookieNotFoundException nne) {
                        LOG.info("No cookie for {} in metadata store", bookieAddress);
                        return 0;
                    }

                    if (delete) {
                        boolean confirm = force;
                        if (!confirm) {
                            confirm = IOUtils
                                    .confirmPrompt("Are you sure you want to delete Cookies from metadata store?");
                        }

                        if (confirm) {
                            cookie.getValue().deleteFromRegistrationManager(rm, bkConf, cookie.getVersion());
                            LOG.info("Deleted Cookie from metadata store for Bookie: {}", bookieAddress);
                        } else {
                            LOG.info("Skipping deleting cookie from metadata store for Bookie: {}", bookieAddress);
                        }
                    }
                } catch (BookieException | IOException e) {
                    return -1;
                }
                return 0;
            });
        }
    }

    /**
     * Update ledger command.
     */
    class UpdateLedgerCmd extends MyCommand {
        private final Options opts = new Options();

        UpdateLedgerCmd() {
            super(CMD_UPDATELEDGER);
            opts.addOption("b", "bookieId", true, "Bookie Id");
            opts.addOption("s", "updatespersec", true,
                    "Number of ledgers updating per second (default: 5 per sec)");
            opts.addOption("l", "limit", true, "Maximum number of ledgers to update (default: no limit)");
            opts.addOption("v", "verbose", true, "Print status of the ledger updation (default: false)");
            opts.addOption("p", "printprogress", true,
                    "Print messages on every configured seconds if verbose turned on (default: 10 secs)");
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Update bookie id in ledgers (this may take a long time).";
        }

        @Override
        String getUsage() {
            return "updateledgers -bookieId <hostname|ip> [-updatespersec N] [-limit N] [-verbose true/false] "
                    + "[-printprogress N]";
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            final String bookieId = cmdLine.getOptionValue("bookieId");
            if (StringUtils.isBlank(bookieId)) {
                LOG.error("Invalid argument list!");
                this.printUsage();
                return -1;
            }
            if (!StringUtils.equals(bookieId, "hostname") && !StringUtils.equals(bookieId, "ip")) {
                LOG.error("Invalid option value {} for bookieId, expected hostname/ip", bookieId);
                this.printUsage();
                return -1;
            }
            boolean useHostName = getOptionalValue(bookieId, "hostname");
            if (!bkConf.getUseHostNameAsBookieID() && useHostName) {
                LOG.error(
                        "Expects configuration useHostNameAsBookieID=true as the option value passed is 'hostname'");
                return -1;
            } else if (bkConf.getUseHostNameAsBookieID() && !useHostName) {
                LOG.error("Expects configuration useHostNameAsBookieID=false as the option value passed is 'ip'");
                return -1;
            }
            final int rate = getOptionIntValue(cmdLine, "updatespersec", 5);
            if (rate <= 0) {
                LOG.error("Invalid updatespersec {}, should be > 0", rate);
                return -1;
            }
            final int limit = getOptionIntValue(cmdLine, "limit", Integer.MIN_VALUE);
            if (limit <= 0 && limit != Integer.MIN_VALUE) {
                LOG.error("Invalid limit {}, should be > 0", limit);
                return -1;
            }
            final boolean verbose = getOptionBooleanValue(cmdLine, "verbose", false);
            final long printprogress;
            if (!verbose) {
                if (cmdLine.hasOption("printprogress")) {
                    LOG.warn("Ignoring option 'printprogress', this is applicable when 'verbose' is true");
                }
                printprogress = Integer.MIN_VALUE;
            } else {
                // defaulting to 10 seconds
                printprogress = getOptionLongValue(cmdLine, "printprogress", 10);
            }
            final ClientConfiguration conf = new ClientConfiguration();
            conf.addConfiguration(bkConf);
            final BookKeeper bk = new BookKeeper(conf);
            final BookKeeperAdmin admin = new BookKeeperAdmin(conf);
            final UpdateLedgerOp updateLedgerOp = new UpdateLedgerOp(bk, admin);
            final ServerConfiguration serverConf = new ServerConfiguration(bkConf);
            final BookieSocketAddress newBookieId = Bookie.getBookieAddress(serverConf);
            serverConf.setUseHostNameAsBookieID(!useHostName);
            final BookieSocketAddress oldBookieId = Bookie.getBookieAddress(serverConf);

            UpdateLedgerNotifier progressable = new UpdateLedgerNotifier() {
                long lastReport = System.nanoTime();

                @Override
                public void progress(long updated, long issued) {
                    if (printprogress <= 0) {
                        return; // disabled
                    }
                    if (TimeUnit.MILLISECONDS.toSeconds(MathUtils.elapsedMSec(lastReport)) >= printprogress) {
                        LOG.info("Number of ledgers issued={}, updated={}", issued, updated);
                        lastReport = MathUtils.nowInNano();
                    }
                }
            };
            try {
                updateLedgerOp.updateBookieIdInLedgers(oldBookieId, newBookieId, rate, limit, progressable);
            } catch (IOException e) {
                LOG.error("Failed to update ledger metadata", e);
                return -1;
            }
            return 0;
        }
    }

    /**
     * Command to delete a given ledger.
     */
    class DeleteLedgerCmd extends MyCommand {
        Options lOpts = new Options();

        DeleteLedgerCmd() {
            super(CMD_DELETELEDGER);
            lOpts.addOption("l", "ledgerid", true, "Ledger ID");
            lOpts.addOption("f", "force", false, "Whether to force delete the Ledger without prompt..?");
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {
            final long lid = getOptionLedgerIdValue(cmdLine, "ledgerid", -1);
            if (lid == -1) {
                System.err.println("Must specify a ledger id");
                return -1;
            }

            boolean force = cmdLine.hasOption("f");
            boolean confirm = false;
            if (!force) {
                confirm = IOUtils.confirmPrompt(
                        "Are you sure to delete Ledger : " + ledgerIdFormatter.formatLedgerId(lid) + "?");
            }

            BookKeeper bk = null;
            try {
                if (force || confirm) {
                    ClientConfiguration conf = new ClientConfiguration();
                    conf.addConfiguration(bkConf);
                    bk = new BookKeeper(conf);
                    bk.deleteLedger(lid);
                }
            } finally {
                if (bk != null) {
                    bk.close();
                }
            }

            return 0;
        }

        @Override
        String getDescription() {
            return "Delete a ledger.";
        }

        @Override
        String getUsage() {
            return "deleteledger -ledgerid <ledgerid> [-force]";
        }

        @Override
        Options getOptions() {
            return lOpts;
        }
    }

    /*
     * Command to retrieve bookie information like free disk space, etc from all
     * the bookies in the cluster.
     */
    class BookieInfoCmd extends MyCommand {
        Options lOpts = new Options();

        BookieInfoCmd() {
            super(CMD_BOOKIEINFO);
        }

        @Override
        String getDescription() {
            return "Retrieve bookie info such as free and total disk space.";
        }

        @Override
        String getUsage() {
            return "bookieinfo";
        }

        @Override
        Options getOptions() {
            return lOpts;
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {
            InfoCommand cmd = new InfoCommand();
            cmd.apply(bkConf, new CliFlags());
            return 0;
        }
    }

    /**
     * Command to trigger AuditTask by resetting lostBookieRecoveryDelay to its current value.
     */
    class TriggerAuditCmd extends MyCommand {
        Options opts = new Options();

        TriggerAuditCmd() {
            super(CMD_TRIGGERAUDIT);
        }

        @Override
        String getDescription() {
            return "Force trigger the Audit by resetting the lostBookieRecoveryDelay.";
        }

        @Override
        String getUsage() {
            return CMD_TRIGGERAUDIT;
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {
            ClientConfiguration adminConf = new ClientConfiguration(bkConf);
            BookKeeperAdmin admin = new BookKeeperAdmin(adminConf);
            try {
                admin.triggerAudit();
            } finally {
                if (admin != null) {
                    admin.close();
                }
            }
            return 0;
        }
    }

    /**
     * Command to trigger AuditTask by resetting lostBookieRecoveryDelay and
     * then make sure the ledgers stored in the bookie are properly replicated
     * and Cookie of the decommissioned bookie should be deleted from metadata
     * server.
     */
    class DecommissionBookieCmd extends MyCommand {
        Options lOpts = new Options();

        DecommissionBookieCmd() {
            super(CMD_DECOMMISSIONBOOKIE);
            lOpts.addOption("bookieid", true, "decommission a remote bookie");
        }

        @Override
        String getDescription() {
            return "Force trigger the Audittask and make sure all the ledgers stored in the decommissioning bookie"
                    + " are replicated and cookie of the decommissioned bookie is deleted from metadata server.";
        }

        @Override
        String getUsage() {
            return CMD_DECOMMISSIONBOOKIE + " [-bookieid <bookieaddress>]";
        }

        @Override
        Options getOptions() {
            return lOpts;
        }

        @Override
        public int runCmd(CommandLine cmdLine) throws Exception {
            ClientConfiguration adminConf = new ClientConfiguration(bkConf);
            BookKeeperAdmin admin = new BookKeeperAdmin(adminConf);
            try {
                final String remoteBookieidToDecommission = cmdLine.getOptionValue("bookieid");
                final BookieSocketAddress bookieAddressToDecommission = (StringUtils
                        .isBlank(remoteBookieidToDecommission) ? Bookie.getBookieAddress(bkConf)
                                : new BookieSocketAddress(remoteBookieidToDecommission));
                admin.decommissionBookie(bookieAddressToDecommission);
                LOG.info("The ledgers stored in the given decommissioning bookie: {} are properly replicated",
                        bookieAddressToDecommission);
                runFunctionWithRegistrationManager(bkConf, rm -> {
                    try {
                        Versioned<Cookie> cookie = Cookie.readFromRegistrationManager(rm,
                                bookieAddressToDecommission);
                        cookie.getValue().deleteFromRegistrationManager(rm, bookieAddressToDecommission,
                                cookie.getVersion());
                    } catch (CookieNotFoundException nne) {
                        LOG.warn(
                                "No cookie to remove for the decommissioning bookie: {}, it could be deleted already",
                                bookieAddressToDecommission, nne);
                    } catch (BookieException be) {
                        throw new UncheckedExecutionException(be.getMessage(), be);
                    }
                    return 0;
                });
                LOG.info("Cookie of the decommissioned bookie: {} is deleted successfully",
                        bookieAddressToDecommission);
                return 0;
            } catch (Exception e) {
                LOG.error("Received exception in DecommissionBookieCmd ", e);
                return -1;
            } finally {
                if (admin != null) {
                    admin.close();
                }
            }
        }
    }

    /**
     * A facility for reporting update ledger progress.
     */
    public interface UpdateLedgerNotifier {
        void progress(long updated, long issued);
    }

    /**
     * Convert bookie indexes from InterleavedStorage to DbLedgerStorage format.
     */
    class ConvertToDbStorageCmd extends MyCommand {
        Options opts = new Options();

        public ConvertToDbStorageCmd() {
            super(CMD_CONVERT_TO_DB_STORAGE);
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Convert bookie indexes from InterleavedStorage to DbLedgerStorage format";
        }

        @Override
        String getUsage() {
            return CMD_CONVERT_TO_DB_STORAGE;
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            LOG.info("=== Converting to DbLedgerStorage ===");
            ServerConfiguration conf = new ServerConfiguration(bkConf);

            InterleavedLedgerStorage interleavedStorage = new InterleavedLedgerStorage();
            Bookie.mountLedgerStorageOffline(conf, interleavedStorage);

            DbLedgerStorage dbStorage = new DbLedgerStorage();
            Bookie.mountLedgerStorageOffline(conf, dbStorage);

            int convertedLedgers = 0;
            for (long ledgerId : interleavedStorage.getActiveLedgersInRange(0, Long.MAX_VALUE)) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Converting ledger {}", ledgerIdFormatter.formatLedgerId(ledgerId));
                }

                LedgerCache.LedgerIndexMetadata fi = interleavedStorage.readLedgerIndexMetadata(ledgerId);

                LedgerCache.PageEntriesIterable pages = interleavedStorage.getIndexEntries(ledgerId);

                long numberOfEntries = dbStorage.addLedgerToIndex(ledgerId, fi.fenced, fi.masterKey, pages);
                if (LOG.isDebugEnabled()) {
                    LOG.debug("   -- done. fenced={} entries={}", fi.fenced, numberOfEntries);
                }

                // Remove index from old storage
                interleavedStorage.deleteLedger(ledgerId);

                if (++convertedLedgers % 1000 == 0) {
                    LOG.info("Converted {} ledgers", convertedLedgers);
                }
            }

            dbStorage.shutdown();
            interleavedStorage.shutdown();

            LOG.info("---- Done Converting ----");
            return 0;
        }
    }

    /**
     * Convert bookie indexes from DbLedgerStorage to InterleavedStorage format.
     */
    class ConvertToInterleavedStorageCmd extends MyCommand {
        Options opts = new Options();

        public ConvertToInterleavedStorageCmd() {
            super(CMD_CONVERT_TO_INTERLEAVED_STORAGE);
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Convert bookie indexes from DbLedgerStorage to InterleavedStorage format";
        }

        @Override
        String getUsage() {
            return CMD_CONVERT_TO_INTERLEAVED_STORAGE;
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            LOG.info("=== Converting DbLedgerStorage ===");
            ServerConfiguration conf = new ServerConfiguration(bkConf);
            LedgerDirsManager ledgerDirsManager = new LedgerDirsManager(bkConf, bkConf.getLedgerDirs(),
                    new DiskChecker(bkConf.getDiskUsageThreshold(), bkConf.getDiskUsageWarnThreshold()));
            LedgerDirsManager ledgerIndexManager = new LedgerDirsManager(bkConf, bkConf.getLedgerDirs(),
                    new DiskChecker(bkConf.getDiskUsageThreshold(), bkConf.getDiskUsageWarnThreshold()));

            DbLedgerStorage dbStorage = new DbLedgerStorage();
            InterleavedLedgerStorage interleavedStorage = new InterleavedLedgerStorage();

            CheckpointSource checkpointSource = new CheckpointSource() {
                @Override
                public Checkpoint newCheckpoint() {
                    return Checkpoint.MAX;
                }

                @Override
                public void checkpointComplete(Checkpoint checkpoint, boolean compact) throws IOException {
                }
            };
            Checkpointer checkpointer = new Checkpointer() {
                @Override
                public void startCheckpoint(Checkpoint checkpoint) {
                    // No-op
                }

                @Override
                public void start() {
                    // no-op
                }
            };

            dbStorage.initialize(conf, null, ledgerDirsManager, ledgerIndexManager, null, checkpointSource,
                    checkpointer, NullStatsLogger.INSTANCE, PooledByteBufAllocator.DEFAULT);
            interleavedStorage.initialize(conf, null, ledgerDirsManager, ledgerIndexManager, null, checkpointSource,
                    checkpointer, NullStatsLogger.INSTANCE, PooledByteBufAllocator.DEFAULT);
            LedgerCache interleavedLedgerCache = interleavedStorage.ledgerCache;

            int convertedLedgers = 0;
            for (long ledgerId : dbStorage.getActiveLedgersInRange(0, Long.MAX_VALUE)) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Converting ledger {}", ledgerIdFormatter.formatLedgerId(ledgerId));
                }

                interleavedStorage.setMasterKey(ledgerId, dbStorage.readMasterKey(ledgerId));
                if (dbStorage.isFenced(ledgerId)) {
                    interleavedStorage.setFenced(ledgerId);
                }

                long lastEntryInLedger = dbStorage.getLastEntryInLedger(ledgerId);
                for (long entryId = 0; entryId <= lastEntryInLedger; entryId++) {
                    try {
                        long location = dbStorage.getLocation(ledgerId, entryId);
                        if (location != 0L) {
                            interleavedLedgerCache.putEntryOffset(ledgerId, entryId, location);
                        }
                    } catch (Bookie.NoEntryException e) {
                        // Ignore entry
                    }
                }

                if (++convertedLedgers % 1000 == 0) {
                    LOG.info("Converted {} ledgers", convertedLedgers);
                }
            }

            dbStorage.shutdown();

            interleavedLedgerCache.flushLedger(true);
            interleavedStorage.flush();
            interleavedStorage.shutdown();

            String baseDir = ledgerDirsManager.getAllLedgerDirs().get(0).toString();

            // Rename databases and keep backup
            Files.move(FileSystems.getDefault().getPath(baseDir, "ledgers"),
                    FileSystems.getDefault().getPath(baseDir, "ledgers.backup"));

            Files.move(FileSystems.getDefault().getPath(baseDir, "locations"),
                    FileSystems.getDefault().getPath(baseDir, "locations.backup"));

            LOG.info("---- Done Converting {} ledgers ----", convertedLedgers);
            return 0;
        }
    }

    /**
     * Rebuild DbLedgerStorage locations index.
     */
    class RebuildDbLedgerLocationsIndexCmd extends MyCommand {
        Options opts = new Options();

        public RebuildDbLedgerLocationsIndexCmd() {
            super(CMD_REBUILD_DB_LEDGER_LOCATIONS_INDEX);
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Rebuild DbLedgerStorage locations index by scanning the entry logs";
        }

        @Override
        String getUsage() {
            return CMD_REBUILD_DB_LEDGER_LOCATIONS_INDEX;
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            LOG.info("=== Rebuilding bookie index ===");
            ServerConfiguration conf = new ServerConfiguration(bkConf);
            new LocationsIndexRebuildOp(conf).initiate();
            LOG.info("-- Done rebuilding bookie index --");
            return 0;
        }
    }

    /**
     * Regenerate an index file for interleaved storage.
     */
    class RegenerateInterleavedStorageIndexFile extends MyCommand {
        Options opts = new Options();

        public RegenerateInterleavedStorageIndexFile() {
            super(CMD_REGENERATE_INTERLEAVED_STORAGE_INDEX_FILE);
            Option ledgerOption = new Option("l", "ledgerIds", true,
                    "Ledger(s) whose index needs to be regenerated."
                            + " Multiple can be specified, comma separated.");
            ledgerOption.setRequired(true);
            ledgerOption.setValueSeparator(',');
            ledgerOption.setArgs(Option.UNLIMITED_VALUES);

            opts.addOption(ledgerOption);
            opts.addOption("dryRun", false, "Process the entryLogger, but don't write anything.");
            opts.addOption("password", true,
                    "The bookie stores the password in the index file, so we need it to regenerate. "
                            + "This must match the value in the ledger metadata.");
            opts.addOption("b64password", true,
                    "The password in base64 encoding, for cases where the password is not UTF-8.");
        }

        @Override
        Options getOptions() {
            return opts;
        }

        @Override
        String getDescription() {
            return "Regenerate an interleaved storage index file, from available entrylogger files.";
        }

        @Override
        String getUsage() {
            return CMD_REGENERATE_INTERLEAVED_STORAGE_INDEX_FILE;
        }

        @Override
        int runCmd(CommandLine cmdLine) throws Exception {
            byte[] password;
            if (cmdLine.hasOption("password")) {
                password = cmdLine.getOptionValue("password").getBytes(UTF_8);
            } else if (cmdLine.hasOption("b64password")) {
                password = Base64.getDecoder().decode(cmdLine.getOptionValue("b64password"));
            } else {
                LOG.error("The password must be specified to regenerate the index file.");
                return 1;
            }
            Set<Long> ledgerIds = Arrays.stream(cmdLine.getOptionValues("ledgerIds"))
                    .map((id) -> Long.parseLong(id)).collect(Collectors.toSet());
            boolean dryRun = cmdLine.hasOption("dryRun");

            LOG.info("=== Rebuilding index file for {} ===", ledgerIds);
            ServerConfiguration conf = new ServerConfiguration(bkConf);
            new InterleavedStorageRegenerateIndexOp(conf, ledgerIds, password).initiate(dryRun);
            LOG.info("-- Done rebuilding index file for {} --", ledgerIds);
            return 0;
        }
    }

    final Map<String, Command> commands = new HashMap<>();

    {
        commands.put(CMD_METAFORMAT, new MetaFormatCmd());
        commands.put(CMD_INITBOOKIE, new InitBookieCmd());
        commands.put(CMD_INITNEWCLUSTER, new InitNewCluster());
        commands.put(CMD_NUKEEXISTINGCLUSTER, new NukeExistingCluster());
        commands.put(CMD_BOOKIEFORMAT, new BookieFormatCmd());
        commands.put(CMD_RECOVER, new RecoverCmd());
        commands.put(CMD_LEDGER, new LedgerCmd());
        commands.put(CMD_READ_LEDGER_ENTRIES, new ReadLedgerEntriesCmd());
        commands.put(CMD_LISTLEDGERS, new ListLedgersCmd());
        commands.put(CMD_LISTUNDERREPLICATED, new ListUnderreplicatedCmd());
        commands.put(CMD_WHOISAUDITOR, new WhoIsAuditorCmd());
        commands.put(CMD_WHATISINSTANCEID, new WhatIsInstanceId());
        commands.put(CMD_LEDGERMETADATA, new LedgerMetadataCmd());
        commands.put(CMD_LOCALCONSISTENCYCHECK, new LocalConsistencyCheck());
        commands.put(CMD_SIMPLETEST, new SimpleTestCmd());
        commands.put(CMD_BOOKIESANITYTEST, new BookieSanityTestCmd());
        commands.put(CMD_READLOG, new ReadLogCmd());
        commands.put(CMD_READLOGMETADATA, new ReadLogMetadataCmd());
        commands.put(CMD_READJOURNAL, new ReadJournalCmd());
        commands.put(CMD_LASTMARK, new LastMarkCmd());
        commands.put(CMD_AUTORECOVERY, new AutoRecoveryCmd());
        commands.put(CMD_LISTBOOKIES, new ListBookiesCmd());
        commands.put(CMD_LISTFILESONDISC, new ListDiskFilesCmd());
        commands.put(CMD_UPDATECOOKIE, new UpdateCookieCmd());
        commands.put(CMD_UPDATELEDGER, new UpdateLedgerCmd());
        commands.put(CMD_DELETELEDGER, new DeleteLedgerCmd());
        commands.put(CMD_BOOKIEINFO, new BookieInfoCmd());
        commands.put(CMD_DECOMMISSIONBOOKIE, new DecommissionBookieCmd());
        commands.put(CMD_CONVERT_TO_DB_STORAGE, new ConvertToDbStorageCmd());
        commands.put(CMD_CONVERT_TO_INTERLEAVED_STORAGE, new ConvertToInterleavedStorageCmd());
        commands.put(CMD_REBUILD_DB_LEDGER_LOCATIONS_INDEX, new RebuildDbLedgerLocationsIndexCmd());
        commands.put(CMD_REGENERATE_INTERLEAVED_STORAGE_INDEX_FILE, new RegenerateInterleavedStorageIndexFile());
        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
    public void setConf(CompositeConfiguration conf) throws Exception {
        bkConf.loadConf(conf);
        journalDirectories = Bookie.getCurrentDirectories(bkConf.getJournalDirs());
        ledgerDirectories = Bookie.getCurrentDirectories(bkConf.getLedgerDirs());
        if (null == bkConf.getIndexDirs()) {
            indexDirectories = ledgerDirectories;
        } else {
            indexDirectories = Bookie.getCurrentDirectories(bkConf.getIndexDirs());
        }
        pageSize = bkConf.getPageSize();
        entriesPerPage = pageSize / 8;
    }

    private void printShellUsage() {
        System.err.println("Usage: bookkeeper shell [-localbookie [<host:port>]] [-ledgeridformat <hex/long/uuid>] "
                + "[-entryformat <hex/string>] [-conf configuration] <command>");
        System.err.println("where command is one of:");
        List<String> commandNames = new ArrayList<String>();
        for (Command c : commands.values()) {
            commandNames.add("       " + c.description());
        }
        Collections.sort(commandNames);
        for (String s : commandNames) {
            System.err.println(s);
        }
    }

    @VisibleForTesting
    public int execute(String... args) throws Exception {
        return run(args);
    }

    @Override
    public int run(String[] args) throws Exception {
        if (args.length <= 0) {
            printShellUsage();
            return -1;
        }
        String cmdName = args[0];
        Command cmd = commands.get(cmdName);
        if (null == cmd) {
            System.err.println("ERROR: Unknown command " + cmdName);
            printShellUsage();
            return -1;
        }
        // prepare new args
        String[] newArgs = new String[args.length - 1];
        System.arraycopy(args, 1, newArgs, 0, newArgs.length);
        return cmd.runCmd(newArgs);
    }

    /**
     * Returns the sorted list of the files in the given folders with the given file extensions.
     * Sorting is done on the basis of CreationTime if the CreationTime is not available or if they are equal
     * then sorting is done by LastModifiedTime
     * @param folderNames - array of folders which we need to look recursively for files with given extensions
     * @param extensions - the file extensions, which we are interested in
     * @return sorted list of files
     */
    public static List<File> listFilesAndSort(File[] folderNames, String... extensions) {
        List<File> completeFilesList = new ArrayList<File>();
        for (int i = 0; i < folderNames.length; i++) {
            Collection<File> filesCollection = FileUtils.listFiles(folderNames[i], extensions, true);
            completeFilesList.addAll(filesCollection);
        }
        Collections.sort(completeFilesList, new FilesTimeComparator());
        return completeFilesList;
    }

    private static class FilesTimeComparator implements Comparator<File>, Serializable {

        private static final long serialVersionUID = 1L;

        @Override
        public int compare(File file1, File file2) {
            Path file1Path = Paths.get(file1.getAbsolutePath());
            Path file2Path = Paths.get(file2.getAbsolutePath());
            try {
                BasicFileAttributes file1Attributes = Files.readAttributes(file1Path, BasicFileAttributes.class);
                BasicFileAttributes file2Attributes = Files.readAttributes(file2Path, BasicFileAttributes.class);
                FileTime file1CreationTime = file1Attributes.creationTime();
                FileTime file2CreationTime = file2Attributes.creationTime();
                int compareValue = file1CreationTime.compareTo(file2CreationTime);
                /*
                 * please check https://docs.oracle.com/javase/7/docs/api/java/nio/file/attribute/BasicFileAttributes.html#creationTime()
                 * So not all file system implementation store creation time, in that case creationTime()
                 * method may return FileTime representing the epoch (1970-01-01T00:00:00Z). So in that case
                 * it would be better to compare lastModifiedTime
                 */
                if (compareValue == 0) {
                    FileTime file1LastModifiedTime = file1Attributes.lastModifiedTime();
                    FileTime file2LastModifiedTime = file2Attributes.lastModifiedTime();
                    compareValue = file1LastModifiedTime.compareTo(file2LastModifiedTime);
                }
                return compareValue;
            } catch (IOException e) {
                return 0;
            }
        }
    }

    public static void main(String argv[]) throws Exception {
        BookieShell shell = new BookieShell();

        // handle some common options for multiple cmds
        Options opts = new Options();
        opts.addOption(CONF_OPT, true, "configuration file");
        opts.addOption(LEDGERID_FORMATTER_OPT, true, "format of ledgerId");
        opts.addOption(ENTRY_FORMATTER_OPT, true, "format of entries");
        BasicParser parser = new BasicParser();
        CommandLine cmdLine = parser.parse(opts, argv, true);

        // load configuration
        CompositeConfiguration conf = new CompositeConfiguration();
        if (cmdLine.hasOption(CONF_OPT)) {
            String val = cmdLine.getOptionValue(CONF_OPT);
            conf.addConfiguration(new PropertiesConfiguration(new File(val).toURI().toURL()));
        }
        shell.setConf(conf);

        // ledgerid format
        if (cmdLine.hasOption(LEDGERID_FORMATTER_OPT)) {
            String val = cmdLine.getOptionValue(LEDGERID_FORMATTER_OPT);
            shell.ledgerIdFormatter = LedgerIdFormatter.newLedgerIdFormatter(val, shell.bkConf);
        } else {
            shell.ledgerIdFormatter = LedgerIdFormatter.newLedgerIdFormatter(shell.bkConf);
        }
        LOG.debug("Using ledgerIdFormatter {}", shell.ledgerIdFormatter.getClass());

        // entry format
        if (cmdLine.hasOption(ENTRY_FORMATTER_OPT)) {
            String val = cmdLine.getOptionValue(ENTRY_FORMATTER_OPT);
            shell.entryFormatter = EntryFormatter.newEntryFormatter(val, shell.bkConf);
        } else {
            shell.entryFormatter = EntryFormatter.newEntryFormatter(shell.bkConf);
        }
        LOG.debug("Using entry formatter {}", shell.entryFormatter.getClass());

        int res = shell.run(cmdLine.getArgs());
        System.exit(res);
    }

    ///
    /// Bookie File Operations
    ///

    /**
     * Get the ledger file of a specified ledger.
     *
     * @param ledgerId Ledger Id
     *
     * @return file object.
     */
    private File getLedgerFile(long ledgerId) {
        String ledgerName = IndexPersistenceMgr.getLedgerName(ledgerId);
        File lf = null;
        for (File d : indexDirectories) {
            lf = new File(d, ledgerName);
            if (lf.exists()) {
                break;
            }
            lf = null;
        }
        return lf;
    }

    private synchronized void initEntryLogger() throws IOException {
        if (null == entryLogger) {
            // provide read only entry logger
            entryLogger = new ReadOnlyEntryLogger(bkConf);
        }
    }

    /**
     * Scan over entry log.
     *
     * @param logId Entry Log Id
     * @param scanner Entry Log Scanner
     */
    protected void scanEntryLog(long logId, EntryLogScanner scanner) throws IOException {
        initEntryLogger();
        entryLogger.scanEntryLog(logId, scanner);
    }

    private synchronized List<Journal> getJournals() throws IOException {
        if (null == journals) {
            journals = Lists.newArrayListWithCapacity(bkConf.getJournalDirs().length);
            int idx = 0;
            for (File journalDir : bkConf.getJournalDirs()) {
                journals.add(new Journal(idx++, new File(journalDir, BookKeeperConstants.CURRENT_DIR), bkConf,
                        new LedgerDirsManager(bkConf, bkConf.getLedgerDirs(), new DiskChecker(
                                bkConf.getDiskUsageThreshold(), bkConf.getDiskUsageWarnThreshold()))));
            }
        }
        return journals;
    }

    /**
     * Scan journal file.
     *
     * @param journalId Journal File Id
     * @param scanner Journal File Scanner
     */
    protected void scanJournal(Journal journal, long journalId, JournalScanner scanner) throws IOException {
        journal.scanJournal(journalId, 0L, scanner);
    }

    ///
    /// Bookie Shell Commands
    ///

    protected void printEntryLogMetadata(long logId) throws IOException {
        LOG.info("Print entryLogMetadata of entrylog {} ({}.log)", logId, Long.toHexString(logId));
        initEntryLogger();
        EntryLogMetadata entryLogMetadata = entryLogger.getEntryLogMetadata(logId);
        entryLogMetadata.getLedgersMap().forEach((ledgerId, size) -> {
            LOG.info("--------- Lid={}, TotalSizeOfEntriesOfLedger={}  ---------",
                    ledgerIdFormatter.formatLedgerId(ledgerId), size);
        });
    }

    /**
     * Scan over an entry log file.
     *
     * @param logId
     *          Entry Log File id.
     * @param printMsg
     *          Whether printing the entry data.
     */
    protected void scanEntryLog(long logId, final boolean printMsg) throws Exception {
        System.out.println("Scan entry log " + logId + " (" + Long.toHexString(logId) + ".log)");
        scanEntryLog(logId, new EntryLogScanner() {
            @Override
            public boolean accept(long ledgerId) {
                return true;
            }

            @Override
            public void process(long ledgerId, long startPos, ByteBuf entry) {
                formatEntry(startPos, entry, printMsg);
            }
        });
    }

    /**
     * Scan over an entry log file for a particular entry.
     *
     * @param logId Entry Log File id.
     * @param ledgerId id of the ledger
     * @param entryId entryId of the ledger we are looking for (-1 for all of the entries of the ledger)
     * @param printMsg Whether printing the entry data.
     * @throws Exception
     */
    protected void scanEntryLogForSpecificEntry(long logId, final long ledgerId, final long entryId,
            final boolean printMsg) throws Exception {
        System.out.println("Scan entry log " + logId + " (" + Long.toHexString(logId) + ".log)" + " for LedgerId "
                + ledgerId + ((entryId == -1) ? "" : " for EntryId " + entryId));
        final MutableBoolean entryFound = new MutableBoolean(false);
        scanEntryLog(logId, new EntryLogScanner() {
            @Override
            public boolean accept(long candidateLedgerId) {
                return ((candidateLedgerId == ledgerId) && ((!entryFound.booleanValue()) || (entryId == -1)));
            }

            @Override
            public void process(long candidateLedgerId, long startPos, ByteBuf entry) {
                long entrysLedgerId = entry.getLong(entry.readerIndex());
                long entrysEntryId = entry.getLong(entry.readerIndex() + 8);
                if ((candidateLedgerId == entrysLedgerId) && (candidateLedgerId == ledgerId)
                        && ((entrysEntryId == entryId) || (entryId == -1))) {
                    entryFound.setValue(true);
                    formatEntry(startPos, entry, printMsg);
                }
            }
        });
        if (!entryFound.booleanValue()) {
            System.out.println("LedgerId " + ledgerId + ((entryId == -1) ? "" : " EntryId " + entryId)
                    + " is not available in the entry log " + logId + " (" + Long.toHexString(logId) + ".log)");
        }
    }

    /**
     * Scan over an entry log file for entries in the given position range.
     *
     * @param logId Entry Log File id.
     * @param rangeStartPos Start position of the entry we are looking for
     * @param rangeEndPos End position of the entry we are looking for (-1 for till the end of the entrylog)
     * @param printMsg Whether printing the entry data.
     * @throws Exception
     */
    protected void scanEntryLogForPositionRange(long logId, final long rangeStartPos, final long rangeEndPos,
            final boolean printMsg) throws Exception {
        System.out.println("Scan entry log " + logId + " (" + Long.toHexString(logId) + ".log)"
                + " for PositionRange: " + rangeStartPos + " - " + rangeEndPos);
        final MutableBoolean entryFound = new MutableBoolean(false);
        scanEntryLog(logId, new EntryLogScanner() {
            private MutableBoolean stopScanning = new MutableBoolean(false);

            @Override
            public boolean accept(long ledgerId) {
                return !stopScanning.booleanValue();
            }

            @Override
            public void process(long ledgerId, long entryStartPos, ByteBuf entry) {
                if (!stopScanning.booleanValue()) {
                    if ((rangeEndPos != -1) && (entryStartPos > rangeEndPos)) {
                        stopScanning.setValue(true);
                    } else {
                        int entrySize = entry.readableBytes();
                        /**
                         * entrySize of an entry (inclusive of payload and
                         * header) value is stored as int value in log file, but
                         * it is not counted in the entrySize, hence for calculating
                         * the end position of the entry we need to add additional
                         * 4 (intsize of entrySize). Please check
                         * EntryLogger.scanEntryLog.
                         */
                        long entryEndPos = entryStartPos + entrySize + 4 - 1;
                        if (((rangeEndPos == -1) || (entryStartPos <= rangeEndPos))
                                && (rangeStartPos <= entryEndPos)) {
                            formatEntry(entryStartPos, entry, printMsg);
                            entryFound.setValue(true);
                        }
                    }
                }
            }
        });
        if (!entryFound.booleanValue()) {
            System.out.println("Entry log " + logId + " (" + Long.toHexString(logId)
                    + ".log) doesn't has any entry in the range " + rangeStartPos + " - " + rangeEndPos
                    + ". Probably the position range, you have provided is lesser than the LOGFILE_HEADER_SIZE (1024) "
                    + "or greater than the current log filesize.");
        }
    }

    /**
     * Scan a journal file.
     *
     * @param journalId Journal File Id
     * @param printMsg Whether printing the entry data.
     */
    protected void scanJournal(Journal journal, long journalId, final boolean printMsg) throws Exception {
        System.out.println("Scan journal " + journalId + " (" + Long.toHexString(journalId) + ".txn)");
        scanJournal(journal, journalId, new JournalScanner() {
            boolean printJournalVersion = false;

            @Override
            public void process(int journalVersion, long offset, ByteBuffer entry) throws IOException {
                if (!printJournalVersion) {
                    System.out.println("Journal Version : " + journalVersion);
                    printJournalVersion = true;
                }
                formatEntry(offset, Unpooled.wrappedBuffer(entry), printMsg);
            }
        });
    }

    /**
     * Print last log mark.
     */
    protected void printLastLogMark() throws IOException {
        for (Journal journal : getJournals()) {
            LogMark lastLogMark = journal.getLastLogMark().getCurMark();
            System.out.println("LastLogMark: Journal Id - " + lastLogMark.getLogFileId() + "("
                    + Long.toHexString(lastLogMark.getLogFileId()) + ".txn), Pos - "
                    + lastLogMark.getLogFileOffset());
        }
    }

    /**
     * Format the entry into a readable format.
     *
     * @param entry
     *          ledgerentry to print
     * @param printMsg
     *          Whether printing the message body
     */
    private void formatEntry(LedgerEntry entry, boolean printMsg) {
        long ledgerId = entry.getLedgerId();
        long entryId = entry.getEntryId();
        long entrySize = entry.getLength();
        System.out.println("--------- Lid=" + ledgerIdFormatter.formatLedgerId(ledgerId) + ", Eid=" + entryId
                + ", EntrySize=" + entrySize + " ---------");
        if (printMsg) {
            entryFormatter.formatEntry(entry.getEntry());
        }
    }

    /**
     * Format the message into a readable format.
     *
     * @param pos
     *          File offset of the message stored in entry log file
     * @param recBuff
     *          Entry Data
     * @param printMsg
     *          Whether printing the message body
     */
    private void formatEntry(long pos, ByteBuf recBuff, boolean printMsg) {
        int entrySize = recBuff.readableBytes();
        long ledgerId = recBuff.readLong();
        long entryId = recBuff.readLong();

        System.out.println("--------- Lid=" + ledgerIdFormatter.formatLedgerId(ledgerId) + ", Eid=" + entryId
                + ", ByteOffset=" + pos + ", EntrySize=" + entrySize + " ---------");
        if (entryId == Bookie.METAENTRY_ID_LEDGER_KEY) {
            int masterKeyLen = recBuff.readInt();
            byte[] masterKey = new byte[masterKeyLen];
            recBuff.readBytes(masterKey);
            System.out.println("Type:           META");
            System.out.println("MasterKey:      " + bytes2Hex(masterKey));
            System.out.println();
            return;
        }
        if (entryId == Bookie.METAENTRY_ID_FENCE_KEY) {
            System.out.println("Type:           META");
            System.out.println("Fenced");
            System.out.println();
            return;
        }
        // process a data entry
        long lastAddConfirmed = recBuff.readLong();
        System.out.println("Type:           DATA");
        System.out.println("LastConfirmed:  " + lastAddConfirmed);
        if (!printMsg) {
            System.out.println();
            return;
        }
        // skip digest checking
        recBuff.skipBytes(8);
        System.out.println("Data:");
        System.out.println();
        try {
            byte[] ret = new byte[recBuff.readableBytes()];
            recBuff.readBytes(ret);
            entryFormatter.formatEntry(ret);
        } catch (Exception e) {
            System.out.println("N/A. Corrupted.");
        }
        System.out.println();
    }

    static String bytes2Hex(byte[] data) {
        StringBuilder sb = new StringBuilder(data.length * 2);
        Formatter formatter = new Formatter(sb);
        for (byte b : data) {
            formatter.format("%02x", b);
        }
        formatter.close();
        return sb.toString();
    }

    private static int getOptionIntValue(CommandLine cmdLine, String option, int defaultVal) {
        if (cmdLine.hasOption(option)) {
            String val = cmdLine.getOptionValue(option);
            try {
                return Integer.parseInt(val);
            } catch (NumberFormatException nfe) {
                System.err.println("ERROR: invalid value for option " + option + " : " + val);
                return defaultVal;
            }
        }
        return defaultVal;
    }

    private static long getOptionLongValue(CommandLine cmdLine, String option, long defaultVal) {
        if (cmdLine.hasOption(option)) {
            String val = cmdLine.getOptionValue(option);
            try {
                return Long.parseLong(val);
            } catch (NumberFormatException nfe) {
                System.err.println("ERROR: invalid value for option " + option + " : " + val);
                return defaultVal;
            }
        }
        return defaultVal;
    }

    private long getOptionLedgerIdValue(CommandLine cmdLine, String option, long defaultVal) {
        if (cmdLine.hasOption(option)) {
            String val = cmdLine.getOptionValue(option);
            try {
                return ledgerIdFormatter.readLedgerId(val);
            } catch (IllegalArgumentException iae) {
                System.err.println("ERROR: invalid value for option " + option + " : " + val);
                return defaultVal;
            }
        }
        return defaultVal;
    }

    private static boolean getOptionBooleanValue(CommandLine cmdLine, String option, boolean defaultVal) {
        if (cmdLine.hasOption(option)) {
            String val = cmdLine.getOptionValue(option);
            return Boolean.parseBoolean(val);
        }
        return defaultVal;
    }

    private static boolean getOptionalValue(String optValue, String optName) {
        if (StringUtils.equals(optValue, optName)) {
            return true;
        }
        return false;
    }
}