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

erose pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 0839c2468e7 HDDS-12078. Improve container reconciliation CLIs (#7944)
0839c2468e7 is described below

commit 0839c2468e7a4a77720fbb8ae4209ed1599c8d64
Author: Ethan Rose <[email protected]>
AuthorDate: Tue Sep 2 23:58:31 2025 -0400

    HDDS-12078. Improve container reconciliation CLIs (#7944)
---
 .../hdds/scm/container/ContainerReplicaInfo.java   |  16 +-
 .../org/apache/hadoop/hdds/server/JsonUtils.java   |  69 +++
 .../scm/cli/container/ContainerIDParameters.java   |  33 ++
 .../hdds/scm/cli/container/InfoSubcommand.java     |   1 +
 .../hdds/scm/cli/container/ListSubcommand.java     |   1 +
 .../scm/cli/container/ReconcileSubcommand.java     | 231 ++++++++-
 .../scm/cli/container/TestReconcileSubcommand.java | 553 +++++++++++++++++++++
 .../src/main/smoketest/admincli/container.robot    |  21 +-
 8 files changed, 895 insertions(+), 30 deletions(-)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerReplicaInfo.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerReplicaInfo.java
index 24bc4d6d32c..b9b9d679d63 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerReplicaInfo.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerReplicaInfo.java
@@ -17,16 +17,11 @@
 
 package org.apache.hadoop.hdds.scm.container;
 
-import static org.apache.hadoop.hdds.HddsUtils.checksumToString;
-
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.databind.JsonSerializer;
-import com.fasterxml.jackson.databind.SerializerProvider;
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
-import java.io.IOException;
 import java.util.UUID;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.server.JsonUtils;
 
 /**
  * Class which stores ContainerReplica details on the client.
@@ -41,7 +36,7 @@ public final class ContainerReplicaInfo {
   private long keyCount;
   private long bytesUsed;
   private int replicaIndex = -1;
-  @JsonSerialize(using = LongToHexJsonSerializer.class)
+  @JsonSerialize(using = JsonUtils.ChecksumSerializer.class)
   private long dataChecksum;
 
   public static ContainerReplicaInfo fromProto(
@@ -100,13 +95,6 @@ public long getDataChecksum() {
     return dataChecksum;
   }
 
-  private static class LongToHexJsonSerializer extends JsonSerializer<Long> {
-    @Override
-    public void serialize(Long value, JsonGenerator gen, SerializerProvider 
provider) throws IOException {
-      gen.writeString(checksumToString(value));
-    }
-  }
-
   /**
    * Builder for ContainerReplicaInfo class.
    */
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/server/JsonUtils.java 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/server/JsonUtils.java
index 633864b2c12..54637458a30 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/server/JsonUtils.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/server/JsonUtils.java
@@ -18,21 +18,26 @@
 package org.apache.hadoop.hdds.server;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.JsonSerializer;
 import com.fasterxml.jackson.databind.MappingIterator;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectReader;
 import com.fasterxml.jackson.databind.ObjectWriter;
 import com.fasterxml.jackson.databind.SequenceWriter;
 import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.SerializerProvider;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.io.Reader;
 import java.util.List;
+import org.apache.hadoop.hdds.HddsUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -76,6 +81,23 @@ public static String toJsonString(Object obj) throws 
IOException {
     return MAPPER.writeValueAsString(obj);
   }
 
+  /**
+   * Returns a {@link SequenceWriter} that will write to and close the 
provided output stream when it is closed.
+   * If the sequence is being written to stdout and more stdout output is 
needed later, use
+   * {@link #getStdoutSequenceWriter} instead.
+   */
+  public static SequenceWriter getSequenceWriter(OutputStream stream) throws 
IOException {
+    return WRITER.writeValuesAsArray(stream);
+  }
+
+  /**
+   * Returns a {@link SequenceWriter} that will write to stdout but not close 
stdout for more output once the sequence
+   * writer is closed.
+   */
+  public static SequenceWriter getStdoutSequenceWriter() throws IOException {
+    return getSequenceWriter(new NonClosingOutputStream(System.out));
+  }
+
   public static String toJsonStringWIthIndent(Object obj)  {
     try {
       return INDENT_OUTPUT_MAPPER.writeValueAsString(obj);
@@ -107,6 +129,10 @@ public static <T> T readFromReader(Reader reader, Class<T> 
valueType) throws IOE
     return MAPPER.readValue(reader, valueType);
   }
 
+  public static ObjectMapper getDefaultMapper() {
+    return MAPPER;
+  }
+
   /**
    * Utility to sequentially write a large collection of items to a file.
    */
@@ -132,4 +158,47 @@ public static <T> List<T> readFromFile(File file, Class<T> 
itemType)
     }
   }
 
+  /**
+   * Serializes a checksum stored as a long into its json string 
representation.
+   */
+  public static class ChecksumSerializer extends JsonSerializer<Long> {
+    @Override
+    public void serialize(Long value, JsonGenerator gen, SerializerProvider 
provider) throws IOException {
+      gen.writeString(HddsUtils.checksumToString(value));
+    }
+  }
+
+  private static class NonClosingOutputStream extends OutputStream {
+
+    private final OutputStream delegate;
+
+    NonClosingOutputStream(OutputStream delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public void write(int b) throws IOException {
+      delegate.write(b);
+    }
+
+    @Override
+    public void write(byte[] b) throws IOException {
+      delegate.write(b);
+    }
+
+    @Override
+    public void write(byte[] b, int off, int len) throws IOException {
+      delegate.write(b, off, len);
+    }
+
+    @Override
+    public void flush() throws IOException {
+      delegate.flush();
+    }
+
+    @Override
+    public void close() {
+      // Ignore close to keep the underlying stream open
+    }
+  }
 }
diff --git 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ContainerIDParameters.java
 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ContainerIDParameters.java
index 4b14b40c13f..36e615a5829 100644
--- 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ContainerIDParameters.java
+++ 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ContainerIDParameters.java
@@ -17,6 +17,7 @@
 
 package org.apache.hadoop.hdds.scm.cli.container;
 
+import java.util.ArrayList;
 import java.util.List;
 import org.apache.hadoop.hdds.cli.ItemsFromStdin;
 import picocli.CommandLine;
@@ -25,10 +26,42 @@
 @CommandLine.Command
 public class ContainerIDParameters extends ItemsFromStdin {
 
+  @CommandLine.Spec
+  private CommandLine.Model.CommandSpec spec;
+
   @CommandLine.Parameters(description = "Container IDs" + FORMAT_DESCRIPTION,
       arity = "1..*",
       paramLabel = "<container ID>")
   public void setContainerIDs(List<String> arguments) {
     setItems(arguments);
   }
+
+  public List<Long> getValidatedIDs() {
+    List<Long> containerIDs = new ArrayList<>(size());
+    List<String> invalidIDs = new ArrayList<>();
+
+    for (String input: this) {
+      boolean idValid = true;
+      try {
+        long id = Long.parseLong(input);
+        if (id <= 0) {
+          idValid = false;
+        } else {
+          containerIDs.add(id);
+        }
+      } catch (NumberFormatException e) {
+        idValid = false;
+      }
+
+      if (!idValid) {
+        invalidIDs.add(input);
+      }
+    }
+
+    if (!invalidIDs.isEmpty()) {
+      throw new CommandLine.ParameterException(spec.commandLine(),
+          "Container IDs must be positive integers. Invalid container IDs: " + 
String.join(" ", invalidIDs));
+    }
+    return containerIDs;
+  }
 }
diff --git 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/InfoSubcommand.java
 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/InfoSubcommand.java
index 2c3ad44c979..34a5c48656f 100644
--- 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/InfoSubcommand.java
+++ 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/InfoSubcommand.java
@@ -69,6 +69,7 @@ public void execute(ScmClient scmClient) throws IOException {
     multiContainer = containerList.size() > 1;
 
     printHeader();
+    // TODO HDDS-13592: Use ContainerIDParameters#getValidatedIDs to 
automatically handle type conversion and fail fast.
     for (String id : containerList) {
       printOutput(scmClient, id, first);
       first = false;
diff --git 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ListSubcommand.java
 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ListSubcommand.java
index ba82c8c1484..0b88cec37f9 100644
--- 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ListSubcommand.java
+++ 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ListSubcommand.java
@@ -182,6 +182,7 @@ private void listAllContainers(ScmClient scmClient, 
SequenceWriter writer,
     } while (fetchedCount > 0);
   }
 
+  // TODO HDDS-13593 Remove this in favor of JsonUtils#getStdoutSequenceWriter.
   private static class NonClosingOutputStream extends OutputStream {
 
     private final OutputStream delegate;
diff --git 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ReconcileSubcommand.java
 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ReconcileSubcommand.java
index 79df162cf09..a714ad759df 100644
--- 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ReconcileSubcommand.java
+++ 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ReconcileSubcommand.java
@@ -17,15 +17,27 @@
 
 package org.apache.hadoop.hdds.scm.cli.container;
 
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.SequenceWriter;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 import org.apache.hadoop.hdds.cli.HddsVersionProvider;
+import org.apache.hadoop.hdds.client.ReplicationConfig;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
 import org.apache.hadoop.hdds.scm.cli.ScmSubcommand;
 import org.apache.hadoop.hdds.scm.client.ScmClient;
+import org.apache.hadoop.hdds.scm.container.ContainerInfo;
+import org.apache.hadoop.hdds.scm.container.ContainerReplicaInfo;
+import org.apache.hadoop.hdds.server.JsonUtils;
 import picocli.CommandLine;
 import picocli.CommandLine.Command;
 
 /**
- * This is the handler that process container list command.
+ * Handle the container reconcile CLI command.
  */
 @Command(
     name = "reconcile",
@@ -34,15 +46,218 @@
     versionProvider = HddsVersionProvider.class)
 public class ReconcileSubcommand extends ScmSubcommand {
 
-  @CommandLine.Parameters(description = "ID of the container to reconcile")
-  private long containerId;
+  @CommandLine.Mixin
+  private ContainerIDParameters containerList;
+
+  @CommandLine.Option(names = { "--status" },
+      defaultValue = "false",
+      fallbackValue = "true",
+      description = "Display the reconciliation status of this container's 
replicas")
+  private boolean status;
 
   @Override
   public void execute(ScmClient scmClient) throws IOException {
-    scmClient.reconcileContainer(containerId);
-    System.out.println("Reconciliation has been triggered for container " + 
containerId);
-    // TODO HDDS-12078 allow status to be checked from the reconcile 
subcommand directly.
-    System.out.println("Use \"ozone admin container info --json " + 
containerId + "\" to see the checksums of each " +
-        "container replica");
+    if (status) {
+      executeStatus(scmClient);
+    } else {
+      executeReconcile(scmClient);
+    }
+  }
+
+  private void executeStatus(ScmClient scmClient) throws IOException {
+    // Do validation outside the json array writer, otherwise failed 
validation will print an empty json array.
+    List<Long> containerIDs = containerList.getValidatedIDs();
+    int failureCount = 0;
+    StringBuilder errorBuilder = new StringBuilder();
+    try (SequenceWriter arrayWriter = JsonUtils.getStdoutSequenceWriter()) {
+      // Since status is retrieved using container info, do client side 
validation that it is only used for Ratis
+      // containers. If EC containers are given, print a  message to stderr 
and eventually exit non-zero, but continue
+      // processing the remaining containers.
+      for (Long containerID : containerIDs) {
+        if (!printReconciliationStatus(scmClient, containerID, arrayWriter, 
errorBuilder)) {
+          failureCount++;
+        }
+      }
+      arrayWriter.flush();
+    }
+    // Sequence writer will not add a newline to the end.
+    System.out.println();
+    System.out.flush();
+    // Flush all json output before printing errors.
+    if (errorBuilder.length() > 0) {
+      System.err.print(errorBuilder);
+    }
+    if (failureCount > 0) {
+      throw new RuntimeException("Failed to process reconciliation status for 
" + failureCount + " container" +
+          (failureCount > 1 ? "s" : ""));
+    }
+  }
+
+  private boolean printReconciliationStatus(ScmClient scmClient, long 
containerID, SequenceWriter arrayWriter,
+      StringBuilder errorBuilder) {
+    try {
+      ContainerInfo containerInfo = scmClient.getContainer(containerID);
+      if (containerInfo.isOpen()) {
+        errorBuilder.append("Cannot get status of container 
").append(containerID)
+            .append(". Reconciliation is not supported for open containers\n");
+        return false;
+      } else if (containerInfo.getReplicationType() != 
HddsProtos.ReplicationType.RATIS) {
+        errorBuilder.append("Cannot get status of container 
").append(containerID)
+            .append(". Reconciliation is only supported for Ratis replicated 
containers\n");
+        return false;
+      }
+      List<ContainerReplicaInfo> replicas = 
scmClient.getContainerReplicas(containerID);
+      arrayWriter.write(new ContainerWrapper(containerInfo, replicas));
+      arrayWriter.flush();
+    } catch (Exception ex) {
+      errorBuilder.append("Failed to get reconciliation status of container ")
+          .append(containerID).append(": 
").append(getExceptionMessage(ex)).append('\n');
+      return false;
+    }
+    return true;
+  }
+
+  private void executeReconcile(ScmClient scmClient) {
+    int failureCount = 0;
+    int successCount = 0;
+    for (Long containerID : containerList.getValidatedIDs()) {
+      try {
+        scmClient.reconcileContainer(containerID);
+        System.out.println("Reconciliation has been triggered for container " 
+ containerID);
+        successCount++;
+      } catch (Exception ex) {
+        System.err.println("Failed to trigger reconciliation for container " + 
containerID + ": " +
+            getExceptionMessage(ex));
+        failureCount++;
+      }
+    }
+
+    if (successCount > 0) {
+      System.out.println("\nUse \"ozone admin container reconcile --status\" 
to see the checksums of each container " +
+          "replica");
+    }
+    if (failureCount > 0) {
+      throw new RuntimeException("Failed to trigger reconciliation for " + 
failureCount + " container" +
+          (failureCount > 1 ? "s" : ""));
+    }
+  }
+
+  /**
+   * Hadoop RPC puts the server side stack trace within the exception message. 
This method is a workaround to not
+   * display that to the user.
+   */
+  private String getExceptionMessage(Exception ex) {
+    return ex.getMessage().split("\n", 2)[0];
+  }
+
+  /**
+   * Used to json serialize the container and replica information for output.
+   */
+  private static class ContainerWrapper {
+    private final long containerID;
+    private final HddsProtos.LifeCycleState state;
+    private final ReplicationConfig replicationConfig;
+    private boolean replicasMatch;
+    private final List<ReplicaWrapper> replicas;
+
+    ContainerWrapper(ContainerInfo info, List<ContainerReplicaInfo> replicas) {
+      this.containerID = info.getContainerID();
+      this.state = info.getState();
+      this.replicationConfig = info.getReplicationConfig();
+
+      this.replicas = new ArrayList<>();
+      this.replicasMatch = true;
+      long firstChecksum = 0;
+      if (!replicas.isEmpty()) {
+        firstChecksum = replicas.get(0).getDataChecksum();
+      }
+      for (ContainerReplicaInfo replica: replicas) {
+        replicasMatch = replicasMatch && (firstChecksum == 
replica.getDataChecksum());
+        this.replicas.add(new ReplicaWrapper(replica));
+      }
+    }
+
+    public long getContainerID() {
+      return containerID;
+    }
+
+    public HddsProtos.LifeCycleState getState() {
+      return state;
+    }
+
+    public ReplicationConfig getReplicationConfig() {
+      return replicationConfig;
+    }
+
+    public boolean getReplicasMatch() {
+      return replicasMatch;
+    }
+
+    public List<ReplicaWrapper> getReplicas() {
+      return replicas;
+    }
+  }
+
+  private static class ReplicaWrapper {
+    private final DatanodeWrapper datanode;
+    private final String state;
+    private int replicaIndex;
+    @JsonSerialize(using = JsonUtils.ChecksumSerializer.class)
+    private final long dataChecksum;
+
+    ReplicaWrapper(ContainerReplicaInfo replica) {
+      this.datanode = new DatanodeWrapper(replica.getDatanodeDetails());
+      this.state = replica.getState();
+      // Only display replica index when it has a positive value for EC.
+      if (replica.getReplicaIndex() > 0) {
+        this.replicaIndex = replica.getReplicaIndex();
+      }
+      this.dataChecksum = replica.getDataChecksum();
+    }
+
+    public DatanodeWrapper getDatanode() {
+      return datanode;
+    }
+
+    public String getState() {
+      return state;
+    }
+
+    /**
+     * Replica index is only included in the output if it is non-zero, which 
will be the case for EC.
+     * For Ratis, avoid printing all zero replica indices to avoid confusion.
+     */
+    @JsonInclude(JsonInclude.Include.NON_DEFAULT)
+    public int getReplicaIndex() {
+      return replicaIndex;
+    }
+
+    public long getDataChecksum() {
+      return dataChecksum;
+    }
+  }
+
+  private static class DatanodeWrapper {
+    private final DatanodeDetails dnDetails;
+
+    DatanodeWrapper(DatanodeDetails dnDetails) {
+      this.dnDetails = dnDetails;
+    }
+
+    @JsonProperty(index = 5)
+    public String getID() {
+      return dnDetails.getUuidString();
+    }
+
+    @JsonProperty(index = 10)
+    public String getHostname() {
+      return dnDetails.getHostName();
+    }
+
+    // Without specifying a value, Jackson will try to serialize this as 
"ipaddress".
+    @JsonProperty(index = 15, value = "ipAddress")
+    public String getIPAddress() {
+      return dnDetails.getIpAddress();
+    }
   }
 }
diff --git 
a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/container/TestReconcileSubcommand.java
 
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/container/TestReconcileSubcommand.java
new file mode 100644
index 00000000000..8a64b327bbf
--- /dev/null
+++ 
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/container/TestReconcileSubcommand.java
@@ -0,0 +1,553 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.hdds.scm.cli.container;
+
+import static 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.LifeCycleState.CLOSED;
+import static 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.LifeCycleState.OPEN;
+import static 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.ONE;
+import static 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.THREE;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import org.apache.hadoop.hdds.client.ECReplicationConfig;
+import org.apache.hadoop.hdds.client.RatisReplicationConfig;
+import org.apache.hadoop.hdds.client.ReplicationConfig;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.scm.client.ScmClient;
+import org.apache.hadoop.hdds.scm.container.ContainerInfo;
+import org.apache.hadoop.hdds.scm.container.ContainerReplicaInfo;
+import org.apache.hadoop.hdds.server.JsonUtils;
+import org.assertj.core.api.AbstractStringAssert;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+/**
+ * Tests the `ozone admin container reconcile` CLI.
+ */
+public class TestReconcileSubcommand {
+
+  private static final String EC_CONTAINER_MESSAGE = "Reconciliation is only 
supported for Ratis replicated containers";
+  private static final String OPEN_CONTAINER_MESSAGE = "Reconciliation is not 
supported for open containers";
+
+  private ScmClient scmClient;
+
+  private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+  private final ByteArrayOutputStream errContent = new ByteArrayOutputStream();
+  private ByteArrayInputStream inContent;
+  private final PrintStream originalOut = System.out;
+  private final PrintStream originalErr = System.err;
+  private final InputStream originalIn = System.in;
+
+  private static final String DEFAULT_ENCODING = StandardCharsets.UTF_8.name();
+
+  @BeforeEach
+  public void setup() throws IOException {
+    scmClient = mock(ScmClient.class);
+
+    doNothing().when(scmClient).reconcileContainer(anyLong());
+
+    System.setOut(new PrintStream(outContent, false, DEFAULT_ENCODING));
+    System.setErr(new PrintStream(errContent, false, DEFAULT_ENCODING));
+  }
+
+  @AfterEach
+  public void after() {
+    System.setOut(originalOut);
+    System.setErr(originalErr);
+    System.setIn(originalIn);
+  }
+
+  @Test
+  public void testWithMatchingReplicas() throws Exception {
+    mockContainer(1);
+    mockContainer(2);
+    mockContainer(3);
+    validateOutput(true, 1, 2, 3);
+  }
+
+  /**
+   * When no replicas are present, the "replicasMatch" field should be set to 
true.
+   */
+  @Test
+  public void testReplicasMatchWithNoReplicas() throws Exception {
+    mockContainer(1, 0, RatisReplicationConfig.getInstance(THREE), true);
+    validateOutput(true, 1);
+  }
+
+  /**
+   * When one replica is present, the "replicasMatch" field should be set to 
true.
+   */
+  @Test
+  public void testReplicasMatchWithOneReplica() throws Exception {
+    mockContainer(1, 1, RatisReplicationConfig.getInstance(ONE), true);
+    validateOutput(true, 1);
+  }
+
+  @Test
+  public void testWithMismatchedReplicas() throws Exception {
+    mockContainer(1, 3, RatisReplicationConfig.getInstance(THREE), false);
+    mockContainer(2, 3, RatisReplicationConfig.getInstance(THREE), false);
+    validateOutput(false, 1, 2);
+  }
+
+  @Test
+  public void testNoInput() throws Exception {
+    // PicoCLI should reject commands with no arguments.
+    assertThrows(CommandLine.MissingParameterException.class, 
this::executeStatusFromArgs);
+    assertThrows(CommandLine.MissingParameterException.class, 
this::executeReconcileFromArgs);
+
+    // When reading from stdin, the arguments are valid, but an empty list 
results in no output.
+    executeReconcileFromStdin();
+    assertThatOutput(outContent).isEmpty();
+    assertThatOutput(errContent).isEmpty();
+    
+    executeStatusFromStdin();
+    // Status command should output empty JSON array
+    String output = outContent.toString(DEFAULT_ENCODING);
+    JsonNode jsonOutput = JsonUtils.readTree(output);
+    assertThat(jsonOutput.isArray()).isTrue();
+    assertThat(jsonOutput.isEmpty()).isTrue();
+    assertThatOutput(errContent).isEmpty();
+  }
+
+  /**
+   * When multiple arguments are given, they are treated as container IDs. 
Mixing "-" to read from stdin with
+   * ID arguments will result in "-" raising an invalid container ID error.
+   */
+  @Test
+  public void testRejectsStdinAndArgs() throws Exception {
+    mockContainer(1);
+    // Test sending reconcile command.
+    Exception reconcileEx = assertThrows(RuntimeException.class, () -> 
parseArgsAndExecute("1", "-"));
+    assertThat(reconcileEx.getMessage())
+        .contains("Container IDs must be positive integers. Invalid container 
IDs: -");
+    assertThatOutput(outContent).isEmpty();
+
+    // Test checking status.
+    Exception statusEx = assertThrows(RuntimeException.class, () -> 
parseArgsAndExecute("--status", "1", "-"));
+    assertThat(statusEx.getMessage())
+        .contains("Container IDs must be positive integers. Invalid container 
IDs: -");
+    assertThatOutput(outContent).isEmpty();
+  }
+
+  /**
+   * When the `--status` flag is passed, the client will check the replication 
type and raise an error if the container
+   * returned is EC. The server lets us get information about containers of 
any type.
+   */
+  @Test
+  public void testStatusRejectsECContainer() throws Exception {
+    mockContainer(1, 3, new ECReplicationConfig(3, 2), true);
+
+    RuntimeException exception = assertThrows(RuntimeException.class, () -> 
executeStatusFromArgs(1));
+    
+    assertThatOutput(errContent).contains("Cannot get status of container 1");
+    assertThatOutput(errContent).contains(EC_CONTAINER_MESSAGE);
+    
+    assertThat(exception.getMessage()).contains("Failed to process 
reconciliation status for 1 container");
+    
+    // Should have empty JSON array output since no containers were processed
+    String output = outContent.toString(DEFAULT_ENCODING);
+    JsonNode jsonOutput = JsonUtils.readTree(output);
+    assertTrue(jsonOutput.isArray());
+    assertTrue(jsonOutput.isEmpty());
+  }
+
+  /**
+   * When the `--status` flag is passed, the client will check the container 
state and raise an error if the container
+   * returned is open. The server lets us get information about containers in 
any state.
+   */
+  @Test
+  public void testStatusRejectsOpenContainer() throws Exception {
+    mockOpenContainer(1, 3, RatisReplicationConfig.getInstance(THREE));
+
+    RuntimeException exception = assertThrows(RuntimeException.class, () -> 
executeStatusFromArgs(1));
+
+    assertThatOutput(errContent).contains("Cannot get status of container 1");
+    assertThatOutput(errContent).contains(OPEN_CONTAINER_MESSAGE);
+
+    assertThat(exception.getMessage()).contains("Failed to process 
reconciliation status for 1 container");
+
+    // Should have empty JSON array output since no containers were processed
+    String output = outContent.toString(DEFAULT_ENCODING);
+    JsonNode jsonOutput = JsonUtils.readTree(output);
+    assertTrue(jsonOutput.isArray());
+    assertTrue(jsonOutput.isEmpty());
+  }
+
+  /**
+   * Reconciliation is not supported for open or EC containers. This is 
checked on the server side by SCM when it gets
+   * a request to reconcile a container. Since the server side is mocked in 
these tests, this test checks that when any
+   * exception is thrown back from the server, its message is printed by the 
client.
+   */
+  @Test
+  public void testReconcileHandlesInvalidContainer() throws Exception {
+    mockContainer(1);
+
+    // Mock reconcile to fail for EC container
+    final String mockMessage = "Mock SCM rejection of container";
+    doThrow(new 
IOException(mockMessage)).when(scmClient).reconcileContainer(1L);
+
+    RuntimeException exception = assertThrows(RuntimeException.class, () -> 
executeReconcileFromArgs(1));
+
+    assertThatOutput(errContent).contains("Failed to trigger reconciliation 
for container 1: " + mockMessage);
+
+    assertThat(exception.getMessage()).contains("Failed to trigger 
reconciliation for 1 container");
+
+    // Should have no successful reconcile output
+    assertThatOutput(outContent).doesNotContain("Reconciliation has been 
triggered for container 1");
+  }
+
+  /**
+   * When`--status` is given and a mix of Open, Ratis, and EC containers are 
returned from the server,
+   * the client should only print results for the closed Ratis containers. 
Errors for the other containers should be
+   * printed.
+   */
+  @Test
+  public void testStatusHandlesValidAndInvalidContainers() throws Exception {
+    mockContainer(1, 3, new ECReplicationConfig(3, 2), true);
+    // Container ID 2 is the only valid one.
+    mockContainer(2, 3, RatisReplicationConfig.getInstance(THREE), true);
+    mockContainer(3, 3, new ECReplicationConfig(6, 3), true);
+    mockOpenContainer(4, 3, RatisReplicationConfig.getInstance(THREE));
+
+    // Test status output - should process Ratis container but fail due to EC 
containers
+    RuntimeException exception = assertThrows(RuntimeException.class, () -> {
+      executeStatusFromArgs(1, 2, 3, 4);
+    });
+
+    // Should have error messages for EC and open containers
+    assertThatOutput(errContent).contains("Cannot get status of container 1");
+    assertThatOutput(errContent).contains("Cannot get status of container 3");
+    assertThatOutput(errContent).contains("Cannot get status of container 4");
+    assertThatOutput(errContent).contains(EC_CONTAINER_MESSAGE);
+    assertThatOutput(errContent).contains(OPEN_CONTAINER_MESSAGE);
+    assertThatOutput(errContent).doesNotContain("2");
+
+    // Exception message should indicate 3 failed containers
+    assertThat(exception.getMessage()).contains("Failed to process 
reconciliation status for 3 containers");
+    
+    // Should have output for only container 2: the closed ratis container.
+    validateStatusOutput(true, 2);
+
+    // Verify that EC containers 1 and 3 and open container 4 are not present 
in JSON output
+    String output = outContent.toString(DEFAULT_ENCODING);
+    JsonNode jsonOutput = JsonUtils.readTree(output);
+    assertThat(jsonOutput.isArray()).isTrue();
+    for (JsonNode containerNode : jsonOutput) {
+      int containerID = containerNode.get("containerID").asInt();
+      assertThat(containerID).isNotIn(1, 3, 4);
+    }
+  }
+
+  /**
+   * Give a mix of valid and invalid containers to reconcile, and mock the 
server to return errors for the invalid ones.
+   * The valid containers should still be processed.
+   */
+  @Test
+  public void testReconcileHandlesValidAndInvalidContainers() throws Exception 
{
+    mockContainer(1, 3, new ECReplicationConfig(3, 2), true);
+    mockContainer(2, 3, RatisReplicationConfig.getInstance(THREE), true);
+    mockContainer(3, 3, new ECReplicationConfig(6, 3), true);
+
+    // Mock reconcile to fail for EC containers
+    doThrow(new 
IOException(EC_CONTAINER_MESSAGE)).when(scmClient).reconcileContainer(1L);
+    doThrow(new 
IOException(EC_CONTAINER_MESSAGE)).when(scmClient).reconcileContainer(3L);
+
+    // Test reconcile command - should process Ratis container but fail for EC 
containers
+    RuntimeException exception = assertThrows(RuntimeException.class, () -> {
+      executeReconcileFromArgs(1, 2, 3);
+    });
+
+    // Should have error messages for EC containers
+    assertThatOutput(errContent).contains("Failed to trigger reconciliation 
for container 1: " + EC_CONTAINER_MESSAGE);
+    assertThatOutput(errContent).contains("Failed to trigger reconciliation 
for container 3: " + EC_CONTAINER_MESSAGE);
+    assertThatOutput(errContent).doesNotContain("Failed to trigger 
reconciliation for container 2");
+
+    // Exception message should indicate 2 failed containers
+    assertThat(exception.getMessage()).contains("Failed to trigger 
reconciliation for 2 containers");
+
+    // Should have reconcile success output for container 2 (Ratis) only
+    validateReconcileOutput(2);
+    assertThatOutput(outContent).doesNotContain("container 1");
+    assertThatOutput(outContent).doesNotContain("container 3");
+  }
+
+  /**
+   * Invalid container IDs are those that cannot be parsed because they are 
not positive integers.
+   * When any invalid container ID is passed, the command should fail early 
instead of proceeding with the valid
+   * entries. All invalid container IDs should be displayed in the error 
message, not just the first one.
+   */
+  @Test
+  public void testSomeInvalidContainerIDs() throws Exception {
+    // Test status command
+    Exception statusEx =
+        assertThrows(RuntimeException.class, () -> 
parseArgsAndExecute("--status", "123", "invalid", "-1", "456"));
+    
+    // Should have error messages for invalid container IDs only.
+    assertThat(statusEx.getMessage())
+        .contains("Container IDs must be positive integers. Invalid container 
IDs: invalid -1")
+        .doesNotContain("123", "456");
+    assertThatOutput(errContent).doesNotContain("123");
+    assertThatOutput(errContent).doesNotContain("456");
+    assertThatOutput(outContent).isEmpty();
+    
+    // Test reconcile command
+    Exception reconcileEx =
+        assertThrows(RuntimeException.class, () -> parseArgsAndExecute("123", 
"invalid", "-1", "456"));
+    
+    // Should have error messages for invalid IDs
+    assertThat(reconcileEx.getMessage())
+        .contains("Container IDs must be positive integers. Invalid container 
IDs: invalid -1")
+        .doesNotContain("123", "456");
+    assertThatOutput(errContent).doesNotContain("123");
+    assertThatOutput(errContent).doesNotContain("456");
+    assertThatOutput(outContent).isEmpty();
+  }
+
+  @Test
+  public void testUnreachableContainers() throws Exception {
+    final String exceptionMessage = "Container not found";
+
+    mockContainer(123);
+    doThrow(new 
IOException(exceptionMessage)).when(scmClient).getContainer(456L);
+
+    // Test status command - should throw exception due to unreachable 
containers
+    assertThrows(RuntimeException.class, () -> parseArgsAndExecute("--status", 
"123", "456"));
+    
+    // Should have error messages for unreachable containers
+    assertThatOutput(errContent).contains("Failed to get reconciliation status 
of container 456: " + exceptionMessage);
+    assertThatOutput(errContent).doesNotContain("123");
+    validateStatusOutput(true, 123);
+
+    // Test reconcile command - should also throw exception
+    doThrow(new 
IOException(exceptionMessage)).when(scmClient).reconcileContainer(456L);
+
+    assertThrows(RuntimeException.class, () -> parseArgsAndExecute("123", 
"456"));
+    // Should have error message for unreachable container
+    assertThatOutput(errContent).contains("Failed to trigger reconciliation 
for container 456: " + exceptionMessage);
+    assertThatOutput(errContent).doesNotContain("123");
+    assertThatOutput(outContent).doesNotContain("Reconciliation has been 
triggered for container 456");
+    validateReconcileOutput(123);
+  }
+
+  private void parseArgsAndExecute(String... args) throws Exception {
+    // Create fresh streams and command objects for each execution, otherwise 
stale results may interfere with tests.
+    if (inContent != null) {
+      inContent.reset();
+    }
+    outContent.reset();
+    errContent.reset();
+    System.setOut(new PrintStream(outContent, false, DEFAULT_ENCODING));
+    System.setErr(new PrintStream(errContent, false, DEFAULT_ENCODING));
+
+    ReconcileSubcommand cmd = new ReconcileSubcommand();
+    new CommandLine(cmd).parseArgs(args);
+    cmd.execute(scmClient);
+  }
+
+  private void validateOutput(boolean replicasMatch, long... containerIDs) 
throws Exception {
+    // Test reconcile and status with arguments.
+    executeStatusFromArgs(containerIDs);
+    validateStatusOutput(replicasMatch, containerIDs);
+    executeReconcileFromArgs(containerIDs);
+    validateReconcileOutput(containerIDs);
+
+    // Test reconcile and status with stdin.
+    executeStatusFromStdin(containerIDs);
+    validateStatusOutput(replicasMatch, containerIDs);
+    executeReconcileFromStdin(containerIDs);
+    validateReconcileOutput(containerIDs);
+  }
+
+  private void executeStatusFromArgs(long... containerIDs) throws Exception {
+    List<String> args = Arrays.stream(containerIDs)
+        .mapToObj(Long::toString)
+        .collect(Collectors.toList());
+    args.add(0, "--status");
+    parseArgsAndExecute(args.toArray(new String[]{}));
+  }
+
+  private void executeReconcileFromArgs(long... containerIDs) throws Exception 
{
+    List<String> args = Arrays.stream(containerIDs)
+        .mapToObj(Long::toString)
+        .collect(Collectors.toList());
+    parseArgsAndExecute(args.toArray(new String[]{}));
+  }
+
+  private void executeStatusFromStdin(long... containerIDs) throws Exception {
+    String inputIDs = Arrays.stream(containerIDs)
+        .mapToObj(Long::toString)
+        .collect(Collectors.joining("\n"));
+    inContent = new ByteArrayInputStream(inputIDs.getBytes(DEFAULT_ENCODING));
+    System.setIn(inContent);
+    parseArgsAndExecute("-", "--status");
+  }
+
+  private void executeReconcileFromStdin(long... containerIDs) throws 
Exception {
+    String inputIDs = Arrays.stream(containerIDs)
+        .mapToObj(Long::toString)
+        .collect(Collectors.joining("\n"));
+    inContent = new ByteArrayInputStream(inputIDs.getBytes(DEFAULT_ENCODING));
+    System.setIn(inContent);
+    parseArgsAndExecute("-");
+  }
+
+  private void validateStatusOutput(boolean replicasMatch, long... 
containerIDs) throws Exception {
+    String output = outContent.toString(DEFAULT_ENCODING);
+    // Output should be pretty-printed and end in a newline.
+    assertThat(output).endsWith("\n");
+
+    List<Object> containerOutputList = JsonUtils.getDefaultMapper()
+        .readValue(new StringReader(output), new TypeReference<List<Object>>() 
{ });
+    assertEquals(containerIDs.length, containerOutputList.size());
+    for (Object containerJson: containerOutputList) {
+      Map<String, Object> containerOutput = (Map<String, Object>)containerJson;
+      long containerID = (Integer)containerOutput.get("containerID");
+      ContainerInfo expectedContainerInfo = 
scmClient.getContainer(containerID);
+      List<ContainerReplicaInfo> expectedReplicas = 
scmClient.getContainerReplicas(containerID);
+
+      Map<String, Object> repConfig = (Map<String, 
Object>)containerOutput.get("replicationConfig");
+
+      // Check container level fields.
+      assertEquals(expectedContainerInfo.getContainerID(), 
((Integer)containerOutput.get("containerID")).longValue());
+      assertEquals(expectedContainerInfo.getState().toString(), 
containerOutput.get("state"));
+      
assertEquals(expectedContainerInfo.getReplicationConfig().getReplicationType().toString(),
+          repConfig.get("replicationType"));
+      assertEquals(replicasMatch, containerOutput.get("replicasMatch"));
+
+      // Check replica fields.
+      List<Object> replicaOutputList = 
(List<Object>)containerOutput.get("replicas");
+      assertEquals(expectedReplicas.size(), replicaOutputList.size());
+      for (int i = 0; i < expectedReplicas.size(); i++) {
+        Map<String, Object> replicaOutput = (Map<String, 
Object>)replicaOutputList.get(i);
+        ContainerReplicaInfo expectedReplica = expectedReplicas.get(i);
+
+        // Check container replica info.
+        assertEquals(expectedReplica.getState(), replicaOutput.get("state"));
+        assertEquals(Long.toHexString(expectedReplica.getDataChecksum()), 
replicaOutput.get("dataChecksum"));
+        // Replica index should only be output for EC containers. It has no 
meaning for Ratis containers.
+        if 
(expectedContainerInfo.getReplicationType().equals(HddsProtos.ReplicationType.RATIS))
 {
+          assertFalse(replicaOutput.containsKey("replicaIndex"));
+        } else {
+          assertEquals(expectedReplica.getReplicaIndex(), 
replicaOutput.get("replicaIndex"));
+        }
+
+        // Check datanode info.
+        Map<String, Object> dnOutput = (Map<String, 
Object>)replicaOutput.get("datanode");
+        DatanodeDetails expectedDnDetails = 
expectedReplica.getDatanodeDetails();
+
+        assertEquals(expectedDnDetails.getHostName(), 
dnOutput.get("hostname"));
+        assertEquals(expectedDnDetails.getUuidString(), dnOutput.get("id"));
+        assertEquals(expectedDnDetails.getIpAddress(), 
dnOutput.get("ipAddress"));
+        // Datanode output should be brief and only contain the above three 
identifiers.
+        assertEquals(3, dnOutput.size());
+      }
+    }
+  }
+
+  private void validateReconcileOutput(long... containerIDs) throws Exception {
+    for (long id: containerIDs) {
+      verify(scmClient, atLeastOnce()).reconcileContainer(id);
+      assertThatOutput(outContent).contains("Reconciliation has been triggered 
for container " + id);
+    }
+  }
+
+  private AbstractStringAssert<?> assertThatOutput(ByteArrayOutputStream 
stream) throws Exception {
+    return assertThat(stream.toString(DEFAULT_ENCODING));
+  }
+
+  private void mockContainer(long containerID) throws Exception {
+    mockContainer(containerID, 3, RatisReplicationConfig.getInstance(THREE), 
true);
+  }
+
+  private void mockOpenContainer(long containerID, int numReplicas, 
ReplicationConfig repConfig) throws Exception {
+    mockContainer(containerID, numReplicas, repConfig, OPEN, true);
+  }
+
+  private void mockContainer(long containerID, int numReplicas, 
ReplicationConfig repConfig, boolean replicasMatch)
+      throws Exception {
+    mockContainer(containerID, numReplicas, repConfig, CLOSED, replicasMatch);
+  }
+
+  private void mockContainer(long containerID, int numReplicas, 
ReplicationConfig repConfig,
+      HddsProtos.LifeCycleState state, boolean replicasMatch) throws Exception 
{
+    ContainerInfo container = new ContainerInfo.Builder()
+        .setContainerID(containerID)
+        .setState(state)
+        .setReplicationConfig(repConfig)
+        .build();
+    when(scmClient.getContainer(containerID)).thenReturn(container);
+
+    List<ContainerReplicaInfo> replicas = new ArrayList<>();
+    int replicaIndex = 1;
+    for (int i = 0; i < numReplicas; i++) {
+      DatanodeDetails dn = DatanodeDetails.newBuilder()
+          .setHostName("dn")
+          .setUuid(UUID.randomUUID())
+          .setIpAddress("127.0.0.1")
+          .build();
+
+      ContainerReplicaInfo.Builder replicaBuilder = new 
ContainerReplicaInfo.Builder()
+          .setContainerID(containerID)
+          .setState(state.name())
+          .setDatanodeDetails(dn);
+      if (repConfig.getReplicationType() != HddsProtos.ReplicationType.RATIS) {
+        replicaBuilder.setReplicaIndex(replicaIndex++);
+      }
+      if (replicasMatch) {
+        if (state == OPEN) {
+          replicaBuilder.setDataChecksum(0);
+        } else {
+          replicaBuilder.setDataChecksum(123);
+        }
+      } else {
+        replicaBuilder.setDataChecksum(i);
+      }
+      replicas.add(replicaBuilder.build());
+    }
+    when(scmClient.getContainerReplicas(containerID)).thenReturn(replicas);
+  }
+}
diff --git a/hadoop-ozone/dist/src/main/smoketest/admincli/container.robot 
b/hadoop-ozone/dist/src/main/smoketest/admincli/container.robot
index d419cbf7aec..e9450c9de59 100644
--- a/hadoop-ozone/dist/src/main/smoketest/admincli/container.robot
+++ b/hadoop-ozone/dist/src/main/smoketest/admincli/container.robot
@@ -36,12 +36,19 @@ Container is closed
 
 Container checksums should match
     [arguments]    ${container}    ${expected_checksum}
-    ${data_checksum1} =  Execute     ozone admin container info "${container}" 
--json | jq -r '.replicas[0].dataChecksum' | head -n1
-    ${data_checksum2} =  Execute     ozone admin container info "${container}" 
--json | jq -r '.replicas[1].dataChecksum' | head -n1
-    ${data_checksum3} =  Execute     ozone admin container info "${container}" 
--json | jq -r '.replicas[2].dataChecksum' | head -n1
+    ${data_checksum1} =  Execute     ozone admin container reconcile --status 
"${container}" | jq -r '.[].replicas[0].dataChecksum'
+    ${data_checksum2} =  Execute     ozone admin container reconcile --status 
"${container}" | jq -r '.[].replicas[1].dataChecksum'
+    ${data_checksum3} =  Execute     ozone admin container reconcile --status 
"${container}" | jq -r '.[].replicas[2].dataChecksum'
                          Should be equal as strings    ${data_checksum1}    
${expected_checksum}
                          Should be equal as strings    ${data_checksum2}    
${expected_checksum}
                          Should be equal as strings    ${data_checksum3}    
${expected_checksum}
+    # Verify that container info shows the same checksums as reconcile status
+    ${info_checksum1} =  Execute     ozone admin container info "${container}" 
--json | jq -r '.replicas[0].dataChecksum'
+    ${info_checksum2} =  Execute     ozone admin container info "${container}" 
--json | jq -r '.replicas[1].dataChecksum'
+    ${info_checksum3} =  Execute     ozone admin container info "${container}" 
--json | jq -r '.replicas[2].dataChecksum'
+                         Should be equal as strings    ${data_checksum1}    
${info_checksum1}
+                         Should be equal as strings    ${data_checksum2}    
${info_checksum2}
+                         Should be equal as strings    ${data_checksum3}    
${info_checksum3}
 
 *** Test Cases ***
 Create container
@@ -181,10 +188,9 @@ Reset user
 Cannot reconcile open container
     # At this point we should have an open Ratis Three container.
     ${container} =      Execute          ozone admin container list --state 
OPEN | jq -r '.[] | select(.replicationConfig.replicationFactor == "THREE") | 
.containerID' | head -n1
+    # Reconciling and querying status of open containers is not supported
     Execute and check rc    ozone admin container reconcile "${container}"    
255
-    # The container should not yet have any replica checksums since it is 
still open.
-    # 0 is the hex value of an empty checksum.
-    Container checksums should match    ${container}    0
+    Execute and check rc    ozone admin container reconcile --status 
"${container}"    255
 
 Close container
     ${container} =      Execute          ozone admin container list --state 
OPEN | jq -r '.[] | select(.replicationConfig.replicationFactor == "THREE") | 
.containerID' | head -1
@@ -196,9 +202,8 @@ Close container
     Wait until keyword succeeds    1min    10sec    Container is closed    
${container}
 
 Reconcile closed container
-    # Check that info does not show replica checksums, since manual 
reconciliation has not yet been triggered.
     ${container} =      Execute          ozone admin container list --state 
CLOSED | jq -r '.[] | select(.replicationConfig.replicationFactor == "THREE") | 
.containerID' | head -1
-    ${data_checksum} =  Execute          ozone admin container info 
"${container}" --json | jq -r '.replicas[].dataChecksum' | head -n1
+    ${data_checksum} =  Execute          ozone admin container reconcile 
--status "${container}" | jq -r '.[].replicas[0].dataChecksum'
     # Once the container is closed, the data checksum should be populated
     Should Not Be Equal As Strings    0    ${data_checksum}
     Container checksums should match    ${container}    ${data_checksum}


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

Reply via email to