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

devmadhuu pushed a commit to branch HDDS-11233
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/HDDS-11233 by this push:
     new 413db6b491d HDDS-16586. Add support for displaying storage type of 
replicas in container info command. (#11347)
413db6b491d is described below

commit 413db6b491df4239a508f9efb39e0aa6ffc2ef03
Author: Anurag Parvatikar <[email protected]>
AuthorDate: Thu Oct 1 17:00:38 2026 +0530

    HDDS-16586. Add support for displaying storage type of replicas in 
container info command. (#11347)
---
 .../hdds/scm/container/ContainerReplicaInfo.java   | 28 +++++++-
 .../scm/container/TestContainerReplicaInfo.java    | 30 +++++++++
 .../container/keyvalue/KeyValueContainerData.java  |  5 ++
 .../container/keyvalue/TestKeyValueContainer.java  | 48 ++++++++++++++
 .../interface-client/src/main/proto/hdds.proto     |  2 +
 .../proto/ScmServerDatanodeHeartbeatProtocol.proto |  1 +
 .../container/AbstractContainerReportHandler.java  |  3 +
 .../hdds/scm/container/ContainerReplica.java       | 16 +++++
 .../hdds/scm/server/SCMClientProtocolServer.java   | 14 ++--
 .../TestIncrementalContainerReportHandler.java     | 70 ++++++++++++++++++++
 .../scm/server/TestSCMClientProtocolServer.java    | 70 ++++++++++++++++++++
 .../hdds/scm/cli/container/InfoSubcommand.java     | 13 +++-
 .../hdds/scm/cli/container/TestInfoSubCommand.java | 77 ++++++++++++++++++++++
 .../TestAllocateContainerWithStorageTier.java      | 59 +++++++++++++++++
 14 files changed, 429 insertions(+), 7 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 2aaa45d695d..82f3e6b74e6 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
@@ -18,6 +18,8 @@
 package org.apache.hadoop.hdds.scm.container;
 
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.apache.hadoop.fs.StorageType;
+import org.apache.hadoop.hdds.client.StorageTypeUtils;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
 import org.apache.hadoop.hdds.protocol.DatanodeID;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
@@ -38,6 +40,8 @@ public final class ContainerReplicaInfo {
   private int replicaIndex = -1;
   @JsonSerialize(using = JsonUtils.ChecksumSerializer.class)
   private long dataChecksum;
+  private StorageType storageType;
+  private StorageType volumeStorageType;
 
   public static ContainerReplicaInfo fromProto(
       HddsProtos.SCMContainerReplicaProto proto) {
@@ -52,7 +56,11 @@ public static ContainerReplicaInfo fromProto(
         .setBytesUsed(proto.getBytesUsed())
         .setReplicaIndex(
             proto.hasReplicaIndex() ? (int)proto.getReplicaIndex() : -1)
-        .setDataChecksum(proto.getDataChecksum());
+        .setDataChecksum(proto.getDataChecksum())
+        .setStorageType(proto.hasStorageType()
+            ? StorageTypeUtils.getFromProtobuf(proto.getStorageType()) : null)
+        .setVolumeStorageType(proto.hasVolumeStorageType()
+            ? StorageTypeUtils.getFromProtobuf(proto.getVolumeStorageType()) : 
null);
     return builder.build();
   }
 
@@ -95,6 +103,14 @@ public long getDataChecksum() {
     return dataChecksum;
   }
 
+  public StorageType getStorageType() {
+    return storageType;
+  }
+
+  public StorageType getVolumeStorageType() {
+    return volumeStorageType;
+  }
+
   /**
    * Builder for ContainerReplicaInfo class.
    */
@@ -147,6 +163,16 @@ public Builder setDataChecksum(long dataChecksum) {
       return this;
     }
 
+    public Builder setStorageType(StorageType storageType) {
+      subject.storageType = storageType;
+      return this;
+    }
+
+    public Builder setVolumeStorageType(StorageType volumeStorageType) {
+      subject.volumeStorageType = volumeStorageType;
+      return this;
+    }
+
     public ContainerReplicaInfo build() {
       return subject;
     }
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/scm/container/TestContainerReplicaInfo.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/scm/container/TestContainerReplicaInfo.java
index 9026a9845a3..d2e81255df6 100644
--- 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/scm/container/TestContainerReplicaInfo.java
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/scm/container/TestContainerReplicaInfo.java
@@ -17,9 +17,11 @@
 
 package org.apache.hadoop.hdds.scm.container;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.util.UUID;
+import org.apache.hadoop.fs.StorageType;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
 import org.apache.hadoop.hdds.protocol.MockDatanodeDetails;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
@@ -57,6 +59,34 @@ public void testObjectCreatedFromProto() {
     assertEquals(proto.getState(), info.getState());
     // If replicaIndex is not in the proto, then -1 should be returned
     assertEquals(-1, info.getReplicaIndex());
+    // Storage fields are absent in the proto, so they must be null rather than
+    // the protobuf defaults, otherwise they would show up in JSON output for
+    // datanodes that do not report them.
+    assertThat(info.getStorageType()).isNull();
+    assertThat(info.getVolumeStorageType()).isNull();
+  }
+
+  @Test
+  public void testObjectCreatedFromProtoWithStorageFields() {
+    HddsProtos.SCMContainerReplicaProto proto =
+        HddsProtos.SCMContainerReplicaProto.newBuilder()
+            .setKeyCount(10)
+            .setBytesUsed(12345)
+            .setContainerID(567)
+            .setPlaceOfBirth(UUID.randomUUID().toString())
+            .setSequenceID(5)
+            .setDatanodeDetails(MockDatanodeDetails.randomDatanodeDetails()
+                .getProtoBufMessage())
+            .setState("CLOSED")
+            .setStorageType(HddsProtos.StorageTypeProto.SSD)
+            .setVolumeStorageType(HddsProtos.StorageTypeProto.SSD)
+            .build();
+
+    ContainerReplicaInfo info = ContainerReplicaInfo.fromProto(proto);
+
+    // The container was stamped SSD and sits on the SSD volume.
+    assertThat(info.getStorageType()).isEqualTo(StorageType.SSD);
+    assertThat(info.getVolumeStorageType()).isEqualTo(StorageType.SSD);
   }
 
   @Test
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueContainerData.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueContainerData.java
index 34c7b23be8a..44c05dc43b2 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueContainerData.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueContainerData.java
@@ -60,6 +60,7 @@
 import org.apache.hadoop.ozone.container.common.impl.ContainerData;
 import org.apache.hadoop.ozone.container.common.impl.ContainerLayoutVersion;
 import org.apache.hadoop.ozone.container.common.interfaces.DBHandle;
+import org.apache.hadoop.ozone.container.common.volume.HddsVolume;
 import 
org.apache.hadoop.ozone.container.keyvalue.helpers.KeyValueContainerUtil;
 import org.apache.hadoop.ozone.container.upgrade.VersionedDatanodeFeatures;
 import org.yaml.snakeyaml.nodes.Tag;
@@ -288,6 +289,10 @@ ContainerReplicaProto buildContainerReplicaProto() throws 
StorageContainerExcept
     if (getStorageType() != null) {
       
builder.setStorageType(StorageTypeUtils.getStorageTypeProto(getStorageType()));
     }
+    final HddsVolume volume = getVolume();
+    if (volume != null) {
+      
builder.setVolumeStorageType(StorageTypeUtils.getStorageTypeProto(volume.getStorageType()));
+    }
     return builder.build();
   }
 
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestKeyValueContainer.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestKeyValueContainer.java
index 811f3ab5257..6ec0493289f 100644
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestKeyValueContainer.java
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestKeyValueContainer.java
@@ -72,6 +72,8 @@
 import org.apache.hadoop.hdds.conf.ConfigurationSource;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos.StorageTypeProto;
+import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.ContainerReplicaProto;
 import 
org.apache.hadoop.hdds.scm.container.common.helpers.StorageContainerException;
 import org.apache.hadoop.hdds.utils.db.CodecBuffer;
 import org.apache.hadoop.hdds.utils.db.DBProfile;
@@ -233,6 +235,52 @@ public void testCreateContainerWithStorageType(
         eq(StorageType.SSD));
   }
 
+  @ContainerTestVersionInfo.ContainerTest
+  public void testContainerReportHasVolumeStorageType(
+      ContainerTestVersionInfo versionInfo) throws Exception {
+    init(versionInfo);
+
+    HddsVolume ssdVolume = new HddsVolume.Builder(
+        new File(folder, "ssd-volume").getAbsolutePath())
+        .conf(CONF)
+        .datanodeUuid(datanodeId.toString())
+        .storageType(StorageType.SSD)
+        .build();
+    StorageVolumeUtil.checkVolume(ssdVolume, scmId, scmId, CONF, null, null);
+    hddsVolumes.add(ssdVolume);
+
+    when(volumeChoosingPolicy.chooseVolume(anyList(), anyLong(),
+        eq(StorageType.SSD))).thenReturn(ssdVolume);
+
+    keyValueContainer.create(volumeSet, volumeChoosingPolicy, scmId,
+        StorageType.SSD);
+
+    ContainerReplicaProto report = keyValueContainer.getContainerReport();
+
+    // The container was created on an SSD volume, so both types agree.
+    assertThat(report.getStorageType()).isEqualTo(StorageTypeProto.SSD);
+    assertThat(report.getVolumeStorageType()).isEqualTo(StorageTypeProto.SSD);
+  }
+
+  @ContainerTestVersionInfo.ContainerTest
+  public void testContainerReportVolumeStorageTypeReflectsRelabelledVolume(
+      ContainerTestVersionInfo versionInfo) throws Exception {
+    init(versionInfo);
+    createContainer();
+
+    // Simulate an operator relabelling the volume: the container keeps the 
type
+    // it was stamped with at creation, while the volume now reports a new one.
+    HddsVolume relabelled = mock(HddsVolume.class);
+    when(relabelled.getStorageType()).thenReturn(StorageType.ARCHIVE);
+    keyValueContainerData.setVolume(relabelled);
+
+    ContainerReplicaProto report = keyValueContainer.getContainerReport();
+
+    assertThat(report.getStorageType()).isEqualTo(StorageTypeProto.DISK);
+    assertThat(report.getVolumeStorageType())
+        .isEqualTo(StorageTypeProto.ARCHIVE);
+  }
+
   /**
    * Tests repair of containers affected by the bug reported in HDDS-6235.
    */
diff --git a/hadoop-hdds/interface-client/src/main/proto/hdds.proto 
b/hadoop-hdds/interface-client/src/main/proto/hdds.proto
index b396af1966f..920ebd38e3f 100644
--- a/hadoop-hdds/interface-client/src/main/proto/hdds.proto
+++ b/hadoop-hdds/interface-client/src/main/proto/hdds.proto
@@ -484,6 +484,8 @@ message SCMContainerReplicaProto {
     required int64 bytesUsed = 7;
     optional int64 replicaIndex = 8;
     optional int64 dataChecksum = 9;
+    optional StorageTypeProto storageType = 10;
+    optional StorageTypeProto volumeStorageType = 11;
 }
 
 message KeyContainerIDList {
diff --git 
a/hadoop-hdds/interface-server/src/main/proto/ScmServerDatanodeHeartbeatProtocol.proto
 
b/hadoop-hdds/interface-server/src/main/proto/ScmServerDatanodeHeartbeatProtocol.proto
index f196d9cb7ce..2b0f285ca9a 100644
--- 
a/hadoop-hdds/interface-server/src/main/proto/ScmServerDatanodeHeartbeatProtocol.proto
+++ 
b/hadoop-hdds/interface-server/src/main/proto/ScmServerDatanodeHeartbeatProtocol.proto
@@ -236,6 +236,7 @@ message ContainerReplicaProto {
   optional bool isEmpty = 15 [default = false];
   optional int64 dataChecksum = 16;
   optional StorageTypeProto storageType = 17;
+  optional StorageTypeProto volumeStorageType = 18;
 }
 
 message CommandStatusReportsProto {
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/AbstractContainerReportHandler.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/AbstractContainerReportHandler.java
index 8b53459787f..22f95f10ab1 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/AbstractContainerReportHandler.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/AbstractContainerReportHandler.java
@@ -414,6 +414,9 @@ private void updateContainerReplica(final DatanodeDetails 
datanodeDetails,
     if (replicaProto.hasStorageType()) {
       
replicaBuilder.setStorageType(StorageTypeUtils.getFromProtobuf(replicaProto.getStorageType()));
     }
+    if (replicaProto.hasVolumeStorageType()) {
+      
replicaBuilder.setVolumeStorageType(StorageTypeUtils.getFromProtobuf(replicaProto.getVolumeStorageType()));
+    }
     ContainerReplica replica = 
replicaBuilder.setChecksums(ContainerChecksums.of(replicaProto.getDataChecksum()))
         .build();
 
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerReplica.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerReplica.java
index 7a296d0d9b2..bb6b3a6ec47 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerReplica.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerReplica.java
@@ -52,6 +52,7 @@ public final class ContainerReplica implements 
Comparable<ContainerReplica> {
   private final boolean isEmpty;
   private final ContainerChecksums checksums;
   private final StorageType storageType;
+  private final StorageType volumeStorageType;
 
   private ContainerReplica(ContainerReplicaBuilder b) {
     this.containerID = Objects.requireNonNull(b.containerID, "containerID == 
null");
@@ -65,6 +66,7 @@ private ContainerReplica(ContainerReplicaBuilder b) {
     this.sequenceId = b.sequenceId;
     this.checksums = Objects.requireNonNull(b.checksums, "checksums == null");
     this.storageType = b.storageType;
+    this.volumeStorageType = b.volumeStorageType;
   }
 
   public ContainerID getContainerID() {
@@ -142,6 +144,12 @@ public StorageType getStorageType() {
     return storageType;
   }
 
+  /** @return the storage type of the volume holding this replica, or null if 
not reported. */
+  @Nullable
+  public StorageType getVolumeStorageType() {
+    return volumeStorageType;
+  }
+
   @Override
   public int hashCode() {
     return new HashCodeBuilder(61, 71)
@@ -204,6 +212,7 @@ public ContainerReplicaBuilder toBuilder() {
         .setSequenceId(sequenceId)
         .setEmpty(isEmpty)
         .setStorageType(storageType)
+        .setVolumeStorageType(volumeStorageType)
         .setChecksums(checksums);
   }
 
@@ -220,6 +229,7 @@ public String toString() {
         + ", " + (isEmpty ? "empty" : "non-empty")
         + ", checksums=" + checksums
         + ", storageType=" + storageType
+        + ", volumeStorageType=" + volumeStorageType
         + '}';
   }
 
@@ -239,6 +249,7 @@ public static class ContainerReplicaBuilder {
     private boolean isEmpty;
     private ContainerChecksums checksums;
     private StorageType storageType;
+    private StorageType volumeStorageType;
 
     /**
      * Set Container Id.
@@ -323,6 +334,11 @@ public ContainerReplicaBuilder setStorageType(StorageType 
storageType) {
       return this;
     }
 
+    public ContainerReplicaBuilder setVolumeStorageType(StorageType 
volumeStorageType) {
+      this.volumeStorageType = volumeStorageType;
+      return this;
+    }
+
     /**
      * Constructs new ContainerReplicaBuilder.
      *
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java
index 79330033d80..a43a0012964 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java
@@ -52,6 +52,7 @@
 import org.apache.hadoop.fs.CommonConfigurationKeysPublic;
 import org.apache.hadoop.hdds.client.ReplicationConfig;
 import org.apache.hadoop.hdds.client.StorageTier;
+import org.apache.hadoop.hdds.client.StorageTypeUtils;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.conf.ReconfigurationHandler;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
@@ -365,7 +366,7 @@ public List<HddsProtos.SCMContainerReplicaProto> 
getContainerReplicas(
       Set<ContainerReplica> replicas = getScm().getContainerManager()
           .getContainerReplicas(ContainerID.valueOf(containerId));
       for (ContainerReplica r : replicas) {
-        results.add(
+        HddsProtos.SCMContainerReplicaProto.Builder builder =
             HddsProtos.SCMContainerReplicaProto.newBuilder()
                 .setContainerID(containerId)
                 .setState(r.getState().toString())
@@ -375,9 +376,14 @@ public List<HddsProtos.SCMContainerReplicaProto> 
getContainerReplicas(
                 .setKeyCount(r.getKeyCount())
                 .setSequenceID(r.getSequenceId())
                 .setReplicaIndex(r.getReplicaIndex())
-                .setDataChecksum(r.getDataChecksum())
-                .build()
-        );
+                .setDataChecksum(r.getDataChecksum());
+        if (r.getStorageType() != null) {
+          
builder.setStorageType(StorageTypeUtils.getStorageTypeProto(r.getStorageType()));
+        }
+        if (r.getVolumeStorageType() != null) {
+          
builder.setVolumeStorageType(StorageTypeUtils.getStorageTypeProto(r.getVolumeStorageType()));
+        }
+        results.add(builder.build());
       }
       AUDIT.logReadSuccess(buildAuditMessageForSuccess(
           SCMAction.GET_CONTAINER_WITH_PIPELINE_BATCH,
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/TestIncrementalContainerReportHandler.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/TestIncrementalContainerReportHandler.java
index 23455f1aae3..815a14f7a39 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/TestIncrementalContainerReportHandler.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/TestIncrementalContainerReportHandler.java
@@ -27,6 +27,7 @@
 import static 
org.apache.hadoop.hdds.scm.container.TestContainerReportHandler.createUniqueDataChecksumForReplica;
 import static 
org.apache.hadoop.hdds.scm.container.TestContainerReportHandler.getContainerReportsProto;
 import static 
org.apache.hadoop.hdds.upgrade.HDDSLayoutVersionManager.maxLayoutVersion;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -796,6 +797,75 @@ public void testReplicaStorageTypeValidation() throws 
IOException {
     }
   }
 
+  @Test
+  public void testReplicaVolumeStorageType() throws IOException {
+    final IncrementalContainerReportHandler reportHandler =
+        new IncrementalContainerReportHandler(
+            nodeManager, containerManager, scmContext);
+    final ContainerInfo container = getContainer(LifeCycleState.CLOSING);
+    final DatanodeDetails datanode = randomDatanodeDetails();
+    nodeManager.register(datanode, null, null);
+    containerStateManager.addContainer(container.getProtobuf());
+
+    // The container was stamped SSD at creation but now sits on an ARCHIVE
+    // volume, which is what a relabelled or migrated replica looks like.
+    final ContainerReplicaProto replicaProto = newReplicaProto(container, 
datanode)
+        .setStorageType(StorageTypeUtils.getStorageTypeProto(StorageType.SSD))
+        .setVolumeStorageType(
+            StorageTypeUtils.getStorageTypeProto(StorageType.ARCHIVE))
+        .build();
+    reportHandler.onMessage(new IncrementalContainerReportFromDatanode(
+        datanode, getIncrementalContainerReportProto(replicaProto)), 
publisher);
+
+    final ContainerReplica replica = getOnlyReplica(container);
+    assertThat(replica.getStorageType()).isEqualTo(StorageType.SSD);
+    assertThat(replica.getVolumeStorageType()).isEqualTo(StorageType.ARCHIVE);
+  }
+
+  @Test
+  public void testReplicaStorageFieldsNullWhenNotReported() throws IOException 
{
+    final IncrementalContainerReportHandler reportHandler =
+        new IncrementalContainerReportHandler(
+            nodeManager, containerManager, scmContext);
+    final ContainerInfo container = getContainer(LifeCycleState.CLOSING);
+    final DatanodeDetails datanode = randomDatanodeDetails();
+    nodeManager.register(datanode, null, null);
+    containerStateManager.addContainer(container.getProtobuf());
+
+    // A datanode running an older version reports none of the storage fields.
+    reportHandler.onMessage(new 
IncrementalContainerReportFromDatanode(datanode,
+        getIncrementalContainerReportProto(
+            newReplicaProto(container, datanode).build())), publisher);
+
+    final ContainerReplica replica = getOnlyReplica(container);
+    assertThat(replica.getStorageType()).isNull();
+    assertThat(replica.getVolumeStorageType()).isNull();
+  }
+
+  private ContainerReplica getOnlyReplica(ContainerInfo container) {
+    final Set<ContainerReplica> replicas =
+        containerStateManager.getContainerReplicas(container.containerID());
+    assertThat(replicas).hasSize(1);
+    return replicas.iterator().next();
+  }
+
+  private static ContainerReplicaProto.Builder newReplicaProto(
+      ContainerInfo container, DatanodeDetails datanode) {
+    return ContainerReplicaProto.newBuilder()
+        .setContainerID(container.containerID().getIdForTesting())
+        .setState(ContainerReplicaProto.State.CLOSED)
+        .setOriginNodeId(datanode.getUuidString())
+        .setSize(5368709120L)
+        .setUsed(2000000000L)
+        .setKeyCount(100000000L)
+        .setReadCount(0)
+        .setWriteCount(0)
+        .setReadBytes(0)
+        .setWriteBytes(0)
+        .setBlockCommitSequenceId(10000L)
+        .setDeleteTransactionId(0);
+  }
+
   private void addIncrContainerReport(ContainerInfo container, DatanodeDetails 
datanode,
       IncrementalContainerReportHandler reportHandler, StorageType 
storageType) {
     final IncrementalContainerReportProto containerReport1 =
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestSCMClientProtocolServer.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestSCMClientProtocolServer.java
index 964fcb3b0d0..b366aed9e7c 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestSCMClientProtocolServer.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestSCMClientProtocolServer.java
@@ -19,6 +19,7 @@
 
 import static 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.LifeCycleState.CLOSED;
 import static 
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_READONLY_ADMINISTRATORS;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -36,22 +37,28 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import org.apache.hadoop.fs.StorageType;
 import org.apache.hadoop.hdds.client.RatisReplicationConfig;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.conf.ReconfigurationHandler;
+import org.apache.hadoop.hdds.protocol.MockDatanodeDetails;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos.LifeCycleState;
+import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.ContainerReplicaProto;
 import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.DecommissionScmRequestProto;
 import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.DecommissionScmResponseProto;
 import org.apache.hadoop.hdds.scm.HddsTestUtils;
+import org.apache.hadoop.hdds.scm.container.ContainerID;
 import org.apache.hadoop.hdds.scm.container.ContainerInfo;
 import org.apache.hadoop.hdds.scm.container.ContainerManagerImpl;
+import org.apache.hadoop.hdds.scm.container.ContainerReplica;
 import org.apache.hadoop.hdds.scm.ha.SCMContext;
 import org.apache.hadoop.hdds.scm.ha.SCMHAManagerStub;
 import org.apache.hadoop.hdds.scm.ha.SCMNodeDetails;
 import org.apache.hadoop.hdds.scm.pipeline.PipelineID;
 import 
org.apache.hadoop.hdds.scm.protocol.StorageContainerLocationProtocolServerSideTranslatorPB;
 import org.apache.hadoop.hdds.utils.ProtocolMessageMetrics;
+import org.apache.hadoop.ozone.ClientVersion;
 import org.apache.hadoop.ozone.container.common.SCMTestUtils;
 import org.apache.hadoop.security.AccessControlException;
 import org.apache.hadoop.security.UserGroupInformation;
@@ -194,6 +201,69 @@ public void testListContainerPaginationHasNoDuplicates() 
throws Exception {
     }
   }
 
+  @Test
+  public void testGetContainerReplicasCopiesStorageFields() throws Exception {
+    final long containerId = 1L;
+    // Stamped SSD at creation, but currently sitting on an ARCHIVE volume.
+    ContainerReplica replica = newReplica(containerId)
+        .setStorageType(StorageType.SSD)
+        .setVolumeStorageType(StorageType.ARCHIVE)
+        .build();
+
+    SCMClientProtocolServer scmServer = new SCMClientProtocolServer(
+        new OzoneConfiguration(), mockStorageContainerManager(containerId, 
replica),
+        mock(ReconfigurationHandler.class));
+    try {
+      List<HddsProtos.SCMContainerReplicaProto> replicas =
+          scmServer.getContainerReplicas(containerId, 
ClientVersion.CURRENT_VERSION);
+
+      assertThat(replicas).hasSize(1);
+      HddsProtos.SCMContainerReplicaProto proto = replicas.get(0);
+      
assertThat(proto.getStorageType()).isEqualTo(HddsProtos.StorageTypeProto.SSD);
+      
assertThat(proto.getVolumeStorageType()).isEqualTo(HddsProtos.StorageTypeProto.ARCHIVE);
+    } finally {
+      scmServer.stop();
+    }
+  }
+
+  @Test
+  public void testGetContainerReplicasOmitsUnsetStorageFields() throws 
Exception {
+    final long containerId = 1L;
+    // A replica reported by a datanode that does not send the storage fields.
+    ContainerReplica replica = newReplica(containerId).build();
+
+    SCMClientProtocolServer scmServer = new SCMClientProtocolServer(
+        new OzoneConfiguration(), mockStorageContainerManager(containerId, 
replica),
+        mock(ReconfigurationHandler.class));
+    try {
+      List<HddsProtos.SCMContainerReplicaProto> replicas =
+          scmServer.getContainerReplicas(containerId, 
ClientVersion.CURRENT_VERSION);
+
+      assertThat(replicas).hasSize(1);
+      HddsProtos.SCMContainerReplicaProto proto = replicas.get(0);
+      assertThat(proto.hasStorageType()).isFalse();
+      assertThat(proto.hasVolumeStorageType()).isFalse();
+    } finally {
+      scmServer.stop();
+    }
+  }
+
+  private static ContainerReplica.ContainerReplicaBuilder newReplica(long 
containerId) {
+    return ContainerReplica.newBuilder()
+        .setContainerID(ContainerID.valueOf(containerId))
+        .setContainerState(ContainerReplicaProto.State.CLOSED)
+        .setDatanodeDetails(MockDatanodeDetails.randomDatanodeDetails())
+        .setSequenceId(1L);
+  }
+
+  private StorageContainerManager mockStorageContainerManager(
+      long containerId, ContainerReplica... replicas) throws IOException {
+    StorageContainerManager scmMock = mockStorageContainerManager();
+    
when(scmMock.getContainerManager().getContainerReplicas(ContainerID.valueOf(containerId)))
+        .thenReturn(new HashSet<>(Arrays.asList(replicas)));
+    return scmMock;
+  }
+
   private StorageContainerManager mockStorageContainerManager() {
     List<ContainerInfo> infos = new ArrayList<>();
     for (int i = 0; i < 10; i++) {
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 ea296ba9870..070d7eb4db4 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
@@ -59,6 +59,11 @@ public class InfoSubcommand extends ScmSubcommand {
       description = "Format output as JSON")
   private boolean json;
 
+  @CommandLine.Option(names = { "--with-storagetype" },
+      defaultValue = "false",
+      description = "Output Container and Volume StorageType info")
+  private boolean withStorageType;
+
   @CommandLine.Mixin
   private ContainerIDParameters containerList;
 
@@ -177,7 +182,7 @@ private void printDetails(ScmClient scmClient, long 
containerID) throws IOExcept
       if (replicas != null) {
         String replicaStr = replicas.stream()
             
.sorted(Comparator.comparing(ContainerReplicaInfo::getReplicaIndex))
-            .map(InfoSubcommand::buildReplicaDetails)
+            .map(replica -> buildReplicaDetails(replica, withStorageType))
             .collect(Collectors.joining("," + System.lineSeparator()));
         System.out.printf("Replicas: [%s]%n", replicaStr);
       }
@@ -188,7 +193,7 @@ private static String buildDatanodeDetails(DatanodeDetails 
details) {
     return details.getUuidString() + "/" + details.getHostName();
   }
 
-  private static String buildReplicaDetails(ContainerReplicaInfo replica) {
+  private static String buildReplicaDetails(ContainerReplicaInfo replica, 
boolean withStorageType) {
     StringBuilder sb = new StringBuilder()
         .append("State: ").append(replica.getState()).append(';');
     if (replica.getReplicaIndex() != -1) {
@@ -197,6 +202,10 @@ private static String 
buildReplicaDetails(ContainerReplicaInfo replica) {
     sb.append(" SequenceId: ").append(replica.getSequenceId()).append(';')
         .append(" Origin: 
").append(replica.getPlaceOfBirth().toString()).append(';')
         .append(" Location: 
").append(buildDatanodeDetails(replica.getDatanodeDetails()));
+    if (withStorageType) {
+      sb.append("; ContainerStorageType: ").append(replica.getStorageType())
+          .append("; VolumeStorageType: 
").append(replica.getVolumeStorageType());
+    }
     return sb.toString();
   }
 
diff --git 
a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/container/TestInfoSubCommand.java
 
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/container/TestInfoSubCommand.java
index 86e3bfdf273..3008d806886 100644
--- 
a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/container/TestInfoSubCommand.java
+++ 
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/container/TestInfoSubCommand.java
@@ -43,6 +43,7 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
+import org.apache.hadoop.fs.StorageType;
 import org.apache.hadoop.hdds.client.ECReplicationConfig;
 import org.apache.hadoop.hdds.client.RatisReplicationConfig;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
@@ -331,6 +332,82 @@ private void testJsonOutput() throws IOException {
     assertTrue(matcher.matches());
   }
 
+  @Test
+  public void testStorageTypesNotPrintedByDefault() throws Exception {
+    when(scmClient.getContainerReplicas(anyLong()))
+        .thenReturn(getReplicasWithStorageInfo());
+    cmd = new InfoSubcommand();
+    new CommandLine(cmd).parseArgs("1");
+    cmd.execute(scmClient);
+
+    String output = outContent.toString(DEFAULT_ENCODING);
+    // Storage types stay hidden unless --with-storagetype is passed.
+    assertThat(output).doesNotContain("ContainerStorageType");
+    assertThat(output).doesNotContain("VolumeStorageType");
+  }
+
+  @Test
+  public void testStorageTypesPrintedWithFlag() throws Exception {
+    when(scmClient.getContainerReplicas(anyLong()))
+        .thenReturn(getReplicasWithStorageInfo());
+    cmd = new InfoSubcommand();
+    new CommandLine(cmd).parseArgs("1", "--with-storagetype");
+    cmd.execute(scmClient);
+
+    String output = outContent.toString(DEFAULT_ENCODING);
+    // Both ContainerStorageType and VolumeStorageType
+    // must appear, and the volume line must not echo the container's value.
+    assertThat(output).contains("ContainerStorageType: SSD");
+    assertThat(output).contains("VolumeStorageType: ARCHIVE");
+  }
+
+  @Test
+  public void testJsonIncludesStorageFields() throws Exception {
+    when(scmClient.getContainerReplicas(anyLong()))
+        .thenReturn(getReplicasWithStorageInfo());
+    cmd = new InfoSubcommand();
+    new CommandLine(cmd).parseArgs("1", "--json");
+    cmd.execute(scmClient);
+
+    // JSON carries the fields regardless of --with-storagetype, for scripting.
+    String output = outContent.toString(DEFAULT_ENCODING);
+    assertThat(output).contains("\"storageType\" : \"SSD\"");
+    assertThat(output).contains("\"volumeStorageType\" : \"ARCHIVE\"");
+  }
+
+  @Test
+  public void testJsonOmitsStorageFieldsWhenNotReported() throws Exception {
+    
when(scmClient.getContainerReplicas(anyLong())).thenReturn(getReplicas(true));
+    cmd = new InfoSubcommand();
+    new CommandLine(cmd).parseArgs("1", "--json");
+    cmd.execute(scmClient);
+
+    // Null fields are dropped by the NON_NULL serialization inclusion, so an
+    // unset type must not surface as "storageType" : "".
+    String output = outContent.toString(DEFAULT_ENCODING);
+    assertThat(output).doesNotContain("storageType");
+  }
+
+  private List<ContainerReplicaInfo> getReplicasWithStorageInfo() {
+    List<ContainerReplicaInfo> replicas = new ArrayList<>();
+    int index = 1;
+    for (DatanodeDetails dn : datanodes) {
+      replicas.add(new ContainerReplicaInfo.Builder()
+          .setContainerID(1)
+          .setBytesUsed(1234)
+          .setState("CLOSED")
+          .setPlaceOfBirth(dn.getID())
+          .setDatanodeDetails(dn)
+          .setKeyCount(1)
+          .setSequenceId(1)
+          .setReplicaIndex(index++)
+          .setStorageType(StorageType.SSD)
+          .setVolumeStorageType(StorageType.ARCHIVE)
+          .build());
+    }
+    return replicas;
+  }
+
   private List<ContainerReplicaInfo> getReplicas(boolean includeIndex) {
     List<ContainerReplicaInfo> replicas = new ArrayList<>();
     int index = 1;
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/container/TestAllocateContainerWithStorageTier.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/container/TestAllocateContainerWithStorageTier.java
index ac54c014cc4..857269477e9 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/container/TestAllocateContainerWithStorageTier.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/container/TestAllocateContainerWithStorageTier.java
@@ -17,8 +17,10 @@
 
 package org.apache.hadoop.hdds.scm.container;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Collections.emptyList;
 import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_DATANODE_PIPELINE_LIMIT;
+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.assertNotNull;
@@ -29,8 +31,10 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.function.BooleanSupplier;
 import org.apache.hadoop.fs.StorageType;
 import org.apache.hadoop.hdds.client.RatisReplicationConfig;
 import org.apache.hadoop.hdds.client.ReplicationConfig;
@@ -38,11 +42,18 @@
 import org.apache.hadoop.hdds.client.StorageTier;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor;
+import org.apache.hadoop.hdds.scm.cli.ContainerOperationClient;
+import org.apache.hadoop.hdds.scm.client.ScmClient;
 import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
 import org.apache.hadoop.hdds.scm.pipeline.Pipeline.PipelineState;
 import org.apache.hadoop.hdds.scm.pipeline.PipelineManager;
 import org.apache.hadoop.hdds.scm.server.StorageContainerManager;
+import org.apache.hadoop.ozone.DataTestUtil;
 import org.apache.hadoop.ozone.MiniOzoneCluster;
+import org.apache.hadoop.ozone.client.OzoneBucket;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.io.OzoneOutputStream;
+import org.apache.ozone.test.GenericTestUtils;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
@@ -222,6 +233,54 @@ public void testAllocateContainerWithStorageTierOnlyDisk() 
throws Exception {
     }
   }
 
+  /**
+   * A replica written to a real datanode volume must report that volume's
+   * storage type back to SCM over a real heartbeat, so that
+   * {@code ozone admin container info} can display it.
+   */
+  @Test
+  public void testReplicaReportsVolumeStorageType() throws Exception {
+    // All volumes are DISK so that the only available tier is DISK, which 
keeps
+    // pipeline creation deterministic under the pipeline limit of 1.
+    createCluster(Arrays.asList(
+        Arrays.asList(StorageType.DISK, StorageType.DISK),
+        Arrays.asList(StorageType.DISK, StorageType.DISK),
+        Arrays.asList(StorageType.DISK, StorageType.DISK)));
+    try (OzoneClient client = cluster.newClient();
+         ScmClient scmClient = new 
ContainerOperationClient(cluster.getConf())) {
+      OzoneBucket bucket = DataTestUtil.createVolumeAndBucket(client);
+      byte[] data = "storage type reporting".getBytes(UTF_8);
+      try (OzoneOutputStream out = bucket.createKey("key1", data.length,
+          ratisThree, new HashMap<>())) {
+        out.write(data);
+      }
+
+      long containerId = bucket.getKey("key1").getOzoneKeyLocations().get(0)
+          .getContainerID();
+
+      // Wait for the datanodes to report the container to SCM.
+      GenericTestUtils.waitFor((BooleanSupplier) () -> {
+        try {
+          List<ContainerReplicaInfo> reported =
+              scmClient.getContainerReplicas(containerId);
+          return reported.size() == ratisThree.getRequiredNodes()
+              && reported.stream().allMatch(r -> r.getVolumeStorageType() != 
null);
+        } catch (IOException e) {
+          return false;
+        }
+      }, 500, 30000);
+
+      for (ContainerReplicaInfo replica : 
scmClient.getContainerReplicas(containerId)) {
+        // On a freshly written container the stamped type and the volume's
+        // current type agree; a mismatch here would mean drift.
+        assertThat(replica.getStorageType()).isEqualTo(StorageType.DISK);
+        assertThat(replica.getVolumeStorageType()).isEqualTo(StorageType.DISK);
+      }
+    } finally {
+      cleanUp();
+    }
+  }
+
   private void assertContainer(ContainerID containerID, ContainerManager 
manager,
       ReplicationConfig replicationConfig, StorageTier expectedStorageTier, 
int expectedTotalCount)
       throws IOException {


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

Reply via email to