HDDS-4611. Add an admin command to cancel "preparation" of an OM quorum. (#1830)

diff --git a/hadoop-ozone/dist/src/main/compose/ozone-om-prepare/.env b/hadoop-ozone/dist/src/main/compose/ozone-om-prepare/.env
index 5c771f1..20bf1c8 100644
--- a/hadoop-ozone/dist/src/main/compose/ozone-om-prepare/.env
+++ b/hadoop-ozone/dist/src/main/compose/ozone-om-prepare/.env
@@ -19,7 +19,7 @@
 OZONE_IMAGE=apache/ozone-runner:20200625-1
 OZONE_DIR=/opt/hadoop
 OZONE_VOLUME=.
-# Inidcate no arguments to the OM.
+# Indicates no arguments to the OM.
 # This variable must be set to some non-empty value, or docker compose will
 # expand it to an empty string and pass that to the OM as an argument.
 OM_HA_ARGS=--
diff --git a/hadoop-ozone/dist/src/main/compose/ozone-om-prepare/test.sh b/hadoop-ozone/dist/src/main/compose/ozone-om-prepare/test.sh
index ef48c3d..cf05472 100755
--- a/hadoop-ozone/dist/src/main/compose/ozone-om-prepare/test.sh
+++ b/hadoop-ozone/dist/src/main/compose/ozone-om-prepare/test.sh
@@ -43,6 +43,12 @@
 
 # Write data and prepare cluster.
 execute_robot_test scm omha/om-prepare.robot
+
+# Cancel preparation.
+execute_robot_test scm omha/om-cancel-prepare.robot
+
+# Prepare cluster again.
+execute_robot_test scm omha/om-prepare.robot
 execute_robot_test scm omha/om-prepared.robot
 
 # re-start cluster and check that it remains prepared.
@@ -55,7 +61,6 @@
 # re-start cluster with --upgrade flag to take it out of prepare.
 KEEP_RUNNING=false stop_docker_env
 export OM_HA_ARGS='--upgrade'
-export OZONE_KEEP_RESULTS=true
 start_docker_env
 
 # Writes should now succeed.
diff --git a/hadoop-ozone/dist/src/main/smoketest/omha/om-cancel-prepare.robot b/hadoop-ozone/dist/src/main/smoketest/omha/om-cancel-prepare.robot
new file mode 100644
index 0000000..7a81f8c
--- /dev/null
+++ b/hadoop-ozone/dist/src/main/smoketest/omha/om-cancel-prepare.robot
@@ -0,0 +1,43 @@
+# 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.
+
+*** Settings ***
+Documentation       Smoke test for ozone manager cancel prepare
+Library             OperatingSystem
+Library             String
+Library             BuiltIn
+Resource            ../commonlib.robot
+Test Timeout        5 minutes
+Test Setup          Run Keyword if    '${SECURITY_ENABLED}' == 'true'    Kinit test user     testuser     testuser.keytab
+Suite Setup         Generate volume and bucket names
+
+** Keywords ***
+Generate volume and bucket names
+    ${random} =         Generate Random String  5  [NUMBERS]
+    Set Suite Variable  ${volume_name}  ${random}-volume-for-cancel
+    Set Suite Variable  ${bucket_name}  ${random}-bucket-for-cancel
+
+** Test Cases ***
+Cancel Ozone Manager Prepare
+    ${result} =        Execute      ozone admin om cancelprepare -id=omservice
+    Wait Until Keyword Succeeds      3min   5sec    Should contain   ${result}   Cancel prepare succeeded
+
+Test write operations
+    Execute            ozone sh volume create /${volume_name}
+    Execute            ozone sh bucket create /${volume_name}/${bucket_name}
+    ${result} =        Execute   ozone sh key put /${volume_name}/${bucket_name}/cancel-key /opt/hadoop/NOTICE.txt
+    ${result} =        Execute             ozone sh key info /${volume_name}/${bucket_name}/cancel-key
+                       Should contain      ${result}       \"name\" : \"cancel-key\"
+
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/admin/om/CancelPrepareSubCommand.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/admin/om/CancelPrepareSubCommand.java
new file mode 100644
index 0000000..8a159ad
--- /dev/null
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/admin/om/CancelPrepareSubCommand.java
@@ -0,0 +1,53 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.hadoop.ozone.admin.om;
+
+import java.util.concurrent.Callable;
+import org.apache.hadoop.hdds.cli.HddsVersionProvider;
+import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol;
+import picocli.CommandLine;
+
+/**
+ * Handler of ozone admin om cancelprepare command.
+ */
+@CommandLine.Command(
+    name = "cancelprepare",
+    description = "Cancel prepare state in the OMs.",
+    mixinStandardHelpOptions = true,
+    versionProvider = HddsVersionProvider.class
+)
+public class CancelPrepareSubCommand implements Callable<Void> {
+  @CommandLine.ParentCommand
+  private OMAdmin parent;
+
+  @CommandLine.Option(
+      names = {"-id", "--service-id"},
+      description = "Ozone Manager Service ID",
+      required = true
+  )
+  private String omServiceId;
+
+  @Override
+  public Void call() throws Exception {
+    OzoneManagerProtocol client = parent.createOmClient(omServiceId);
+    client.cancelOzoneManagerPrepare();
+    System.out.println("Cancel prepare succeeded, cluster can now accept " +
+        "write requests.");
+    return null;
+  }
+}
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/admin/om/OMAdmin.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/admin/om/OMAdmin.java
index 59b2a1b..cc736ad 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/admin/om/OMAdmin.java
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/admin/om/OMAdmin.java
@@ -55,7 +55,8 @@
     subcommands = {
         FinalizeUpgradeSubCommand.class,
         GetServiceRolesSubcommand.class,
-        PrepareSubCommand.class
+        PrepareSubCommand.class,
+        CancelPrepareSubCommand.class
     })
 @MetaInfServices(SubcommandWithParent.class)
 public class OMAdmin extends GenericCli implements SubcommandWithParent {