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);
+ }
+ }
+}