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

rpuch pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new ac8696610d IGNITE-22879 Add CLI for migrating running nodes to new CMG 
(#4458)
ac8696610d is described below

commit ac8696610d3cb423dff06573b6c3d01c9b0c9306
Author: Roman Puchkovskiy <[email protected]>
AuthorDate: Fri Sep 27 12:53:00 2024 +0400

    IGNITE-22879 Add CLI for migrating running nodes to new CMG (#4458)
---
 .../cluster/ItMigrateToClusterCommandTest.java     | 94 ++++++++++++++++++++++
 .../recovery/cluster/ItResetClusterTest.java       | 29 +------
 ...t.java => ItSystemDisasterRecoveryCliTest.java} | 47 +----------
 .../recovery/cluster/MigrateToClusterCall.java     | 75 +++++++++++++++++
 .../cluster/MigrateToClusterCallInput.java         | 86 ++++++++++++++++++++
 .../ignite/internal/cli/commands/Options.java      | 14 ++++
 .../recovery/cluster/RecoveryClusterCommand.java   |  4 +-
 .../MigrateToClusterCommand.java}                  | 25 +++---
 .../cluster/migrate/MigrateToClusterMixin.java     | 56 +++++++++++++
 .../cluster/reset/ResetClusterCommand.java         |  2 -
 .../recovery/cluster/reset/ResetClusterMixin.java  |  4 +-
 .../cluster/reset/ResetClusterReplCommand.java     |  2 -
 .../internal/rest/api/cluster/ClusterState.java    | 32 +++++---
 .../apache/ignite/internal/rest/RestManager.java   | 33 ++++++--
 .../rest/cluster/ClusterManagementController.java  |  3 +-
 .../system/SystemDisasterRecoveryController.java   |  2 +
 .../ignite/internal/rest/RestManagerTest.java      | 21 ++++-
 .../disaster/system/ItCmgDisasterRecoveryTest.java |  2 +-
 .../system/ItSystemGroupDisasterRecoveryTest.java  | 16 ++--
 .../system/SystemDisasterRecoveryClient.java       | 36 ++++++---
 20 files changed, 452 insertions(+), 131 deletions(-)

diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItMigrateToClusterCommandTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItMigrateToClusterCommandTest.java
new file mode 100644
index 0000000000..ebf25d8e28
--- /dev/null
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItMigrateToClusterCommandTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.ignite.internal.cli.commands.recovery.cluster;
+
+import static org.apache.ignite.internal.TestWrappers.unwrapIgniteImpl;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.CLUSTER_URL_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.RECOVERY_CMG_NODES_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.RECOVERY_NEW_CLUSTER_URL_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.RECOVERY_OLD_CLUSTER_URL_OPTION;
+
+import org.apache.ignite.InitParametersBuilder;
+import 
org.apache.ignite.internal.cli.commands.recovery.cluster.migrate.MigrateToClusterCommand;
+import org.apache.ignite.network.NodeMetadata;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+
+/** Test class for {@link MigrateToClusterCommand}. */
+class ItMigrateToClusterCommandTest extends ItSystemDisasterRecoveryCliTest {
+    @Override
+    protected int initialNodes() {
+        return 2;
+    }
+
+    @Override
+    protected void configureInitParameters(InitParametersBuilder builder) {
+        builder.cmgNodeNames(CLUSTER.nodeName(0));
+        builder.metaStorageNodeNames(CLUSTER.nodeName(1));
+    }
+
+    @Test
+    @Order(Integer.MAX_VALUE) // This test is run last as it restarts the node.
+    void initiatesNodesMigration() throws Exception {
+        String restUrl0 = restUrl(0);
+        String restUrl1 = restUrl(1);
+
+        // Stop the CMG majority.
+        CLUSTER.stopNode(0);
+
+        // First just initiate repair (we need it to have ability to trigger a 
migration).
+        repairClusterOnNode1(restUrl1);
+
+        CLUSTER.startEmbeddedNode(0);
+
+        // Now actually do migration.
+        execute(
+                "recovery", "cluster", "migrate",
+                RECOVERY_OLD_CLUSTER_URL_OPTION, restUrl0,
+                RECOVERY_NEW_CLUSTER_URL_OPTION, restUrl1
+        );
+
+        try {
+            assertErrOutputIsEmpty();
+            assertOutputContains("Node has gone, this most probably means that 
migration is initiated and the node restarts.");
+        } finally {
+            waitTillNodeRestartsInternally(0);
+        }
+    }
+
+    private void repairClusterOnNode1(String restUrl1) throws 
InterruptedException {
+        execute(
+                "recovery", "cluster", "reset",
+                CLUSTER_URL_OPTION, restUrl1,
+                RECOVERY_CMG_NODES_OPTION, CLUSTER.nodeName(1)
+        );
+
+        try {
+            assertErrOutputIsEmpty();
+            assertOutputContains("Node has gone, this most probably means that 
cluster repair is initiated and the node restarts.");
+        } finally {
+            waitTillNodeRestartsInternally(1);
+        }
+    }
+
+    private static String restUrl(int nodeIndex) {
+        NodeMetadata nodeMetadata = 
unwrapIgniteImpl(CLUSTER.node(nodeIndex)).node().nodeMetadata();
+
+        return "http://"; + nodeMetadata.restHost() + ":" + 
nodeMetadata.httpPort();
+    }
+}
diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItResetClusterTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItResetClusterTest.java
index e5b2a023b4..2d24c194ac 100644
--- 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItResetClusterTest.java
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItResetClusterTest.java
@@ -17,19 +17,9 @@
 
 package org.apache.ignite.internal.cli.commands.recovery.cluster;
 
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.apache.ignite.internal.TestWrappers.unwrapIgniteImpl;
 import static 
org.apache.ignite.internal.cli.commands.Options.Constants.CLUSTER_URL_OPTION;
 import static 
org.apache.ignite.internal.cli.commands.Options.Constants.RECOVERY_CMG_NODES_OPTION;
-import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
-import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import java.util.concurrent.CompletableFuture;
-import org.apache.ignite.internal.app.IgniteServerImpl;
-import org.apache.ignite.internal.cli.CliIntegrationTest;
-import org.jetbrains.annotations.Nullable;
 import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
 import org.junit.jupiter.api.Order;
 import org.junit.jupiter.api.Test;
@@ -37,7 +27,7 @@ import org.junit.jupiter.api.TestMethodOrder;
 
 /** Base test class for reset cluster commands. */
 @TestMethodOrder(OrderAnnotation.class)
-abstract class ItResetClusterTest extends CliIntegrationTest {
+abstract class ItResetClusterTest extends ItSystemDisasterRecoveryCliTest {
     @Override
     protected int initialNodes() {
         return 1;
@@ -59,23 +49,6 @@ abstract class ItResetClusterTest extends CliIntegrationTest 
{
         }
     }
 
-    private static void waitTillNodeRestartsInternally(int nodeIndex) throws 
InterruptedException {
-        // restartOrShutdownFuture() becomes non-null when restart or shutdown 
is initiated; we know it's restart.
-
-        assertTrue(
-                waitForCondition(() -> restartOrShutdownFuture(nodeIndex) != 
null, SECONDS.toMillis(20)),
-                "Node did not attempt to be restarted (or shut down) in time"
-        );
-        assertThat(restartOrShutdownFuture(nodeIndex), 
willCompleteSuccessfully());
-
-        unwrapIgniteImpl(CLUSTER.server(nodeIndex).api());
-    }
-
-    @Nullable
-    private static CompletableFuture<Void> restartOrShutdownFuture(int 
nodeIndex) {
-        return ((IgniteServerImpl) 
CLUSTER.server(nodeIndex)).restartOrShutdownFuture();
-    }
-
     @Test
     void handlesErrors() {
         execute(
diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItResetClusterTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItSystemDisasterRecoveryCliTest.java
similarity index 54%
copy from 
modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItResetClusterTest.java
copy to 
modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItSystemDisasterRecoveryCliTest.java
index e5b2a023b4..1250042d18 100644
--- 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItResetClusterTest.java
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItSystemDisasterRecoveryCliTest.java
@@ -18,9 +18,6 @@
 package org.apache.ignite.internal.cli.commands.recovery.cluster;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.apache.ignite.internal.TestWrappers.unwrapIgniteImpl;
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.CLUSTER_URL_OPTION;
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.RECOVERY_CMG_NODES_OPTION;
 import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -30,36 +27,9 @@ import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.internal.app.IgniteServerImpl;
 import org.apache.ignite.internal.cli.CliIntegrationTest;
 import org.jetbrains.annotations.Nullable;
-import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestMethodOrder;
 
-/** Base test class for reset cluster commands. */
-@TestMethodOrder(OrderAnnotation.class)
-abstract class ItResetClusterTest extends CliIntegrationTest {
-    @Override
-    protected int initialNodes() {
-        return 1;
-    }
-
-    @Test
-    @Order(Integer.MAX_VALUE) // This test is run last as it restarts the node.
-    void initiatesCmgRepair() throws Exception {
-        execute(
-                CLUSTER_URL_OPTION, NODE_URL,
-                RECOVERY_CMG_NODES_OPTION, CLUSTER.aliveNode().name()
-        );
-
-        try {
-            assertErrOutputIsEmpty();
-            assertOutputContains("Node has gone, this most probably means that 
cluster repair is initiated and the node restarts.");
-        } finally {
-            waitTillNodeRestartsInternally(0);
-        }
-    }
-
-    private static void waitTillNodeRestartsInternally(int nodeIndex) throws 
InterruptedException {
+abstract class ItSystemDisasterRecoveryCliTest extends CliIntegrationTest {
+    static void waitTillNodeRestartsInternally(int nodeIndex) throws 
InterruptedException {
         // restartOrShutdownFuture() becomes non-null when restart or shutdown 
is initiated; we know it's restart.
 
         assertTrue(
@@ -67,23 +37,10 @@ abstract class ItResetClusterTest extends 
CliIntegrationTest {
                 "Node did not attempt to be restarted (or shut down) in time"
         );
         assertThat(restartOrShutdownFuture(nodeIndex), 
willCompleteSuccessfully());
-
-        unwrapIgniteImpl(CLUSTER.server(nodeIndex).api());
     }
 
     @Nullable
     private static CompletableFuture<Void> restartOrShutdownFuture(int 
nodeIndex) {
         return ((IgniteServerImpl) 
CLUSTER.server(nodeIndex)).restartOrShutdownFuture();
     }
-
-    @Test
-    void handlesErrors() {
-        execute(
-                CLUSTER_URL_OPTION, NODE_URL,
-                RECOVERY_CMG_NODES_OPTION, "no-such-node"
-        );
-
-        assertErrOutputContains("Current node is not contained in the new CMG, 
so it cannot conduct a cluster reset.");
-        assertOutputIsEmpty();
-    }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/cluster/MigrateToClusterCall.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/cluster/MigrateToClusterCall.java
new file mode 100644
index 0000000000..1f2b56a489
--- /dev/null
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/cluster/MigrateToClusterCall.java
@@ -0,0 +1,75 @@
+/*
+ * 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.ignite.internal.cli.call.recovery.cluster;
+
+import jakarta.inject.Singleton;
+import java.io.IOException;
+import org.apache.ignite.internal.cli.core.call.Call;
+import org.apache.ignite.internal.cli.core.call.DefaultCallOutput;
+import org.apache.ignite.internal.cli.core.exception.IgniteCliApiException;
+import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
+import org.apache.ignite.rest.client.api.ClusterManagementApi;
+import org.apache.ignite.rest.client.api.RecoveryApi;
+import org.apache.ignite.rest.client.invoker.ApiException;
+import org.apache.ignite.rest.client.model.ClusterState;
+import org.apache.ignite.rest.client.model.MigrateRequest;
+
+/** Call to migrate nodes from old to new cluster. */
+@Singleton
+public class MigrateToClusterCall implements Call<MigrateToClusterCallInput, 
String> {
+    private final ApiClientFactory clientFactory;
+
+    public MigrateToClusterCall(ApiClientFactory clientFactory) {
+        this.clientFactory = clientFactory;
+    }
+
+    @Override
+    public DefaultCallOutput<String> execute(MigrateToClusterCallInput input) {
+        ClusterManagementApi newClusterManagementClient = new 
ClusterManagementApi(clientFactory.getClient(input.newClusterUrl()));
+        RecoveryApi oldRecoveryClient = new 
RecoveryApi(clientFactory.getClient(input.oldClusterUrl()));
+
+        ClusterState newClusterState;
+        try {
+            newClusterState = newClusterManagementClient.clusterState();
+        } catch (ApiException e) {
+            return DefaultCallOutput.failure(new IgniteCliApiException(e, 
input.newClusterUrl()));
+        }
+
+        MigrateRequest command = new MigrateRequest();
+
+        command.setCmgNodes(newClusterState.getCmgNodes());
+        command.setMetaStorageNodes(newClusterState.getMsNodes());
+        command.setVersion(newClusterState.getIgniteVersion());
+        command.setClusterId(newClusterState.getClusterTag().getClusterId());
+        
command.setClusterName(newClusterState.getClusterTag().getClusterName());
+        command.setFormerClusterIds(newClusterState.getFormerClusterIds());
+
+        try {
+            oldRecoveryClient.migrate(command);
+        } catch (ApiException e) {
+            if (e.getCause() instanceof IOException) {
+                return DefaultCallOutput.success("Node has gone, this most 
probably means that migration is initiated and "
+                        + "the node restarts.");
+            }
+
+            return DefaultCallOutput.failure(new IgniteCliApiException(e, 
input.oldClusterUrl()));
+        }
+
+        return DefaultCallOutput.success("Successfully initiated migration.");
+    }
+}
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/cluster/MigrateToClusterCallInput.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/cluster/MigrateToClusterCallInput.java
new file mode 100644
index 0000000000..c8e70f2480
--- /dev/null
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/cluster/MigrateToClusterCallInput.java
@@ -0,0 +1,86 @@
+/*
+ * 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.ignite.internal.cli.call.recovery.cluster;
+
+import java.util.Objects;
+import 
org.apache.ignite.internal.cli.commands.recovery.cluster.migrate.MigrateToClusterMixin;
+import org.apache.ignite.internal.cli.core.call.CallInput;
+
+/** Input for the {@link MigrateToClusterCall} call. */
+public class MigrateToClusterCallInput implements CallInput {
+    private final String oldClusterUrl;
+    private final String newClusterUrl;
+
+    /** Old cluster url. */
+    public String oldClusterUrl() {
+        return oldClusterUrl;
+    }
+
+    /** New cluster url. */
+    public String newClusterUrl() {
+        return newClusterUrl;
+    }
+
+    private MigrateToClusterCallInput(String oldClusterUrl, String 
newClusterUrl) {
+        Objects.requireNonNull(oldClusterUrl);
+        Objects.requireNonNull(newClusterUrl);
+
+        this.oldClusterUrl = oldClusterUrl;
+        this.newClusterUrl = newClusterUrl;
+    }
+
+    /** Returns {@link MigrateToClusterCallInput} with specified arguments. */
+    public static MigrateToClusterCallInput of(MigrateToClusterMixin 
statesArgs) {
+        return builder()
+                .oldClusterUrl(statesArgs.getOldClusterUrl())
+                .newClusterUrl(statesArgs.getNewClusterUrl())
+                .build();
+    }
+
+    /**
+     * Builder method provider.
+     *
+     * @return new instance of {@link MigrateToClusterCallInput}.
+     */
+    private static MigrateToClusterCallInputBuilder builder() {
+        return new MigrateToClusterCallInputBuilder();
+    }
+
+    /** Builder for {@link MigrateToClusterCallInput}. */
+    private static class MigrateToClusterCallInputBuilder {
+        private String oldClusterUrl;
+        private String newClusterUrl;
+
+        /** Sets old cluster URL. */
+        MigrateToClusterCallInputBuilder oldClusterUrl(String oldClusterUrl) {
+            this.oldClusterUrl = oldClusterUrl;
+            return this;
+        }
+
+        /** Sets new cluster URL. */
+        MigrateToClusterCallInputBuilder newClusterUrl(String newClusterUrl) {
+            this.newClusterUrl = newClusterUrl;
+            return this;
+        }
+
+        /** Builds {@link MigrateToClusterCallInput}. */
+        MigrateToClusterCallInput build() {
+            return new MigrateToClusterCallInput(oldClusterUrl, newClusterUrl);
+        }
+    }
+}
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/Options.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/Options.java
index 58372d6072..68b27cf92f 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/Options.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/Options.java
@@ -298,6 +298,20 @@ public enum Options {
                 + "'--cluster-management-group node1, node2' "
                 + "to specify more than one node) that will host the Cluster 
Management Group.";
 
+        /** Old cluster endpoint URL option long name. */
+        public static final String RECOVERY_OLD_CLUSTER_URL_OPTION = 
"--old-cluster-url";
+
+        /** Old cluster endpoint URL option description. */
+        public static final String RECOVERY_OLD_CLUSTER_URL_OPTION_DESC = "URL 
of old cluster endpoint (nodes of this cluster will be "
+                + "migrated to a new cluster). It can be URL of any node of 
the old cluster.";
+
+        /** New cluster endpoint URL option long name. */
+        public static final String RECOVERY_NEW_CLUSTER_URL_OPTION = 
"--new-cluster-url";
+
+        /** New cluster endpoint URL option description. */
+        public static final String RECOVERY_NEW_CLUSTER_URL_OPTION_DESC = "URL 
of new cluster endpoint (nodes of old cluster will be "
+                + "migrated to this cluster). It can be URL of any node of the 
new cluster.";
+
         public static final String CONFIG_FORMAT_OPTION = "--format";
 
         public static final String CONFIG_FORMAT_OPTION_DESC = "Output format. 
Valid values: ${COMPLETION-CANDIDATES}";
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/RecoveryClusterCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/RecoveryClusterCommand.java
index 8b5f9ca368..aac33eccce 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/RecoveryClusterCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/RecoveryClusterCommand.java
@@ -18,13 +18,15 @@
 package org.apache.ignite.internal.cli.commands.recovery.cluster;
 
 import org.apache.ignite.internal.cli.commands.BaseCommand;
+import 
org.apache.ignite.internal.cli.commands.recovery.cluster.migrate.MigrateToClusterCommand;
 import 
org.apache.ignite.internal.cli.commands.recovery.cluster.reset.ResetClusterCommand;
 import picocli.CommandLine.Command;
 
 /** Cluster disaster recovery commands. */
 @Command(name = "cluster",
         subcommands = {
-                ResetClusterCommand.class
+                ResetClusterCommand.class,
+                MigrateToClusterCommand.class
         },
         description = "Manages disaster recovery of CMG/Metastorage group")
 public class RecoveryClusterCommand extends BaseCommand {
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/migrate/MigrateToClusterCommand.java
similarity index 57%
copy from 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterCommand.java
copy to 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/migrate/MigrateToClusterCommand.java
index 199dd1ba5d..868f59b763 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/migrate/MigrateToClusterCommand.java
@@ -15,37 +15,30 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.commands.recovery.cluster.reset;
+package org.apache.ignite.internal.cli.commands.recovery.cluster.migrate;
 
 import jakarta.inject.Inject;
 import java.util.concurrent.Callable;
-import org.apache.ignite.internal.cli.call.recovery.cluster.ResetClusterCall;
-import 
org.apache.ignite.internal.cli.call.recovery.cluster.ResetClusterCallInput;
+import 
org.apache.ignite.internal.cli.call.recovery.cluster.MigrateToClusterCall;
+import 
org.apache.ignite.internal.cli.call.recovery.cluster.MigrateToClusterCallInput;
 import org.apache.ignite.internal.cli.commands.BaseCommand;
-import org.apache.ignite.internal.cli.commands.cluster.ClusterUrlProfileMixin;
 import org.apache.ignite.internal.cli.core.call.CallExecutionPipeline;
-import 
org.apache.ignite.internal.cli.core.exception.handler.ClusterNotInitializedExceptionHandler;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Mixin;
 
-/** Command to reset cluster (that is, initiate CMG/Metastorage group repair). 
*/
-@Command(name = "reset", description = "Resets cluster.")
-public class ResetClusterCommand extends BaseCommand implements 
Callable<Integer> {
-    /** Cluster endpoint URL option. */
+/** Command to initiate migration of nodes from old (unrepaired) cluster to 
the new (repaired) one. */
+@Command(name = "migrate", description = "Migrates nodes missed during repair 
to repaired cluster.")
+public class MigrateToClusterCommand extends BaseCommand implements 
Callable<Integer> {
     @Mixin
-    private ClusterUrlProfileMixin clusterUrl;
-
-    @Mixin
-    private ResetClusterMixin options;
+    private MigrateToClusterMixin options;
 
     @Inject
-    private ResetClusterCall call;
+    private MigrateToClusterCall call;
 
     @Override
     public Integer call() {
         return runPipeline(CallExecutionPipeline.builder(call)
-                .inputProvider(() -> ResetClusterCallInput.of(options, 
clusterUrl.getClusterUrl()))
-                
.exceptionHandler(ClusterNotInitializedExceptionHandler.createHandler("Cannot 
reset cluster"))
+                .inputProvider(() -> MigrateToClusterCallInput.of(options))
         );
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/migrate/MigrateToClusterMixin.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/migrate/MigrateToClusterMixin.java
new file mode 100644
index 0000000000..30de5bd17d
--- /dev/null
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/migrate/MigrateToClusterMixin.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.ignite.internal.cli.commands.recovery.cluster.migrate;
+
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.RECOVERY_NEW_CLUSTER_URL_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.RECOVERY_NEW_CLUSTER_URL_OPTION_DESC;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.RECOVERY_OLD_CLUSTER_URL_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.RECOVERY_OLD_CLUSTER_URL_OPTION_DESC;
+
+import java.net.URL;
+import org.apache.ignite.internal.cli.core.converters.UrlConverter;
+import picocli.CommandLine.Option;
+
+/** Arguments for 'recovery cluster migrate' command. */
+public class MigrateToClusterMixin {
+    /** Cluster endpoint URL option for the old cluster (nodes of this cluster 
will be migrated to a new cluster). */
+    @Option(
+            names = RECOVERY_OLD_CLUSTER_URL_OPTION,
+            description = RECOVERY_OLD_CLUSTER_URL_OPTION_DESC,
+            converter = UrlConverter.class,
+            required = true
+    )
+    private URL oldClusterUrl;
+
+    /** Cluster endpoint URL option for the new cluster (nodes of old cluster 
will be migrated to this cluster). */
+    @Option(
+            names = RECOVERY_NEW_CLUSTER_URL_OPTION,
+            description = RECOVERY_NEW_CLUSTER_URL_OPTION_DESC,
+            converter = UrlConverter.class,
+            required = true
+    )
+    private URL newClusterUrl;
+
+    public String getOldClusterUrl() {
+        return oldClusterUrl.toString();
+    }
+
+    public String getNewClusterUrl() {
+        return newClusterUrl.toString();
+    }
+}
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterCommand.java
index 199dd1ba5d..df1f079def 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterCommand.java
@@ -24,7 +24,6 @@ import 
org.apache.ignite.internal.cli.call.recovery.cluster.ResetClusterCallInpu
 import org.apache.ignite.internal.cli.commands.BaseCommand;
 import org.apache.ignite.internal.cli.commands.cluster.ClusterUrlProfileMixin;
 import org.apache.ignite.internal.cli.core.call.CallExecutionPipeline;
-import 
org.apache.ignite.internal.cli.core.exception.handler.ClusterNotInitializedExceptionHandler;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Mixin;
 
@@ -45,7 +44,6 @@ public class ResetClusterCommand extends BaseCommand 
implements Callable<Integer
     public Integer call() {
         return runPipeline(CallExecutionPipeline.builder(call)
                 .inputProvider(() -> ResetClusterCallInput.of(options, 
clusterUrl.getClusterUrl()))
-                
.exceptionHandler(ClusterNotInitializedExceptionHandler.createHandler("Cannot 
reset cluster"))
         );
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterMixin.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterMixin.java
index 7846c129ef..65dd687c1b 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterMixin.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterMixin.java
@@ -23,9 +23,9 @@ import static 
org.apache.ignite.internal.cli.commands.Options.Constants.RECOVERY
 import java.util.List;
 import picocli.CommandLine.Option;
 
-/** Arguments for 'recovery reset cluster' command. */
+/** Arguments for 'recovery cluster reset' command. */
 public class ResetClusterMixin {
-    @Option(names = RECOVERY_CMG_NODES_OPTION, description = 
RECOVERY_CMG_NODES_OPTION_DESC, split = ",")
+    @Option(names = RECOVERY_CMG_NODES_OPTION, description = 
RECOVERY_CMG_NODES_OPTION_DESC, split = ",", required = true)
     private List<String> cmgNodeNames;
 
     /** Returns names of the proposed CMG nodes. */
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterReplCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterReplCommand.java
index f0e233ad05..a84279ea3f 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterReplCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterReplCommand.java
@@ -23,7 +23,6 @@ import 
org.apache.ignite.internal.cli.call.recovery.cluster.ResetClusterCallInpu
 import org.apache.ignite.internal.cli.commands.BaseCommand;
 import org.apache.ignite.internal.cli.commands.cluster.ClusterUrlMixin;
 import 
org.apache.ignite.internal.cli.commands.questions.ConnectToClusterQuestion;
-import 
org.apache.ignite.internal.cli.core.exception.handler.ClusterNotInitializedExceptionHandler;
 import org.apache.ignite.internal.cli.core.flow.builder.Flows;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Mixin;
@@ -49,7 +48,6 @@ public class ResetClusterReplCommand extends BaseCommand 
implements Runnable {
         runFlow(question.askQuestionIfNotConnected(clusterUrl.getClusterUrl())
                 .map(url -> ResetClusterCallInput.of(options, url))
                 .then(Flows.fromCall(call))
-                
.exceptionHandler(ClusterNotInitializedExceptionHandler.createReplHandler("Cannot
 reset cluster"))
                 .print()
         );
     }
diff --git 
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterState.java
 
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterState.java
index 4fc341f8a7..e70f2ece32 100644
--- 
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterState.java
+++ 
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterState.java
@@ -22,7 +22,12 @@ import com.fasterxml.jackson.annotation.JsonGetter;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import java.util.Collection;
+import java.util.List;
 import java.util.Objects;
+import java.util.UUID;
+import org.apache.ignite.internal.tostring.IgniteToStringInclude;
+import org.apache.ignite.internal.tostring.S;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * REST representation of internal ClusterState.
@@ -30,9 +35,11 @@ import java.util.Objects;
 @Schema(description = "Information about current cluster state.")
 public class ClusterState {
     @Schema(description = "List of cluster management group nodes. These nodes 
are responsible for maintaining RAFT cluster topology.")
+    @IgniteToStringInclude
     private final Collection<String> cmgNodes;
 
     @Schema(description = "List of metastorage nodes. These nodes are 
responsible for storing RAFT cluster metadata.")
+    @IgniteToStringInclude
     private final Collection<String> msNodes;
 
     @Schema(description = "Version of Apache Ignite that the cluster was 
created on.")
@@ -41,6 +48,10 @@ public class ClusterState {
     @Schema(description = "Unique tag that identifies the cluster.")
     private final ClusterTag clusterTag;
 
+    @Schema(description = "IDs the cluster had before.")
+    @IgniteToStringInclude
+    private final @Nullable List<UUID> formerClusterIds;
+
     /**
      * Creates a new cluster state.
      *
@@ -48,18 +59,21 @@ public class ClusterState {
      * @param msNodes Node names that host the Meta Storage.
      * @param igniteVersion Version of Ignite nodes that comprise this cluster.
      * @param clusterTag Cluster tag.
+     * @param formerClusterIds Former cluster IDs.
      */
     @JsonCreator
     public ClusterState(
             @JsonProperty("cmgNodes") Collection<String> cmgNodes,
             @JsonProperty("msNodes") Collection<String> msNodes,
             @JsonProperty("igniteVersion") String igniteVersion,
-            @JsonProperty("clusterTag") ClusterTag clusterTag
+            @JsonProperty("clusterTag") ClusterTag clusterTag,
+            @JsonProperty("formerClusterIds") @Nullable List<UUID> 
formerClusterIds
     ) {
-        this.cmgNodes = cmgNodes;
-        this.msNodes = msNodes;
+        this.cmgNodes = List.copyOf(cmgNodes);
+        this.msNodes = List.copyOf(msNodes);
         this.igniteVersion = igniteVersion;
         this.clusterTag = clusterTag;
+        this.formerClusterIds = formerClusterIds == null ? null : 
List.copyOf(formerClusterIds);
     }
 
     @JsonGetter("cmgNodes")
@@ -82,6 +96,11 @@ public class ClusterState {
         return clusterTag;
     }
 
+    @JsonGetter("formerClusterIds")
+    public @Nullable List<UUID> formerClusterIds() {
+        return formerClusterIds;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
@@ -102,11 +121,6 @@ public class ClusterState {
 
     @Override
     public String toString() {
-        return "ClusterState{"
-                + "cmgNodes=" + cmgNodes
-                + ", msNodes=" + msNodes
-                + ", igniteVersion=" + igniteVersion
-                + ", clusterTag=" + clusterTag
-                + '}';
+        return S.toString(this);
     }
 }
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestManager.java 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestManager.java
index 044d7ebf25..73d42b42f8 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestManager.java
+++ 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestManager.java
@@ -38,7 +38,7 @@ public class RestManager {
     private static final String CLUSTER_NOT_INITIALIZED_REASON = "Cluster is 
not initialized. "
             + "Call /management/v1/cluster/init in order to initialize 
cluster.";
 
-    private static final String[] DEFAULT_ENDPOINTS = {
+    private static final String[] DEFAULT_AVAILABLE_ON_START_ENDPOINTS = {
             "/management/v1/configuration/node",
             "/management/v1/cluster/init",
             "/management/v1/cluster/topology/physical",
@@ -46,16 +46,22 @@ public class RestManager {
             "/management/v1/recovery/cluster"
     };
 
+    private static final String[] 
DEFAULT_AVAILABLE_DURING_INITIALIZATION_ENDPOINTS = {
+            "/management/v1/recovery/cluster"
+    };
+
     private final String[] availableOnStartEndpoints;
+    private final String[] availableDuringInitializationEndpoints;
 
     private RestState state = RestState.NOT_INITIALIZED;
 
     public RestManager() {
-        this(DEFAULT_ENDPOINTS);
+        this(DEFAULT_AVAILABLE_ON_START_ENDPOINTS, 
DEFAULT_AVAILABLE_DURING_INITIALIZATION_ENDPOINTS);
     }
 
-    public RestManager(String[] availableOnStartEndpoints) {
+    public RestManager(String[] availableOnStartEndpoints, String[] 
availableDuringInitializationEndpoints) {
         this.availableOnStartEndpoints = availableOnStartEndpoints;
+        this.availableDuringInitializationEndpoints = 
availableDuringInitializationEndpoints;
     }
 
     /**
@@ -69,7 +75,9 @@ public class RestManager {
             case INITIALIZED:
                 return available();
             case INITIALIZATION:
-                return unavailable(DURING_INITIALIZATION_TITLE, 
DURING_INITIALIZATION_REASON);
+                return pathDisabledForInitializationPhase(requestPath)
+                        ? unavailable(DURING_INITIALIZATION_TITLE, 
DURING_INITIALIZATION_REASON)
+                        : available();
             case NOT_INITIALIZED:
                 return pathDisabledForNotInitializedCluster(requestPath)
                         ? unavailable(CLUSTER_NOT_INITIALIZED_TITLE, 
CLUSTER_NOT_INITIALIZED_REASON)
@@ -80,7 +88,22 @@ public class RestManager {
     }
 
     /**
-     * Returns disabled or not path of REST method.
+     * Returns disabled or not path of REST method during {@link 
RestState#INITIALIZATION} phase.
+     *
+     * @param path REST method path.
+     * @return {@code true} in case when path disable or {@code false} if not.
+     */
+    private boolean pathDisabledForInitializationPhase(String path) {
+        for (String enabledPath : availableDuringInitializationEndpoints) {
+            if (path.startsWith(enabledPath)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns disabled or not path of REST method during {@link 
RestState#NOT_INITIALIZED} phase.
      *
      * @param path REST method path.
      * @return {@code true} in case when path disable or {@code false} if not.
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
index bf9264ae3d..a9ae750979 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
+++ 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
@@ -89,7 +89,8 @@ public class ClusterManagementController implements 
ClusterManagementApi, Resour
                 clusterState.cmgNodes(),
                 clusterState.metaStorageNodes(),
                 clusterState.igniteVersion().toString(),
-                new ClusterTag(clusterState.clusterTag().clusterName(), 
clusterState.clusterTag().clusterId())
+                new ClusterTag(clusterState.clusterTag().clusterName(), 
clusterState.clusterTag().clusterId()),
+                clusterState.formerClusterIds()
         );
     }
 
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/recovery/system/SystemDisasterRecoveryController.java
 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/recovery/system/SystemDisasterRecoveryController.java
index 8063a0abd1..c982e7ec75 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/recovery/system/SystemDisasterRecoveryController.java
+++ 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/recovery/system/SystemDisasterRecoveryController.java
@@ -66,6 +66,8 @@ public class SystemDisasterRecoveryController implements 
SystemDisasterRecoveryA
 
     @Override
     public CompletableFuture<Void> migrate(MigrateRequest command) {
+        LOG.info("Migrate command is {}", command);
+
         return 
systemDisasterRecoveryManager.migrate(migrateRequestToClusterState(command));
     }
 
diff --git 
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestManagerTest.java
 
b/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestManagerTest.java
index 22b60554a7..6c07763be5 100644
--- 
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestManagerTest.java
+++ 
b/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestManagerTest.java
@@ -40,9 +40,22 @@ public class RestManagerTest {
             "cluster/v2/method"
     };
 
+    private static final String[] AVAILABLE_DURING_INITIALIZATION = {
+            "cluster/v1",
+    };
+
+    private static final String[] UNAVAILABLE_DURING_INITIALIZATION = {
+            "node/v1/test",
+            "cluster/v2/test",
+            "cluster/v2",
+            "node/v1",
+            "node",
+            "cluster/v2/method"
+    };
+
     @Test
     public void pathAvailabilityTest() {
-        RestManager restManager = new RestManager(AVAILABLE_ON_START);
+        RestManager restManager = new RestManager(AVAILABLE_ON_START, 
AVAILABLE_DURING_INITIALIZATION);
 
         for (String availablePath : AVAILABLE_ON_START) {
             assertThat(restManager.pathAvailability(availablePath), 
is(available()));
@@ -54,11 +67,11 @@ public class RestManagerTest {
 
         restManager.setState(RestState.INITIALIZATION);
 
-        for (String availablePath : AVAILABLE_ON_START) {
-            
assertThat(restManager.pathAvailability(availablePath).isAvailable(), 
is(false));
+        for (String availablePath : AVAILABLE_DURING_INITIALIZATION) {
+            assertThat(restManager.pathAvailability(availablePath), 
is(available()));
         }
 
-        for (String unavailablePath : UNAVAILABLE_ON_START) {
+        for (String unavailablePath : UNAVAILABLE_DURING_INITIALIZATION) {
             
assertThat(restManager.pathAvailability(unavailablePath).isAvailable(), 
is(false));
         }
 
diff --git 
a/modules/system-disaster-recovery/src/integrationTest/java/org/apache/ignite/internal/disaster/system/ItCmgDisasterRecoveryTest.java
 
b/modules/system-disaster-recovery/src/integrationTest/java/org/apache/ignite/internal/disaster/system/ItCmgDisasterRecoveryTest.java
index 0949367a07..8e667ee3a9 100644
--- 
a/modules/system-disaster-recovery/src/integrationTest/java/org/apache/ignite/internal/disaster/system/ItCmgDisasterRecoveryTest.java
+++ 
b/modules/system-disaster-recovery/src/integrationTest/java/org/apache/ignite/internal/disaster/system/ItCmgDisasterRecoveryTest.java
@@ -85,7 +85,7 @@ class ItCmgDisasterRecoveryTest extends 
ItSystemGroupDisasterRecoveryTest {
     private void initiateCmgRepairVia(IgniteImpl conductor, int... 
newCmgIndexes) throws InterruptedException {
         NodeMetadata nodeMetadata = conductor.node().nodeMetadata();
 
-        recoveryClient.initiateCmgRepairVia(nodeMetadata.restHost(), 
nodeMetadata.httpPort(), nodeNames(newCmgIndexes));
+        recoveryClient.initiateCmgRepair(nodeMetadata.restHost(), 
nodeMetadata.httpPort(), nodeNames(newCmgIndexes));
     }
 
     @Test
diff --git 
a/modules/system-disaster-recovery/src/integrationTest/java/org/apache/ignite/internal/disaster/system/ItSystemGroupDisasterRecoveryTest.java
 
b/modules/system-disaster-recovery/src/integrationTest/java/org/apache/ignite/internal/disaster/system/ItSystemGroupDisasterRecoveryTest.java
index 26798353dd..a00573cf38 100644
--- 
a/modules/system-disaster-recovery/src/integrationTest/java/org/apache/ignite/internal/disaster/system/ItSystemGroupDisasterRecoveryTest.java
+++ 
b/modules/system-disaster-recovery/src/integrationTest/java/org/apache/ignite/internal/disaster/system/ItSystemGroupDisasterRecoveryTest.java
@@ -34,6 +34,7 @@ import 
org.apache.ignite.internal.ClusterPerTestIntegrationTest;
 import org.apache.ignite.internal.app.IgniteImpl;
 import org.apache.ignite.internal.app.IgniteServerImpl;
 import org.apache.ignite.internal.cluster.management.ClusterState;
+import org.apache.ignite.network.NodeMetadata;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -116,12 +117,15 @@ abstract class ItSystemGroupDisasterRecoveryTest extends 
ClusterPerTestIntegrati
         waitTillNodeRestartsInternally(oldClusterNodeIndex);
     }
 
-    static void initiateMigrationToNewCluster(IgniteImpl nodeMissingRepair, 
IgniteImpl repairedNode) throws Exception {
-        // TODO: IGNITE-22879 - initiate migration via CLI.
+    void initiateMigrationToNewCluster(IgniteImpl nodeMissingRepair, 
IgniteImpl repairedNode) throws Exception {
+        NodeMetadata missingRepairMetadata = 
nodeMissingRepair.node().nodeMetadata();
+        NodeMetadata repairedMetadata = repairedNode.node().nodeMetadata();
 
-        ClusterState newClusterState = clusterState(repairedNode);
-
-        CompletableFuture<Void> migrationFuture = 
nodeMissingRepair.systemDisasterRecoveryManager().migrate(newClusterState);
-        assertThat(migrationFuture, willCompleteSuccessfully());
+        recoveryClient.initiateMigration(
+                missingRepairMetadata.restHost(),
+                missingRepairMetadata.httpPort(),
+                repairedMetadata.restHost(),
+                repairedMetadata.httpPort()
+        );
     }
 }
diff --git 
a/modules/system-disaster-recovery/src/integrationTest/java/org/apache/ignite/internal/disaster/system/SystemDisasterRecoveryClient.java
 
b/modules/system-disaster-recovery/src/integrationTest/java/org/apache/ignite/internal/disaster/system/SystemDisasterRecoveryClient.java
index 1528f0669d..90fc69da66 100644
--- 
a/modules/system-disaster-recovery/src/integrationTest/java/org/apache/ignite/internal/disaster/system/SystemDisasterRecoveryClient.java
+++ 
b/modules/system-disaster-recovery/src/integrationTest/java/org/apache/ignite/internal/disaster/system/SystemDisasterRecoveryClient.java
@@ -22,7 +22,9 @@ import static java.util.concurrent.TimeUnit.SECONDS;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Stream;
 import org.apache.ignite.internal.cli.Main;
 import org.apache.ignite.internal.logger.IgniteLogger;
 import org.apache.ignite.internal.logger.Loggers;
@@ -34,23 +36,28 @@ import org.apache.ignite.internal.logger.Loggers;
 class SystemDisasterRecoveryClient {
     private static final IgniteLogger LOG = 
Loggers.forClass(SystemDisasterRecoveryClient.class);
 
-    void initiateCmgRepairVia(String httpHost, int httpPort, String... 
newCmgNodeNames) throws InterruptedException {
+    void initiateCmgRepair(String httpHost, int httpPort, String... 
newCmgNodeNames) throws InterruptedException {
         LOG.info("Initiating CMG repair via {}:{}, new CMG {}", httpHost, 
httpPort, List.of(newCmgNodeNames));
 
+        executeWithSameJavaBinaryAndClasspath(
+                Main.class.getName(),
+                "recovery", "cluster", "reset",
+                "--url", "http://"; + httpHost + ":" + httpPort,
+                "--cluster-management-group", String.join(",", newCmgNodeNames)
+        );
+    }
+
+    private static void executeWithSameJavaBinaryAndClasspath(String... args) 
throws InterruptedException {
         String javaBinaryPath = 
ProcessHandle.current().info().command().orElseThrow();
         String javaClassPath = System.getProperty("java.class.path");
 
         LOG.info("Java binary is {}, classpath is {}", javaBinaryPath, 
javaClassPath);
 
+        String[] fullArgs = Stream.concat(Stream.of(javaBinaryPath, "-cp", 
javaClassPath), Arrays.stream(args))
+                .toArray(String[]::new);
+
         //noinspection UseOfProcessBuilder
-        ProcessBuilder processBuilder = new ProcessBuilder(
-                javaBinaryPath,
-                "-cp", javaClassPath,
-                Main.class.getName(),
-                "recovery", "cluster", "reset",
-                "--url", "http://"; + httpHost + ":" + httpPort,
-                "--cluster-management-group", String.join(",", newCmgNodeNames)
-        );
+        ProcessBuilder processBuilder = new ProcessBuilder(fullArgs);
         executeProcessFrom(processBuilder);
     }
 
@@ -88,4 +95,15 @@ class SystemDisasterRecoveryClient {
             throw new RuntimeException(e);
         }
     }
+
+    void initiateMigration(String oldHttpHost, int oldHttpPort, String 
newHttpHost, int newHttpPort) throws InterruptedException {
+        LOG.info("Initiating migration, old {}:{}, new {}:{}", oldHttpHost, 
oldHttpPort, newHttpHost, newHttpPort);
+
+        executeWithSameJavaBinaryAndClasspath(
+                Main.class.getName(),
+                "recovery", "cluster", "migrate",
+                "--old-cluster-url", "http://"; + oldHttpHost + ":" + 
oldHttpPort,
+                "--new-cluster-url", "http://"; + newHttpHost + ":" + 
newHttpPort
+        );
+    }
 }

Reply via email to