From f6c1b57231f011847b8b78b8b63cd9658c2b604a Mon Sep 17 00:00:00 2001 From: XiChen <32928346+xichen01@users.noreply.github.com> Date: Wed, 8 Apr 2026 02:03:39 +0800 Subject: [PATCH 1/6] HDDS-14990. Dispaly the failed Disk detail info in the command and Metrics --- .../src/main/proto/hdds.proto | 1 + .../hadoop/hdds/scm/node/SCMNodeMetrics.java | 8 ++++ .../scm/server/SCMClientProtocolServer.java | 12 +++++ .../scm/cli/datanode/BasicDatanodeInfo.java | 14 ++++++ .../scm/cli/datanode/ListInfoSubcommand.java | 38 ++++++++++++++-- .../cli/datanode/TestListInfoSubcommand.java | 44 +++++++++++++++++++ 6 files changed, 114 insertions(+), 3 deletions(-) diff --git a/hadoop-hdds/interface-client/src/main/proto/hdds.proto b/hadoop-hdds/interface-client/src/main/proto/hdds.proto index 6cd75c7bf825..0085aa62466c 100644 --- a/hadoop-hdds/interface-client/src/main/proto/hdds.proto +++ b/hadoop-hdds/interface-client/src/main/proto/hdds.proto @@ -202,6 +202,7 @@ message Node { repeated NodeOperationalState nodeOperationalStates = 3; optional int32 totalVolumeCount = 4; optional int32 healthyVolumeCount = 5; + repeated string failedVolumes = 6; } message NodePool { diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/SCMNodeMetrics.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/SCMNodeMetrics.java index 323b7dbe1e0c..48576ee71b20 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/SCMNodeMetrics.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/node/SCMNodeMetrics.java @@ -166,6 +166,14 @@ public void getMetrics(MetricsCollector collector, boolean all) { Integer.parseInt(nonWritableNodes)); } + String volumeFailures = nodeStatistics.get("VolumeFailures"); + if (volumeFailures != null) { + metrics.addGauge( + Interns.info("VolumeFailures", + "Number of datanodes with at least one failed volume"), + Integer.parseInt(volumeFailures)); + } + for (Map.Entry e : nodeInfo.entrySet()) { metrics.addGauge( Interns.info(e.getKey(), diskMetricDescription(e.getKey())), 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 3bd5214ea0ec..2c487b49f983 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 @@ -61,6 +61,7 @@ import org.apache.hadoop.hdds.protocol.proto.HddsProtos.DeletedBlocksTransactionInfo; import org.apache.hadoop.hdds.protocol.proto.HddsProtos.DeletedBlocksTransactionSummary; import org.apache.hadoop.hdds.protocol.proto.ReconfigureProtocolProtos.ReconfigureProtocolService; +import org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.StorageReportProto; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ContainerBalancerStatusInfoResponseProto; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.DecommissionScmResponseProto; @@ -655,6 +656,7 @@ public List queryNode( if (datanodeInfo != null) { nodeBuilder.setTotalVolumeCount(datanodeInfo.getStorageReports().size()); nodeBuilder.setHealthyVolumeCount(datanodeInfo.getHealthyVolumeCount()); + addFailedVolumes(nodeBuilder, datanodeInfo); } result.add(nodeBuilder.build()); } @@ -687,6 +689,7 @@ public HddsProtos.Node queryNode(UUID uuid) if (datanodeInfo != null) { nodeBuilder.setTotalVolumeCount(datanodeInfo.getStorageReports().size()); nodeBuilder.setHealthyVolumeCount(datanodeInfo.getHealthyVolumeCount()); + addFailedVolumes(nodeBuilder, datanodeInfo); } result = nodeBuilder.build(); } @@ -702,6 +705,15 @@ public HddsProtos.Node queryNode(UUID uuid) return result; } + private static void addFailedVolumes(HddsProtos.Node.Builder nodeBuilder, + DatanodeInfo datanodeInfo) { + for (StorageReportProto report : datanodeInfo.getStorageReports()) { + if (report.hasFailed() && report.getFailed()) { + nodeBuilder.addFailedVolumes(report.getStorageLocation()); + } + } + } + @Override public List decommissionNodes(List nodes, boolean force) throws IOException { diff --git a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/BasicDatanodeInfo.java b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/BasicDatanodeInfo.java index ae0be5faab54..8e20d7276d4e 100644 --- a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/BasicDatanodeInfo.java +++ b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/BasicDatanodeInfo.java @@ -41,6 +41,8 @@ public final class BasicDatanodeInfo { private Integer totalVolumeCount = null; @JsonInclude(JsonInclude.Include.NON_NULL) private Integer healthyVolumeCount = null; + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List failedVolumes = null; private BasicDatanodeInfo(Builder builder) { this.dn = builder.dn; @@ -51,6 +53,7 @@ private BasicDatanodeInfo(Builder builder) { this.percentUsed = builder.percentUsed; this.totalVolumeCount = builder.totalVolumeCount; this.healthyVolumeCount = builder.healthyVolumeCount; + this.failedVolumes = builder.failedVolumes; } /** @@ -65,6 +68,7 @@ public static class Builder { private Double percentUsed; private Integer totalVolumeCount; private Integer healthyVolumeCount; + private List failedVolumes; public Builder(DatanodeDetails dn, HddsProtos.NodeOperationalState opState, HddsProtos.NodeState healthState) { @@ -86,6 +90,11 @@ public Builder withVolumeCounts(Integer total, Integer healthy) { return this; } + public Builder withFailedVolumes(List volumes) { + this.failedVolumes = volumes; + return this; + } + public BasicDatanodeInfo build() { return new BasicDatanodeInfo(this); } @@ -206,6 +215,11 @@ public Integer getHealthyVolumeCount() { return healthyVolumeCount; } + @JsonProperty(index = 112) + public List getFailedVolumes() { + return failedVolumes; + } + @JsonIgnore public DatanodeDetails getDatanodeDetails() { return dn; diff --git a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java index 0ed71c99e708..1501678dc933 100644 --- a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java +++ b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java @@ -19,6 +19,7 @@ import com.google.common.base.Strings; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -65,6 +66,11 @@ public class ListInfoSubcommand extends ScmSubcommand { defaultValue = "false") private boolean json; + @CommandLine.Option(names = {"--failed-volumes"}, + description = "Only show datanodes that have at least one failed volume.", + defaultValue = "false") + private boolean failedVolumesOnly; + @CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1") private ExclusiveNodeOptions exclusiveNodeOptions; @@ -92,7 +98,8 @@ public void execute(ScmClient scmClient) throws IOException { Integer healthyVolumeCount = node.hasHealthyVolumeCount() ? node.getHealthyVolumeCount() : null; BasicDatanodeInfo singleNodeInfo = new BasicDatanodeInfo.Builder( DatanodeDetails.getFromProtoBuf(node.getNodeID()), node.getNodeOperationalStates(0), - node.getNodeStates(0)).withVolumeCounts(totalVolumeCount, healthyVolumeCount).build(); + node.getNodeStates(0)).withVolumeCounts(totalVolumeCount, healthyVolumeCount) + .withFailedVolumes(getFailedVolumes(node)).build(); if (json) { List dtoList = Collections.singletonList(singleNodeInfo); System.out.println(JsonUtils.toJsonStringWithDefaultPrettyPrinter(dtoList)); @@ -118,6 +125,10 @@ public void execute(ScmClient scmClient) throws IOException { allNodes = allNodes.filter(p -> p.getHealthState().toString() .compareToIgnoreCase(nodeState) == 0); } + if (failedVolumesOnly) { + allNodes = allNodes.filter(p -> + p.getFailedVolumes() != null && !p.getFailedVolumes().isEmpty()); + } if (!listLimitOptions.isAll()) { allNodes = allNodes.limit(listLimitOptions.getLimit()); @@ -160,7 +171,8 @@ private List getAllNodes(ScmClient scmClient) DatanodeDetails.getFromProtoBuf(node.getNodeID()), node.getNodeOperationalStates(0), node.getNodeStates(0)) .withUsageInfo(used, capacity, percentUsed) - .withVolumeCounts(totalVolumeCount, healthyVolumeCount).build(); + .withVolumeCounts(totalVolumeCount, healthyVolumeCount) + .withFailedVolumes(getFailedVolumes(node)).build(); } catch (Exception e) { String reason = "Could not process info for an unknown datanode"; if (p != null && p.getNode() != null && !Strings.isNullOrEmpty(p.getNode().getUuid())) { @@ -182,11 +194,24 @@ private List getAllNodes(ScmClient scmClient) Integer healthyVolumeCount = p.hasHealthyVolumeCount() ? p.getHealthyVolumeCount() : null; return new BasicDatanodeInfo.Builder( DatanodeDetails.getFromProtoBuf(p.getNodeID()), p.getNodeOperationalStates(0), p.getNodeStates(0)) - .withVolumeCounts(totalVolumeCount, healthyVolumeCount).build(); }) + .withVolumeCounts(totalVolumeCount, healthyVolumeCount) + .withFailedVolumes(getFailedVolumes(p)).build(); }) .sorted(Comparator.comparing(BasicDatanodeInfo::getHealthState)) .collect(Collectors.toList()); } + private static List getFailedVolumes(HddsProtos.Node node) { + int count = node.getFailedVolumesCount(); + if (count == 0) { + return Collections.emptyList(); + } + List result = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + result.add(node.getFailedVolumes(i)); + } + return result; + } + private void printDatanodeInfo(BasicDatanodeInfo dn) { StringBuilder pipelineListInfo = new StringBuilder(); DatanodeDetails datanode = dn.getDatanodeDetails(); @@ -210,6 +235,7 @@ private void printDatanodeInfo(BasicDatanodeInfo dn) { } } else { pipelineListInfo.append("No pipelines in cluster."); + pipelineListInfo.append(System.getProperty("line.separator")); } System.out.println("Datanode: " + datanode.getUuid().toString() + " (" + datanode.getNetworkLocation() + "/" + datanode.getIpAddress() @@ -221,6 +247,12 @@ private void printDatanodeInfo(BasicDatanodeInfo dn) { System.out.println("Total volume count: " + dn.getTotalVolumeCount() + "\n" + "Healthy volume count: " + dn.getHealthyVolumeCount()); } + if (dn.getFailedVolumes() != null && !dn.getFailedVolumes().isEmpty()) { + System.out.println("Failed volumes:"); + for (String vol : dn.getFailedVolumes()) { + System.out.println(" " + vol); + } + } System.out.println("Related pipelines:\n" + pipelineListInfo); if (dn.getUsed() != null && dn.getCapacity() != null && dn.getUsed() >= 0 && dn.getCapacity() > 0) { diff --git a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java index 13ae6a35f10d..b5b054bb2205 100644 --- a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java +++ b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java @@ -357,6 +357,50 @@ public void testVolumeCounters() throws Exception { assertTrue(output.contains("Healthy volume count:"), "Should display healthy volume count"); } + @Test + public void testFailedVolumesFilter() throws Exception { + ScmClient scmClient = mock(ScmClient.class); + List baseNodes = getNodeDetails(); + + List nodes = new ArrayList<>(); + // node 0: 1 failed volume + nodes.add(HddsProtos.Node.newBuilder(baseNodes.get(0)) + .setTotalVolumeCount(4).setHealthyVolumeCount(3) + .addFailedVolumes("/data/disk2").build()); + // node 1: healthy, no failed volumes + nodes.add(HddsProtos.Node.newBuilder(baseNodes.get(1)) + .setTotalVolumeCount(4).setHealthyVolumeCount(4).build()); + // node 2: 2 failed volumes + nodes.add(HddsProtos.Node.newBuilder(baseNodes.get(2)) + .setTotalVolumeCount(6).setHealthyVolumeCount(4) + .addFailedVolumes("/data/disk1") + .addFailedVolumes("/data/disk5").build()); + // node 3: healthy, no failed volumes + nodes.add(HddsProtos.Node.newBuilder(baseNodes.get(3)) + .setTotalVolumeCount(4).setHealthyVolumeCount(4).build()); + + when(scmClient.queryNode(any(), any(), any(), any())).thenReturn(nodes); + when(scmClient.listPipelines()).thenReturn(new ArrayList<>()); + + CommandLine c = new CommandLine(cmd); + c.parseArgs("--failed-volumes"); + cmd.execute(scmClient); + String output = outContent.toString(DEFAULT_ENCODING); + + // Only 2 datanodes (those with failed volumes) should appear + Matcher m = Pattern.compile("^Datanode:", Pattern.MULTILINE) + .matcher(output); + int count = 0; + while (m.find()) { + count++; + } + assertEquals(2, count, "Only datanodes with failed volumes should be listed"); + assertTrue(output.contains("Failed volume")); + assertTrue(output.contains("/data/disk2")); + assertTrue(output.contains("/data/disk1")); + assertTrue(output.contains("/data/disk5")); + } + private void validateOrdering(JsonNode root, String orderDirection) { for (int i = 0; i < root.size() - 1; i++) { long usedCurrent = root.get(i).get("used").asLong(); From 4ab01e6307ac75d68ae13ba9c741aa99838f9113 Mon Sep 17 00:00:00 2001 From: XiChen <32928346+xichen01@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:10:19 +0800 Subject: [PATCH 2/6] Fxi PMD Failure --- .../hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java index 1501678dc933..928aa2a91c1b 100644 --- a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java +++ b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java @@ -234,8 +234,9 @@ private void printDatanodeInfo(BasicDatanodeInfo dn) { .append(System.getProperty("line.separator"))); } } else { - pipelineListInfo.append("No pipelines in cluster."); - pipelineListInfo.append(System.getProperty("line.separator")); + pipelineListInfo + .append("No pipelines in cluster.") + .append(System.getProperty("line.separator")); } System.out.println("Datanode: " + datanode.getUuid().toString() + " (" + datanode.getNetworkLocation() + "/" + datanode.getIpAddress() From c4d22d3390b837144bd4ea4485037960da0a947f Mon Sep 17 00:00:00 2001 From: XiChen <32928346+xichen01@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:11:19 +0800 Subject: [PATCH 3/6] Address some comments --- .../scm/cli/datanode/BasicDatanodeInfo.java | 52 +++++++++++-------- .../scm/cli/datanode/ListInfoSubcommand.java | 47 ++++------------- .../cli/datanode/TestListInfoSubcommand.java | 11 ++-- 3 files changed, 44 insertions(+), 66 deletions(-) diff --git a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/BasicDatanodeInfo.java b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/BasicDatanodeInfo.java index 8e20d7276d4e..a439494cdd4e 100644 --- a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/BasicDatanodeInfo.java +++ b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/BasicDatanodeInfo.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.apache.hadoop.hdds.protocol.DatanodeDetails; import org.apache.hadoop.hdds.protocol.proto.HddsProtos; @@ -60,21 +62,23 @@ private BasicDatanodeInfo(Builder builder) { * Builder class for creating instances of BasicDatanodeInfo. */ public static class Builder { - private DatanodeDetails dn; - private HddsProtos.NodeOperationalState opState; - private HddsProtos.NodeState healthState; + private final DatanodeDetails dn; + private final HddsProtos.NodeOperationalState opState; + private final HddsProtos.NodeState healthState; private Long used; private Long capacity; private Double percentUsed; - private Integer totalVolumeCount; - private Integer healthyVolumeCount; - private List failedVolumes; - - public Builder(DatanodeDetails dn, HddsProtos.NodeOperationalState opState, - HddsProtos.NodeState healthState) { - this.dn = dn; - this.opState = opState; - this.healthState = healthState; + private final Integer totalVolumeCount; + private final Integer healthyVolumeCount; + private final List failedVolumes; + + public Builder(HddsProtos.Node node) { + dn = DatanodeDetails.getFromProtoBuf(node.getNodeID()); + healthState = node.getNodeStates(0); + opState = node.getNodeOperationalStates(0); + totalVolumeCount = node.hasTotalVolumeCount() ? node.getTotalVolumeCount() : null; + healthyVolumeCount = node.hasHealthyVolumeCount() ? node.getHealthyVolumeCount() : null; + failedVolumes = getFailedVolumes(node); } public Builder withUsageInfo(long usedBytes, long capacityBytes, double percentUsedBytes) { @@ -84,17 +88,6 @@ public Builder withUsageInfo(long usedBytes, long capacityBytes, double percentU return this; } - public Builder withVolumeCounts(Integer total, Integer healthy) { - this.totalVolumeCount = total; - this.healthyVolumeCount = healthy; - return this; - } - - public Builder withFailedVolumes(List volumes) { - this.failedVolumes = volumes; - return this; - } - public BasicDatanodeInfo build() { return new BasicDatanodeInfo(this); } @@ -224,4 +217,17 @@ public List getFailedVolumes() { public DatanodeDetails getDatanodeDetails() { return dn; } + + private static List getFailedVolumes(HddsProtos.Node node) { + int count = node.getFailedVolumesCount(); + if (count == 0) { + return Collections.emptyList(); + } + List result = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + result.add(node.getFailedVolumes(i)); + } + return result; + } + } diff --git a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java index 928aa2a91c1b..e6d5f280e5c8 100644 --- a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java +++ b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java @@ -19,7 +19,6 @@ import com.google.common.base.Strings; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -66,10 +65,10 @@ public class ListInfoSubcommand extends ScmSubcommand { defaultValue = "false") private boolean json; - @CommandLine.Option(names = {"--failed-volumes"}, + @CommandLine.Option(names = {"--nodes-with-failed-volumes"}, description = "Only show datanodes that have at least one failed volume.", defaultValue = "false") - private boolean failedVolumesOnly; + private boolean nodeWithFailedVolumes; @CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1") private ExclusiveNodeOptions exclusiveNodeOptions; @@ -94,12 +93,7 @@ public void execute(ScmClient scmClient) throws IOException { pipelines = scmClient.listPipelines(); if (exclusiveNodeOptions != null && !Strings.isNullOrEmpty(exclusiveNodeOptions.getNodeId())) { HddsProtos.Node node = scmClient.queryNode(UUID.fromString(exclusiveNodeOptions.getNodeId())); - Integer totalVolumeCount = node.hasTotalVolumeCount() ? node.getTotalVolumeCount() : null; - Integer healthyVolumeCount = node.hasHealthyVolumeCount() ? node.getHealthyVolumeCount() : null; - BasicDatanodeInfo singleNodeInfo = new BasicDatanodeInfo.Builder( - DatanodeDetails.getFromProtoBuf(node.getNodeID()), node.getNodeOperationalStates(0), - node.getNodeStates(0)).withVolumeCounts(totalVolumeCount, healthyVolumeCount) - .withFailedVolumes(getFailedVolumes(node)).build(); + BasicDatanodeInfo singleNodeInfo = new BasicDatanodeInfo.Builder(node).build(); if (json) { List dtoList = Collections.singletonList(singleNodeInfo); System.out.println(JsonUtils.toJsonStringWithDefaultPrettyPrinter(dtoList)); @@ -125,7 +119,7 @@ public void execute(ScmClient scmClient) throws IOException { allNodes = allNodes.filter(p -> p.getHealthState().toString() .compareToIgnoreCase(nodeState) == 0); } - if (failedVolumesOnly) { + if (nodeWithFailedVolumes) { allNodes = allNodes.filter(p -> p.getFailedVolumes() != null && !p.getFailedVolumes().isEmpty()); } @@ -165,14 +159,9 @@ private List getAllNodes(ScmClient scmClient) long capacity = p.getCapacity(); long used = capacity - p.getRemaining(); double percentUsed = (capacity > 0) ? (used * 100.0) / capacity : 0.0; - Integer totalVolumeCount = node.hasTotalVolumeCount() ? node.getTotalVolumeCount() : null; - Integer healthyVolumeCount = node.hasHealthyVolumeCount() ? node.getHealthyVolumeCount() : null; - return new BasicDatanodeInfo.Builder( - DatanodeDetails.getFromProtoBuf(node.getNodeID()), - node.getNodeOperationalStates(0), node.getNodeStates(0)) + return new BasicDatanodeInfo.Builder(node) .withUsageInfo(used, capacity, percentUsed) - .withVolumeCounts(totalVolumeCount, healthyVolumeCount) - .withFailedVolumes(getFailedVolumes(node)).build(); + .build(); } catch (Exception e) { String reason = "Could not process info for an unknown datanode"; if (p != null && p.getNode() != null && !Strings.isNullOrEmpty(p.getNode().getUuid())) { @@ -189,29 +178,11 @@ private List getAllNodes(ScmClient scmClient) List nodes = scmClient.queryNode(null, null, HddsProtos.QueryScope.CLUSTER, ""); - return nodes.stream().map(p -> { - Integer totalVolumeCount = p.hasTotalVolumeCount() ? p.getTotalVolumeCount() : null; - Integer healthyVolumeCount = p.hasHealthyVolumeCount() ? p.getHealthyVolumeCount() : null; - return new BasicDatanodeInfo.Builder( - DatanodeDetails.getFromProtoBuf(p.getNodeID()), p.getNodeOperationalStates(0), p.getNodeStates(0)) - .withVolumeCounts(totalVolumeCount, healthyVolumeCount) - .withFailedVolumes(getFailedVolumes(p)).build(); }) + return nodes.stream().map(p -> new BasicDatanodeInfo.Builder(p).build()) .sorted(Comparator.comparing(BasicDatanodeInfo::getHealthState)) .collect(Collectors.toList()); } - private static List getFailedVolumes(HddsProtos.Node node) { - int count = node.getFailedVolumesCount(); - if (count == 0) { - return Collections.emptyList(); - } - List result = new ArrayList<>(count); - for (int i = 0; i < count; i++) { - result.add(node.getFailedVolumes(i)); - } - return result; - } - private void printDatanodeInfo(BasicDatanodeInfo dn) { StringBuilder pipelineListInfo = new StringBuilder(); DatanodeDetails datanode = dn.getDatanodeDetails(); @@ -231,12 +202,12 @@ private void printDatanodeInfo(BasicDatanodeInfo dn) { .append('/').append(p.getPipelineState().toString()).append('/') .append(datanode.getID().equals(p.getLeaderId()) ? "Leader" : "Follower") - .append(System.getProperty("line.separator"))); + .append("\n")); } } else { pipelineListInfo .append("No pipelines in cluster.") - .append(System.getProperty("line.separator")); + .append("\n"); } System.out.println("Datanode: " + datanode.getUuid().toString() + " (" + datanode.getNetworkLocation() + "/" + datanode.getIpAddress() diff --git a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java index b5b054bb2205..d46fb380b8fc 100644 --- a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java +++ b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java @@ -17,6 +17,7 @@ package org.apache.hadoop.hdds.scm.cli.datanode; +import static org.assertj.core.api.AssertionsForClassTypes.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; @@ -383,7 +384,7 @@ public void testFailedVolumesFilter() throws Exception { when(scmClient.listPipelines()).thenReturn(new ArrayList<>()); CommandLine c = new CommandLine(cmd); - c.parseArgs("--failed-volumes"); + c.parseArgs("--nodes-with-failed-volumes"); cmd.execute(scmClient); String output = outContent.toString(DEFAULT_ENCODING); @@ -395,10 +396,10 @@ public void testFailedVolumesFilter() throws Exception { count++; } assertEquals(2, count, "Only datanodes with failed volumes should be listed"); - assertTrue(output.contains("Failed volume")); - assertTrue(output.contains("/data/disk2")); - assertTrue(output.contains("/data/disk1")); - assertTrue(output.contains("/data/disk5")); + assertThat(output).contains("Failed volume"); + assertThat(output).contains("/data/disk2"); + assertThat(output).contains("/data/disk1"); + assertThat(output).contains("/data/disk5"); } private void validateOrdering(JsonNode root, String orderDirection) { From b48fb6355488fce5d80fb989532c8597ce13f39b Mon Sep 17 00:00:00 2001 From: XiChen <32928346+xichen01@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:12:52 +0800 Subject: [PATCH 4/6] fix PMD warn --- .../hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java index e6d5f280e5c8..c0824b033779 100644 --- a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java +++ b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java @@ -202,12 +202,12 @@ private void printDatanodeInfo(BasicDatanodeInfo dn) { .append('/').append(p.getPipelineState().toString()).append('/') .append(datanode.getID().equals(p.getLeaderId()) ? "Leader" : "Follower") - .append("\n")); + .append('\n')); } } else { pipelineListInfo .append("No pipelines in cluster.") - .append("\n"); + .append('\n'); } System.out.println("Datanode: " + datanode.getUuid().toString() + " (" + datanode.getNetworkLocation() + "/" + datanode.getIpAddress() From 008d8ee8a909b74f9f9c2d2d06f54aed236ddd7b Mon Sep 17 00:00:00 2001 From: XiChen <32928346+xichen01@users.noreply.github.com> Date: Sat, 11 Apr 2026 13:48:03 +0800 Subject: [PATCH 5/6] make --nodes-with-failed-volumes and --node-id mutually exclusive. --- .../hdds/scm/cli/datanode/ListInfoSubcommand.java | 6 ++++++ .../scm/cli/datanode/TestListInfoSubcommand.java | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java index c0824b033779..2ca9e906d96e 100644 --- a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java +++ b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java @@ -90,6 +90,12 @@ static class ExclusiveNodeOptions extends NodeSelectionMixin { @Override public void execute(ScmClient scmClient) throws IOException { + if (nodeWithFailedVolumes && exclusiveNodeOptions != null + && !Strings.isNullOrEmpty(exclusiveNodeOptions.getNodeId())) { + throw new IOException( + "--nodes-with-failed-volumes cannot be used with --id/--node-id. " + + "Use them separately."); + } pipelines = scmClient.listPipelines(); if (exclusiveNodeOptions != null && !Strings.isNullOrEmpty(exclusiveNodeOptions.getNodeId())) { HddsProtos.Node node = scmClient.queryNode(UUID.fromString(exclusiveNodeOptions.getNodeId())); diff --git a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java index d46fb380b8fc..dd0646034b71 100644 --- a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java +++ b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java @@ -402,6 +402,18 @@ public void testFailedVolumesFilter() throws Exception { assertThat(output).contains("/data/disk5"); } + @Test + public void testFailedVolumesFilterRejectsNodeId() throws Exception { + ScmClient scmClient = mock(ScmClient.class); + List nodes = getNodeDetails(); + when(scmClient.listPipelines()).thenReturn(new ArrayList<>()); + + CommandLine c = new CommandLine(cmd); + c.parseArgs("--nodes-with-failed-volumes", + "--id", nodes.get(0).getNodeID().getUuid()); + assertThrows(IOException.class, () -> cmd.execute(scmClient)); + } + private void validateOrdering(JsonNode root, String orderDirection) { for (int i = 0; i < root.size() - 1; i++) { long usedCurrent = root.get(i).get("used").asLong(); From 782048ab76e8ba2a12c876a1a3416f7e441bf732 Mon Sep 17 00:00:00 2001 From: XiChen <32928346+xichen01@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:37:10 +0800 Subject: [PATCH 6/6] Use CommandLine.ParameterException; Add test --- .../hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java | 5 ++++- .../hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java index 2ca9e906d96e..055e8d0970e2 100644 --- a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java +++ b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/ListInfoSubcommand.java @@ -70,6 +70,9 @@ public class ListInfoSubcommand extends ScmSubcommand { defaultValue = "false") private boolean nodeWithFailedVolumes; + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; + @CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1") private ExclusiveNodeOptions exclusiveNodeOptions; @@ -92,7 +95,7 @@ static class ExclusiveNodeOptions extends NodeSelectionMixin { public void execute(ScmClient scmClient) throws IOException { if (nodeWithFailedVolumes && exclusiveNodeOptions != null && !Strings.isNullOrEmpty(exclusiveNodeOptions.getNodeId())) { - throw new IOException( + throw new CommandLine.ParameterException(spec.commandLine(), "--nodes-with-failed-volumes cannot be used with --id/--node-id. " + "Use them separately."); } diff --git a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java index dd0646034b71..3527b33eb2e9 100644 --- a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java +++ b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestListInfoSubcommand.java @@ -411,7 +411,10 @@ public void testFailedVolumesFilterRejectsNodeId() throws Exception { CommandLine c = new CommandLine(cmd); c.parseArgs("--nodes-with-failed-volumes", "--id", nodes.get(0).getNodeID().getUuid()); - assertThrows(IOException.class, () -> cmd.execute(scmClient)); + CommandLine.ParameterException ex = assertThrows( + CommandLine.ParameterException.class, () -> cmd.execute(scmClient)); + assertTrue(ex.getMessage().contains( + "--nodes-with-failed-volumes cannot be used with --id/--node-id")); } private void validateOrdering(JsonNode root, String orderDirection) {