You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@geode.apache.org by GitBox <gi...@apache.org> on 2020/04/29 12:55:34 UTC

[GitHub] [geode] jujoramos commented on a change in pull request #5004: GEODE-7954: restore redundancy and status redundancy gfsh commands

jujoramos commented on a change in pull request #5004:
URL: https://github.com/apache/geode/pull/5004#discussion_r417251767



##########
File path: geode-gfsh/src/test/java/org/apache/geode/management/internal/cli/functions/RedundancyCommandFunctionTest.java
##########
@@ -0,0 +1,137 @@
+/*
+ * 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.geode.management.internal.cli.functions;
+
+import static org.apache.geode.management.internal.functions.CliFunctionResult.StatusState.ERROR;
+import static org.apache.geode.management.internal.functions.CliFunctionResult.StatusState.OK;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.geode.cache.Cache;
+import org.apache.geode.cache.control.RestoreRedundancyOperation;
+import org.apache.geode.cache.control.RestoreRedundancyResults;
+import org.apache.geode.cache.execute.FunctionContext;
+import org.apache.geode.management.internal.functions.CliFunctionResult;
+
+public class RedundancyCommandFunctionTest {
+
+  @SuppressWarnings("unchecked")
+  private FunctionContext<Object[]> mockContext = mock(FunctionContext.class);
+  private Cache mockCache = mock(Cache.class, RETURNS_DEEP_STUBS);
+  private RestoreRedundancyOperation mockOperation =
+      mock(RestoreRedundancyOperation.class, RETURNS_DEEP_STUBS);
+  private RestoreRedundancyResults mockResults = mock(RestoreRedundancyResults.class);
+  private String message = "expected message";

Review comment:
       These attributes can all be made `final`.

##########
File path: geode-gfsh/src/test/java/org/apache/geode/management/internal/cli/commands/StatusRedundancyCommandTest.java
##########
@@ -0,0 +1,253 @@
+/*
+ * 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.geode.management.internal.cli.commands;
+
+import static org.apache.geode.management.cli.Result.Status.ERROR;
+import static org.apache.geode.management.cli.Result.Status.OK;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.REDUNDANCY_COMMAND_ADDED_VERSION;
+import static org.hamcrest.CoreMatchers.everyItem;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.geode.cache.Cache;
+import org.apache.geode.distributed.DistributedMember;
+import org.apache.geode.internal.cache.InternalCache;
+import org.apache.geode.management.internal.functions.CliFunctionResult;
+import org.apache.geode.management.internal.operation.RebalanceOperationPerformer;
+
+public class StatusRedundancyCommandTest {
+
+  private StatusRedundancyCommand command;
+  private final String includeRegion1 = "include1";
+  private final String includeRegion2 = "include2";
+  private String[] includeRegions = {includeRegion1, includeRegion2};

Review comment:
       This field can be made `final`.

##########
File path: geode-gfsh/src/test/java/org/apache/geode/management/internal/cli/commands/StatusRedundancyCommandTest.java
##########
@@ -0,0 +1,253 @@
+/*
+ * 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.geode.management.internal.cli.commands;
+
+import static org.apache.geode.management.cli.Result.Status.ERROR;
+import static org.apache.geode.management.cli.Result.Status.OK;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.REDUNDANCY_COMMAND_ADDED_VERSION;
+import static org.hamcrest.CoreMatchers.everyItem;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.geode.cache.Cache;
+import org.apache.geode.distributed.DistributedMember;
+import org.apache.geode.internal.cache.InternalCache;
+import org.apache.geode.management.internal.functions.CliFunctionResult;
+import org.apache.geode.management.internal.operation.RebalanceOperationPerformer;
+
+public class StatusRedundancyCommandTest {
+
+  private StatusRedundancyCommand command;
+  private final String includeRegion1 = "include1";
+  private final String includeRegion2 = "include2";
+  private String[] includeRegions = {includeRegion1, includeRegion2};
+  private final String excludeRegion1 = "exclude1";
+  private final String excludeRegion2 = "exclude2";
+  private String[] excludeRegions = {excludeRegion1, excludeRegion2};

Review comment:
       This field can be made `final`.

##########
File path: geode-gfsh/src/main/java/org/apache/geode/management/internal/cli/commands/RestoreRedundancyCommand.java
##########
@@ -0,0 +1,128 @@
+/*
+ * 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.geode.management.internal.cli.commands;
+
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.REDUNDANCY_COMMAND_ADDED_VERSION;
+import static org.apache.geode.management.internal.functions.CliFunctionResult.StatusState.ERROR;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.shell.core.annotation.CliCommand;
+import org.springframework.shell.core.annotation.CliOption;
+
+import org.apache.geode.distributed.DistributedMember;
+import org.apache.geode.internal.cache.InternalCache;
+import org.apache.geode.management.cli.CliMetaData;
+import org.apache.geode.management.cli.GfshCommand;
+import org.apache.geode.management.internal.cli.functions.RedundancyCommandFunction;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.functions.CliFunctionResult;
+import org.apache.geode.management.internal.i18n.CliStrings;
+import org.apache.geode.management.internal.operation.RebalanceOperationPerformer;
+import org.apache.geode.management.internal.security.ResourceOperation;
+import org.apache.geode.security.ResourcePermission;
+
+public class RestoreRedundancyCommand extends GfshCommand {
+  static final String COMMAND_NAME = "restore redundancy";
+  private static final String COMMAND_HELP =
+      "Restore redundancy and optionally reassign primary bucket hosting for partitioned regions in connected members. The default is for all regions to have redundancy restored and for primary buckets to be reassigned for better load balance.";
+  static final String INCLUDE_REGION = "include-region";
+  private static final String INCLUDE_REGION_HELP =
+      "Partitioned regions to be included when restoring redundancy. If a colocated region is included, all regions colocated with that region will also be included automatically. Includes take precedence over excludes.";
+  static final String EXCLUDE_REGION = "exclude-region";

Review comment:
       Maybe use `regions` instead of `region`?.

##########
File path: geode-gfsh/src/main/java/org/apache/geode/management/internal/cli/commands/RestoreRedundancyCommand.java
##########
@@ -0,0 +1,128 @@
+/*
+ * 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.geode.management.internal.cli.commands;
+
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.REDUNDANCY_COMMAND_ADDED_VERSION;
+import static org.apache.geode.management.internal.functions.CliFunctionResult.StatusState.ERROR;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.shell.core.annotation.CliCommand;
+import org.springframework.shell.core.annotation.CliOption;
+
+import org.apache.geode.distributed.DistributedMember;
+import org.apache.geode.internal.cache.InternalCache;
+import org.apache.geode.management.cli.CliMetaData;
+import org.apache.geode.management.cli.GfshCommand;
+import org.apache.geode.management.internal.cli.functions.RedundancyCommandFunction;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.functions.CliFunctionResult;
+import org.apache.geode.management.internal.i18n.CliStrings;
+import org.apache.geode.management.internal.operation.RebalanceOperationPerformer;
+import org.apache.geode.management.internal.security.ResourceOperation;
+import org.apache.geode.security.ResourcePermission;
+
+public class RestoreRedundancyCommand extends GfshCommand {
+  static final String COMMAND_NAME = "restore redundancy";
+  private static final String COMMAND_HELP =
+      "Restore redundancy and optionally reassign primary bucket hosting for partitioned regions in connected members. The default is for all regions to have redundancy restored and for primary buckets to be reassigned for better load balance.";
+  static final String INCLUDE_REGION = "include-region";

Review comment:
       Maybe use `regions` instead of `region`?.

##########
File path: geode-gfsh/src/distributedTest/java/org/apache/geode/management/internal/cli/commands/RestoreRedundancyCommandDUnitTest.java
##########
@@ -0,0 +1,618 @@
+/*
+ * 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.geode.management.internal.cli.commands;
+
+import static org.apache.geode.cache.PartitionAttributesFactory.GLOBAL_MAX_BUCKETS_DEFAULT;
+import static org.apache.geode.cache.Region.SEPARATOR;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.NO_REDUNDANT_COPIES_FOR_REGIONS;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.PRIMARY_TRANSFERS_COMPLETED;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.PRIMARY_TRANSFER_TIME;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.REDUNDANCY_NOT_SATISFIED_FOR_REGIONS;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.REDUNDANCY_SATISFIED_FOR_REGIONS;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.FULLY_SATISFIED_REDUNDANCY;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_FOR_REGION_HEADER;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_FOR_REGION_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_HEADER;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.PARTIALLY_SATISFIED_REDUNDANCY;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.PRIMARIES_INFO_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.SATISFIED_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.SUMMARY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.UNDER_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.ZERO_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.ZERO_REDUNDANT_COPIES;
+import static org.apache.geode.management.internal.cli.commands.RestoreRedundancyCommand.COMMAND_NAME;
+import static org.apache.geode.management.internal.cli.commands.RestoreRedundancyCommand.EXCLUDE_REGION;
+import static org.apache.geode.management.internal.cli.commands.RestoreRedundancyCommand.INCLUDE_REGION;
+import static org.apache.geode.management.internal.cli.commands.RestoreRedundancyCommand.REASSIGN_PRIMARIES;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.IntStream;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.apache.geode.cache.Cache;
+import org.apache.geode.cache.Region;
+import org.apache.geode.cache.RegionShortcut;
+import org.apache.geode.internal.cache.InternalCache;
+import org.apache.geode.internal.cache.PartitionAttributesImpl;
+import org.apache.geode.internal.cache.PartitionedRegion;
+import org.apache.geode.management.internal.cli.util.CommandStringBuilder;
+import org.apache.geode.test.dunit.rules.ClusterStartupRule;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.junit.assertions.CommandResultAssert;
+import org.apache.geode.test.junit.assertions.InfoResultModelAssert;
+import org.apache.geode.test.junit.rules.GfshCommandRule;
+
+public class RestoreRedundancyCommandDUnitTest {
+  @Rule
+  public ClusterStartupRule cluster = new ClusterStartupRule();
+
+  @Rule
+  public GfshCommandRule gfsh = new GfshCommandRule();
+
+  private int locatorPort;
+  private MemberVM locator;
+  private List<MemberVM> servers;
+  private static final int SERVERS_TO_START = 3;
+  private static final String HIGH_REDUNDANCY_REGION_NAME = "highRedundancy";
+  private static final int HIGH_REDUNDANCY_COPIES = SERVERS_TO_START - 1;
+  private static final String LOW_REDUNDANCY_REGION_NAME = "lowRedundancy";
+  private static final String PARENT_REGION_NAME = "colocatedParent";
+  private static final String CHILD_REGION_NAME = "colocatedChild";
+  private static final int SINGLE_REDUNDANT_COPY = 1;
+  private static final String ZERO_REDUNDANCY_REGION_NAME = "zeroRedundancy";
+
+  @Before
+  public void setUp() throws Exception {
+    servers = new ArrayList<>();
+    locator = cluster.startLocatorVM(0);
+    locatorPort = locator.getPort();
+    IntStream.range(0, SERVERS_TO_START)
+        .forEach(i -> servers.add(cluster.startServerVM(i + 1, locatorPort)));
+    gfsh.connectAndVerify(locator);
+  }
+
+  @Test
+  public void restoreRedundancyWithNoArgumentsRestoresRedundancyForAllRegions() {
+    List<String> regionNames = getAllRegionNames();
+    createAndPopulateRegions(regionNames);
+
+    int numberOfServers = servers.size();
+    regionNames.forEach(region -> locator
+        .waitUntilRegionIsReadyOnExactlyThisManyServers(SEPARATOR + region, numberOfServers));
+
+    String command = new CommandStringBuilder(COMMAND_NAME).getCommandString();
+
+    CommandResultAssert commandResult = gfsh.executeAndAssertThat(command).statusIsSuccess()
+        .hasNoSection(ZERO_REDUNDANCY_SECTION)
+        .hasNoSection(UNDER_REDUNDANCY_SECTION);
+
+    InfoResultModelAssert summary = commandResult.hasInfoSection(SUMMARY_SECTION);
+    summary.hasOutput().contains(ZERO_REDUNDANT_COPIES + 0);
+    summary.hasOutput().contains(PARTIALLY_SATISFIED_REDUNDANCY + 0);
+    summary.hasOutput().contains(FULLY_SATISFIED_REDUNDANCY + regionNames.size());
+
+    InfoResultModelAssert satisfiedSection =
+        commandResult.hasInfoSection(SATISFIED_REDUNDANCY_SECTION);
+    satisfiedSection.hasHeader().isEqualTo(REDUNDANCY_SATISFIED_FOR_REGIONS);
+    satisfiedSection.hasOutput().contains(regionNames);

Review comment:
       This code block is duplicated within the test, it can be extracted to a separate method.

##########
File path: geode-gfsh/src/distributedTest/java/org/apache/geode/management/internal/cli/commands/RestoreRedundancyCommandDUnitTest.java
##########
@@ -0,0 +1,618 @@
+/*
+ * 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.geode.management.internal.cli.commands;
+
+import static org.apache.geode.cache.PartitionAttributesFactory.GLOBAL_MAX_BUCKETS_DEFAULT;
+import static org.apache.geode.cache.Region.SEPARATOR;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.NO_REDUNDANT_COPIES_FOR_REGIONS;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.PRIMARY_TRANSFERS_COMPLETED;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.PRIMARY_TRANSFER_TIME;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.REDUNDANCY_NOT_SATISFIED_FOR_REGIONS;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.REDUNDANCY_SATISFIED_FOR_REGIONS;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.FULLY_SATISFIED_REDUNDANCY;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_FOR_REGION_HEADER;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_FOR_REGION_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_HEADER;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.PARTIALLY_SATISFIED_REDUNDANCY;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.PRIMARIES_INFO_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.SATISFIED_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.SUMMARY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.UNDER_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.ZERO_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.ZERO_REDUNDANT_COPIES;
+import static org.apache.geode.management.internal.cli.commands.RestoreRedundancyCommand.COMMAND_NAME;
+import static org.apache.geode.management.internal.cli.commands.RestoreRedundancyCommand.EXCLUDE_REGION;
+import static org.apache.geode.management.internal.cli.commands.RestoreRedundancyCommand.INCLUDE_REGION;
+import static org.apache.geode.management.internal.cli.commands.RestoreRedundancyCommand.REASSIGN_PRIMARIES;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.IntStream;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.apache.geode.cache.Cache;
+import org.apache.geode.cache.Region;
+import org.apache.geode.cache.RegionShortcut;
+import org.apache.geode.internal.cache.InternalCache;
+import org.apache.geode.internal.cache.PartitionAttributesImpl;
+import org.apache.geode.internal.cache.PartitionedRegion;
+import org.apache.geode.management.internal.cli.util.CommandStringBuilder;
+import org.apache.geode.test.dunit.rules.ClusterStartupRule;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.junit.assertions.CommandResultAssert;
+import org.apache.geode.test.junit.assertions.InfoResultModelAssert;
+import org.apache.geode.test.junit.rules.GfshCommandRule;
+
+public class RestoreRedundancyCommandDUnitTest {
+  @Rule
+  public ClusterStartupRule cluster = new ClusterStartupRule();
+
+  @Rule
+  public GfshCommandRule gfsh = new GfshCommandRule();
+
+  private int locatorPort;
+  private MemberVM locator;
+  private List<MemberVM> servers;
+  private static final int SERVERS_TO_START = 3;
+  private static final String HIGH_REDUNDANCY_REGION_NAME = "highRedundancy";
+  private static final int HIGH_REDUNDANCY_COPIES = SERVERS_TO_START - 1;
+  private static final String LOW_REDUNDANCY_REGION_NAME = "lowRedundancy";
+  private static final String PARENT_REGION_NAME = "colocatedParent";
+  private static final String CHILD_REGION_NAME = "colocatedChild";
+  private static final int SINGLE_REDUNDANT_COPY = 1;
+  private static final String ZERO_REDUNDANCY_REGION_NAME = "zeroRedundancy";
+
+  @Before
+  public void setUp() throws Exception {
+    servers = new ArrayList<>();
+    locator = cluster.startLocatorVM(0);
+    locatorPort = locator.getPort();
+    IntStream.range(0, SERVERS_TO_START)
+        .forEach(i -> servers.add(cluster.startServerVM(i + 1, locatorPort)));
+    gfsh.connectAndVerify(locator);
+  }
+
+  @Test
+  public void restoreRedundancyWithNoArgumentsRestoresRedundancyForAllRegions() {
+    List<String> regionNames = getAllRegionNames();
+    createAndPopulateRegions(regionNames);
+
+    int numberOfServers = servers.size();
+    regionNames.forEach(region -> locator
+        .waitUntilRegionIsReadyOnExactlyThisManyServers(SEPARATOR + region, numberOfServers));
+
+    String command = new CommandStringBuilder(COMMAND_NAME).getCommandString();
+
+    CommandResultAssert commandResult = gfsh.executeAndAssertThat(command).statusIsSuccess()
+        .hasNoSection(ZERO_REDUNDANCY_SECTION)
+        .hasNoSection(UNDER_REDUNDANCY_SECTION);
+
+    InfoResultModelAssert summary = commandResult.hasInfoSection(SUMMARY_SECTION);
+    summary.hasOutput().contains(ZERO_REDUNDANT_COPIES + 0);
+    summary.hasOutput().contains(PARTIALLY_SATISFIED_REDUNDANCY + 0);
+    summary.hasOutput().contains(FULLY_SATISFIED_REDUNDANCY + regionNames.size());
+
+    InfoResultModelAssert satisfiedSection =
+        commandResult.hasInfoSection(SATISFIED_REDUNDANCY_SECTION);
+    satisfiedSection.hasHeader().isEqualTo(REDUNDANCY_SATISFIED_FOR_REGIONS);
+    satisfiedSection.hasOutput().contains(regionNames);
+
+    InfoResultModelAssert primariesSection = commandResult.hasInfoSection(PRIMARIES_INFO_SECTION);
+    primariesSection.hasOutput().contains(PRIMARY_TRANSFERS_COMPLETED, PRIMARY_TRANSFER_TIME);
+
+    // Confirm all regions have their configured redundancy and that primaries were balanced
+    int numberOfActiveServers = servers.size();
+    servers.get(0).invoke(() -> {
+      for (String regionName : regionNames) {
+        assertRedundancyStatusForRegion(regionName, true);
+        assertPrimariesBalanced(regionName, numberOfActiveServers, true);
+      }
+    });
+  }
+
+  @Test
+  public void restoreRedundancyWithIncludeRegionArgumentRestoresOnlyThoseRegions() {
+    List<String> regionNames = getAllRegionNames();
+    createAndPopulateRegions(regionNames);
+
+    int numberOfServers = servers.size();
+    regionNames.forEach(region -> locator
+        .waitUntilRegionIsReadyOnExactlyThisManyServers(SEPARATOR + region, numberOfServers));
+
+    String regionToInclude = HIGH_REDUNDANCY_REGION_NAME;
+    List<String> nonIncludedRegions = new ArrayList<>(regionNames);
+    nonIncludedRegions.remove(regionToInclude);
+
+    String command = new CommandStringBuilder(COMMAND_NAME)
+        .addOption(INCLUDE_REGION, regionToInclude).getCommandString();
+
+    CommandResultAssert commandResult = gfsh.executeAndAssertThat(command).statusIsSuccess()
+        .hasNoSection(ZERO_REDUNDANCY_SECTION)
+        .hasNoSection(UNDER_REDUNDANCY_SECTION)
+        .doesNotContainOutput(nonIncludedRegions.toArray(new String[0]));
+
+    InfoResultModelAssert summary = commandResult.hasInfoSection(SUMMARY_SECTION);
+    summary.hasOutput().contains(ZERO_REDUNDANT_COPIES + 0);
+    summary.hasOutput().contains(PARTIALLY_SATISFIED_REDUNDANCY + 0);
+    summary.hasOutput().contains(FULLY_SATISFIED_REDUNDANCY + 1);
+
+    InfoResultModelAssert satisfiedSection =
+        commandResult.hasInfoSection(SATISFIED_REDUNDANCY_SECTION);
+    satisfiedSection.hasHeader().isEqualTo(REDUNDANCY_SATISFIED_FOR_REGIONS);
+    satisfiedSection.hasOutput().contains(regionToInclude);

Review comment:
       This code block is duplicated within the test, it can be extracted to a separate method.

##########
File path: geode-gfsh/src/distributedTest/java/org/apache/geode/management/internal/cli/commands/RestoreRedundancyCommandDUnitTest.java
##########
@@ -0,0 +1,618 @@
+/*
+ * 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.geode.management.internal.cli.commands;
+
+import static org.apache.geode.cache.PartitionAttributesFactory.GLOBAL_MAX_BUCKETS_DEFAULT;
+import static org.apache.geode.cache.Region.SEPARATOR;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.NO_REDUNDANT_COPIES_FOR_REGIONS;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.PRIMARY_TRANSFERS_COMPLETED;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.PRIMARY_TRANSFER_TIME;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.REDUNDANCY_NOT_SATISFIED_FOR_REGIONS;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.REDUNDANCY_SATISFIED_FOR_REGIONS;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.FULLY_SATISFIED_REDUNDANCY;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_FOR_REGION_HEADER;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_FOR_REGION_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_HEADER;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.PARTIALLY_SATISFIED_REDUNDANCY;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.PRIMARIES_INFO_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.SATISFIED_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.SUMMARY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.UNDER_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.ZERO_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.ZERO_REDUNDANT_COPIES;
+import static org.apache.geode.management.internal.cli.commands.RestoreRedundancyCommand.COMMAND_NAME;
+import static org.apache.geode.management.internal.cli.commands.RestoreRedundancyCommand.EXCLUDE_REGION;
+import static org.apache.geode.management.internal.cli.commands.RestoreRedundancyCommand.INCLUDE_REGION;
+import static org.apache.geode.management.internal.cli.commands.RestoreRedundancyCommand.REASSIGN_PRIMARIES;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.IntStream;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.apache.geode.cache.Cache;
+import org.apache.geode.cache.Region;
+import org.apache.geode.cache.RegionShortcut;
+import org.apache.geode.internal.cache.InternalCache;
+import org.apache.geode.internal.cache.PartitionAttributesImpl;
+import org.apache.geode.internal.cache.PartitionedRegion;
+import org.apache.geode.management.internal.cli.util.CommandStringBuilder;
+import org.apache.geode.test.dunit.rules.ClusterStartupRule;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.junit.assertions.CommandResultAssert;
+import org.apache.geode.test.junit.assertions.InfoResultModelAssert;
+import org.apache.geode.test.junit.rules.GfshCommandRule;
+
+public class RestoreRedundancyCommandDUnitTest {
+  @Rule
+  public ClusterStartupRule cluster = new ClusterStartupRule();
+
+  @Rule
+  public GfshCommandRule gfsh = new GfshCommandRule();
+
+  private int locatorPort;
+  private MemberVM locator;
+  private List<MemberVM> servers;
+  private static final int SERVERS_TO_START = 3;
+  private static final String HIGH_REDUNDANCY_REGION_NAME = "highRedundancy";
+  private static final int HIGH_REDUNDANCY_COPIES = SERVERS_TO_START - 1;
+  private static final String LOW_REDUNDANCY_REGION_NAME = "lowRedundancy";
+  private static final String PARENT_REGION_NAME = "colocatedParent";
+  private static final String CHILD_REGION_NAME = "colocatedChild";
+  private static final int SINGLE_REDUNDANT_COPY = 1;
+  private static final String ZERO_REDUNDANCY_REGION_NAME = "zeroRedundancy";
+
+  @Before
+  public void setUp() throws Exception {
+    servers = new ArrayList<>();
+    locator = cluster.startLocatorVM(0);
+    locatorPort = locator.getPort();
+    IntStream.range(0, SERVERS_TO_START)
+        .forEach(i -> servers.add(cluster.startServerVM(i + 1, locatorPort)));
+    gfsh.connectAndVerify(locator);
+  }
+
+  @Test
+  public void restoreRedundancyWithNoArgumentsRestoresRedundancyForAllRegions() {
+    List<String> regionNames = getAllRegionNames();
+    createAndPopulateRegions(regionNames);
+
+    int numberOfServers = servers.size();
+    regionNames.forEach(region -> locator
+        .waitUntilRegionIsReadyOnExactlyThisManyServers(SEPARATOR + region, numberOfServers));
+
+    String command = new CommandStringBuilder(COMMAND_NAME).getCommandString();
+
+    CommandResultAssert commandResult = gfsh.executeAndAssertThat(command).statusIsSuccess()
+        .hasNoSection(ZERO_REDUNDANCY_SECTION)
+        .hasNoSection(UNDER_REDUNDANCY_SECTION);
+
+    InfoResultModelAssert summary = commandResult.hasInfoSection(SUMMARY_SECTION);
+    summary.hasOutput().contains(ZERO_REDUNDANT_COPIES + 0);
+    summary.hasOutput().contains(PARTIALLY_SATISFIED_REDUNDANCY + 0);
+    summary.hasOutput().contains(FULLY_SATISFIED_REDUNDANCY + regionNames.size());
+
+    InfoResultModelAssert satisfiedSection =
+        commandResult.hasInfoSection(SATISFIED_REDUNDANCY_SECTION);
+    satisfiedSection.hasHeader().isEqualTo(REDUNDANCY_SATISFIED_FOR_REGIONS);
+    satisfiedSection.hasOutput().contains(regionNames);
+
+    InfoResultModelAssert primariesSection = commandResult.hasInfoSection(PRIMARIES_INFO_SECTION);
+    primariesSection.hasOutput().contains(PRIMARY_TRANSFERS_COMPLETED, PRIMARY_TRANSFER_TIME);
+
+    // Confirm all regions have their configured redundancy and that primaries were balanced
+    int numberOfActiveServers = servers.size();
+    servers.get(0).invoke(() -> {
+      for (String regionName : regionNames) {
+        assertRedundancyStatusForRegion(regionName, true);
+        assertPrimariesBalanced(regionName, numberOfActiveServers, true);
+      }
+    });
+  }
+
+  @Test
+  public void restoreRedundancyWithIncludeRegionArgumentRestoresOnlyThoseRegions() {
+    List<String> regionNames = getAllRegionNames();
+    createAndPopulateRegions(regionNames);
+
+    int numberOfServers = servers.size();
+    regionNames.forEach(region -> locator
+        .waitUntilRegionIsReadyOnExactlyThisManyServers(SEPARATOR + region, numberOfServers));
+
+    String regionToInclude = HIGH_REDUNDANCY_REGION_NAME;
+    List<String> nonIncludedRegions = new ArrayList<>(regionNames);
+    nonIncludedRegions.remove(regionToInclude);
+
+    String command = new CommandStringBuilder(COMMAND_NAME)
+        .addOption(INCLUDE_REGION, regionToInclude).getCommandString();

Review comment:
       This code block is duplicated within the test, it can be extracted to a separate method.

##########
File path: geode-gfsh/src/main/java/org/apache/geode/management/internal/cli/commands/StatusRedundancyCommand.java
##########
@@ -0,0 +1,121 @@
+/*
+ * 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.geode.management.internal.cli.commands;
+
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.REDUNDANCY_COMMAND_ADDED_VERSION;
+import static org.apache.geode.management.internal.functions.CliFunctionResult.StatusState.ERROR;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.shell.core.annotation.CliCommand;
+import org.springframework.shell.core.annotation.CliOption;
+
+import org.apache.geode.distributed.DistributedMember;
+import org.apache.geode.internal.cache.InternalCache;
+import org.apache.geode.management.cli.CliMetaData;
+import org.apache.geode.management.cli.GfshCommand;
+import org.apache.geode.management.internal.cli.functions.RedundancyCommandFunction;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.functions.CliFunctionResult;
+import org.apache.geode.management.internal.i18n.CliStrings;
+import org.apache.geode.management.internal.operation.RebalanceOperationPerformer;
+import org.apache.geode.management.internal.security.ResourceOperation;
+import org.apache.geode.security.ResourcePermission;
+
+public class StatusRedundancyCommand extends GfshCommand {
+  static final String COMMAND_NAME = "status redundancy";
+  private static final String COMMAND_HELP =
+      "Report the redundancy status for partitioned regions in connected members. The default is to report status for all regions.";
+  static final String INCLUDE_REGION = "include-region";

Review comment:
       Maybe use `regions` instead of `region`?.
   
   

##########
File path: geode-gfsh/src/distributedTest/java/org/apache/geode/management/internal/cli/commands/StatusRedundancyCommandDUnitTest.java
##########
@@ -0,0 +1,281 @@
+/*
+ * 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.geode.management.internal.cli.commands;
+
+import static org.apache.geode.cache.PartitionAttributesFactory.GLOBAL_MAX_BUCKETS_DEFAULT;
+import static org.apache.geode.cache.Region.SEPARATOR;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.NO_REDUNDANT_COPIES_FOR_REGIONS;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.REDUNDANCY_NOT_SATISFIED_FOR_REGIONS;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.REDUNDANCY_SATISFIED_FOR_REGIONS;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.FULLY_SATISFIED_REDUNDANCY;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_FOR_REGION_HEADER;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_FOR_REGION_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_HEADER;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.PARTIALLY_SATISFIED_REDUNDANCY;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.SATISFIED_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.SUMMARY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.UNDER_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.ZERO_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.ZERO_REDUNDANT_COPIES;
+import static org.apache.geode.management.internal.cli.commands.StatusRedundancyCommand.COMMAND_NAME;
+import static org.apache.geode.management.internal.cli.commands.StatusRedundancyCommand.EXCLUDE_REGION;
+import static org.apache.geode.management.internal.cli.commands.StatusRedundancyCommand.INCLUDE_REGION;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.IntStream;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.cache.RegionShortcut;
+import org.apache.geode.internal.cache.InternalCache;
+import org.apache.geode.internal.cache.PartitionAttributesImpl;
+import org.apache.geode.management.internal.cli.util.CommandStringBuilder;
+import org.apache.geode.test.dunit.rules.ClusterStartupRule;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.junit.assertions.CommandResultAssert;
+import org.apache.geode.test.junit.assertions.InfoResultModelAssert;
+import org.apache.geode.test.junit.rules.GfshCommandRule;
+
+public class StatusRedundancyCommandDUnitTest {
+  @Rule
+  public ClusterStartupRule cluster = new ClusterStartupRule();
+
+  @Rule
+  public GfshCommandRule gfsh = new GfshCommandRule();
+
+  private int locatorPort;
+  private MemberVM locator;
+  private List<MemberVM> servers;
+  private static final int SERVERS_TO_START = 2;
+  private static final String SATISFIED_REGION = "satisfiedRedundancy";
+  private static final int SATISFIABLE_COPIES = SERVERS_TO_START - 1;
+  private static final String UNSATISFIED_REGION = "unsatisfiedRedundancy";
+  private static final int UNSATISFIABLE_COPIES = SERVERS_TO_START;
+  private static final String NO_CONFIGURED_REDUNDANCY_REGION = "noConfiguredRedundancy";
+  private static final String ZERO_COPIES_REGION = "zeroRedundantCopies";
+  private static final List<String> regionNames = Arrays.asList(SATISFIED_REGION,
+      UNSATISFIED_REGION, NO_CONFIGURED_REDUNDANCY_REGION, ZERO_COPIES_REGION);
+
+  @Before
+  public void setUp() throws Exception {
+    servers = new ArrayList<>();
+    locator = cluster.startLocatorVM(0);
+    locatorPort = locator.getPort();
+    IntStream.range(0, SERVERS_TO_START)
+        .forEach(i -> servers.add(cluster.startServerVM(i + 1, locatorPort)));
+    gfsh.connectAndVerify(locator);
+  }
+
+  @Test
+  public void statusRedundancyWithNoArgumentsReturnsStatusForAllRegions() {
+    createAndPopulateRegions();
+
+    String command = new CommandStringBuilder(COMMAND_NAME).getCommandString();
+
+    CommandResultAssert commandResult = gfsh.executeAndAssertThat(command);
+    commandResult.statusIsSuccess();
+
+    verifySummarySection(commandResult, 1, 1, 2);
+
+    InfoResultModelAssert zeroRedundancy = commandResult.hasInfoSection(ZERO_REDUNDANCY_SECTION);
+    zeroRedundancy.hasHeader().isEqualTo(NO_REDUNDANT_COPIES_FOR_REGIONS);
+    zeroRedundancy.hasOutput().contains(ZERO_COPIES_REGION);
+
+    InfoResultModelAssert underRedundancy = commandResult.hasInfoSection(UNDER_REDUNDANCY_SECTION);
+    underRedundancy.hasHeader().isEqualTo(REDUNDANCY_NOT_SATISFIED_FOR_REGIONS);
+    underRedundancy.hasOutput().contains(UNSATISFIED_REGION);
+
+    InfoResultModelAssert satisfiedRedundancy =
+        commandResult.hasInfoSection(SATISFIED_REDUNDANCY_SECTION);
+    satisfiedRedundancy.hasHeader().isEqualTo(REDUNDANCY_SATISFIED_FOR_REGIONS);
+    satisfiedRedundancy.hasOutput().contains(SATISFIED_REGION,
+        NO_CONFIGURED_REDUNDANCY_REGION);
+  }
+
+  @Test
+  public void statusRedundancyWithIncludeRegionArgumentReturnsStatusForOnlyThatRegion() {
+    createAndPopulateRegions();
+
+    String command = new CommandStringBuilder(COMMAND_NAME)
+        .addOption(INCLUDE_REGION, SATISFIED_REGION)
+        .getCommandString();
+
+    CommandResultAssert commandResult = gfsh.executeAndAssertThat(command);
+    commandResult.statusIsSuccess()
+        .hasNoSection(ZERO_REDUNDANCY_SECTION)
+        .hasNoSection(UNDER_REDUNDANCY_SECTION)
+        .doesNotContainOutput(UNSATISFIED_REGION)
+        .doesNotContainOutput(ZERO_COPIES_REGION)
+        .doesNotContainOutput(NO_CONFIGURED_REDUNDANCY_REGION);
+
+    verifySummarySection(commandResult, 0, 0, 1);
+
+    InfoResultModelAssert satisfiedRedundancy =
+        commandResult.hasInfoSection(SATISFIED_REDUNDANCY_SECTION);
+    satisfiedRedundancy.hasHeader().isEqualTo(REDUNDANCY_SATISFIED_FOR_REGIONS);
+    satisfiedRedundancy.hasOutput().contains(SATISFIED_REGION);

Review comment:
       This code block is duplicated within the test, it can be extracted to a separate method.

##########
File path: geode-gfsh/src/test/java/org/apache/geode/management/internal/cli/commands/RestoreRedundancyCommandTest.java
##########
@@ -0,0 +1,256 @@
+/*
+ * 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.geode.management.internal.cli.commands;
+
+import static org.apache.geode.management.cli.Result.Status.ERROR;
+import static org.apache.geode.management.cli.Result.Status.OK;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.REDUNDANCY_COMMAND_ADDED_VERSION;
+import static org.hamcrest.CoreMatchers.everyItem;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.geode.cache.Cache;
+import org.apache.geode.distributed.DistributedMember;
+import org.apache.geode.internal.cache.InternalCache;
+import org.apache.geode.management.internal.functions.CliFunctionResult;
+import org.apache.geode.management.internal.operation.RebalanceOperationPerformer;
+
+public class RestoreRedundancyCommandTest {
+
+  private RestoreRedundancyCommand command;
+  private final String includeRegion1 = "include1";
+  private final String includeRegion2 = "include2";
+  private String[] includeRegions = {includeRegion1, includeRegion2};
+  private final String excludeRegion1 = "exclude1";
+  private final String excludeRegion2 = "exclude2";
+  private String[] excludeRegions = {excludeRegion1, excludeRegion2};
+  private final boolean shouldReassignPrimaries = true;
+  private DistributedMember mockMember1;
+  private DistributedMember mockMember2;
+  private CliFunctionResult successFunctionResult;
+  private CliFunctionResult errorFunctionResult;
+  private RedundancyCommandUtils mockUtils;
+  private Object[] expectedArguments;
+
+  @Before
+  public void setUp() {
+    mockMember1 = mock(DistributedMember.class);
+    mockMember2 = mock(DistributedMember.class);
+    expectedArguments = new Object[] {includeRegions, excludeRegions, shouldReassignPrimaries};
+
+    successFunctionResult = mock(CliFunctionResult.class);
+    when(successFunctionResult.getStatus()).thenReturn(OK.name());
+
+    errorFunctionResult = mock(CliFunctionResult.class);
+    when(errorFunctionResult.getStatus()).thenReturn(ERROR.name());
+
+    mockUtils = mock(RedundancyCommandUtils.class);
+    Cache mockCache = mock(InternalCache.class);
+
+    command = spy(new RestoreRedundancyCommand());
+    doReturn(mockUtils).when(command).getUtils();
+    doReturn(mockCache).when(command).getCache();
+  }
+
+  @Test
+  public void executeCallsGetNoViableMembersResultWhenNoViableMembersAreFoundForAtLeastOneRegion() {
+    RebalanceOperationPerformer.MemberPRInfo firstMemberPRInfo =
+        new RebalanceOperationPerformer.MemberPRInfo();
+    String region1 = "region1";
+    firstMemberPRInfo.region = region1;
+    RebalanceOperationPerformer.MemberPRInfo secondMemberPRInfo =
+        new RebalanceOperationPerformer.MemberPRInfo();
+    String region2 = "region2";
+    secondMemberPRInfo.region = region2;
+
+    // Put something in the members list when we call populateLists
+    doAnswer(invocation -> {
+      List<RebalanceOperationPerformer.MemberPRInfo> memberPRInfoList = invocation.getArgument(0);
+      memberPRInfoList.add(firstMemberPRInfo);
+      memberPRInfoList.add(secondMemberPRInfo);
+      return null;
+    }).when(mockUtils).populateLists(any(), any(), any(), any(), any());
+
+    List<DistributedMember> viableMembers = new ArrayList<>();
+    viableMembers.add(mock(DistributedMember.class));
+
+    when(mockUtils.filterViableMembersForVersion(eq(firstMemberPRInfo), any()))
+        .thenReturn(viableMembers);
+    when(mockUtils.filterViableMembersForVersion(eq(secondMemberPRInfo), any()))
+        .thenReturn(Collections.emptyList());
+
+    command.execute(includeRegions, excludeRegions, shouldReassignPrimaries);
+
+    verify(mockUtils, times(1)).getNoViableMembersResult(any(), eq(region2));
+    verify(mockUtils, times(0)).getNoViableMembersResult(any(), eq(region1));
+  }
+
+  @Test
+  public void executeCallsMethodsWithCorrectArgumentsWhenViableMembersAreFoundForAllRegions() {
+    RebalanceOperationPerformer.MemberPRInfo firstMemberPRInfo =
+        mock(RebalanceOperationPerformer.MemberPRInfo.class);
+    RebalanceOperationPerformer.MemberPRInfo secondMemberPRInfo =
+        mock(RebalanceOperationPerformer.MemberPRInfo.class);
+    List<RebalanceOperationPerformer.MemberPRInfo> membersForEachRegion = new ArrayList<>();
+    membersForEachRegion.add(firstMemberPRInfo);
+    membersForEachRegion.add(secondMemberPRInfo);
+
+    // Put something in the members list when we call populateLists
+    doAnswer(invocation -> {
+      List<RebalanceOperationPerformer.MemberPRInfo> memberPRInfoList = invocation.getArgument(0);
+      memberPRInfoList.add(firstMemberPRInfo);
+      memberPRInfoList.add(secondMemberPRInfo);
+      return null;
+    }).when(mockUtils).populateLists(any(), any(), any(), any(), any());
+
+    // Allow us to get past the filter viable members step
+    List<DistributedMember> viableMembers = new ArrayList<>();
+    viableMembers.add(mock(DistributedMember.class));
+    when(mockUtils.filterViableMembersForVersion(any(), any())).thenReturn(viableMembers);
+
+    // Prevent the executeFunctionOnMembers method from doing anything
+    List<CliFunctionResult> returnedList = new ArrayList<>();
+    doReturn(returnedList).when(command).executeFunctionOnMembers(any(), any(), anyBoolean(),
+        any());
+
+    command.execute(includeRegions, excludeRegions, shouldReassignPrimaries);
+
+    // Confirm we set the correct viable members on the memberPRInfos
+    verify(mockUtils, times(1)).filterViableMembersForVersion(firstMemberPRInfo,
+        REDUNDANCY_COMMAND_ADDED_VERSION);
+    verify(mockUtils, times(1)).filterViableMembersForVersion(secondMemberPRInfo,
+        REDUNDANCY_COMMAND_ADDED_VERSION);
+    assertThat(firstMemberPRInfo.dsMemberList, is(viableMembers));
+    assertThat(secondMemberPRInfo.dsMemberList, is(viableMembers));
+
+    // Confirm we called executeFunctionOnMembers and buildResultModelFromFunctionResults with the
+    // correct arguments
+    verify(command, times(1)).executeFunctionOnMembers(includeRegions, excludeRegions,
+        shouldReassignPrimaries, membersForEachRegion);
+    boolean isStatusCommand = false;

Review comment:
       No need for this variable.

##########
File path: geode-gfsh/src/main/java/org/apache/geode/management/internal/cli/commands/RedundancyCommandUtils.java
##########
@@ -0,0 +1,269 @@
+/*
+ * 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.geode.management.internal.cli.commands;
+
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.NO_REDUNDANT_COPIES_FOR_REGIONS;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.PRIMARY_TRANSFERS_COMPLETED;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.PRIMARY_TRANSFER_TIME;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.REDUNDANCY_NOT_SATISFIED_FOR_REGIONS;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.REDUNDANCY_SATISFIED_FOR_REGIONS;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.apache.geode.cache.control.RegionRedundancyStatus;
+import org.apache.geode.cache.control.RestoreRedundancyResults;
+import org.apache.geode.distributed.DistributedMember;
+import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
+import org.apache.geode.internal.cache.InternalCache;
+import org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl;
+import org.apache.geode.internal.serialization.Version;
+import org.apache.geode.management.ManagementService;
+import org.apache.geode.management.cli.Result;
+import org.apache.geode.management.internal.cli.result.model.InfoResultModel;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.functions.CliFunctionResult;
+import org.apache.geode.management.internal.operation.RebalanceOperationPerformer;
+
+public class RedundancyCommandUtils {
+  public static final String NO_MEMBERS_WITH_VERSION_FOR_REGION =
+      "No members with a version greater than or equal to %s were found for region %s";
+  public static final String NO_MEMBERS_SECTION = "no-members";
+  public static final String NO_MEMBERS_HEADER =
+      "No partitioned regions were found.";
+  public static final String NO_MEMBERS_FOR_REGION_SECTION = "no-members-for-region";
+  public static final String NO_MEMBERS_FOR_REGION_HEADER =
+      "No members hosting the following regions were found: ";
+  public static final String ERROR_SECTION = "errors";
+  public static final String ERROR_SECTION_HEADER =
+      "The following errors or exceptions were encountered: ";
+  public static final String SUMMARY_SECTION = "summary-section";
+  public static final String ZERO_REDUNDANT_COPIES =
+      "Number of regions with zero redundant copies = ";
+  public static final String PARTIALLY_SATISFIED_REDUNDANCY =
+      "Number of regions with partially satisfied redundancy = ";
+  public static final String FULLY_SATISFIED_REDUNDANCY =
+      "Number of regions with fully satisfied redundancy = ";
+  public static final String ZERO_REDUNDANCY_SECTION = "zero-redundancy";
+  public static final String UNDER_REDUNDANCY_SECTION = "under-redundancy";
+  public static final String SATISFIED_REDUNDANCY_SECTION = "satisfied-redundancy";
+  public static final String PRIMARIES_INFO_SECTION = "primaries-info";
+  public static final String EXCEPTION_MEMBER_MESSAGE = "Exception occurred on member %s: %s";
+  public static final Version REDUNDANCY_COMMAND_ADDED_VERSION = Version.GEODE_1_13_0;
+  public static final String INDENT = "  ";
+
+  void populateLists(List<RebalanceOperationPerformer.MemberPRInfo> membersForEachRegion,
+      List<String> noMemberRegions, String[] includeRegions, String[] excludeRegions,
+      InternalCache cache) {
+    // Include all regions
+    if (includeRegions == null) {
+      // Exclude these regions
+      List<String> excludedRegionList =
+          excludeRegions != null ? Arrays.asList(excludeRegions) : new ArrayList<>();
+
+      List<RebalanceOperationPerformer.MemberPRInfo> memberRegionList =
+          getMembersForEachRegion(excludedRegionList, cache);
+      membersForEachRegion.addAll(memberRegionList);
+    } else {
+      for (String regionName : includeRegions) {
+        DistributedMember memberForRegion = getOneMemberForRegion(regionName, cache);
+
+        // If we did not find a member for this region name, add it to the list of regions with no
+        // members
+        if (memberForRegion == null) {
+          noMemberRegions.add(regionName);
+        } else {
+          RebalanceOperationPerformer.MemberPRInfo memberPRInfo =
+              new RebalanceOperationPerformer.MemberPRInfo();
+          memberPRInfo.region = regionName;
+          memberPRInfo.dsMemberList.add(memberForRegion);
+          membersForEachRegion.add(memberPRInfo);
+        }
+      }
+    }
+  }
+
+  // Extracted for testing
+  List<RebalanceOperationPerformer.MemberPRInfo> getMembersForEachRegion(
+      List<String> excludedRegionList, InternalCache cache) {
+    return RebalanceOperationPerformer.getMemberRegionList(
+        ManagementService.getManagementService(cache), cache, excludedRegionList);
+  }
+
+  // Extracted for testing
+  DistributedMember getOneMemberForRegion(String regionName, InternalCache cache) {
+    String regionNameWithSeparator = regionName;
+    // The getAssociatedMembers method requires region names start with '/'
+    if (!regionName.startsWith("/")) {
+      regionNameWithSeparator = "/" + regionName;

Review comment:
       Maybe use `CacheConfig.SEPARATOR` instead of hardcoding the character? (I know we don't it consistently across the code, but...).

##########
File path: geode-gfsh/src/distributedTest/java/org/apache/geode/management/internal/cli/commands/StatusRedundancyCommandDUnitTest.java
##########
@@ -0,0 +1,281 @@
+/*
+ * 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.geode.management.internal.cli.commands;
+
+import static org.apache.geode.cache.PartitionAttributesFactory.GLOBAL_MAX_BUCKETS_DEFAULT;
+import static org.apache.geode.cache.Region.SEPARATOR;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.NO_REDUNDANT_COPIES_FOR_REGIONS;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.REDUNDANCY_NOT_SATISFIED_FOR_REGIONS;
+import static org.apache.geode.internal.cache.control.RestoreRedundancyResultsImpl.REDUNDANCY_SATISFIED_FOR_REGIONS;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.FULLY_SATISFIED_REDUNDANCY;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_FOR_REGION_HEADER;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_FOR_REGION_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_HEADER;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.NO_MEMBERS_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.PARTIALLY_SATISFIED_REDUNDANCY;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.SATISFIED_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.SUMMARY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.UNDER_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.ZERO_REDUNDANCY_SECTION;
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.ZERO_REDUNDANT_COPIES;
+import static org.apache.geode.management.internal.cli.commands.StatusRedundancyCommand.COMMAND_NAME;
+import static org.apache.geode.management.internal.cli.commands.StatusRedundancyCommand.EXCLUDE_REGION;
+import static org.apache.geode.management.internal.cli.commands.StatusRedundancyCommand.INCLUDE_REGION;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.IntStream;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.cache.RegionShortcut;
+import org.apache.geode.internal.cache.InternalCache;
+import org.apache.geode.internal.cache.PartitionAttributesImpl;
+import org.apache.geode.management.internal.cli.util.CommandStringBuilder;
+import org.apache.geode.test.dunit.rules.ClusterStartupRule;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.junit.assertions.CommandResultAssert;
+import org.apache.geode.test.junit.assertions.InfoResultModelAssert;
+import org.apache.geode.test.junit.rules.GfshCommandRule;
+
+public class StatusRedundancyCommandDUnitTest {
+  @Rule
+  public ClusterStartupRule cluster = new ClusterStartupRule();
+
+  @Rule
+  public GfshCommandRule gfsh = new GfshCommandRule();
+
+  private int locatorPort;
+  private MemberVM locator;
+  private List<MemberVM> servers;
+  private static final int SERVERS_TO_START = 2;
+  private static final String SATISFIED_REGION = "satisfiedRedundancy";
+  private static final int SATISFIABLE_COPIES = SERVERS_TO_START - 1;
+  private static final String UNSATISFIED_REGION = "unsatisfiedRedundancy";
+  private static final int UNSATISFIABLE_COPIES = SERVERS_TO_START;
+  private static final String NO_CONFIGURED_REDUNDANCY_REGION = "noConfiguredRedundancy";
+  private static final String ZERO_COPIES_REGION = "zeroRedundantCopies";
+  private static final List<String> regionNames = Arrays.asList(SATISFIED_REGION,
+      UNSATISFIED_REGION, NO_CONFIGURED_REDUNDANCY_REGION, ZERO_COPIES_REGION);
+
+  @Before
+  public void setUp() throws Exception {
+    servers = new ArrayList<>();
+    locator = cluster.startLocatorVM(0);
+    locatorPort = locator.getPort();
+    IntStream.range(0, SERVERS_TO_START)
+        .forEach(i -> servers.add(cluster.startServerVM(i + 1, locatorPort)));
+    gfsh.connectAndVerify(locator);
+  }
+
+  @Test
+  public void statusRedundancyWithNoArgumentsReturnsStatusForAllRegions() {
+    createAndPopulateRegions();
+
+    String command = new CommandStringBuilder(COMMAND_NAME).getCommandString();
+
+    CommandResultAssert commandResult = gfsh.executeAndAssertThat(command);
+    commandResult.statusIsSuccess();
+
+    verifySummarySection(commandResult, 1, 1, 2);
+
+    InfoResultModelAssert zeroRedundancy = commandResult.hasInfoSection(ZERO_REDUNDANCY_SECTION);
+    zeroRedundancy.hasHeader().isEqualTo(NO_REDUNDANT_COPIES_FOR_REGIONS);
+    zeroRedundancy.hasOutput().contains(ZERO_COPIES_REGION);
+
+    InfoResultModelAssert underRedundancy = commandResult.hasInfoSection(UNDER_REDUNDANCY_SECTION);
+    underRedundancy.hasHeader().isEqualTo(REDUNDANCY_NOT_SATISFIED_FOR_REGIONS);
+    underRedundancy.hasOutput().contains(UNSATISFIED_REGION);
+
+    InfoResultModelAssert satisfiedRedundancy =
+        commandResult.hasInfoSection(SATISFIED_REDUNDANCY_SECTION);
+    satisfiedRedundancy.hasHeader().isEqualTo(REDUNDANCY_SATISFIED_FOR_REGIONS);
+    satisfiedRedundancy.hasOutput().contains(SATISFIED_REGION,
+        NO_CONFIGURED_REDUNDANCY_REGION);
+  }
+
+  @Test
+  public void statusRedundancyWithIncludeRegionArgumentReturnsStatusForOnlyThatRegion() {
+    createAndPopulateRegions();
+
+    String command = new CommandStringBuilder(COMMAND_NAME)
+        .addOption(INCLUDE_REGION, SATISFIED_REGION)
+        .getCommandString();
+
+    CommandResultAssert commandResult = gfsh.executeAndAssertThat(command);
+    commandResult.statusIsSuccess()
+        .hasNoSection(ZERO_REDUNDANCY_SECTION)
+        .hasNoSection(UNDER_REDUNDANCY_SECTION)
+        .doesNotContainOutput(UNSATISFIED_REGION)
+        .doesNotContainOutput(ZERO_COPIES_REGION)
+        .doesNotContainOutput(NO_CONFIGURED_REDUNDANCY_REGION);
+
+    verifySummarySection(commandResult, 0, 0, 1);
+
+    InfoResultModelAssert satisfiedRedundancy =
+        commandResult.hasInfoSection(SATISFIED_REDUNDANCY_SECTION);
+    satisfiedRedundancy.hasHeader().isEqualTo(REDUNDANCY_SATISFIED_FOR_REGIONS);
+    satisfiedRedundancy.hasOutput().contains(SATISFIED_REGION);
+  }
+
+  @Test
+  public void statusRedundancyWithExcludeRegionArgumentReturnsStatusForAllExceptThatRegion() {
+    createAndPopulateRegions();
+
+    String command = new CommandStringBuilder(COMMAND_NAME)
+        .addOption(EXCLUDE_REGION, ZERO_COPIES_REGION)
+        .getCommandString();
+
+    CommandResultAssert commandResult = gfsh.executeAndAssertThat(command);
+    commandResult.statusIsSuccess()
+        .hasNoSection(ZERO_REDUNDANCY_SECTION)
+        .doesNotContainOutput(ZERO_COPIES_REGION);
+
+    verifySummarySection(commandResult, 0, 1, 2);
+
+    InfoResultModelAssert underRedundancy = commandResult.hasInfoSection(UNDER_REDUNDANCY_SECTION);
+    underRedundancy.hasHeader().isEqualTo(REDUNDANCY_NOT_SATISFIED_FOR_REGIONS);
+    underRedundancy.hasOutput().contains(UNSATISFIED_REGION);
+
+    InfoResultModelAssert satisfiedRedundancy =
+        commandResult.hasInfoSection(SATISFIED_REDUNDANCY_SECTION);
+    satisfiedRedundancy.hasHeader().isEqualTo(REDUNDANCY_SATISFIED_FOR_REGIONS);
+    satisfiedRedundancy.hasOutput().contains(SATISFIED_REGION,
+        NO_CONFIGURED_REDUNDANCY_REGION);

Review comment:
       This code block is duplicated within the test, it can be extracted to a separate method.

##########
File path: geode-gfsh/src/main/java/org/apache/geode/management/internal/cli/commands/StatusRedundancyCommand.java
##########
@@ -0,0 +1,121 @@
+/*
+ * 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.geode.management.internal.cli.commands;
+
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.REDUNDANCY_COMMAND_ADDED_VERSION;
+import static org.apache.geode.management.internal.functions.CliFunctionResult.StatusState.ERROR;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.shell.core.annotation.CliCommand;
+import org.springframework.shell.core.annotation.CliOption;
+
+import org.apache.geode.distributed.DistributedMember;
+import org.apache.geode.internal.cache.InternalCache;
+import org.apache.geode.management.cli.CliMetaData;
+import org.apache.geode.management.cli.GfshCommand;
+import org.apache.geode.management.internal.cli.functions.RedundancyCommandFunction;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.functions.CliFunctionResult;
+import org.apache.geode.management.internal.i18n.CliStrings;
+import org.apache.geode.management.internal.operation.RebalanceOperationPerformer;
+import org.apache.geode.management.internal.security.ResourceOperation;
+import org.apache.geode.security.ResourcePermission;
+
+public class StatusRedundancyCommand extends GfshCommand {
+  static final String COMMAND_NAME = "status redundancy";
+  private static final String COMMAND_HELP =
+      "Report the redundancy status for partitioned regions in connected members. The default is to report status for all regions.";
+  static final String INCLUDE_REGION = "include-region";
+  private static final String INCLUDE_REGION_HELP =
+      "Partitioned regions to be included when reporting redundancy status. Includes take precedence over excludes.";
+  static final String EXCLUDE_REGION = "exclude-region";
+  private static final String EXCLUDE_REGION_HELP =
+      "Partitioned regions to be excluded when reporting redundancy status.";
+
+  @CliCommand(value = COMMAND_NAME, help = COMMAND_HELP)
+  @CliMetaData(relatedTopic = {CliStrings.TOPIC_GEODE_DATA, CliStrings.TOPIC_GEODE_REGION})
+  @ResourceOperation(resource = ResourcePermission.Resource.DATA,
+      operation = ResourcePermission.Operation.READ)
+  public ResultModel execute(
+      @CliOption(key = INCLUDE_REGION, help = INCLUDE_REGION_HELP) String[] includeRegions,
+      @CliOption(key = EXCLUDE_REGION, help = EXCLUDE_REGION_HELP) String[] excludeRegions) {
+    RedundancyCommandUtils utils = getUtils();
+
+    List<RebalanceOperationPerformer.MemberPRInfo> membersForEachRegion = new ArrayList<>();
+    List<String> includedRegionsWithNoMembers = new ArrayList<>();
+
+    utils.populateLists(membersForEachRegion, includedRegionsWithNoMembers, includeRegions,
+        excludeRegions, (InternalCache) getCache());
+
+    for (RebalanceOperationPerformer.MemberPRInfo prInfo : membersForEachRegion) {
+      // Filter out any members using older versions of Geode
+      List<DistributedMember> viableMembers =
+          utils.filterViableMembersForVersion(prInfo, REDUNDANCY_COMMAND_ADDED_VERSION);
+
+      if (viableMembers.size() == 0) {
+        // If no viable members were found, return with error status
+        return utils.getNoViableMembersResult(REDUNDANCY_COMMAND_ADDED_VERSION, prInfo.region);
+      } else {
+        // Update the MemberPRInfo with the viable members
+        prInfo.dsMemberList = viableMembers;
+      }
+    }
+
+    List<CliFunctionResult> functionResults =
+        executeFunctionOnMembers(includeRegions, excludeRegions, membersForEachRegion);
+
+    return utils.buildResultModelFromFunctionResults(functionResults, includedRegionsWithNoMembers,
+        true);
+  }
+
+  List<CliFunctionResult> executeFunctionOnMembers(String[] includeRegions, String[] excludeRegions,
+      List<RebalanceOperationPerformer.MemberPRInfo> membersForEachRegion) {
+
+    List<CliFunctionResult> functionResults = new ArrayList<>();
+    boolean shouldNotReassignPrimaries = false;

Review comment:
       Unneeded variable.

##########
File path: geode-gfsh/src/main/java/org/apache/geode/management/internal/cli/commands/StatusRedundancyCommand.java
##########
@@ -0,0 +1,121 @@
+/*
+ * 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.geode.management.internal.cli.commands;
+
+import static org.apache.geode.management.internal.cli.commands.RedundancyCommandUtils.REDUNDANCY_COMMAND_ADDED_VERSION;
+import static org.apache.geode.management.internal.functions.CliFunctionResult.StatusState.ERROR;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.shell.core.annotation.CliCommand;
+import org.springframework.shell.core.annotation.CliOption;
+
+import org.apache.geode.distributed.DistributedMember;
+import org.apache.geode.internal.cache.InternalCache;
+import org.apache.geode.management.cli.CliMetaData;
+import org.apache.geode.management.cli.GfshCommand;
+import org.apache.geode.management.internal.cli.functions.RedundancyCommandFunction;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.functions.CliFunctionResult;
+import org.apache.geode.management.internal.i18n.CliStrings;
+import org.apache.geode.management.internal.operation.RebalanceOperationPerformer;
+import org.apache.geode.management.internal.security.ResourceOperation;
+import org.apache.geode.security.ResourcePermission;
+
+public class StatusRedundancyCommand extends GfshCommand {
+  static final String COMMAND_NAME = "status redundancy";
+  private static final String COMMAND_HELP =
+      "Report the redundancy status for partitioned regions in connected members. The default is to report status for all regions.";
+  static final String INCLUDE_REGION = "include-region";
+  private static final String INCLUDE_REGION_HELP =
+      "Partitioned regions to be included when reporting redundancy status. Includes take precedence over excludes.";
+  static final String EXCLUDE_REGION = "exclude-region";

Review comment:
       Maybe use `regions` instead of `region`?.
   
   




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org