Migrate command `autorecovery`

Descriptions of the changes in this PR:

- Using `bkctl` run command `autorecovery`

### Motivation

#2009 

Reviewers: Jia Zhai <zhaijia@apache.org>, Sijie Guo <sijie@apache.org>

This closes #2010 from zymap/command-autorecovery and squashes the following commits:

6fcd96904 [Yong Zhang] Fix validation
cd383f372 [Yong Zhang] Rename package
efb573221 [Yong Zhang] Rename command
89da2852e [Yong Zhang] Fix error in bookieshell
a037501ee [Yong Zhang] Rename args
4bb36b0b3 [Yong Zhang] Fix imports postion
e41a742d3 [Yong Zhang] Add unit test for command `autorecovery`
b0c91f704 [Yong Zhang] Rename file
cb06f66de [Yong Zhang] Migrate command `autorecovery`
ed008f278 [Yong Zhang] Migrate command `whoisauditor`
5b8e0971a [Yong Zhang] Migrate command `Whatisinstanceid`
90c79444d [Yong Zhang] Migrate command `rebuild-db-ledger-locations-index`
848f8527f [Nicolas Michael] ISSUE #2053: Bugfix for Percentile Calculation in FastCodahale Timer Implementation
06f2b6f50 [Yong Zhang] Migrate command `updateledgers`
7ad5849b1 [Yong Zhang] Migrate command `regenerate-interleaved-storage-index-file`
d4dbb6bfb [Dongfa,Huang] Avoid useless verify if LedgerEntryRequest completed
5c150f283 [Enrico Olivelli] Release notes for 4.9.1
1246826ba [Yong Zhang] Migrate command `recover`
1d4cc71fd [Yong Zhang] Migrate command `localconsistencycheck`
67f83620e [Yong Zhang] Migrate command `readledger`
bfbd6b023 [Yong Zhang] Migrate command `decommission`
d40b8b69f [Yong Zhang] Migrate command `readlog`
95d145a15 [Yong Zhang] Migrate command `nukeexistingcluster`
e2b1dc7f3 [Yong Zhang] Migrate command `listunderreplicated`
0988e12c7 [bd2019us] ISSUE #2023: change cached thread pool to fixed thread pool
6a6d7bbd9 [Yong Zhang] Migrate command `initnewcluster`
c391fe58d [Yong Zhang] Migrate command `readlogmetadata`
120d67737 [Yong Zhang] Migrate command `lostbookierecoverydelay`
bf66235e5 [Yong Zhang] Migrate command `deleteledger`
751e55fa4 [Arvin] ISSUE #2020: close db properly to avoid open RocksDB failure at the second time
138a7ae85 [Yong Zhang] Migrate command `metadataformat`
b043d1694 [Yong Zhang] Migrate command `listledgers`
4573285db [Ivan Kelly] Docker autobuild hook
e3d807a32 [Like] Fix IDE complain as there are multi choices for error code
9524a9f4a [Yong Zhang] Migrate command `readjournal`
6c3f33f55 [Yong Zhang] Fix when met unexpect entry id crashed
e35a108c7 [Like] Fix error message for unrecognized number-of-bookies
5902ee27b [Boyang Jerry Peng] fix potential NPE when releasing entry that is null
6aa73ce05 [Ivan Kelly] [RELEASE] Update website to include documentation for 4.8.2
1448d12aa [Yong Zhang] Migrate command `listfilesondisk`
4de598379 [Yong Zhang] Issue #1987: Migrate command `convert-to-interleaved-storage`
468743e7e [Matteo Merli] In DbLedgerStorage use default values when config key is present but empty
f26a4cae0 [Ivan Kelly] Release notes for v4.8.2
ec2636cd2 [Yong Zhang] Issue #1985: Migrate command `convert-to-db-storage`
8cc7239ac [Yong Zhang] Issue #1982: Migrate command `bookiesanity`
fa90f0185 [Yong Zhang] Issue #1980: Migrate command `ledger` from shell to bkctl
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BookieShell.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BookieShell.java
index 3f307f5..eff70ed 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BookieShell.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BookieShell.java
@@ -55,11 +55,10 @@
 import org.apache.bookkeeper.conf.ServerConfiguration;
 import org.apache.bookkeeper.meta.LedgerManager;
 import org.apache.bookkeeper.meta.LedgerMetadataSerDe;
-import org.apache.bookkeeper.meta.LedgerUnderreplicationManager;
 import org.apache.bookkeeper.net.BookieSocketAddress;
-import org.apache.bookkeeper.replication.ReplicationException;
 import org.apache.bookkeeper.tools.cli.commands.autorecovery.ListUnderReplicatedCommand;
 import org.apache.bookkeeper.tools.cli.commands.autorecovery.LostBookieRecoveryDelayCommand;
+import org.apache.bookkeeper.tools.cli.commands.autorecovery.ToggleCommand;
 import org.apache.bookkeeper.tools.cli.commands.autorecovery.WhoIsAuditorCommand;
 import org.apache.bookkeeper.tools.cli.commands.bookie.ConvertToDBStorageCommand;
 import org.apache.bookkeeper.tools.cli.commands.bookie.ConvertToInterleavedStorageCommand;
@@ -114,7 +113,6 @@
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang3.ArrayUtils;
-import org.apache.zookeeper.KeeperException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -1290,43 +1288,10 @@
             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;
-            });
+            ToggleCommand.AutoRecoveryFlags flags = new ToggleCommand.AutoRecoveryFlags()
+                .enable(enable).status(!disable && !enable);
+            ToggleCommand cmd = new ToggleCommand();
+            cmd.apply(bkConf, flags);
 
             return 0;
         }
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/autorecovery/ToggleCommand.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/autorecovery/ToggleCommand.java
new file mode 100644
index 0000000..c145715
--- /dev/null
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/tools/cli/commands/autorecovery/ToggleCommand.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.bookkeeper.tools.cli.commands.autorecovery;
+
+import com.beust.jcommander.Parameter;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import java.util.concurrent.ExecutionException;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import org.apache.bookkeeper.conf.ServerConfiguration;
+import org.apache.bookkeeper.meta.LedgerUnderreplicationManager;
+import org.apache.bookkeeper.meta.MetadataDrivers;
+import org.apache.bookkeeper.meta.exceptions.MetadataException;
+import org.apache.bookkeeper.replication.ReplicationException;
+import org.apache.bookkeeper.tools.cli.helpers.BookieCommand;
+import org.apache.bookkeeper.tools.framework.CliFlags;
+import org.apache.bookkeeper.tools.framework.CliSpec;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Command to enable or disable auto recovery in the cluster.
+ */
+public class ToggleCommand extends BookieCommand<ToggleCommand.AutoRecoveryFlags> {
+
+    static final Logger LOG = LoggerFactory.getLogger(ToggleCommand.class);
+
+    private static final String NAME = "toggle";
+    private static final String DESC = "Enable or disable auto recovery in the cluster. Default is disable.";
+
+    public ToggleCommand() {
+        this(new AutoRecoveryFlags());
+    }
+
+    private ToggleCommand(AutoRecoveryFlags flags) {
+        super(CliSpec.<ToggleCommand.AutoRecoveryFlags>newBuilder()
+            .withName(NAME).withDescription(DESC)
+            .withFlags(flags).build());
+    }
+
+    /**
+     * Flags for auto recovery command.
+     */
+    @Accessors(fluent = true)
+    @Setter
+    public static class AutoRecoveryFlags extends CliFlags {
+
+        @Parameter(names = { "-e", "--enable" }, description = "Enable or disable auto recovery of under replicated "
+                                                               + "ledgers.")
+        private boolean enable;
+
+        @Parameter(names = {"-s", "--status"}, description = "Check the auto recovery status.")
+        private boolean status;
+
+    }
+
+    @Override
+    public boolean apply(ServerConfiguration conf, AutoRecoveryFlags cmdFlags) {
+        try {
+            return handler(conf, cmdFlags);
+        } catch (MetadataException | ExecutionException e) {
+            throw new UncheckedExecutionException(e.getMessage(), e);
+        }
+    }
+
+    private boolean handler(ServerConfiguration conf, AutoRecoveryFlags flags)
+        throws MetadataException, ExecutionException {
+        MetadataDrivers.runFunctionWithLedgerManagerFactory(conf, mFactory -> {
+            try {
+                try (LedgerUnderreplicationManager underreplicationManager = mFactory
+                         .newLedgerUnderreplicationManager()) {
+                    if (flags.status) {
+                        System.out.println("Autorecovery is " + (underreplicationManager.isLedgerReplicationEnabled()
+                                                                     ? "enabled." : "disabled."));
+                        return null;
+                    }
+                    if (flags.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 true;
+    }
+}
diff --git a/tools/ledger/src/main/java/org/apache/bookkeeper/tools/cli/commands/AutoRecoveryCommandGroup.java b/tools/ledger/src/main/java/org/apache/bookkeeper/tools/cli/commands/AutoRecoveryCommandGroup.java
index 814976a1..aa4d7f4 100644
--- a/tools/ledger/src/main/java/org/apache/bookkeeper/tools/cli/commands/AutoRecoveryCommandGroup.java
+++ b/tools/ledger/src/main/java/org/apache/bookkeeper/tools/cli/commands/AutoRecoveryCommandGroup.java
@@ -20,6 +20,7 @@
 
 import static org.apache.bookkeeper.tools.common.BKCommandCategories.CATEGORY_INFRA_SERVICE;
 
+import org.apache.bookkeeper.tools.cli.commands.autorecovery.ToggleCommand;
 import org.apache.bookkeeper.tools.cli.commands.autorecovery.WhoIsAuditorCommand;
 import org.apache.bookkeeper.tools.common.BKFlags;
 import org.apache.bookkeeper.tools.framework.CliCommandGroup;
@@ -38,6 +39,7 @@
         .withDescription(DESC)
         .withCategory(CATEGORY_INFRA_SERVICE)
         .addCommand(new WhoIsAuditorCommand())
+        .addCommand(new ToggleCommand())
         .build();
 
     public AutoRecoveryCommandGroup() {
diff --git a/tools/ledger/src/test/java/org/apache/bookkeeper/tools/cli/commands/autorecovery/AutoRecoveryCommandTest.java b/tools/ledger/src/test/java/org/apache/bookkeeper/tools/cli/commands/autorecovery/AutoRecoveryCommandTest.java
new file mode 100644
index 0000000..50c68d7
--- /dev/null
+++ b/tools/ledger/src/test/java/org/apache/bookkeeper/tools/cli/commands/autorecovery/AutoRecoveryCommandTest.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.bookkeeper.tools.cli.commands.autorecovery;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.powermock.api.mockito.PowerMockito.mock;
+import static org.powermock.api.mockito.PowerMockito.when;
+
+import java.util.function.Function;
+import org.apache.bookkeeper.conf.ServerConfiguration;
+import org.apache.bookkeeper.meta.LedgerManagerFactory;
+import org.apache.bookkeeper.meta.LedgerUnderreplicationManager;
+import org.apache.bookkeeper.meta.MetadataDrivers;
+import org.apache.bookkeeper.replication.ReplicationException;
+import org.apache.bookkeeper.tools.cli.helpers.BookieCommandTestBase;
+import org.apache.zookeeper.KeeperException;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+
+/**
+ * Unit test for {@link ToggleCommand}.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ ToggleCommand.class, MetadataDrivers.class })
+public class AutoRecoveryCommandTest extends BookieCommandTestBase {
+
+    private LedgerManagerFactory ledgerManagerFactory;
+    private LedgerUnderreplicationManager ledgerUnderreplicationManager;
+
+    public AutoRecoveryCommandTest() {
+        super(3, 0);
+    }
+
+    @Override
+    public void setup() throws Exception {
+        super.setup();
+
+        PowerMockito.whenNew(ServerConfiguration.class).withNoArguments().thenReturn(conf);
+
+        ledgerManagerFactory = mock(LedgerManagerFactory.class);
+
+        PowerMockito.mockStatic(MetadataDrivers.class);
+        PowerMockito.doAnswer(invocationOnMock -> {
+            Function<LedgerManagerFactory, ?> function = invocationOnMock.getArgument(1);
+            function.apply(ledgerManagerFactory);
+            return true;
+        }).when(MetadataDrivers.class, "runFunctionWithLedgerManagerFactory", any(ServerConfiguration.class),
+                any(Function.class));
+
+        ledgerUnderreplicationManager = mock(LedgerUnderreplicationManager.class);
+        when(ledgerManagerFactory.newLedgerUnderreplicationManager()).thenReturn(ledgerUnderreplicationManager);
+    }
+
+    @Test
+    public void testWithEnable()
+        throws InterruptedException, ReplicationException.CompatibilityException, KeeperException,
+               ReplicationException.UnavailableException {
+        testCommand("-e");
+        verify(ledgerManagerFactory, times(1)).newLedgerUnderreplicationManager();
+        verify(ledgerUnderreplicationManager, times(1)).isLedgerReplicationEnabled();
+    }
+
+    @Test
+    public void testWithEnableLongArgs() throws ReplicationException.UnavailableException {
+        when(ledgerUnderreplicationManager.isLedgerReplicationEnabled()).thenReturn(false);
+        testCommand("--enable");
+        verify(ledgerUnderreplicationManager, times(1)).enableLedgerReplication();
+    }
+
+    @Test
+    public void testWithLook()
+        throws InterruptedException, ReplicationException.CompatibilityException, KeeperException,
+               ReplicationException.UnavailableException {
+        testCommand("s");
+        verify(ledgerManagerFactory, times(1)).newLedgerUnderreplicationManager();
+        verify(ledgerUnderreplicationManager, times(1)).isLedgerReplicationEnabled();
+    }
+
+    @Test
+    public void testWithNoArgs()
+        throws InterruptedException, ReplicationException.CompatibilityException, KeeperException,
+               ReplicationException.UnavailableException {
+        testCommand("");
+        verify(ledgerManagerFactory, times(1)).newLedgerUnderreplicationManager();
+        verify(ledgerUnderreplicationManager, times(1)).isLedgerReplicationEnabled();
+    }
+
+    @Test
+    public void testWithNoArgsDisable() throws ReplicationException.UnavailableException {
+        when(ledgerUnderreplicationManager.isLedgerReplicationEnabled()).thenReturn(true);
+        testCommand("");
+        verify(ledgerUnderreplicationManager, times(1)).isLedgerReplicationEnabled();
+        verify(ledgerUnderreplicationManager, times(1)).disableLedgerReplication();
+    }
+
+    private void testCommand(String... args) {
+        ToggleCommand cmd = new ToggleCommand();
+        Assert.assertTrue(cmd.apply(bkFlags, args));
+    }
+}