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

errose28 pushed a commit to branch HDDS-14496-zdu
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/HDDS-14496-zdu by this push:
     new c46d5edf657 HDDS-16025. Allow old clients to start finalization on a 
new server. (#11074)
c46d5edf657 is described below

commit c46d5edf657e1838b581d587d8ea3b94678ad4f4
Author: Ethan Rose <[email protected]>
AuthorDate: Thu Aug 27 11:24:01 2026 -0400

    HDDS-16025. Allow old clients to start finalization on a new server. 
(#11074)
---
 .../apache/hadoop/hdds/scm/client/ScmClient.java   |   2 -
 .../java/org/apache/hadoop/hdds/utils/HAUtils.java |   7 +-
 .../ozone/upgrade/RatisBasedVersionManager.java    |   2 +-
 .../hdds/scm/server/SCMClientProtocolServer.java   |   5 +-
 .../scm/server/TestSCMClientProtocolServer.java    |   8 +-
 .../hdds/scm/cli/ContainerOperationClient.java     |   5 -
 .../admin/om/FinalizationStatusSubCommand.java     |   7 +-
 .../ozone/admin/om/FinalizeUpgradeSubCommand.java  |   7 +-
 .../admin/scm/FinalizationScmStatusSubcommand.java |   8 +-
 .../admin/scm/FinalizeScmUpgradeSubcommand.java    |   8 +-
 .../main/java/org/apache/hadoop/ozone/OmUtils.java |   2 +
 .../ozone/om/protocol/OzoneManagerProtocol.java    |   5 +
 .../om/TestUpgradeFinalizationWithOldClients.java  | 158 +++++++++++++
 .../src/main/proto/OmClientProtocol.proto          |  11 +
 .../org/apache/hadoop/ozone/om/OzoneManager.java   |  19 +-
 .../om/ratis/utils/OzoneManagerRatisUtils.java     |   7 +-
 ....java => OMCompleteFinalizeUpgradeRequest.java} |  54 ++---
 ...uest.java => OMFinalizeUpgradeRequestBase.java} |  66 +++---
 .../upgrade/OMStartFinalizeUpgradeRequest.java     | 132 +----------
 .../OMStartFinalizeUpgradeRequestLegacy.java       |  56 +++++
 ...java => OMCompleteFinalizeUpgradeResponse.java} |   8 +-
 .../upgrade/OMStartFinalizeUpgradeResponse.java    |  16 ++
 .../ozone/om/upgrade/OMUpgradeFinalizeService.java |   2 +-
 .../om/ratis/TestOzoneManagerRatisRequest.java     |  14 ++
 ...a => TestOMCompleteFinalizeUpgradeRequest.java} |  22 +-
 .../upgrade/TestOMStartFinalizeUpgradeRequest.java | 249 +--------------------
 ... => TestOMStartFinalizeUpgradeRequestBase.java} | 225 +++++++++----------
 .../TestOMStartFinalizeUpgradeRequestLegacy.java   |  63 ++++++
 ... => TestOMCompleteFinalizeUpgradeResponse.java} |   9 +-
 .../TestOMStartFinalizeUpgradeResponse.java        |  15 ++
 30 files changed, 576 insertions(+), 616 deletions(-)

diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/client/ScmClient.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/client/ScmClient.java
index ea8e575c2c5..2c9e13fb5d3 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/client/ScmClient.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/client/ScmClient.java
@@ -473,8 +473,6 @@ StatusAndMessages queryUpgradeFinalizationProgress(
       String upgradeClientID, boolean force, boolean readonly)
       throws IOException;
 
-  void finalizeUpgrade() throws IOException;
-
   HddsProtos.UpgradeStatus queryUpgradeStatus() throws IOException;
 
   DecommissionScmResponseProto decommissionScm(
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/HAUtils.java 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/HAUtils.java
index 3ae1b451e1f..e4769fae7dd 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/HAUtils.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/HAUtils.java
@@ -165,8 +165,13 @@ public static StorageContainerLocationProtocol 
getScmContainerClient(
 
   public static StorageContainerLocationProtocol getScmContainerClientForNode(
       ConfigurationSource conf, ScmNodeTarget targetScmNode) {
+    return getScmContainerClientForNode(conf, targetScmNode, null);
+  }
+
+  public static StorageContainerLocationProtocol getScmContainerClientForNode(
+      ConfigurationSource conf, ScmNodeTarget targetScmNode, 
UserGroupInformation ugi) {
     SCMContainerLocationFailoverProxyProvider proxyProvider =
-        new SCMContainerLocationFailoverProxyProvider(conf, null);
+        new SCMContainerLocationFailoverProxyProvider(conf, ugi);
     StorageContainerLocationProtocol scmContainerClient =
         TracingUtil.createProxy(
             new StorageContainerLocationProtocolClientSideTranslatorPB(
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/upgrade/RatisBasedVersionManager.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/upgrade/RatisBasedVersionManager.java
index 25f13ca40c3..e6d31cc4275 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/upgrade/RatisBasedVersionManager.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/ozone/upgrade/RatisBasedVersionManager.java
@@ -53,7 +53,7 @@ public void finalizeFromSnapshotIfRequired(Table<String, 
String> finalizationSto
           apparentVersionInNewDB);
       finalizeUpgrade();
       // Update the apparent version in the DB to match the VERSION file.
-      // When finalization is not done with a snapshot, this DB value is 
updated by OMFinalizeUpgradeRequest.
+      // When finalization is not done with a snapshot, this DB value is 
updated by OMCompleteFinalizeUpgradeRequest.
       finalizationStore.put(APPARENT_VERSION_KEY, 
String.valueOf(getApparentVersion().serialize()));
     }
   }
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java
index a58801f0842..d6304413a5a 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java
@@ -1222,7 +1222,10 @@ private void validatePeerScmVersionsBeforeFinalize() 
throws SCMException {
       target.setNodeId(peerId);
       StorageContainerLocationProtocol peerClient = null;
       try {
-        peerClient = HAUtils.getScmContainerClientForNode(conf, target);
+        // Contact the peer SCM as this SCM's service (Kerberos keytab) 
identity, not the remote client's identity.
+        // This runs inside the finalize RPC handler's doAs context, whose UGI 
has no credentials to open a fresh
+        // outbound RPC to the peer SCM.
+        peerClient = HAUtils.getScmContainerClientForNode(conf, target, 
UserGroupInformation.getLoginUser());
         HDDSVersion peerVersion = peerClient.getPeerUpgradeStatus();
         if (!peerVersion.equals(leaderVersion)) {
           LOG.warn("SCM peer {} is running software version {} but leader is 
running version {}. "
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestSCMClientProtocolServer.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestSCMClientProtocolServer.java
index c21fcf11877..564f9433b85 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestSCMClientProtocolServer.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestSCMClientProtocolServer.java
@@ -312,7 +312,7 @@ public void testFinalizeProceedsWhenAllPeersMatch() throws 
IOException {
     try (SCMClientProtocolServer testServer =
              peerCheckServer(finalizationManager, 
Arrays.asList(peerNode("scm2"), peerNode("scm3")));
          MockedStatic<HAUtils> haUtils = mockStatic(HAUtils.class)) {
-      haUtils.when(() -> HAUtils.getScmContainerClientForNode(any(), 
any())).thenReturn(matching);
+      haUtils.when(() -> HAUtils.getScmContainerClientForNode(any(), any(), 
any())).thenReturn(matching);
       testServer.finalizeUpgrade();
     }
     verify(finalizationManager).finalizeUpgrade();
@@ -327,7 +327,7 @@ public void testFinalizeRejectsOlderPeerUnlessForced() 
throws IOException {
     try (SCMClientProtocolServer testServer =
              peerCheckServer(finalizationManager, 
Arrays.asList(peerNode("scm2"), peerNode("scm3")));
          MockedStatic<HAUtils> haUtils = mockStatic(HAUtils.class)) {
-      haUtils.when(() -> HAUtils.getScmContainerClientForNode(any(), 
any())).thenReturn(matching, older);
+      haUtils.when(() -> HAUtils.getScmContainerClientForNode(any(), any(), 
any())).thenReturn(matching, older);
       // A peer on an older version is rejected without force.
       assertThrows(SCMException.class, testServer::finalizeUpgrade);
       verify(finalizationManager, never()).finalizeUpgrade();
@@ -347,7 +347,7 @@ public void 
testFinalizeRejectsUnknownFuturePeerUnlessForced() throws IOExceptio
     try (SCMClientProtocolServer testServer =
              peerCheckServer(finalizationManager, 
Arrays.asList(peerNode("scm2"), peerNode("scm3")));
          MockedStatic<HAUtils> haUtils = mockStatic(HAUtils.class)) {
-      haUtils.when(() -> HAUtils.getScmContainerClientForNode(any(), any()))
+      haUtils.when(() -> HAUtils.getScmContainerClientForNode(any(), any(), 
any()))
           .thenReturn(matching, unknown);
       // A peer on an unrecognized future version is rejected without force.
       assertThrows(SCMException.class, testServer::finalizeUpgrade);
@@ -367,7 +367,7 @@ public void 
testFinalizeRejectsUnreachablePeerUnlessForced() throws IOException
     try (SCMClientProtocolServer testServer =
              peerCheckServer(finalizationManager, 
Collections.singletonList(peerNode("scm2")));
          MockedStatic<HAUtils> haUtils = mockStatic(HAUtils.class)) {
-      haUtils.when(() -> HAUtils.getScmContainerClientForNode(any(), 
any())).thenReturn(unreachable);
+      haUtils.when(() -> HAUtils.getScmContainerClientForNode(any(), any(), 
any())).thenReturn(unreachable);
       // An unreachable peer is rejected without force.
       assertThrows(SCMException.class, testServer::finalizeUpgrade);
       verify(finalizationManager, never()).finalizeUpgrade();
diff --git 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerOperationClient.java
 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerOperationClient.java
index d03a6215a68..4edfc16c852 100644
--- 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerOperationClient.java
+++ 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerOperationClient.java
@@ -614,11 +614,6 @@ public StatusAndMessages queryUpgradeFinalizationProgress(
         upgradeClientID, force, readonly);
   }
 
-  @Override
-  public void finalizeUpgrade() throws IOException {
-    storageContainerLocationClient.finalizeUpgrade();
-  }
-
   @Override
   public HddsProtos.UpgradeStatus queryUpgradeStatus() throws IOException {
     return storageContainerLocationClient.queryUpgradeStatus();
diff --git 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/FinalizationStatusSubCommand.java
 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/FinalizationStatusSubCommand.java
index ef739454b65..af3cfa4ac5c 100644
--- 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/FinalizationStatusSubCommand.java
+++ 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/FinalizationStatusSubCommand.java
@@ -25,14 +25,17 @@
 import picocli.CommandLine;
 
 /**
- * Handler of ozone admin om finalizationstatus command.
+ * Handler of `ozone admin om finalizationstatus` command for new clients to 
finalize old servers that do not support
+ * the current `ozone admin upgrade status` command.
  */
 @CommandLine.Command(
     name = "finalizationstatus",
     description = "Get the finalization status of om cluster.",
     mixinStandardHelpOptions = true,
-    versionProvider = HddsVersionProvider.class
+    versionProvider = HddsVersionProvider.class,
+    hidden = true
 )
+@Deprecated
 public class FinalizationStatusSubCommand implements Callable<Void> {
 
   @CommandLine.Mixin
diff --git 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/FinalizeUpgradeSubCommand.java
 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/FinalizeUpgradeSubCommand.java
index df1e69f147a..9645658bd4a 100644
--- 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/FinalizeUpgradeSubCommand.java
+++ 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/FinalizeUpgradeSubCommand.java
@@ -42,7 +42,8 @@
 import picocli.CommandLine;
 
 /**
- * Handler of ozone admin om finalizeUpgrade command.
+ * Handler of `ozone admin om finalizeUpgrade` command for new clients to 
finalize old servers that do not support the
+ * current `ozone admin upgrade finalize` command.
  */
 @CommandLine.Command(
     name = "finalizeupgrade",
@@ -52,8 +53,10 @@
         + "or the Ozone manager host in a non-HA environment, if none provided 
"
         + "the default from configuration is being used if not ambiguous.",
     mixinStandardHelpOptions = true,
-    versionProvider = HddsVersionProvider.class
+    versionProvider = HddsVersionProvider.class,
+    hidden = true
 )
+@Deprecated
 public class FinalizeUpgradeSubCommand implements Callable<Void> {
 
   @CommandLine.Mixin
diff --git 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/scm/FinalizationScmStatusSubcommand.java
 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/scm/FinalizationScmStatusSubcommand.java
index 97151c49b0f..717ec36d50a 100644
--- 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/scm/FinalizationScmStatusSubcommand.java
+++ 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/scm/FinalizationScmStatusSubcommand.java
@@ -26,13 +26,17 @@
 import picocli.CommandLine;
 
 /**
- * Handler of FinalizationStatus SCM command.
+ * Handler of `ozone admin scm finalizeUpgrade` command for new clients to 
finalize old servers that do not support the
+ * current `ozone admin upgrade status` command.
  */
 @CommandLine.Command(
     name = "finalizationstatus",
     description = "Finalization SCM Status",
     mixinStandardHelpOptions = true,
-    versionProvider = HddsVersionProvider.class)
+    versionProvider = HddsVersionProvider.class,
+    hidden = true
+)
+@Deprecated
 public class FinalizationScmStatusSubcommand extends ScmSubcommand {
 
   @CommandLine.ParentCommand
diff --git 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/scm/FinalizeScmUpgradeSubcommand.java
 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/scm/FinalizeScmUpgradeSubcommand.java
index 4b123c943c1..33da77f9efc 100644
--- 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/scm/FinalizeScmUpgradeSubcommand.java
+++ 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/scm/FinalizeScmUpgradeSubcommand.java
@@ -44,13 +44,17 @@
 import picocli.CommandLine;
 
 /**
- * Handler of Finalize SCM command.
+ * Handler of `ozone admin scm finalizeUpgrade` command for new clients to 
finalize old servers that do not support the
+ * current `ozone admin upgrade finalize` command.
  */
 @CommandLine.Command(
     name = "finalizeupgrade",
     description = "Finalize SCM Upgrade",
     mixinStandardHelpOptions = true,
-    versionProvider = HddsVersionProvider.class)
+    versionProvider = HddsVersionProvider.class,
+    hidden = true
+)
+@Deprecated
 public class FinalizeScmUpgradeSubcommand extends ScmSubcommand {
 
   @CommandLine.ParentCommand
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
index 6de380da870..d589e674b94 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
@@ -305,6 +305,7 @@ public static boolean isReadOnly(OMRequest omRequest) {
       // as deprecated
     case FinalizeUpgrade:
     case StartFinalizeUpgrade:
+    case CompleteFinalizeUpgrade:
     case DeleteOpenKeys:
     case SetS3Secret:
     case RevokeS3Secret:
@@ -430,6 +431,7 @@ public static boolean shouldSendToFollower(OMRequest 
omRequest) {
       // as deprecated
     case FinalizeUpgrade:
     case StartFinalizeUpgrade:
+    case CompleteFinalizeUpgrade:
     case Prepare:
     case CancelPrepare:
     case DeleteOpenKeys:
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
index 22bf9518e9e..7de1715d3ef 100644
--- 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
@@ -494,6 +494,7 @@ ListOpenFilesResult listOpenFiles(String path, int maxKeys, 
String contToken)
    * @throws OMException
    *            when finalization is already in progress.
    */
+  @Deprecated
   UpgradeFinalization.StatusAndMessages finalizeUpgrade(String 
upgradeClientID) throws IOException;
 
   /**
@@ -530,6 +531,9 @@ ListOpenFilesResult listOpenFiles(String path, int maxKeys, 
String contToken)
   OzoneManagerProtocolProtos.QueryUpgradeStatusResponse queryUpgradeStatus() 
throws IOException;
 
   /**
+   * This command is retained so that new clients can query status of old OM 
servers. All new finalize requests should
+   * use `void queryUpgradeStatus()`.
+   *
    * Queries the current status of finalization.
    * This method when called, returns the status messages from the finalization
    * progress, if any. The status returned is
@@ -557,6 +561,7 @@ ListOpenFilesResult listOpenFiles(String path, int maxKeys, 
String contToken)
    * @throws OMException
    *            if finalization is needed but not yet started
    */
+  @Deprecated
   UpgradeFinalization.StatusAndMessages queryUpgradeFinalizationProgress(
       String upgradeClientID, boolean takeover, boolean readonly
   ) throws IOException;
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestUpgradeFinalizationWithOldClients.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestUpgradeFinalizationWithOldClients.java
new file mode 100644
index 00000000000..8f41417aec3
--- /dev/null
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestUpgradeFinalizationWithOldClients.java
@@ -0,0 +1,158 @@
+/*
+ * 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.hadoop.ozone.om;
+
+import static org.apache.ozone.test.GenericTestUtils.waitFor;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.UUID;
+import org.apache.hadoop.hdds.HDDSVersion;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.scm.protocol.StorageContainerLocationProtocol;
+import org.apache.hadoop.hdds.scm.server.SCMStorageConfig;
+import org.apache.hadoop.hdds.scm.server.StorageContainerManager;
+import org.apache.hadoop.hdds.upgrade.HDDSLayoutFeature;
+import org.apache.hadoop.ozone.HddsDatanodeService;
+import org.apache.hadoop.ozone.MiniOzoneCluster;
+import org.apache.hadoop.ozone.OzoneManagerVersion;
+import org.apache.hadoop.ozone.UniformDatanodesFactory;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.container.upgrade.DatanodeVersionManager;
+import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol;
+import org.apache.hadoop.ozone.om.upgrade.OMLayoutFeature;
+import org.apache.hadoop.ozone.upgrade.UpgradeFinalization;
+import org.apache.hadoop.ozone.upgrade.UpgradeFinalization.StatusAndMessages;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Verifies that the finalize upgrade RPCs sent by old CLIs still finalize a 
new server.
+ *
+ * <p>In the new finalization model a single call to OM drives the whole 
cluster: OM triggers SCM
+ * finalization and then finalizes itself once SCM reports done. Old CLIs 
predate this and issue
+ * the deprecated {@code FinalizeUpgrade} RPC to OM (and could target SCM 
directly). The server must
+ * keep honoring those RPCs.
+ */
+class TestUpgradeFinalizationWithOldClients {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(TestUpgradeFinalizationWithOldClients.class);
+
+  /**
+   * An old {@code ozone admin om finalizeupgrade} CLI issues the deprecated 
{@code FinalizeUpgrade}
+   * RPC via {@link OzoneManagerProtocol#finalizeUpgrade(String)} and then 
polls
+   * {@link OzoneManagerProtocol#queryUpgradeFinalizationProgress(String, 
boolean, boolean)} until
+   * finalization is done. This drives the whole cluster: OM, SCM, and the 
datanode all start
+   * pre-finalized and end finalized.
+   *
+   * <p>It also exercises the deprecated SCM finalize API used by an old
+   * {@code ozone admin scm finalizeupgrade} CLI: finalizing SCM directly is 
now a no-op (SCM
+   * finalization is driven from OM), while SCM's finalization status remains 
individually queryable.
+   */
+  @Test
+  void testFinalizeUpgradeWithOldClientRpcs() throws Exception {
+    OzoneConfiguration conf = new OzoneConfiguration();
+    conf.set(OMConfigKeys.OZONE_OM_UPGRADE_FINALIZATION_CHECK_INTERVAL, 
"10ms");
+    // Start OM, SCM, and the datanode pre-finalized: new binaries sitting on 
the initial apparent version,
+    conf.setInt(OMStorage.TESTING_INIT_APPARENT_VERSION_KEY, 
OMLayoutFeature.INITIAL_VERSION.serialize());
+    conf.setInt(SCMStorageConfig.TESTING_INIT_APPARENT_VERSION_KEY, 
HDDSLayoutFeature.INITIAL_VERSION.serialize());
+
+    try (MiniOzoneCluster cluster = MiniOzoneCluster.newBuilder(conf)
+        .setNumDatanodes(1)
+        .setDatanodeFactory(UniformDatanodesFactory.newBuilder()
+            .setApparentVersion(HDDSLayoutFeature.INITIAL_VERSION)
+            .build())
+        .build()) {
+      cluster.waitForClusterToBeReady();
+      OzoneManager om = cluster.getOzoneManager();
+      StorageContainerManager scm = cluster.getStorageContainerManager();
+      StorageContainerLocationProtocol scmClient = 
cluster.getStorageContainerLocationClient();
+
+      try (OzoneClient client = cluster.newClient()) {
+        OzoneManagerProtocol omClient = 
client.getObjectStore().getClientProxy().getOzoneManagerClient();
+
+        // OM, SCM, and the datanode all start pre-finalized (needing 
finalization). The datanode's
+        // state is read directly off the running datanode instance rather 
than through a client RPC.
+        assertEquals(OMLayoutFeature.INITIAL_VERSION, 
om.getVersionManager().getApparentVersion());
+        assertTrue(om.getVersionManager().needsFinalization());
+        assertEquals(HDDSLayoutFeature.INITIAL_VERSION, 
scm.getVersionManager().getApparentVersion());
+        assertTrue(scm.getVersionManager().needsFinalization());
+        for (HddsDatanodeService dn : cluster.getHddsDatanodes()) {
+          DatanodeVersionManager dnVersionManager = 
dn.getDatanodeStateMachine().getVersionManager();
+          assertEquals(HDDSLayoutFeature.INITIAL_VERSION, 
dnVersionManager.getApparentVersion());
+          assertTrue(dnVersionManager.needsFinalization());
+        }
+
+        // SCM's finalization status is individually queryable through the 
deprecated SCM API.
+        StatusAndMessages scmProgressBefore = 
scmClient.queryUpgradeFinalizationProgress(
+            "Upgrade-Client-" + UUID.randomUUID(), false, false);
+        assertEquals(UpgradeFinalization.Status.FINALIZATION_REQUIRED, 
scmProgressBefore.status());
+
+        // Finalizing SCM directly via the deprecated API is a no-op: it 
reports ALREADY_FINALIZED to
+        // let old scripts move on, but it does not actually finalize SCM 
(that is driven from OM).
+        StatusAndMessages scmFinalizeResponse =
+            scmClient.finalizeScmUpgrade("Upgrade-Client-" + 
UUID.randomUUID());
+        
assertTrue(UpgradeFinalization.isFinalized(scmFinalizeResponse.status()),
+            "Expected ALREADY_FINALIZED but got " + 
scmFinalizeResponse.status());
+        assertTrue(scm.getVersionManager().needsFinalization(),
+            "Finalizing SCM directly through the legacy API must not finalize 
SCM");
+        assertEquals(UpgradeFinalization.Status.FINALIZATION_REQUIRED,
+            scmClient.queryUpgradeFinalizationProgress("Upgrade-Client-" + 
UUID.randomUUID(), false, false).status());
+
+        // The old CLI initiates finalization with the deprecated 
FinalizeUpgrade RPC. The server
+        // reports STARTING_FINALIZATION so the old CLI proceeds to monitor 
progress.
+        String upgradeClientID = "Upgrade-Client-" + UUID.randomUUID();
+        StatusAndMessages started = omClient.finalizeUpgrade(upgradeClientID);
+        assertTrue(UpgradeFinalization.isStarting(started.status()),
+            "Expected STARTING_FINALIZATION but got " + started.status());
+
+        // The old CLI polls progress until finalization is done, exactly as 
FinalizeUpgradeSubCommand does.
+        waitFor(() -> {
+          try {
+            StatusAndMessages progress =
+                omClient.queryUpgradeFinalizationProgress(upgradeClientID, 
false, false);
+            LOG.info("Waiting for OM finalization to finish. Current status: 
{}", progress.status());
+            return UpgradeFinalization.isDone(progress.status());
+          } catch (IOException e) {
+            throw new UncheckedIOException(e);
+          }
+        }, 2000, 60000);
+
+        // OM and SCM are finalized: OM advanced to the software version and 
both report FINALIZED.
+        assertEquals(OzoneManagerVersion.SOFTWARE_VERSION, 
om.getVersionManager().getApparentVersion());
+        assertEquals(HDDSVersion.SOFTWARE_VERSION, 
scm.getVersionManager().getApparentVersion());
+
+        // SCM now reports done through the deprecated SCM API too.
+        assertTrue(UpgradeFinalization.isDone(
+            scmClient.queryUpgradeFinalizationProgress(upgradeClientID, false, 
false).status()));
+
+        // Server side enforces finalization order of SCM->DNs->OM. Since we 
already waited for OM to finalize,
+        // Datanodes should now be finalized.
+        for (HddsDatanodeService dn : cluster.getHddsDatanodes()) {
+          DatanodeVersionManager dnVersionManager = 
dn.getDatanodeStateMachine().getVersionManager();
+          assertFalse(dnVersionManager.needsFinalization());
+          assertEquals(HDDSVersion.SOFTWARE_VERSION, 
dnVersionManager.getApparentVersion());
+        }
+      }
+    }
+  }
+}
diff --git 
a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto 
b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index 1b6ada0edca..11d9f1ea414 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -169,6 +169,7 @@ enum Type {
   SaveLifecycleScanState       = 152;
   StartFinalizeUpgrade = 153;
   QueryUpgradeStatus = 154;
+  CompleteFinalizeUpgrade = 155;
 }
 
 enum SafeMode {
@@ -334,6 +335,7 @@ message OMRequest {
   optional SaveLifecycleScanStateRequest saveLifecycleScanStateRequest = 153;
   optional StartFinalizeUpgradeRequest      startFinalizeUpgradeRequest     = 
154;
   optional QueryUpgradeStatusRequest        queryUpgradeStatusRequest       = 
155;
+  optional CompleteFinalizeUpgradeRequest   completeFinalizeUpgradeRequest  = 
156;
 }
 
 message OMResponse {
@@ -482,6 +484,7 @@ message OMResponse {
   optional SaveLifecycleScanStateResponse saveLifecycleScanStateResponse = 152;
   optional StartFinalizeUpgradeResponse      startFinalizeUpgradeResponse  = 
153;
   optional QueryUpgradeStatusResponse        queryUpgradeStatusResponse    = 
154;
+  optional CompleteFinalizeUpgradeResponse   completeFinalizeUpgradeResponse = 
155;
 }
 
 enum Status {
@@ -1693,6 +1696,14 @@ message StartFinalizeUpgradeRequest {
 message StartFinalizeUpgradeResponse {
 }
 
+// Internal request submitted by OMUpgradeFinalizeService to complete OM 
finalization
+// once SCM reports FINALIZED.
+message CompleteFinalizeUpgradeRequest {
+}
+
+message CompleteFinalizeUpgradeResponse {
+}
+
 message QueryUpgradeStatusRequest {
 }
 
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
index 79c8f0314be..e9b1a92f832 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
@@ -111,8 +111,6 @@
 import static 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OzoneManagerService;
 import static 
org.apache.hadoop.ozone.upgrade.UpgradeFinalization.FINALIZATION_DONE_MSG;
 import static 
org.apache.hadoop.ozone.upgrade.UpgradeFinalization.FINALIZATION_REQUIRED_MSG;
-import static 
org.apache.hadoop.ozone.upgrade.UpgradeFinalization.FINALIZED_MSG;
-import static org.apache.hadoop.ozone.upgrade.UpgradeFinalization.STARTING_MSG;
 import static org.apache.hadoop.security.UserGroupInformation.getCurrentUser;
 import static org.apache.hadoop.util.ExitUtil.terminate;
 import static org.apache.hadoop.util.Time.monotonicNow;
@@ -3760,22 +3758,15 @@ public boolean triggerSnapshotDefrag(boolean noWait) 
throws IOException {
   }
 
   @Override
-  public StatusAndMessages finalizeUpgrade(String unusedUpgradeClientId)
-      throws IOException {
-    if (!versionManager.needsFinalization()) {
-      return FINALIZED_MSG;
-    }
-    versionManager.finalizeUpgrade();
-    // Old OM clients currently require STARTING_MSG to be returned when this 
method succeeds.
-
-    return STARTING_MSG;
+  public StatusAndMessages finalizeUpgrade(String unusedUpgradeClientId) {
+    // Server-side stub; the real implementation is handled via the Ratis 
request path through
+    // OMStartFinalizeUpgradeRequestLegacy
+    throw new UnsupportedOperationException();
   }
 
   @Override
   public void finalizeUpgrade() throws IOException {
-    // Server-side stub; the real implementation is handled via the Ratis 
request path through
-    // OMStartFinalizeUpgradeRequest
-    throw new UnsupportedOperationException();
+    versionManager.finalizeUpgrade();
   }
 
   @Override
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java
index 01fe6b10d0b..65da8398c1e 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/utils/OzoneManagerRatisUtils.java
@@ -90,8 +90,9 @@
 import org.apache.hadoop.ozone.om.request.snapshot.OMSnapshotPurgeRequest;
 import org.apache.hadoop.ozone.om.request.snapshot.OMSnapshotRenameRequest;
 import 
org.apache.hadoop.ozone.om.request.snapshot.OMSnapshotSetPropertyRequest;
-import org.apache.hadoop.ozone.om.request.upgrade.OMFinalizeUpgradeRequest;
+import 
org.apache.hadoop.ozone.om.request.upgrade.OMCompleteFinalizeUpgradeRequest;
 import 
org.apache.hadoop.ozone.om.request.upgrade.OMStartFinalizeUpgradeRequest;
+import 
org.apache.hadoop.ozone.om.request.upgrade.OMStartFinalizeUpgradeRequestLegacy;
 import org.apache.hadoop.ozone.om.request.util.OMEchoRPCWriteRequest;
 import org.apache.hadoop.ozone.om.request.volume.OMQuotaRepairRequest;
 import org.apache.hadoop.ozone.om.request.volume.OMVolumeCreateRequest;
@@ -189,9 +190,11 @@ public static OMClientRequest 
createClientRequest(OMRequest omRequest,
     case GetS3Secret:
       return new S3GetSecretRequest(omRequest);
     case FinalizeUpgrade:
-      return new OMFinalizeUpgradeRequest(omRequest);
+      return new OMStartFinalizeUpgradeRequestLegacy(omRequest);
     case StartFinalizeUpgrade:
       return new OMStartFinalizeUpgradeRequest(omRequest);
+    case CompleteFinalizeUpgrade:
+      return new OMCompleteFinalizeUpgradeRequest(omRequest);
     case SetS3Secret:
       return new OMSetSecretRequest(omRequest);
     case RevokeS3Secret:
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMFinalizeUpgradeRequest.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMCompleteFinalizeUpgradeRequest.java
similarity index 64%
rename from 
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMFinalizeUpgradeRequest.java
rename to 
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMCompleteFinalizeUpgradeRequest.java
index 06c3a9bcaf5..2f8c42f0a45 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMFinalizeUpgradeRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMCompleteFinalizeUpgradeRequest.java
@@ -18,11 +18,10 @@
 package org.apache.hadoop.ozone.om.request.upgrade;
 
 import static org.apache.hadoop.ozone.OzoneConsts.APPARENT_VERSION_KEY;
-import static 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type.FinalizeUpgrade;
+import static 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type.CompleteFinalizeUpgrade;
 
 import java.io.IOException;
 import java.util.HashMap;
-import 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.UpgradeFinalizationStatus;
 import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
 import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
 import org.apache.hadoop.ozone.OzoneConsts;
@@ -30,30 +29,27 @@
 import org.apache.hadoop.ozone.audit.OMAction;
 import org.apache.hadoop.ozone.om.OMMetadataManager;
 import org.apache.hadoop.ozone.om.OzoneManager;
-import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
 import org.apache.hadoop.ozone.om.request.OMClientRequest;
 import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
 import org.apache.hadoop.ozone.om.response.OMClientResponse;
-import org.apache.hadoop.ozone.om.response.upgrade.OMFinalizeUpgradeResponse;
+import 
org.apache.hadoop.ozone.om.response.upgrade.OMCompleteFinalizeUpgradeResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
-import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.FinalizeUpgradeRequest;
-import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.FinalizeUpgradeResponse;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CompleteFinalizeUpgradeResponse;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
-import org.apache.hadoop.ozone.upgrade.UpgradeFinalization.StatusAndMessages;
-import org.apache.hadoop.security.UserGroupInformation;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Handles finalizeUpgrade request.
+ * Completes OM finalization once SCM has finalized. Submitted internally over 
Ratis by
+ * {@code OMUpgradeFinalizeService}; no client ever sends this request.
  */
-public class OMFinalizeUpgradeRequest extends OMClientRequest {
+public class OMCompleteFinalizeUpgradeRequest extends OMClientRequest {
   private static final Logger LOG =
-      LoggerFactory.getLogger(OMFinalizeUpgradeRequest.class);
+      LoggerFactory.getLogger(OMCompleteFinalizeUpgradeRequest.class);
 
-  public OMFinalizeUpgradeRequest(OMRequest omRequest) {
+  public OMCompleteFinalizeUpgradeRequest(OMRequest omRequest) {
     super(omRequest);
   }
 
@@ -64,32 +60,12 @@ public OMClientResponse validateAndUpdateCache(OzoneManager 
ozoneManager, Execut
     OzoneManagerProtocolProtos.UserInfo userInfo = 
getOmRequest().getUserInfo();
     OMResponse.Builder responseBuilder =
         OmResponseUtil.getOMResponseBuilder(getOmRequest());
-    responseBuilder.setCmdType(FinalizeUpgrade);
+    responseBuilder.setCmdType(CompleteFinalizeUpgrade);
     OMClientResponse response = null;
     Exception exception = null;
 
     try {
-      if (ozoneManager.isAdminAuthorizationEnabled()) {
-        UserGroupInformation ugi = createUGIForApi();
-        if (!ozoneManager.isAdmin(ugi)) {
-          throw new OMException("Access denied for user " + ugi + ". "
-              + "Superuser privilege is required to finalize upgrade.",
-              OMException.ResultCodes.ACCESS_DENIED);
-        }
-      }
-
-      FinalizeUpgradeRequest request =
-          getOmRequest().getFinalizeUpgradeRequest();
-      String upgradeClientID = request.getUpgradeClientId();
-      StatusAndMessages omStatus =
-          ozoneManager.finalizeUpgrade(upgradeClientID);
-
-      UpgradeFinalizationStatus.Status protoStatus =
-          UpgradeFinalizationStatus.Status.valueOf(omStatus.status().name());
-      UpgradeFinalizationStatus responseStatus =
-          UpgradeFinalizationStatus.newBuilder()
-              .setStatus(protoStatus)
-              .build();
+      ozoneManager.finalizeUpgrade();
 
       OMMetadataManager omMetadataManager = ozoneManager.getMetadataManager();
       int apparentVersion = 
ozoneManager.getVersionManager().getApparentVersion().serialize();
@@ -101,17 +77,13 @@ public OMClientResponse 
validateAndUpdateCache(OzoneManager ozoneManager, Execut
           new CacheKey<>(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY), 
CacheValue.get(context.getIndex()));
       ozoneManager.getMetrics().setFinalizationInProgress(false);
 
-      FinalizeUpgradeResponse omResponse =
-          FinalizeUpgradeResponse.newBuilder()
-              .setStatus(responseStatus)
-              .build();
-      responseBuilder.setFinalizeUpgradeResponse(omResponse);
-      response = new OMFinalizeUpgradeResponse(responseBuilder.build(),
+      
responseBuilder.setCompleteFinalizeUpgradeResponse(CompleteFinalizeUpgradeResponse.newBuilder().build());
+      response = new OMCompleteFinalizeUpgradeResponse(responseBuilder.build(),
           ozoneManager.getVersionManager().getApparentVersion().serialize());
       LOG.trace("Returning response: {}", response);
     } catch (IOException e) {
       exception = e;
-      response = new OMFinalizeUpgradeResponse(
+      response = new OMCompleteFinalizeUpgradeResponse(
           createErrorOMResponse(responseBuilder, e), -1);
     }
 
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMStartFinalizeUpgradeRequest.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMFinalizeUpgradeRequestBase.java
similarity index 69%
copy from 
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMStartFinalizeUpgradeRequest.java
copy to 
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMFinalizeUpgradeRequestBase.java
index cce820afdc4..4467a74905c 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMStartFinalizeUpgradeRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMFinalizeUpgradeRequestBase.java
@@ -17,9 +17,6 @@
 
 package org.apache.hadoop.ozone.om.request.upgrade;
 
-import static org.apache.hadoop.hdds.utils.HddsServerUtil.getRemoteUser;
-import static 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type.StartFinalizeUpgrade;
-
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -52,12 +49,15 @@
 import org.slf4j.LoggerFactory;
 
 /**
- * Starts the cluster upgrade finalization process.
+ * Shared logic for the client-initiated finalize requests between the current 
request and legacy request kept for
+ * backwards compatibility with old clients. It triggers SCM finalization
+ * and writes the finalization-in-progress key so that {@code 
OMUpgradeFinalizeService}
+ * can drive OM finalization to completion asynchronously.
  */
-public class OMStartFinalizeUpgradeRequest extends OMClientRequest {
-  private static final Logger LOG = 
LoggerFactory.getLogger(OMStartFinalizeUpgradeRequest.class);
+public abstract class OMFinalizeUpgradeRequestBase extends OMClientRequest {
+  private static final Logger LOG = 
LoggerFactory.getLogger(OMFinalizeUpgradeRequestBase.class);
 
-  public OMStartFinalizeUpgradeRequest(OMRequest omRequest) {
+  public OMFinalizeUpgradeRequestBase(OMRequest omRequest) {
     super(omRequest);
   }
 
@@ -68,10 +68,10 @@ public OMRequest preExecute(OzoneManager ozoneManager) 
throws IOException {
       UserGroupInformation ugi = createUGIForApi();
       if (!ozoneManager.isAdmin(ugi)) {
         throw new OMException("Access denied for user " + ugi + ". "
-            + "Superuser privilege is required to start finalize upgrade.", 
OMException.ResultCodes.ACCESS_DENIED);
+            + "Superuser privilege is required to finalize upgrade.", 
OMException.ResultCodes.ACCESS_DENIED);
       }
     }
-    boolean force = getOmRequest().getStartFinalizeUpgradeRequest().getForce();
+    boolean force = isForce();
     if (force) {
       LOG.warn("Forcing upgrade finalization by skipping OM peer software 
version checks");
     } else {
@@ -100,22 +100,26 @@ public OMClientResponse 
validateAndUpdateCache(OzoneManager ozoneManager, Execut
     AuditLogger auditLogger = ozoneManager.getSystemAuditLogger();
     OzoneManagerProtocolProtos.UserInfo userInfo = 
getOmRequest().getUserInfo();
     OMResponse.Builder responseBuilder = 
OmResponseUtil.getOMResponseBuilder(getOmRequest());
-    responseBuilder.setCmdType(StartFinalizeUpgrade);
     OMClientResponse response = null;
     Exception exception = null;
 
     try {
-      OMMetadataManager omMetadataManager = ozoneManager.getMetadataManager();
-      omMetadataManager.getMetaTable().addCacheEntry(
-          new CacheKey<>(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY), 
CacheValue.get(context.getIndex(), "ignored"));
-      ozoneManager.getMetrics().setFinalizationInProgress(true);
-
+      // Only mark finalization in progress when OM actually needs to 
finalize. If an admin initiates finalize on an
+      // already-finalized cluster, the marker would be orphaned: the async 
OMUpgradeFinalizeService shuts down because
+      // needsFinalization() is false and would never clear it. The response 
mirrors this so the marker is not
+      // persisted to the DB when the double buffer flushes either.
+      boolean finalizationNeeded = 
ozoneManager.getVersionManager().needsFinalization();
+      if (finalizationNeeded) {
+        OMMetadataManager omMetadataManager = 
ozoneManager.getMetadataManager();
+        omMetadataManager.getMetaTable().addCacheEntry(
+            new CacheKey<>(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY), 
CacheValue.get(context.getIndex(), "ignored"));
+        ozoneManager.getMetrics().setFinalizationInProgress(true);
+      } else {
+        LOG.info("OM does not need finalization; skipping 
finalization-in-progress marker.");
+      }
 
-      OzoneManagerProtocolProtos.StartFinalizeUpgradeResponse omResponse =
-          OzoneManagerProtocolProtos.StartFinalizeUpgradeResponse.newBuilder()
-              .build();
-      responseBuilder.setStartFinalizeUpgradeResponse(omResponse);
-      response = new OMStartFinalizeUpgradeResponse(responseBuilder.build());
+      setResponseBody(responseBuilder, ozoneManager);
+      response = new OMStartFinalizeUpgradeResponse(responseBuilder.build(), 
finalizationNeeded);
       LOG.trace("Returning response: {}", response);
     } catch (Exception e) {
       exception = e;
@@ -123,13 +127,23 @@ public OMClientResponse 
validateAndUpdateCache(OzoneManager ozoneManager, Execut
     }
 
     Map<String, String> auditMap = new HashMap<>();
-    auditMap.put("force", 
String.valueOf(getOmRequest().getStartFinalizeUpgradeRequest().getForce()));
+    auditMap.put("force", String.valueOf(isForce()));
     markForAudit(auditLogger, buildAuditMessage(OMAction.UPGRADE_FINALIZE, 
auditMap, exception, userInfo));
     return response;
   }
 
-  private static void validatePeerOmVersionsBeforeFinalize(List<OMNodeDetails> 
peerNodes,
-      OzoneConfiguration configuration) throws OMException {
+  /**
+   * Whether SCM finalization should be forced (skipping OM peer software 
version checks).
+   */
+  protected abstract boolean isForce();
+
+  /**
+   * Sets the command-type-specific response body on the given builder 
(including cmdType).
+   */
+  protected abstract void setResponseBody(OMResponse.Builder builder, 
OzoneManager ozoneManager);
+
+  static void validatePeerOmVersionsBeforeFinalize(List<OMNodeDetails> 
peerNodes, OzoneConfiguration configuration)
+      throws OMException {
     if (peerNodes.isEmpty()) {
       return;
     }
@@ -139,7 +153,10 @@ private static void 
validatePeerOmVersionsBeforeFinalize(List<OMNodeDetails> pee
       String peerId = peerDetails.getNodeId();
       OMAdminProtocolClientSideImpl client = null;
       try {
-        client = 
OMAdminProtocolClientSideImpl.createProxyForSingleOM(configuration, 
getRemoteUser(), peerDetails);
+        // Use the OM service login (Kerberos keytab) identity, not the remote 
client's identity: this runs inside the
+        // client's RPC handler thread, whose UGI has no credentials to open a 
fresh outbound RPC to the peer OM.
+        client = OMAdminProtocolClientSideImpl.createProxyForSingleOM(
+            configuration, UserGroupInformation.getLoginUser(), peerDetails);
         OzoneManagerVersion peerVersion = client.getPeerUpgradeStatus();
         if (!peerVersion.equals(leaderVersion)) {
           LOG.warn("OM peer {} is running software version {} but leader is 
running version {}. "
@@ -159,5 +176,4 @@ private static void 
validatePeerOmVersionsBeforeFinalize(List<OMNodeDetails> pee
           OMException.ResultCodes.NOT_SUPPORTED_OPERATION);
     }
   }
-
 }
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMStartFinalizeUpgradeRequest.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMStartFinalizeUpgradeRequest.java
index cce820afdc4..7441645f585 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMStartFinalizeUpgradeRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMStartFinalizeUpgradeRequest.java
@@ -17,147 +17,31 @@
 
 package org.apache.hadoop.ozone.om.request.upgrade;
 
-import static org.apache.hadoop.hdds.utils.HddsServerUtil.getRemoteUser;
 import static 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type.StartFinalizeUpgrade;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.apache.hadoop.hdds.conf.OzoneConfiguration;
-import org.apache.hadoop.hdds.scm.exceptions.SCMException;
-import org.apache.hadoop.hdds.utils.IOUtils;
-import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
-import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
-import org.apache.hadoop.ozone.OzoneConsts;
-import org.apache.hadoop.ozone.OzoneManagerVersion;
-import org.apache.hadoop.ozone.audit.AuditLogger;
-import org.apache.hadoop.ozone.audit.OMAction;
-import org.apache.hadoop.ozone.om.OMMetadataManager;
 import org.apache.hadoop.ozone.om.OzoneManager;
-import org.apache.hadoop.ozone.om.exceptions.OMException;
-import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
-import org.apache.hadoop.ozone.om.helpers.OMNodeDetails;
-import org.apache.hadoop.ozone.om.protocolPB.OMAdminProtocolClientSideImpl;
-import org.apache.hadoop.ozone.om.request.OMClientRequest;
-import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
-import org.apache.hadoop.ozone.om.response.OMClientResponse;
-import 
org.apache.hadoop.ozone.om.response.upgrade.OMStartFinalizeUpgradeResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
-import org.apache.hadoop.security.UserGroupInformation;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
- * Starts the cluster upgrade finalization process.
+ * Starts the cluster upgrade finalization process via {@code ozone admin 
upgrade finalize}.
  */
-public class OMStartFinalizeUpgradeRequest extends OMClientRequest {
-  private static final Logger LOG = 
LoggerFactory.getLogger(OMStartFinalizeUpgradeRequest.class);
+public class OMStartFinalizeUpgradeRequest extends 
OMFinalizeUpgradeRequestBase {
 
   public OMStartFinalizeUpgradeRequest(OMRequest omRequest) {
     super(omRequest);
   }
 
   @Override
-  public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
-    OMRequest omRequest = super.preExecute(ozoneManager);
-    if (ozoneManager.isAdminAuthorizationEnabled()) {
-      UserGroupInformation ugi = createUGIForApi();
-      if (!ozoneManager.isAdmin(ugi)) {
-        throw new OMException("Access denied for user " + ugi + ". "
-            + "Superuser privilege is required to start finalize upgrade.", 
OMException.ResultCodes.ACCESS_DENIED);
-      }
-    }
-    boolean force = getOmRequest().getStartFinalizeUpgradeRequest().getForce();
-    if (force) {
-      LOG.warn("Forcing upgrade finalization by skipping OM peer software 
version checks");
-    } else {
-      validatePeerOmVersionsBeforeFinalize(ozoneManager.getPeerNodes(), 
ozoneManager.getConfiguration());
-    }
-
-    try {
-      if (force) {
-        
ozoneManager.getScmClient().getContainerClient().forceFinalizeUpgrade();
-      } else {
-        ozoneManager.getScmClient().getContainerClient().finalizeUpgrade();
-      }
-    } catch (SCMException e) {
-      if (e.getResult() == SCMException.ResultCodes.UNSUPPORTED_OPERATION) {
-        throw new OMException(e.getMessage(), e, 
OMException.ResultCodes.NOT_SUPPORTED_OPERATION);
-      }
-      throw e;
-    }
-    LOG.info("Successfully triggered the finalize upgrade process in SCM");
-    return omRequest;
+  protected boolean isForce() {
+    return getOmRequest().getStartFinalizeUpgradeRequest().getForce();
   }
 
   @Override
-  public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, 
ExecutionContext context) {
-    LOG.trace("Request: {}", getOmRequest());
-    AuditLogger auditLogger = ozoneManager.getSystemAuditLogger();
-    OzoneManagerProtocolProtos.UserInfo userInfo = 
getOmRequest().getUserInfo();
-    OMResponse.Builder responseBuilder = 
OmResponseUtil.getOMResponseBuilder(getOmRequest());
-    responseBuilder.setCmdType(StartFinalizeUpgrade);
-    OMClientResponse response = null;
-    Exception exception = null;
-
-    try {
-      OMMetadataManager omMetadataManager = ozoneManager.getMetadataManager();
-      omMetadataManager.getMetaTable().addCacheEntry(
-          new CacheKey<>(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY), 
CacheValue.get(context.getIndex(), "ignored"));
-      ozoneManager.getMetrics().setFinalizationInProgress(true);
-
-
-      OzoneManagerProtocolProtos.StartFinalizeUpgradeResponse omResponse =
-          OzoneManagerProtocolProtos.StartFinalizeUpgradeResponse.newBuilder()
-              .build();
-      responseBuilder.setStartFinalizeUpgradeResponse(omResponse);
-      response = new OMStartFinalizeUpgradeResponse(responseBuilder.build());
-      LOG.trace("Returning response: {}", response);
-    } catch (Exception e) {
-      exception = e;
-      response = new 
OMStartFinalizeUpgradeResponse(createErrorOMResponse(responseBuilder, e));
-    }
-
-    Map<String, String> auditMap = new HashMap<>();
-    auditMap.put("force", 
String.valueOf(getOmRequest().getStartFinalizeUpgradeRequest().getForce()));
-    markForAudit(auditLogger, buildAuditMessage(OMAction.UPGRADE_FINALIZE, 
auditMap, exception, userInfo));
-    return response;
-  }
-
-  private static void validatePeerOmVersionsBeforeFinalize(List<OMNodeDetails> 
peerNodes,
-      OzoneConfiguration configuration) throws OMException {
-    if (peerNodes.isEmpty()) {
-      return;
-    }
-    OzoneManagerVersion leaderVersion = OzoneManagerVersion.SOFTWARE_VERSION;
-    List<String> failedPeers = new ArrayList<>();
-    for (OMNodeDetails peerDetails : peerNodes) {
-      String peerId = peerDetails.getNodeId();
-      OMAdminProtocolClientSideImpl client = null;
-      try {
-        client = 
OMAdminProtocolClientSideImpl.createProxyForSingleOM(configuration, 
getRemoteUser(), peerDetails);
-        OzoneManagerVersion peerVersion = client.getPeerUpgradeStatus();
-        if (!peerVersion.equals(leaderVersion)) {
-          LOG.warn("OM peer {} is running software version {} but leader is 
running version {}. "
-              + "Rejecting finalize command.", peerId, peerVersion, 
leaderVersion);
-          failedPeers.add(peerId + " (version: " + peerVersion + ")");
-        }
-      } catch (IOException e) {
-        LOG.warn("Failed to contact OM peer {} to check software version 
before finalize.", peerId, e);
-        failedPeers.add(peerId + " (unreachable: " + e.getMessage() + ")");
-      } finally {
-        IOUtils.cleanupWithLogger(LOG, client);
-      }
-    }
-    if (!failedPeers.isEmpty()) {
-      throw new OMException("Finalize rejected: the following OM peers did not 
confirm matching software version "
-          + "(expected version=" + leaderVersion + "): " + String.join(", ", 
failedPeers),
-          OMException.ResultCodes.NOT_SUPPORTED_OPERATION);
-    }
+  protected void setResponseBody(OMResponse.Builder builder, OzoneManager 
ozoneManager) {
+    builder.setCmdType(StartFinalizeUpgrade);
+    builder.setStartFinalizeUpgradeResponse(
+        
OzoneManagerProtocolProtos.StartFinalizeUpgradeResponse.newBuilder().build());
   }
-
 }
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMStartFinalizeUpgradeRequestLegacy.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMStartFinalizeUpgradeRequestLegacy.java
new file mode 100644
index 00000000000..061af43601b
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/upgrade/OMStartFinalizeUpgradeRequestLegacy.java
@@ -0,0 +1,56 @@
+/*
+ * 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.hadoop.ozone.om.request.upgrade;
+
+import static 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type.FinalizeUpgrade;
+
+import 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.UpgradeFinalizationStatus;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.FinalizeUpgradeResponse;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+
+/**
+ * Handles the finalizeUpgrade request sent by the old CLI ({@code ozone admin 
om finalizeupgrade}).
+ * It initiates the same asynchronous finalization flow as {@link 
OMStartFinalizeUpgradeRequest} and
+ * returns {@code STARTING_FINALIZATION} to be compatible with old clients.
+ * The old CLI then polls finalization progress until done.
+ */
+public class OMStartFinalizeUpgradeRequestLegacy extends 
OMFinalizeUpgradeRequestBase {
+
+  public OMStartFinalizeUpgradeRequestLegacy(OMRequest omRequest) {
+    super(omRequest);
+  }
+
+  @Override
+  protected boolean isForce() {
+    // The old request has no force field.
+    return false;
+  }
+
+  @Override
+  protected void setResponseBody(OMResponse.Builder builder, OzoneManager 
ozoneManager) {
+    builder.setCmdType(FinalizeUpgrade);
+    UpgradeFinalizationStatus status = UpgradeFinalizationStatus.newBuilder()
+        .setStatus(UpgradeFinalizationStatus.Status.STARTING_FINALIZATION)
+        .build();
+    builder.setFinalizeUpgradeResponse(FinalizeUpgradeResponse.newBuilder()
+        .setStatus(status)
+        .build());
+  }
+}
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/upgrade/OMFinalizeUpgradeResponse.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/upgrade/OMCompleteFinalizeUpgradeResponse.java
similarity index 89%
rename from 
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/upgrade/OMFinalizeUpgradeResponse.java
rename to 
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/upgrade/OMCompleteFinalizeUpgradeResponse.java
index 8dc981ba5ed..c5cdda6ec97 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/upgrade/OMFinalizeUpgradeResponse.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/upgrade/OMCompleteFinalizeUpgradeResponse.java
@@ -31,15 +31,15 @@
 import org.slf4j.LoggerFactory;
 
 /**
- * Response for finalizeUpgrade request.
+ * Response for completing OM finalization (submitted by 
OMUpgradeFinalizeService).
  */
 @CleanupTableInfo(cleanupTables = {META_TABLE})
-public class OMFinalizeUpgradeResponse extends OMClientResponse {
+public class OMCompleteFinalizeUpgradeResponse extends OMClientResponse {
   private static final Logger LOG =
-      LoggerFactory.getLogger(OMFinalizeUpgradeResponse.class);
+      LoggerFactory.getLogger(OMCompleteFinalizeUpgradeResponse.class);
   private int serializedApparentVersion = -1;
 
-  public OMFinalizeUpgradeResponse(
+  public OMCompleteFinalizeUpgradeResponse(
       OzoneManagerProtocolProtos.OMResponse omResponse,
       int serializedApparentVersion) {
     super(omResponse);
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/upgrade/OMStartFinalizeUpgradeResponse.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/upgrade/OMStartFinalizeUpgradeResponse.java
index baf160c428b..6687e5a6950 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/upgrade/OMStartFinalizeUpgradeResponse.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/upgrade/OMStartFinalizeUpgradeResponse.java
@@ -36,13 +36,29 @@
 public class OMStartFinalizeUpgradeResponse extends OMClientResponse {
   private static final Logger LOG = 
LoggerFactory.getLogger(OMStartFinalizeUpgradeResponse.class);
 
+  private final boolean finalizationNeeded;
+
   public OMStartFinalizeUpgradeResponse(OzoneManagerProtocolProtos.OMResponse 
omResponse) {
+    this(omResponse, true);
+  }
+
+  /**
+   * @param finalizationNeeded whether OM still needs to finalize. When {@code 
false} the finalization-in-progress
+   *     marker is not persisted, so that initiating finalize on an 
already-finalized cluster does not orphan the key
+   *     in the DB (the async {@code OMUpgradeFinalizeService} would never 
clear it since finalization is not needed).
+   */
+  public OMStartFinalizeUpgradeResponse(OzoneManagerProtocolProtos.OMResponse 
omResponse, boolean finalizationNeeded) {
     super(omResponse);
+    this.finalizationNeeded = finalizationNeeded;
   }
 
   @Override
   protected void addToDBBatch(OMMetadataManager omMetadataManager,
       BatchOperation batchOperation) throws IOException {
+    if (!finalizationNeeded) {
+      LOG.info("OM does not need finalization; skipping persistence of the 
finalization-in-progress key.");
+      return;
+    }
     LOG.info("Persisting Finalization In Progress Key to the Meta DB table");
     omMetadataManager.getMetaTable().putWithBatch(batchOperation, 
OzoneConsts.FINALIZATION_IN_PROGRESS_KEY, "ignored");
   }
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/upgrade/OMUpgradeFinalizeService.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/upgrade/OMUpgradeFinalizeService.java
index f45f963a45e..b168d14c111 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/upgrade/OMUpgradeFinalizeService.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/upgrade/OMUpgradeFinalizeService.java
@@ -121,7 +121,7 @@ public BackgroundTaskResult call() {
             LOG.info("The SCM Upgrade has been finalized. OM will now 
finalize. Run count {}", run);
 
             OzoneManagerProtocolProtos.OMRequest omRequest = 
OzoneManagerProtocolProtos.OMRequest.newBuilder()
-                .setCmdType(OzoneManagerProtocolProtos.Type.FinalizeUpgrade)
+                
.setCmdType(OzoneManagerProtocolProtos.Type.CompleteFinalizeUpgrade)
                 .setClientId(clientId.toString())
                 .build();
             OzoneManagerProtocolProtos.OMResponse response = 
OzoneManagerRatisUtils.submitRequest(
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOzoneManagerRatisRequest.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOzoneManagerRatisRequest.java
index b5c96eaded5..99b308dcfa1 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOzoneManagerRatisRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOzoneManagerRatisRequest.java
@@ -42,6 +42,7 @@
 import org.apache.hadoop.ozone.om.helpers.OmVolumeArgs;
 import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerRatisUtils;
 import org.apache.hadoop.ozone.om.request.OMRequestTestUtils;
+import 
org.apache.hadoop.ozone.om.request.upgrade.OMCompleteFinalizeUpgradeRequest;
 import 
org.apache.hadoop.ozone.om.request.upgrade.OMStartFinalizeUpgradeRequest;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
 import 
org.apache.hadoop.ozone.protocolPB.OzoneManagerProtocolServerSideTranslatorPB;
@@ -105,6 +106,19 @@ public void testStartFinalizeUpgradeRequestIsDispatched() 
throws IOException {
         OzoneManagerRatisUtils.createClientRequest(omRequest, mockOm));
   }
 
+  @Test
+  public void testCompleteFinalizeUpgradeRequestIsDispatched() throws 
IOException {
+    OzoneManagerProtocolProtos.OMRequest omRequest =
+        OzoneManagerProtocolProtos.OMRequest.newBuilder()
+            
.setCmdType(OzoneManagerProtocolProtos.Type.CompleteFinalizeUpgrade)
+            .setClientId("test-client-id")
+            .build();
+
+    OzoneManager mockOm = mock(OzoneManager.class);
+    assertInstanceOf(OMCompleteFinalizeUpgradeRequest.class,
+        OzoneManagerRatisUtils.createClientRequest(omRequest, mockOm));
+  }
+
   @Test
   public void testUnknownRequestHandling()
       throws IOException, ServiceException {
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMFinalizeUpgradeRequest.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMCompleteFinalizeUpgradeRequest.java
similarity index 85%
rename from 
hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMFinalizeUpgradeRequest.java
rename to 
hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMCompleteFinalizeUpgradeRequest.java
index e282ce8dc0c..bb22eae1e1a 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMFinalizeUpgradeRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMCompleteFinalizeUpgradeRequest.java
@@ -22,12 +22,11 @@
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.io.IOException;
-import java.util.Collections;
 import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
 import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
 import org.apache.hadoop.ozone.OzoneConsts;
@@ -41,23 +40,20 @@
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Status;
 import org.apache.hadoop.ozone.upgrade.UpgradeException;
-import org.apache.hadoop.ozone.upgrade.UpgradeFinalization;
 import org.apache.ratis.protocol.ClientId;
 import org.apache.ratis.server.protocol.TermIndex;
 import org.junit.jupiter.api.Test;
 
 /**
- * Tests for the OMFinalizeUpgradeRequest class.
+ * Tests for the OMCompleteFinalizeUpgradeRequest class.
  */
-public class TestOMFinalizeUpgradeRequest extends OMKeyRequestTests {
+public class TestOMCompleteFinalizeUpgradeRequest extends OMKeyRequestTests {
 
   @Test
   public void testFinalizationInProgressKeyRemoved() throws IOException {
     OMVersionManager omVersionManager = mock(OMVersionManager.class);
     
when(omVersionManager.getApparentVersion()).thenReturn(OzoneManagerVersion.DEFAULT_VERSION);
     when(ozoneManager.getVersionManager()).thenReturn(omVersionManager);
-    when(ozoneManager.finalizeUpgrade(any())).thenReturn(new 
UpgradeFinalization.StatusAndMessages(
-         UpgradeFinalization.Status.FINALIZATION_IN_PROGRESS, 
Collections.singletonList("Finalization in progress")));
 
     
omMetadataManager.getMetaTable().put(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY, 
"ignored");
     omMetadataManager.getMetaTable().addCacheEntry(
@@ -89,14 +85,14 @@ public void testFinalizeFailureMapsToInternalError() throws 
IOException {
     OMVersionManager omVersionManager = mock(OMVersionManager.class);
     
when(omVersionManager.getApparentVersion()).thenReturn(OzoneManagerVersion.DEFAULT_VERSION);
     when(ozoneManager.getVersionManager()).thenReturn(omVersionManager);
-    when(ozoneManager.finalizeUpgrade(any())).thenThrow(
-        new 
UpgradeException(UpgradeException.ResultCodes.FINALIZE_UPGRADE_ACTION_FAILED));
+    doThrow(new 
UpgradeException(UpgradeException.ResultCodes.FINALIZE_UPGRADE_ACTION_FAILED))
+        .when(ozoneManager).finalizeUpgrade();
 
     OzoneManagerProtocolProtos.OMRequest omRequest = 
OzoneManagerProtocolProtos.OMRequest.newBuilder()
-        .setCmdType(OzoneManagerProtocolProtos.Type.FinalizeUpgrade)
+        .setCmdType(OzoneManagerProtocolProtos.Type.CompleteFinalizeUpgrade)
         .setClientId(ClientId.randomId().toString())
         .build();
-    OMFinalizeUpgradeRequest request = new OMFinalizeUpgradeRequest(omRequest);
+    OMCompleteFinalizeUpgradeRequest request = new 
OMCompleteFinalizeUpgradeRequest(omRequest);
     ExecutionContext context = ExecutionContext.of(1, TermIndex.INITIAL_VALUE);
     request.preExecute(ozoneManager);
 
@@ -108,11 +104,11 @@ public void testFinalizeFailureMapsToInternalError() 
throws IOException {
 
   private void submitRequest() throws IOException {
     OzoneManagerProtocolProtos.OMRequest omRequest = 
OzoneManagerProtocolProtos.OMRequest.newBuilder()
-        .setCmdType(OzoneManagerProtocolProtos.Type.FinalizeUpgrade)
+        .setCmdType(OzoneManagerProtocolProtos.Type.CompleteFinalizeUpgrade)
         .setClientId(ClientId.randomId().toString())
         .build();
 
-    OMFinalizeUpgradeRequest request = new OMFinalizeUpgradeRequest(omRequest);
+    OMCompleteFinalizeUpgradeRequest request = new 
OMCompleteFinalizeUpgradeRequest(omRequest);
     ExecutionContext context = ExecutionContext.of(1, TermIndex.INITIAL_VALUE);
 
     OzoneManagerProtocolProtos.OMRequest modifiedOmRequest = 
request.preExecute(ozoneManager);
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMStartFinalizeUpgradeRequest.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMStartFinalizeUpgradeRequest.java
index 88746f0308a..376c80ce7f5 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMStartFinalizeUpgradeRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMStartFinalizeUpgradeRequest.java
@@ -18,14 +18,8 @@
 package org.apache.hadoop.ozone.om.request.upgrade;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.never;
@@ -33,51 +27,26 @@
 import static org.mockito.Mockito.when;
 
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.Collections;
-import org.apache.hadoop.hdds.scm.exceptions.SCMException;
-import org.apache.hadoop.ozone.OzoneConsts;
 import org.apache.hadoop.ozone.OzoneManagerVersion;
-import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
-import org.apache.hadoop.ozone.om.helpers.OMNodeDetails;
 import org.apache.hadoop.ozone.om.protocolPB.OMAdminProtocolClientSideImpl;
-import org.apache.hadoop.ozone.om.request.key.OMKeyRequestTests;
-import org.apache.hadoop.ozone.om.response.OMClientResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
-import org.apache.hadoop.security.UserGroupInformation;
 import org.apache.ratis.protocol.ClientId;
 import org.apache.ratis.server.protocol.TermIndex;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.MockedStatic;
 
 /**
- * Tests for OMStartFinalizeUpgradeRequest.
+ * Tests for OMStartFinalizeUpgradeRequest, the current ({@code ozone admin 
upgrade finalize}) initiated
+ * request. Shared initiate behavior is covered by {@link 
TestOMStartFinalizeUpgradeRequestBase}; the cases
+ * here exercise the {@code force} flag that is unique to this request.
  */
-public class TestOMStartFinalizeUpgradeRequest extends OMKeyRequestTests {
+public class TestOMStartFinalizeUpgradeRequest extends 
TestOMStartFinalizeUpgradeRequestBase {
 
-  @BeforeEach
-  public void stubPeerNodes() {
-    when(ozoneManager.getPeerNodes()).thenReturn(Collections.emptyList());
-  }
-
-  @Test
-  public void testPreExecuteCallsScmFinalizeUpgrade() throws IOException {
-    doNothing().when(scmContainerLocationProtocol).finalizeUpgrade();
-
-    OzoneManagerProtocolProtos.OMRequest original = buildRequest();
-    OMStartFinalizeUpgradeRequest request = new 
OMStartFinalizeUpgradeRequest(original);
-
-    OzoneManagerProtocolProtos.OMRequest modified = 
request.preExecute(ozoneManager);
-
-    // UserInfo must have been added by the base class preExecute.
-    assertNotEquals(original, modified);
-    assertNotNull(modified.getUserInfo());
-
-    // SCM must have been asked to begin finalization.
-    verify(scmContainerLocationProtocol).finalizeUpgrade();
-    verify(scmContainerLocationProtocol, never()).forceFinalizeUpgrade();
+  @Override
+  protected OMFinalizeUpgradeRequestBase newRequest() {
+    return new OMStartFinalizeUpgradeRequest(buildRequest(false));
   }
 
   @Test
@@ -91,70 +60,6 @@ public void 
testForcePreExecuteCallsScmForceFinalizeUpgrade() throws IOException
     verify(scmContainerLocationProtocol, never()).finalizeUpgrade();
   }
 
-  @Test
-  public void testScmFinalizeFailurePropagatesToClient() throws IOException {
-    IOException scmFailure = new IOException("SCM finalize upgrade failed");
-    doThrow(scmFailure).when(scmContainerLocationProtocol).finalizeUpgrade();
-
-    OMStartFinalizeUpgradeRequest request = new 
OMStartFinalizeUpgradeRequest(buildRequest());
-
-    // The exception raised by SCM must propagate out of preExecute so the OM
-    // client sees the failure instead of a successful finalize.
-    IOException ex = assertThrows(IOException.class, () -> 
request.preExecute(ozoneManager));
-    assertSame(scmFailure, ex);
-
-    verify(scmContainerLocationProtocol).finalizeUpgrade();
-  }
-
-  @Test
-  public void testScmUnsupportedOperationBecomesOmNotSupportedOperation() 
throws IOException {
-    SCMException scmFailure =
-        new SCMException("SCM version mismatch", 
SCMException.ResultCodes.UNSUPPORTED_OPERATION);
-    doThrow(scmFailure).when(scmContainerLocationProtocol).finalizeUpgrade();
-
-    OMStartFinalizeUpgradeRequest request = new 
OMStartFinalizeUpgradeRequest(buildRequest());
-
-    // An SCM UNSUPPORTED_OPERATION is re-mapped to an OM 
NOT_SUPPORTED_OPERATION,
-    // preserving the original message and chaining the SCM exception as the 
cause.
-    OMException ex = assertThrows(OMException.class, () -> 
request.preExecute(ozoneManager));
-    assertEquals(OMException.ResultCodes.NOT_SUPPORTED_OPERATION, 
ex.getResult());
-    assertEquals(scmFailure.getMessage(), ex.getMessage());
-    assertSame(scmFailure, ex.getCause());
-
-    verify(scmContainerLocationProtocol).finalizeUpgrade();
-  }
-
-  @Test
-  public void testOtherScmExceptionPropagatesUnchanged() throws IOException {
-    SCMException scmFailure = new SCMException("SCM is in safe mode", 
SCMException.ResultCodes.SAFE_MODE_EXCEPTION);
-    doThrow(scmFailure).when(scmContainerLocationProtocol).finalizeUpgrade();
-
-    OMStartFinalizeUpgradeRequest request = new 
OMStartFinalizeUpgradeRequest(buildRequest());
-
-    // Only UNSUPPORTED_OPERATION is re-mapped; any other SCM exception 
propagates as-is.
-    SCMException ex = assertThrows(SCMException.class, () -> 
request.preExecute(ozoneManager));
-    assertSame(scmFailure, ex);
-
-    verify(scmContainerLocationProtocol).finalizeUpgrade();
-  }
-
-  @Test
-  public void testValidateAndUpdateCacheAddsFinalizationInProgressKey() throws 
IOException {
-    doNothing().when(scmContainerLocationProtocol).finalizeUpgrade();
-
-    
assertNull(omMetadataManager.getMetaTable().get(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY),
-        "key should not exist before the request");
-    assertEquals(0, omMetrics.getFinalizationInProgress(),
-        "metric should be 0 before the request");
-
-    submitRequest();
-
-    
assertNotNull(omMetadataManager.getMetaTable().get(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY),
-        "key should be present in the cache after validateAndUpdateCache");
-    assertEquals(1, omMetrics.getFinalizationInProgress(),
-        "metric should be 1 after the request");
-  }
-
   @Test
   public void testAuditMapRecordsForceFlag() throws IOException {
     doNothing().when(scmContainerLocationProtocol).finalizeUpgrade();
@@ -171,116 +76,6 @@ public void testAuditMapRecordsForceFlag() throws 
IOException {
     assertEquals("false", normal.getAuditBuilder().getAuditMap().get("force"));
   }
 
-  @Test
-  public void testAccessDeniedWhenUserIsNotAdmin() throws IOException {
-    when(ozoneManager.isAdminAuthorizationEnabled()).thenReturn(true);
-    when(ozoneManager.isAdmin(any())).thenReturn(false);
-
-    OzoneManagerProtocolProtos.OMRequest original = buildRequest();
-    OMStartFinalizeUpgradeRequest request = new 
OMStartFinalizeUpgradeRequest(original);
-    // In the test environment there is no live RPC thread, so
-    // ProtobufRpcEngine.Server.getRemoteUser() returns null and 
super.preExecute()
-    // cannot resolve a username. setUGI() pre-seeds the identity so that
-    // createUGIForApi() succeeds without needing the RPC thread-local.
-    request.setUGI(UserGroupInformation.createRemoteUser("testuser"));
-
-    // With auth in preExecute(), a non-admin is rejected before the request
-    // reaches Raft or touches SCM.
-    OMException ex = assertThrows(OMException.class,
-        () -> request.preExecute(ozoneManager));
-    assertEquals(OMException.ResultCodes.ACCESS_DENIED, ex.getResult(),
-        "non-admin user should receive ACCESS_DENIED from preExecute");
-
-    // SCM must NOT have been called — auth is checked before the SCM call.
-    verify(scmContainerLocationProtocol, never()).finalizeUpgrade();
-  }
-
-  @Test
-  public void testPeerVersionCheckPassesWhenNoPeers() throws IOException {
-    // @BeforeEach already stubs getPeerNodes() to return an empty list.
-    // preExecute must complete normally and call SCM finalize.
-    doNothing().when(scmContainerLocationProtocol).finalizeUpgrade();
-
-    OzoneManagerProtocolProtos.OMRequest original = buildRequest();
-    new OMStartFinalizeUpgradeRequest(original).preExecute(ozoneManager);
-
-    verify(scmContainerLocationProtocol).finalizeUpgrade();
-  }
-
-  @Test
-  public void testPeerVersionCheckPassesWhenAllPeersMatch() throws IOException 
{
-    doNothing().when(scmContainerLocationProtocol).finalizeUpgrade();
-    
when(ozoneManager.getPeerNodes()).thenReturn(Arrays.asList(buildPeer("om2"), 
buildPeer("om3")));
-    OMAdminProtocolClientSideImpl matchingClient = 
peerClientWithVersion(OzoneManagerVersion.SOFTWARE_VERSION);
-
-    try (MockedStatic<OMAdminProtocolClientSideImpl> factory =
-             mockStatic(OMAdminProtocolClientSideImpl.class)) {
-      factory.when(() -> 
OMAdminProtocolClientSideImpl.createProxyForSingleOM(any(), any(), any()))
-          .thenReturn(matchingClient);
-
-      new 
OMStartFinalizeUpgradeRequest(buildRequest()).preExecute(ozoneManager);
-    }
-
-    verify(scmContainerLocationProtocol).finalizeUpgrade();
-  }
-
-  @Test
-  public void testPeerVersionCheckRejectsOneOlderPeer() throws IOException {
-    
when(ozoneManager.getPeerNodes()).thenReturn(Arrays.asList(buildPeer("om2"), 
buildPeer("om3")));
-    OMAdminProtocolClientSideImpl matchingClient = 
peerClientWithVersion(OzoneManagerVersion.SOFTWARE_VERSION);
-    OMAdminProtocolClientSideImpl olderClient = 
peerClientWithVersion(OzoneManagerVersion.HBASE_SUPPORT);
-
-    try (MockedStatic<OMAdminProtocolClientSideImpl> factory =
-             mockStatic(OMAdminProtocolClientSideImpl.class)) {
-      factory.when(() -> 
OMAdminProtocolClientSideImpl.createProxyForSingleOM(any(), any(), any()))
-          .thenReturn(matchingClient, olderClient);
-
-      OMException ex = assertThrows(OMException.class,
-          () -> new 
OMStartFinalizeUpgradeRequest(buildRequest()).preExecute(ozoneManager));
-      assertEquals(OMException.ResultCodes.NOT_SUPPORTED_OPERATION, 
ex.getResult());
-    }
-
-    verify(scmContainerLocationProtocol, never()).finalizeUpgrade();
-  }
-
-  @Test
-  public void testPeerVersionCheckRejectsOneUnknownFuturePeer() throws 
IOException {
-    
when(ozoneManager.getPeerNodes()).thenReturn(Arrays.asList(buildPeer("om2"), 
buildPeer("om3")));
-    OMAdminProtocolClientSideImpl matchingClient = 
peerClientWithVersion(OzoneManagerVersion.SOFTWARE_VERSION);
-    OMAdminProtocolClientSideImpl unknownClient = 
peerClientWithVersion(OzoneManagerVersion.UNKNOWN_VERSION);
-
-    try (MockedStatic<OMAdminProtocolClientSideImpl> factory =
-             mockStatic(OMAdminProtocolClientSideImpl.class)) {
-      factory.when(() -> 
OMAdminProtocolClientSideImpl.createProxyForSingleOM(any(), any(), any()))
-          .thenReturn(matchingClient, unknownClient);
-
-      OMException ex = assertThrows(OMException.class,
-          () -> new 
OMStartFinalizeUpgradeRequest(buildRequest()).preExecute(ozoneManager));
-      assertEquals(OMException.ResultCodes.NOT_SUPPORTED_OPERATION, 
ex.getResult());
-    }
-
-    verify(scmContainerLocationProtocol, never()).finalizeUpgrade();
-  }
-
-  @Test
-  public void testPeerVersionCheckRejectsUnreachablePeer() throws IOException {
-    
when(ozoneManager.getPeerNodes()).thenReturn(Collections.singletonList(buildPeer("om2")));
-    OMAdminProtocolClientSideImpl unreachableClient = 
mock(OMAdminProtocolClientSideImpl.class);
-    when(unreachableClient.getPeerUpgradeStatus()).thenThrow(new 
IOException("connection refused"));
-
-    try (MockedStatic<OMAdminProtocolClientSideImpl> factory =
-             mockStatic(OMAdminProtocolClientSideImpl.class)) {
-      factory.when(() -> 
OMAdminProtocolClientSideImpl.createProxyForSingleOM(any(), any(), any()))
-          .thenReturn(unreachableClient);
-
-      OMException ex = assertThrows(OMException.class,
-          () -> new 
OMStartFinalizeUpgradeRequest(buildRequest()).preExecute(ozoneManager));
-      assertEquals(OMException.ResultCodes.NOT_SUPPORTED_OPERATION, 
ex.getResult());
-    }
-
-    verify(scmContainerLocationProtocol, never()).finalizeUpgrade();
-  }
-
   @Test
   public void testForceSkipsPeerVersionCheckForUnreachablePeer() throws 
IOException {
     doNothing().when(scmContainerLocationProtocol).finalizeUpgrade();
@@ -324,36 +119,6 @@ public void 
testForceSkipsPeerVersionCheckForMismatchedPeer() throws IOException
     verify(scmContainerLocationProtocol, never()).finalizeUpgrade();
   }
 
-  private static OMNodeDetails buildPeer(String nodeId) {
-    return new OMNodeDetails.Builder()
-        .setOMServiceId("testService")
-        .setOMNodeId(nodeId)
-        .setHostAddress("127.0.0.1")
-        .setRpcPort(1)
-        .build();
-  }
-
-  private static OMAdminProtocolClientSideImpl 
peerClientWithVersion(OzoneManagerVersion version) throws IOException {
-    OMAdminProtocolClientSideImpl client = 
mock(OMAdminProtocolClientSideImpl.class);
-    when(client.getPeerUpgradeStatus()).thenReturn(version);
-    return client;
-  }
-
-  private OMClientResponse submitRequest() throws IOException {
-    OzoneManagerProtocolProtos.OMRequest original = buildRequest();
-    OMStartFinalizeUpgradeRequest request = new 
OMStartFinalizeUpgradeRequest(original);
-    ExecutionContext context = ExecutionContext.of(1, TermIndex.INITIAL_VALUE);
-
-    OzoneManagerProtocolProtos.OMRequest modified = 
request.preExecute(ozoneManager);
-    assertNotEquals(original, modified);
-
-    return request.validateAndUpdateCache(ozoneManager, context);
-  }
-
-  private OzoneManagerProtocolProtos.OMRequest buildRequest() {
-    return buildRequest(false);
-  }
-
   private OzoneManagerProtocolProtos.OMRequest buildRequest(boolean force) {
     return OzoneManagerProtocolProtos.OMRequest.newBuilder()
         .setCmdType(OzoneManagerProtocolProtos.Type.StartFinalizeUpgrade)
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMStartFinalizeUpgradeRequest.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMStartFinalizeUpgradeRequestBase.java
similarity index 63%
copy from 
hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMStartFinalizeUpgradeRequest.java
copy to 
hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMStartFinalizeUpgradeRequestBase.java
index 88746f0308a..47cb54c6a77 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMStartFinalizeUpgradeRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMStartFinalizeUpgradeRequestBase.java
@@ -23,6 +23,7 @@
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doThrow;
@@ -36,6 +37,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import org.apache.hadoop.hdds.scm.exceptions.SCMException;
+import org.apache.hadoop.hdds.utils.db.BatchOperation;
 import org.apache.hadoop.ozone.OzoneConsts;
 import org.apache.hadoop.ozone.OzoneManagerVersion;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
@@ -44,59 +46,62 @@
 import org.apache.hadoop.ozone.om.protocolPB.OMAdminProtocolClientSideImpl;
 import org.apache.hadoop.ozone.om.request.key.OMKeyRequestTests;
 import org.apache.hadoop.ozone.om.response.OMClientResponse;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
+import org.apache.hadoop.ozone.om.upgrade.OMVersionManager;
+import org.apache.hadoop.ozone.om.upgrade.OMVersionManagerTestUtils;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
 import org.apache.hadoop.security.UserGroupInformation;
-import org.apache.ratis.protocol.ClientId;
 import org.apache.ratis.server.protocol.TermIndex;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.MockedStatic;
 
 /**
- * Tests for OMStartFinalizeUpgradeRequest.
+ * Shared tests for the client-initiated finalize requests handled by
+ * {@link OMFinalizeUpgradeRequestBase}: admin authorization, SCM finalization
+ * triggering and error mapping, OM peer software version validation, and 
writing
+ * the finalization-in-progress marker. Subclasses supply the concrete request 
via
+ * {@link #newRequest()} and add tests for behavior unique to their command 
type.
  */
-public class TestOMStartFinalizeUpgradeRequest extends OMKeyRequestTests {
+public abstract class TestOMStartFinalizeUpgradeRequestBase extends 
OMKeyRequestTests {
+
+  /**
+   * A fresh, non-forced initiate request for the command type under test.
+   */
+  protected abstract OMFinalizeUpgradeRequestBase newRequest();
 
   @BeforeEach
-  public void stubPeerNodes() {
+  public void mockPreFinalizedOM() {
     when(ozoneManager.getPeerNodes()).thenReturn(Collections.emptyList());
+    // A finalize request models a cluster that still needs finalization; the 
parent test harness defaults to a
+    // finalized version manager, which is the special already-finalized case 
exercised separately below.
+    OMVersionManager versionManager = 
OMVersionManagerTestUtils.mockPreFinalizedOmVersionManager();
+    when(ozoneManager.getVersionManager()).thenReturn(versionManager);
   }
 
   @Test
   public void testPreExecuteCallsScmFinalizeUpgrade() throws IOException {
     doNothing().when(scmContainerLocationProtocol).finalizeUpgrade();
 
-    OzoneManagerProtocolProtos.OMRequest original = buildRequest();
-    OMStartFinalizeUpgradeRequest request = new 
OMStartFinalizeUpgradeRequest(original);
+    OMFinalizeUpgradeRequestBase request = newRequest();
+    OMRequest original = request.getOmRequest();
 
-    OzoneManagerProtocolProtos.OMRequest modified = 
request.preExecute(ozoneManager);
+    OMRequest modified = request.preExecute(ozoneManager);
 
     // UserInfo must have been added by the base class preExecute.
     assertNotEquals(original, modified);
     assertNotNull(modified.getUserInfo());
 
-    // SCM must have been asked to begin finalization.
+    // A non-forced initiate request must route to SCM's non-force finalize 
path.
     verify(scmContainerLocationProtocol).finalizeUpgrade();
     verify(scmContainerLocationProtocol, never()).forceFinalizeUpgrade();
   }
 
-  @Test
-  public void testForcePreExecuteCallsScmForceFinalizeUpgrade() throws 
IOException {
-    OMStartFinalizeUpgradeRequest request = new 
OMStartFinalizeUpgradeRequest(buildRequest(true));
-
-    request.preExecute(ozoneManager);
-
-    // A forced request must route to SCM's force path so SCM skips its own 
version checks.
-    verify(scmContainerLocationProtocol).forceFinalizeUpgrade();
-    verify(scmContainerLocationProtocol, never()).finalizeUpgrade();
-  }
-
   @Test
   public void testScmFinalizeFailurePropagatesToClient() throws IOException {
     IOException scmFailure = new IOException("SCM finalize upgrade failed");
     doThrow(scmFailure).when(scmContainerLocationProtocol).finalizeUpgrade();
 
-    OMStartFinalizeUpgradeRequest request = new 
OMStartFinalizeUpgradeRequest(buildRequest());
+    OMFinalizeUpgradeRequestBase request = newRequest();
 
     // The exception raised by SCM must propagate out of preExecute so the OM
     // client sees the failure instead of a successful finalize.
@@ -112,7 +117,7 @@ public void 
testScmUnsupportedOperationBecomesOmNotSupportedOperation() throws I
         new SCMException("SCM version mismatch", 
SCMException.ResultCodes.UNSUPPORTED_OPERATION);
     doThrow(scmFailure).when(scmContainerLocationProtocol).finalizeUpgrade();
 
-    OMStartFinalizeUpgradeRequest request = new 
OMStartFinalizeUpgradeRequest(buildRequest());
+    OMFinalizeUpgradeRequestBase request = newRequest();
 
     // An SCM UNSUPPORTED_OPERATION is re-mapped to an OM 
NOT_SUPPORTED_OPERATION,
     // preserving the original message and chaining the SCM exception as the 
cause.
@@ -129,7 +134,7 @@ public void testOtherScmExceptionPropagatesUnchanged() 
throws IOException {
     SCMException scmFailure = new SCMException("SCM is in safe mode", 
SCMException.ResultCodes.SAFE_MODE_EXCEPTION);
     doThrow(scmFailure).when(scmContainerLocationProtocol).finalizeUpgrade();
 
-    OMStartFinalizeUpgradeRequest request = new 
OMStartFinalizeUpgradeRequest(buildRequest());
+    OMFinalizeUpgradeRequestBase request = newRequest();
 
     // Only UNSUPPORTED_OPERATION is re-mapped; any other SCM exception 
propagates as-is.
     SCMException ex = assertThrows(SCMException.class, () -> 
request.preExecute(ozoneManager));
@@ -138,46 +143,12 @@ public void testOtherScmExceptionPropagatesUnchanged() 
throws IOException {
     verify(scmContainerLocationProtocol).finalizeUpgrade();
   }
 
-  @Test
-  public void testValidateAndUpdateCacheAddsFinalizationInProgressKey() throws 
IOException {
-    doNothing().when(scmContainerLocationProtocol).finalizeUpgrade();
-
-    
assertNull(omMetadataManager.getMetaTable().get(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY),
-        "key should not exist before the request");
-    assertEquals(0, omMetrics.getFinalizationInProgress(),
-        "metric should be 0 before the request");
-
-    submitRequest();
-
-    
assertNotNull(omMetadataManager.getMetaTable().get(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY),
-        "key should be present in the cache after validateAndUpdateCache");
-    assertEquals(1, omMetrics.getFinalizationInProgress(),
-        "metric should be 1 after the request");
-  }
-
-  @Test
-  public void testAuditMapRecordsForceFlag() throws IOException {
-    doNothing().when(scmContainerLocationProtocol).finalizeUpgrade();
-    ExecutionContext context = ExecutionContext.of(1, TermIndex.INITIAL_VALUE);
-
-    OMStartFinalizeUpgradeRequest forced = new 
OMStartFinalizeUpgradeRequest(buildRequest(true));
-    forced.preExecute(ozoneManager);
-    forced.validateAndUpdateCache(ozoneManager, context);
-    assertEquals("true", forced.getAuditBuilder().getAuditMap().get("force"));
-
-    OMStartFinalizeUpgradeRequest normal = new 
OMStartFinalizeUpgradeRequest(buildRequest(false));
-    normal.preExecute(ozoneManager);
-    normal.validateAndUpdateCache(ozoneManager, context);
-    assertEquals("false", normal.getAuditBuilder().getAuditMap().get("force"));
-  }
-
   @Test
   public void testAccessDeniedWhenUserIsNotAdmin() throws IOException {
     when(ozoneManager.isAdminAuthorizationEnabled()).thenReturn(true);
     when(ozoneManager.isAdmin(any())).thenReturn(false);
 
-    OzoneManagerProtocolProtos.OMRequest original = buildRequest();
-    OMStartFinalizeUpgradeRequest request = new 
OMStartFinalizeUpgradeRequest(original);
+    OMFinalizeUpgradeRequestBase request = newRequest();
     // In the test environment there is no live RPC thread, so
     // ProtobufRpcEngine.Server.getRemoteUser() returns null and 
super.preExecute()
     // cannot resolve a username. setUGI() pre-seeds the identity so that
@@ -186,8 +157,7 @@ public void testAccessDeniedWhenUserIsNotAdmin() throws 
IOException {
 
     // With auth in preExecute(), a non-admin is rejected before the request
     // reaches Raft or touches SCM.
-    OMException ex = assertThrows(OMException.class,
-        () -> request.preExecute(ozoneManager));
+    OMException ex = assertThrows(OMException.class, () -> 
request.preExecute(ozoneManager));
     assertEquals(OMException.ResultCodes.ACCESS_DENIED, ex.getResult(),
         "non-admin user should receive ACCESS_DENIED from preExecute");
 
@@ -197,12 +167,11 @@ public void testAccessDeniedWhenUserIsNotAdmin() throws 
IOException {
 
   @Test
   public void testPeerVersionCheckPassesWhenNoPeers() throws IOException {
-    // @BeforeEach already stubs getPeerNodes() to return an empty list.
+    assertTrue(ozoneManager.getPeerNodes().isEmpty());
     // preExecute must complete normally and call SCM finalize.
     doNothing().when(scmContainerLocationProtocol).finalizeUpgrade();
 
-    OzoneManagerProtocolProtos.OMRequest original = buildRequest();
-    new OMStartFinalizeUpgradeRequest(original).preExecute(ozoneManager);
+    newRequest().preExecute(ozoneManager);
 
     verify(scmContainerLocationProtocol).finalizeUpgrade();
   }
@@ -218,7 +187,7 @@ public void testPeerVersionCheckPassesWhenAllPeersMatch() 
throws IOException {
       factory.when(() -> 
OMAdminProtocolClientSideImpl.createProxyForSingleOM(any(), any(), any()))
           .thenReturn(matchingClient);
 
-      new 
OMStartFinalizeUpgradeRequest(buildRequest()).preExecute(ozoneManager);
+      newRequest().preExecute(ozoneManager);
     }
 
     verify(scmContainerLocationProtocol).finalizeUpgrade();
@@ -235,8 +204,7 @@ public void testPeerVersionCheckRejectsOneOlderPeer() 
throws IOException {
       factory.when(() -> 
OMAdminProtocolClientSideImpl.createProxyForSingleOM(any(), any(), any()))
           .thenReturn(matchingClient, olderClient);
 
-      OMException ex = assertThrows(OMException.class,
-          () -> new 
OMStartFinalizeUpgradeRequest(buildRequest()).preExecute(ozoneManager));
+      OMException ex = assertThrows(OMException.class, () -> 
newRequest().preExecute(ozoneManager));
       assertEquals(OMException.ResultCodes.NOT_SUPPORTED_OPERATION, 
ex.getResult());
     }
 
@@ -254,8 +222,7 @@ public void 
testPeerVersionCheckRejectsOneUnknownFuturePeer() throws IOException
       factory.when(() -> 
OMAdminProtocolClientSideImpl.createProxyForSingleOM(any(), any(), any()))
           .thenReturn(matchingClient, unknownClient);
 
-      OMException ex = assertThrows(OMException.class,
-          () -> new 
OMStartFinalizeUpgradeRequest(buildRequest()).preExecute(ozoneManager));
+      OMException ex = assertThrows(OMException.class, () -> 
newRequest().preExecute(ozoneManager));
       assertEquals(OMException.ResultCodes.NOT_SUPPORTED_OPERATION, 
ex.getResult());
     }
 
@@ -273,8 +240,7 @@ public void testPeerVersionCheckRejectsUnreachablePeer() 
throws IOException {
       factory.when(() -> 
OMAdminProtocolClientSideImpl.createProxyForSingleOM(any(), any(), any()))
           .thenReturn(unreachableClient);
 
-      OMException ex = assertThrows(OMException.class,
-          () -> new 
OMStartFinalizeUpgradeRequest(buildRequest()).preExecute(ozoneManager));
+      OMException ex = assertThrows(OMException.class, () -> 
newRequest().preExecute(ozoneManager));
       assertEquals(OMException.ResultCodes.NOT_SUPPORTED_OPERATION, 
ex.getResult());
     }
 
@@ -282,49 +248,84 @@ public void testPeerVersionCheckRejectsUnreachablePeer() 
throws IOException {
   }
 
   @Test
-  public void testForceSkipsPeerVersionCheckForUnreachablePeer() throws 
IOException {
+  public void testValidateAndUpdateCacheAddsFinalizationInProgressKey() throws 
IOException {
     doNothing().when(scmContainerLocationProtocol).finalizeUpgrade();
-    
when(ozoneManager.getPeerNodes()).thenReturn(Collections.singletonList(buildPeer("om2")));
-    OMAdminProtocolClientSideImpl unreachableClient = 
mock(OMAdminProtocolClientSideImpl.class);
-    when(unreachableClient.getPeerUpgradeStatus()).thenThrow(new 
IOException("connection refused"));
 
-    try (MockedStatic<OMAdminProtocolClientSideImpl> factory =
-             mockStatic(OMAdminProtocolClientSideImpl.class)) {
-      factory.when(() -> 
OMAdminProtocolClientSideImpl.createProxyForSingleOM(any(), any(), any()))
-          .thenReturn(unreachableClient);
+    
assertNull(omMetadataManager.getMetaTable().get(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY),
+        "key should not exist before the request");
+    assertEquals(0, omMetrics.getFinalizationInProgress(),
+        "metric should be 0 before the request");
 
-      // With force=true the peer version check is skipped, so an unreachable 
peer does not
-      // prevent finalization and SCM is still asked to force finalization 
(skipping its own checks).
-      new 
OMStartFinalizeUpgradeRequest(buildRequest(true)).preExecute(ozoneManager);
-    }
+    OMClientResponse response = submitRequest();
 
-    verify(unreachableClient, never()).getPeerUpgradeStatus();
-    verify(scmContainerLocationProtocol).forceFinalizeUpgrade();
-    verify(scmContainerLocationProtocol, never()).finalizeUpgrade();
+    
assertNotNull(omMetadataManager.getMetaTable().get(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY),
+        "key should be present in the cache after validateAndUpdateCache");
+    assertEquals(1, omMetrics.getFinalizationInProgress(),
+        "metric should be 1 after the request");
+
+    // Applying the response as the double buffer would must also persist the 
marker to the DB.
+    flushResponseToDb(response);
+    assertEquals("ignored", 
omMetadataManager.getMetaTable().getSkipCache(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY),
+        "key should be persisted to the DB after the response is flushed");
   }
 
   @Test
-  public void testForceSkipsPeerVersionCheckForMismatchedPeer() throws 
IOException {
+  public void testValidateAndUpdateCacheSkipsMarkerWhenAlreadyFinalized() 
throws IOException {
     doNothing().when(scmContainerLocationProtocol).finalizeUpgrade();
-    
when(ozoneManager.getPeerNodes()).thenReturn(Collections.singletonList(buildPeer("om2")));
-    OMAdminProtocolClientSideImpl olderClient = 
peerClientWithVersion(OzoneManagerVersion.HBASE_SUPPORT);
+    // Simulate an admin initiating finalize on a cluster that is already 
finalized.
+    OMVersionManager finalizedVersionManager = 
OMVersionManagerTestUtils.mockFinalizedOmVersionManager();
+    when(ozoneManager.getVersionManager()).thenReturn(finalizedVersionManager);
 
-    try (MockedStatic<OMAdminProtocolClientSideImpl> factory =
-             mockStatic(OMAdminProtocolClientSideImpl.class)) {
-      factory.when(() -> 
OMAdminProtocolClientSideImpl.createProxyForSingleOM(any(), any(), any()))
-          .thenReturn(olderClient);
+    
assertNull(omMetadataManager.getMetaTable().get(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY),
+        "key should not exist before the request");
+    assertEquals(0, omMetrics.getFinalizationInProgress(),
+        "metric should be 0 before the request");
+
+    OMClientResponse response = submitRequest();
 
-      // With force=true the peer version check is skipped, so a peer running 
a different software
-      // version does not prevent finalization and SCM is still asked to force 
finalization.
-      new 
OMStartFinalizeUpgradeRequest(buildRequest(true)).preExecute(ozoneManager);
+    // The marker and metric must NOT be set: OMUpgradeFinalizeService would 
shut down because needsFinalization()
+    // is false and would never clear a marker written here, leaving it and 
the metric stuck.
+    
assertNull(omMetadataManager.getMetaTable().get(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY),
+        "key must not be written when the cluster is already finalized");
+    assertEquals(0, omMetrics.getFinalizationInProgress(),
+        "metric must remain 0 when the cluster is already finalized");
+    // Finalizing an already-finalized cluster is a successful no-op.
+    assertTrue(response.getOMResponse().getSuccess(),
+        "response should still report success for an already-finalized 
cluster");
+
+    // Even the success response must not persist the marker when the double 
buffer flushes; otherwise it would be
+    // orphaned in the DB.
+    flushResponseToDb(response);
+    
assertNull(omMetadataManager.getMetaTable().getSkipCache(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY),
+        "key must not be persisted to the DB when the cluster is already 
finalized");
+  }
+
+  /**
+   * Applies the response to the DB the way the OM double buffer does on 
flush: status-gated addToDBBatch followed by
+   * a batch commit.
+   */
+  private void flushResponseToDb(OMClientResponse response) throws IOException 
{
+    try (BatchOperation batch = 
omMetadataManager.getStore().initBatchOperation()) {
+      response.checkAndUpdateDB(omMetadataManager, batch);
+      omMetadataManager.getStore().commitBatchOperation(batch);
     }
+  }
 
-    verify(olderClient, never()).getPeerUpgradeStatus();
-    verify(scmContainerLocationProtocol).forceFinalizeUpgrade();
-    verify(scmContainerLocationProtocol, never()).finalizeUpgrade();
+  /**
+   * Runs {@link #newRequest()} through preExecute and validateAndUpdateCache, 
returning the response.
+   */
+  protected OMClientResponse submitRequest() throws IOException {
+    OMFinalizeUpgradeRequestBase request = newRequest();
+    OMRequest original = request.getOmRequest();
+    ExecutionContext context = ExecutionContext.of(1, TermIndex.INITIAL_VALUE);
+
+    OMRequest modified = request.preExecute(ozoneManager);
+    assertNotEquals(original, modified);
+
+    return request.validateAndUpdateCache(ozoneManager, context);
   }
 
-  private static OMNodeDetails buildPeer(String nodeId) {
+  protected static OMNodeDetails buildPeer(String nodeId) {
     return new OMNodeDetails.Builder()
         .setOMServiceId("testService")
         .setOMNodeId(nodeId)
@@ -333,33 +334,9 @@ private static OMNodeDetails buildPeer(String nodeId) {
         .build();
   }
 
-  private static OMAdminProtocolClientSideImpl 
peerClientWithVersion(OzoneManagerVersion version) throws IOException {
+  protected static OMAdminProtocolClientSideImpl 
peerClientWithVersion(OzoneManagerVersion version) throws IOException {
     OMAdminProtocolClientSideImpl client = 
mock(OMAdminProtocolClientSideImpl.class);
     when(client.getPeerUpgradeStatus()).thenReturn(version);
     return client;
   }
-
-  private OMClientResponse submitRequest() throws IOException {
-    OzoneManagerProtocolProtos.OMRequest original = buildRequest();
-    OMStartFinalizeUpgradeRequest request = new 
OMStartFinalizeUpgradeRequest(original);
-    ExecutionContext context = ExecutionContext.of(1, TermIndex.INITIAL_VALUE);
-
-    OzoneManagerProtocolProtos.OMRequest modified = 
request.preExecute(ozoneManager);
-    assertNotEquals(original, modified);
-
-    return request.validateAndUpdateCache(ozoneManager, context);
-  }
-
-  private OzoneManagerProtocolProtos.OMRequest buildRequest() {
-    return buildRequest(false);
-  }
-
-  private OzoneManagerProtocolProtos.OMRequest buildRequest(boolean force) {
-    return OzoneManagerProtocolProtos.OMRequest.newBuilder()
-        .setCmdType(OzoneManagerProtocolProtos.Type.StartFinalizeUpgrade)
-        .setClientId(ClientId.randomId().toString())
-        
.setStartFinalizeUpgradeRequest(OzoneManagerProtocolProtos.StartFinalizeUpgradeRequest.newBuilder()
-            .setForce(force))
-        .build();
-  }
 }
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMStartFinalizeUpgradeRequestLegacy.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMStartFinalizeUpgradeRequestLegacy.java
new file mode 100644
index 00000000000..28451c6931f
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/upgrade/TestOMStartFinalizeUpgradeRequestLegacy.java
@@ -0,0 +1,63 @@
+/*
+ * 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.hadoop.ozone.om.request.upgrade;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.doNothing;
+
+import java.io.IOException;
+import 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.UpgradeFinalizationStatus;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+import org.apache.ratis.protocol.ClientId;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for OMStartFinalizeUpgradeRequestLegacy, the old CLI ({@code ozone 
admin om finalizeupgrade}) initiate
+ * request. Shared initiate behavior is covered by {@link 
TestOMStartFinalizeUpgradeRequestBase}; the case here
+ * asserts the {@code STARTING_FINALIZATION} response the old CLI requires 
before it polls progress.
+ */
+public class TestOMStartFinalizeUpgradeRequestLegacy extends 
TestOMStartFinalizeUpgradeRequestBase {
+
+  @Override
+  protected OMFinalizeUpgradeRequestBase newRequest() {
+    return new OMStartFinalizeUpgradeRequestLegacy(buildRequest());
+  }
+
+  @Test
+  public void testValidateAndUpdateCacheReturnsStartingFinalization() throws 
IOException {
+    doNothing().when(scmContainerLocationProtocol).finalizeUpgrade();
+
+    OMClientResponse response = submitRequest();
+    OMResponse omResponse = response.getOMResponse();
+
+    // The old CLI requires STARTING_FINALIZATION back before it polls 
progress.
+    assertEquals(UpgradeFinalizationStatus.Status.STARTING_FINALIZATION,
+        omResponse.getFinalizeUpgradeResponse().getStatus().getStatus());
+  }
+
+  private OzoneManagerProtocolProtos.OMRequest buildRequest() {
+    return OzoneManagerProtocolProtos.OMRequest.newBuilder()
+        .setCmdType(OzoneManagerProtocolProtos.Type.FinalizeUpgrade)
+        .setClientId(ClientId.randomId().toString())
+        
.setFinalizeUpgradeRequest(OzoneManagerProtocolProtos.FinalizeUpgradeRequest.newBuilder()
+            .setUpgradeClientId("client-id"))
+        .build();
+  }
+}
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/upgrade/TestOMFinalizeUpgradeResponse.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/upgrade/TestOMCompleteFinalizeUpgradeResponse.java
similarity index 90%
rename from 
hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/upgrade/TestOMFinalizeUpgradeResponse.java
rename to 
hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/upgrade/TestOMCompleteFinalizeUpgradeResponse.java
index 15ad3c848b5..0546b1ac96c 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/upgrade/TestOMFinalizeUpgradeResponse.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/upgrade/TestOMCompleteFinalizeUpgradeResponse.java
@@ -34,9 +34,9 @@
 import org.junit.jupiter.api.io.TempDir;
 
 /**
- * Tests for the OMFinalizeUpgradeResponse class.
+ * Tests for the OMCompleteFinalizeUpgradeResponse class.
  */
-public class TestOMFinalizeUpgradeResponse {
+public class TestOMCompleteFinalizeUpgradeResponse {
 
   @TempDir
   private Path folder;
@@ -60,7 +60,8 @@ public void testFinalizationInProgressKeyRemoved() throws 
IOException {
     String value = 
omMetadataManager.getMetaTable().get(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY);
     assertEquals("ignored", value);
 
-    OMFinalizeUpgradeResponse finalizeUpgradeResponse = new 
OMFinalizeUpgradeResponse(createRequest(), 1);
+    OMCompleteFinalizeUpgradeResponse finalizeUpgradeResponse =
+        new OMCompleteFinalizeUpgradeResponse(createRequest(), 1);
     finalizeUpgradeResponse.addToDBBatch(omMetadataManager, batchOperation);
 
     omMetadataManager.getStore().commitBatchOperation(batchOperation);
@@ -72,7 +73,7 @@ public void testFinalizationInProgressKeyRemoved() throws 
IOException {
 
   private OzoneManagerProtocolProtos.OMResponse createRequest() {
     return OzoneManagerProtocolProtos.OMResponse.newBuilder()
-        .setCmdType(OzoneManagerProtocolProtos.Type.FinalizeUpgrade)
+        .setCmdType(OzoneManagerProtocolProtos.Type.CompleteFinalizeUpgrade)
         .setStatus(OzoneManagerProtocolProtos.Status.OK)
         .build();
   }
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/upgrade/TestOMStartFinalizeUpgradeResponse.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/upgrade/TestOMStartFinalizeUpgradeResponse.java
index b5564218513..b3c8b1af85d 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/upgrade/TestOMStartFinalizeUpgradeResponse.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/upgrade/TestOMStartFinalizeUpgradeResponse.java
@@ -66,6 +66,21 @@ public void 
testAddToDBBatchPersistsFinalizationInProgressKey() throws IOExcepti
         "key should be persisted to the meta table after the response is 
committed");
   }
 
+  @Test
+  public void testAddToDBBatchSkipsPersistWhenFinalizationNotNeeded() throws 
IOException {
+    
assertNull(omMetadataManager.getMetaTable().get(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY),
+        "key should not exist before the response is applied");
+
+    // finalizationNeeded=false models initiating finalize on an 
already-finalized cluster: the key must not be
+    // persisted, otherwise it would be orphaned since 
OMUpgradeFinalizeService never clears it.
+    OMStartFinalizeUpgradeResponse response = new 
OMStartFinalizeUpgradeResponse(buildOkResponse(), false);
+    response.addToDBBatch(omMetadataManager, batchOperation);
+    omMetadataManager.getStore().commitBatchOperation(batchOperation);
+
+    
assertNull(omMetadataManager.getMetaTable().get(OzoneConsts.FINALIZATION_IN_PROGRESS_KEY),
+        "key must not be persisted when finalization is not needed");
+  }
+
   private OzoneManagerProtocolProtos.OMResponse buildOkResponse() {
     return OzoneManagerProtocolProtos.OMResponse.newBuilder()
         .setCmdType(OzoneManagerProtocolProtos.Type.StartFinalizeUpgrade)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to