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 d2f0b316f2 IGNITE-22812 Add CLI for initiating CMG repair (#4444)
d2f0b316f2 is described below

commit d2f0b316f2b56acd5ff954e98c01db15e2d65819
Author: Roman Puchkovskiy <[email protected]>
AuthorDate: Thu Sep 26 17:01:29 2024 +0400

    IGNITE-22812 Add CLI for initiating CMG repair (#4444)
---
 .../cluster/ItResetClusterCommandTest.java}        | 22 +++---
 .../cluster/ItResetClusterReplCommandTest.java}    | 19 ++---
 .../recovery/cluster/ItResetClusterTest.java       | 89 +++++++++++++++++++++
 .../call/recovery/cluster/ResetClusterCall.java    | 60 ++++++++++++++
 .../recovery/cluster/ResetClusterCallInput.java    | 88 +++++++++++++++++++++
 .../recovery/reset/ResetPartitionsCallInput.java   |  2 +-
 .../ignite/internal/cli/commands/Options.java      |  6 ++
 .../cli/commands/recovery/RecoveryCommand.java     |  4 +-
 .../cli/commands/recovery/RecoveryReplCommand.java |  4 +-
 .../RecoveryClusterCommand.java}                   | 14 ++--
 .../RecoveryClusterReplCommand.java}               | 14 ++--
 .../cluster/reset/ResetClusterCommand.java         | 51 ++++++++++++
 .../reset/ResetClusterMixin.java}                  | 26 ++++---
 .../cluster/reset/ResetClusterReplCommand.java     | 56 +++++++++++++
 .../api/recovery/system/ResetClusterRequest.java   |  4 +-
 .../recovery/system/SystemDisasterRecoveryApi.java |  2 +-
 .../system/SystemDisasterRecoveryController.java   |  8 +-
 modules/system-disaster-recovery/build.gradle      |  1 +
 .../disaster/system/ItCmgDisasterRecoveryTest.java |  9 +--
 .../system/ItSystemGroupDisasterRecoveryTest.java  |  2 +
 .../system/SystemDisasterRecoveryClient.java       | 91 ++++++++++++++++++++++
 21 files changed, 513 insertions(+), 59 deletions(-)

diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItResetClusterCommandTest.java
similarity index 60%
copy from 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
copy to 
modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItResetClusterCommandTest.java
index 8f54161e79..185f952bf0 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItResetClusterCommandTest.java
@@ -15,17 +15,17 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.commands.recovery;
+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.partitions.PartitionsCommand;
-import picocli.CommandLine.Command;
+import 
org.apache.ignite.internal.cli.commands.recovery.cluster.reset.ResetClusterCommand;
+import org.apache.ignite.internal.util.ArrayUtils;
 
-/** Disaster recovery command. */
-@Command(name = "recovery",
-        subcommands = {
-                PartitionsCommand.class
-        },
-        description = "Manages disaster recovery of Ignite cluster")
-public class RecoveryCommand extends BaseCommand {
+/** Test class for {@link ResetClusterCommand}. */
+class ItResetClusterCommandTest extends ItResetClusterTest {
+    @Override
+    protected void execute(String... args) {
+        String[] fullArgs = ArrayUtils.concat(new String[] {"recovery", 
"cluster", "reset"}, args);
+
+        super.execute(fullArgs);
+    }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItResetClusterReplCommandTest.java
similarity index 61%
copy from 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
copy to 
modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItResetClusterReplCommandTest.java
index 8f54161e79..a03e3f7790 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItResetClusterReplCommandTest.java
@@ -15,17 +15,14 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.commands.recovery;
+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.partitions.PartitionsCommand;
-import picocli.CommandLine.Command;
+import 
org.apache.ignite.internal.cli.commands.recovery.cluster.reset.ResetClusterReplCommand;
 
-/** Disaster recovery command. */
-@Command(name = "recovery",
-        subcommands = {
-                PartitionsCommand.class
-        },
-        description = "Manages disaster recovery of Ignite cluster")
-public class RecoveryCommand extends BaseCommand {
+/** Test class for {@link ResetClusterReplCommand}. */
+class ItResetClusterReplCommandTest extends ItResetClusterTest {
+    @Override
+    protected Class<?> getCommandClass() {
+        return ResetClusterReplCommand.class;
+    }
 }
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
new file mode 100644
index 0000000000..e5b2a023b4
--- /dev/null
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/recovery/cluster/ItResetClusterTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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 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;
+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 {
+        // 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(
+                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/ResetClusterCall.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/cluster/ResetClusterCall.java
new file mode 100644
index 0000000000..c6566044f4
--- /dev/null
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/cluster/ResetClusterCall.java
@@ -0,0 +1,60 @@
+/*
+ * 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.RecoveryApi;
+import org.apache.ignite.rest.client.invoker.ApiException;
+import org.apache.ignite.rest.client.model.ResetClusterRequest;
+
+/** Call to reset cluster (that is, initiate CMG/Metastorage group repair). */
+@Singleton
+public class ResetClusterCall implements Call<ResetClusterCallInput, String> {
+    private final ApiClientFactory clientFactory;
+
+    public ResetClusterCall(ApiClientFactory clientFactory) {
+        this.clientFactory = clientFactory;
+    }
+
+    @Override
+    public DefaultCallOutput<String> execute(ResetClusterCallInput input) {
+        RecoveryApi client = new 
RecoveryApi(clientFactory.getClient(input.clusterUrl()));
+
+        ResetClusterRequest command = new ResetClusterRequest();
+
+        command.setCmgNodeNames(input.cmgNodeNames());
+
+        try {
+            client.resetCluster(command);
+
+            return DefaultCallOutput.success("Successfully initiated cluster 
repair.");
+        } catch (ApiException e) {
+            if (e.getCause() instanceof IOException) {
+                return DefaultCallOutput.success("Node has gone, this most 
probably means that cluster repair is initiated and "
+                        + "the node restarts.");
+            }
+
+            return DefaultCallOutput.failure(new IgniteCliApiException(e, 
input.clusterUrl()));
+        }
+    }
+}
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/cluster/ResetClusterCallInput.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/cluster/ResetClusterCallInput.java
new file mode 100644
index 0000000000..b5767c1690
--- /dev/null
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/cluster/ResetClusterCallInput.java
@@ -0,0 +1,88 @@
+/*
+ * 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.List;
+import java.util.Objects;
+import 
org.apache.ignite.internal.cli.commands.recovery.cluster.reset.ResetClusterMixin;
+import org.apache.ignite.internal.cli.core.call.CallInput;
+
+/** Input for the {@link ResetClusterCall} call. */
+public class ResetClusterCallInput implements CallInput {
+    private final String clusterUrl;
+
+    private final List<String> cmgNodeNames;
+
+    /** Cluster url. */
+    public String clusterUrl() {
+        return clusterUrl;
+    }
+
+    /** Returns names of the proposed CMG nodes. */
+    public List<String> cmgNodeNames() {
+        return cmgNodeNames;
+    }
+
+    private ResetClusterCallInput(String clusterUrl, List<String> 
cmgNodeNames) {
+        Objects.requireNonNull(cmgNodeNames);
+
+        this.clusterUrl = clusterUrl;
+        this.cmgNodeNames = List.copyOf(cmgNodeNames);
+    }
+
+    /** Returns {@link ResetClusterCallInput} with specified arguments. */
+    public static ResetClusterCallInput of(ResetClusterMixin statesArgs, 
String clusterUrl) {
+        return builder()
+                .cmgNodeNames(statesArgs.cmgNodeNames())
+                .clusterUrl(clusterUrl)
+                .build();
+    }
+
+    /**
+     * Builder method provider.
+     *
+     * @return new instance of {@link ResetClusterCallInput}.
+     */
+    private static ResetClusterCallInputBuilder builder() {
+        return new ResetClusterCallInputBuilder();
+    }
+
+    /** Builder for {@link ResetClusterCallInput}. */
+    private static class ResetClusterCallInputBuilder {
+        private String clusterUrl;
+
+        private List<String> cmgNodeNames;
+
+        /** Set cluster URL. */
+        ResetClusterCallInputBuilder clusterUrl(String clusterUrl) {
+            this.clusterUrl = clusterUrl;
+            return this;
+        }
+
+        /** Names of the proposed CMG nodes. */
+        ResetClusterCallInputBuilder cmgNodeNames(List<String> cmgNodeNames) {
+            this.cmgNodeNames = cmgNodeNames;
+            return this;
+        }
+
+        /** Build {@link ResetClusterCallInput}. */
+        ResetClusterCallInput build() {
+            return new ResetClusterCallInput(clusterUrl, cmgNodeNames);
+        }
+    }
+}
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/reset/ResetPartitionsCallInput.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/reset/ResetPartitionsCallInput.java
index 4f879215fd..3c5b55a809 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/reset/ResetPartitionsCallInput.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/recovery/reset/ResetPartitionsCallInput.java
@@ -112,7 +112,7 @@ public class ResetPartitionsCallInput implements CallInput {
             return this;
         }
 
-        /** Names of zones to reset partitions of. */
+        /** IDs of partitions to reset. */
         ResetPartitionsCallInputBuilder partitionIds(@Nullable List<Integer> 
partitionIds) {
             this.partitionIds = partitionIds;
             return this;
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 0202049181..58372d6072 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
@@ -292,6 +292,12 @@ public enum Options {
         public static final String RECOVERY_NODE_NAMES_OPTION_DESC = "Names 
specifying nodes to get partition states from. "
                 + "Case-sensitive, without quotes, all nodes if not set";
 
+        public static final String RECOVERY_CMG_NODES_OPTION = 
"--cluster-management-group";
+
+        public static final String RECOVERY_CMG_NODES_OPTION_DESC = "Names of 
nodes (use comma-separated list of node names "
+                + "'--cluster-management-group node1, node2' "
+                + "to specify more than one node) that will host the Cluster 
Management Group.";
+
         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/RecoveryCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
index 8f54161e79..7ae1519768 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
@@ -18,13 +18,15 @@
 package org.apache.ignite.internal.cli.commands.recovery;
 
 import org.apache.ignite.internal.cli.commands.BaseCommand;
+import 
org.apache.ignite.internal.cli.commands.recovery.cluster.RecoveryClusterCommand;
 import 
org.apache.ignite.internal.cli.commands.recovery.partitions.PartitionsCommand;
 import picocli.CommandLine.Command;
 
 /** Disaster recovery command. */
 @Command(name = "recovery",
         subcommands = {
-                PartitionsCommand.class
+                PartitionsCommand.class,
+                RecoveryClusterCommand.class
         },
         description = "Manages disaster recovery of Ignite cluster")
 public class RecoveryCommand extends BaseCommand {
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryReplCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryReplCommand.java
index bfe5b76403..e50e101d28 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryReplCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryReplCommand.java
@@ -18,13 +18,15 @@
 package org.apache.ignite.internal.cli.commands.recovery;
 
 import org.apache.ignite.internal.cli.commands.BaseCommand;
+import 
org.apache.ignite.internal.cli.commands.recovery.cluster.RecoveryClusterReplCommand;
 import 
org.apache.ignite.internal.cli.commands.recovery.partitions.PartitionsReplCommand;
 import picocli.CommandLine.Command;
 
 /** Disaster recovery command. */
 @Command(name = "recovery",
         subcommands = {
-                PartitionsReplCommand.class
+                PartitionsReplCommand.class,
+                RecoveryClusterReplCommand.class
         },
         description = "Manages disaster recovery of Ignite cluster")
 public class RecoveryReplCommand extends BaseCommand {
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/RecoveryClusterCommand.java
similarity index 69%
copy from 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
copy to 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/RecoveryClusterCommand.java
index 8f54161e79..8b5f9ca368 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/RecoveryClusterCommand.java
@@ -15,17 +15,17 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.commands.recovery;
+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.partitions.PartitionsCommand;
+import 
org.apache.ignite.internal.cli.commands.recovery.cluster.reset.ResetClusterCommand;
 import picocli.CommandLine.Command;
 
-/** Disaster recovery command. */
-@Command(name = "recovery",
+/** Cluster disaster recovery commands. */
+@Command(name = "cluster",
         subcommands = {
-                PartitionsCommand.class
+                ResetClusterCommand.class
         },
-        description = "Manages disaster recovery of Ignite cluster")
-public class RecoveryCommand extends BaseCommand {
+        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/RecoveryCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/RecoveryClusterReplCommand.java
similarity index 69%
copy from 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
copy to 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/RecoveryClusterReplCommand.java
index 8f54161e79..76bff3b433 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/RecoveryClusterReplCommand.java
@@ -15,17 +15,17 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.commands.recovery;
+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.partitions.PartitionsCommand;
+import 
org.apache.ignite.internal.cli.commands.recovery.cluster.reset.ResetClusterReplCommand;
 import picocli.CommandLine.Command;
 
-/** Disaster recovery command. */
-@Command(name = "recovery",
+/** Cluster disaster recovery commands. */
+@Command(name = "cluster",
         subcommands = {
-                PartitionsCommand.class
+                ResetClusterReplCommand.class
         },
-        description = "Manages disaster recovery of Ignite cluster")
-public class RecoveryCommand extends BaseCommand {
+        description = "Manages disaster recovery of CMG/Metastorage group")
+public class RecoveryClusterReplCommand 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/reset/ResetClusterCommand.java
new file mode 100644
index 0000000000..199dd1ba5d
--- /dev/null
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterCommand.java
@@ -0,0 +1,51 @@
+/*
+ * 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.reset;
+
+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.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. */
+    @Mixin
+    private ClusterUrlProfileMixin clusterUrl;
+
+    @Mixin
+    private ResetClusterMixin options;
+
+    @Inject
+    private ResetClusterCall call;
+
+    @Override
+    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/RecoveryCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterMixin.java
similarity index 53%
copy from 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
copy to 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterMixin.java
index 8f54161e79..7846c129ef 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/RecoveryCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterMixin.java
@@ -15,17 +15,21 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.commands.recovery;
+package org.apache.ignite.internal.cli.commands.recovery.cluster.reset;
 
-import org.apache.ignite.internal.cli.commands.BaseCommand;
-import 
org.apache.ignite.internal.cli.commands.recovery.partitions.PartitionsCommand;
-import picocli.CommandLine.Command;
+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_CMG_NODES_OPTION_DESC;
 
-/** Disaster recovery command. */
-@Command(name = "recovery",
-        subcommands = {
-                PartitionsCommand.class
-        },
-        description = "Manages disaster recovery of Ignite cluster")
-public class RecoveryCommand extends BaseCommand {
+import java.util.List;
+import picocli.CommandLine.Option;
+
+/** Arguments for 'recovery reset cluster' command. */
+public class ResetClusterMixin {
+    @Option(names = RECOVERY_CMG_NODES_OPTION, description = 
RECOVERY_CMG_NODES_OPTION_DESC, split = ",")
+    private List<String> cmgNodeNames;
+
+    /** Returns names of the proposed CMG nodes. */
+    public List<String> cmgNodeNames() {
+        return cmgNodeNames;
+    }
 }
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
new file mode 100644
index 0000000000..f0e233ad05
--- /dev/null
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/recovery/cluster/reset/ResetClusterReplCommand.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.reset;
+
+import jakarta.inject.Inject;
+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.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;
+
+/** Command to reset cluster (that is, initiate CMG/Metastorage group repair). 
*/
+@Command(name = "reset", description = "Resets cluster.")
+public class ResetClusterReplCommand extends BaseCommand implements Runnable {
+    /** Cluster endpoint URL option. */
+    @Mixin
+    private ClusterUrlMixin clusterUrl;
+
+    @Mixin
+    private ResetClusterMixin options;
+
+    @Inject
+    private ConnectToClusterQuestion question;
+
+    @Inject
+    private ResetClusterCall call;
+
+    @Override
+    public void run() {
+        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/recovery/system/ResetClusterRequest.java
 
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/recovery/system/ResetClusterRequest.java
index 03907a3a05..978f5c3da2 100644
--- 
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/recovery/system/ResetClusterRequest.java
+++ 
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/recovery/system/ResetClusterRequest.java
@@ -29,7 +29,7 @@ import org.apache.ignite.internal.tostring.S;
 /** Request to reset cluster. */
 @Schema(description = "Reset cluster.")
 public class ResetClusterRequest {
-    @Schema(description = "Names of the proposed CMG node names.")
+    @Schema(description = "Names of the proposed CMG nodes.")
     @IgniteToStringInclude
     private final List<String> cmgNodeNames;
 
@@ -41,7 +41,7 @@ public class ResetClusterRequest {
         this.cmgNodeNames = List.copyOf(cmgNodeNames);
     }
 
-    /** Returns names of the proposed CMG node names. */
+    /** Returns names of the proposed CMG nodes. */
     @JsonGetter("cmgNodeNames")
     public List<String> cmgNodeNames() {
         return cmgNodeNames;
diff --git 
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/recovery/system/SystemDisasterRecoveryApi.java
 
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/recovery/system/SystemDisasterRecoveryApi.java
index f67ce6e1aa..2a4c204358 100644
--- 
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/recovery/system/SystemDisasterRecoveryApi.java
+++ 
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/recovery/system/SystemDisasterRecoveryApi.java
@@ -42,7 +42,7 @@ import org.apache.ignite.internal.rest.constants.MediaType;
 public interface SystemDisasterRecoveryApi {
     @Post("reset")
     @Operation(
-            operationId = "reset",
+            operationId = "resetCluster",
             description = "Initiates cluster reset to repair CMG/Metastorage 
group/both."
     )
     @ApiResponse(responseCode = "200", description = "Cluster reset 
initiated.")
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 d6d8c59ed1..8063a0abd1 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
@@ -27,6 +27,8 @@ import 
org.apache.ignite.internal.cluster.management.ClusterState;
 import org.apache.ignite.internal.cluster.management.ClusterTag;
 import 
org.apache.ignite.internal.cluster.management.network.messages.CmgMessagesFactory;
 import 
org.apache.ignite.internal.disaster.system.SystemDisasterRecoveryManager;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
 import org.apache.ignite.internal.rest.ResourceHolder;
 import org.apache.ignite.internal.rest.api.recovery.system.MigrateRequest;
 import org.apache.ignite.internal.rest.api.recovery.system.ResetClusterRequest;
@@ -38,13 +40,15 @@ import 
org.apache.ignite.internal.rest.exception.handler.MigrateExceptionHandler
 /**
  * Controller for system groups disaster recovery.
  */
-@Controller("/management/v1/recovery/cluster")
+@Controller("/management/v1/recovery/cluster/")
 @Requires(classes = {
         ClusterResetExceptionHandler.class,
         MigrateExceptionHandler.class,
         IgniteInternalExceptionHandler.class
 })
 public class SystemDisasterRecoveryController implements 
SystemDisasterRecoveryApi, ResourceHolder {
+    private static final IgniteLogger LOG = 
Loggers.forClass(SystemDisasterRecoveryController.class);
+
     private SystemDisasterRecoveryManager systemDisasterRecoveryManager;
 
     private final CmgMessagesFactory cmgMessagesFactory = new 
CmgMessagesFactory();
@@ -55,6 +59,8 @@ public class SystemDisasterRecoveryController implements 
SystemDisasterRecoveryA
 
     @Override
     public CompletableFuture<Void> reset(ResetClusterRequest command) {
+        LOG.info("Reset command is {}", command);
+
         return 
systemDisasterRecoveryManager.resetCluster(command.cmgNodeNames());
     }
 
diff --git a/modules/system-disaster-recovery/build.gradle 
b/modules/system-disaster-recovery/build.gradle
index a4fdd55722..06057fef1f 100644
--- a/modules/system-disaster-recovery/build.gradle
+++ b/modules/system-disaster-recovery/build.gradle
@@ -47,6 +47,7 @@ dependencies {
     integrationTestImplementation project(':ignite-catalog')
     integrationTestImplementation project(':ignite-metastorage-api')
     integrationTestImplementation project(':ignite-client')
+    integrationTestImplementation project(':ignite-cli')
     integrationTestImplementation testFixtures(project(':ignite-core'))
     integrationTestImplementation testFixtures(project(':ignite-api'))
     integrationTestImplementation testFixtures(project(':ignite-runner'))
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 1228bd02d7..0949367a07 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
@@ -41,6 +41,7 @@ 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.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.network.NodeMetadata;
 import org.apache.ignite.table.KeyValueView;
 import org.junit.jupiter.api.Test;
 
@@ -81,12 +82,10 @@ class ItCmgDisasterRecoveryTest extends 
ItSystemGroupDisasterRecoveryTest {
         assertThat(ignite.logicalTopologyService().logicalTopologyOnLeader(), 
willCompleteSuccessfully());
     }
 
-    private void initiateCmgRepairVia(IgniteImpl conductor, int... 
newCmgIndexes) {
-        // TODO: IGNITE-22812 - initiate repair via CLI.
+    private void initiateCmgRepairVia(IgniteImpl conductor, int... 
newCmgIndexes) throws InterruptedException {
+        NodeMetadata nodeMetadata = conductor.node().nodeMetadata();
 
-        CompletableFuture<Void> initiationFuture = 
conductor.systemDisasterRecoveryManager()
-                .resetCluster(List.of(nodeNames(newCmgIndexes)));
-        assertThat(initiationFuture, willCompleteSuccessfully());
+        recoveryClient.initiateCmgRepairVia(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 5d322da662..26798353dd 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
@@ -40,6 +40,8 @@ import org.jetbrains.annotations.Nullable;
  * Base for tests of CMG and Metastorage group disaster recovery.
  */
 abstract class ItSystemGroupDisasterRecoveryTest extends 
ClusterPerTestIntegrationTest {
+    final SystemDisasterRecoveryClient recoveryClient = new 
SystemDisasterRecoveryClient();
+
     @Override
     protected int initialNodes() {
         return 0;
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
new file mode 100644
index 0000000000..1528f0669d
--- /dev/null
+++ 
b/modules/system-disaster-recovery/src/integrationTest/java/org/apache/ignite/internal/disaster/system/SystemDisasterRecoveryClient.java
@@ -0,0 +1,91 @@
+/*
+ * 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.disaster.system;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import org.apache.ignite.internal.cli.Main;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+
+/**
+ * Used to run system disaster recovery CLI commands.
+ */
+@SuppressWarnings("UseOfProcessBuilder")
+class SystemDisasterRecoveryClient {
+    private static final IgniteLogger LOG = 
Loggers.forClass(SystemDisasterRecoveryClient.class);
+
+    void initiateCmgRepairVia(String httpHost, int httpPort, String... 
newCmgNodeNames) throws InterruptedException {
+        LOG.info("Initiating CMG repair via {}:{}, new CMG {}", httpHost, 
httpPort, List.of(newCmgNodeNames));
+
+        String javaBinaryPath = 
ProcessHandle.current().info().command().orElseThrow();
+        String javaClassPath = System.getProperty("java.class.path");
+
+        LOG.info("Java binary is {}, classpath is {}", javaBinaryPath, 
javaClassPath);
+
+        //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)
+        );
+        executeProcessFrom(processBuilder);
+    }
+
+    private static void executeProcessFrom(ProcessBuilder processBuilder) 
throws InterruptedException {
+        try {
+            Process process = processBuilder.start();
+
+            if (!process.waitFor(10, SECONDS)) {
+                throw new RuntimeException("Process did not finish in 10 
seconds");
+            }
+            if (process.exitValue() != 0) {
+                throw new RuntimeException("Return code " + process.exitValue()
+                        + ", stdout: " + stdoutString(process) + ", stderr: " 
+ stderrString(process));
+            }
+
+            LOG.info("stdout is '{}'", stdoutString(process));
+            LOG.info("stderr is '{}'", stderrString(process));
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static String stdoutString(Process process) {
+        try (InputStream stdout = process.getInputStream()) {
+            return new String(stdout.readAllBytes(), UTF_8);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static String stderrString(Process process) {
+        try (InputStream stderr = process.getErrorStream()) {
+            return new String(stderr.readAllBytes(), UTF_8);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}


Reply via email to