This is an automated email from the ASF dual-hosted git repository.
tejaskriya 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 e0b804ef996 HDDS-13095. Support sorting by most/least used nodes in
ozone admin datanode list (#8520)
e0b804ef996 is described below
commit e0b804ef996f7ab3af726c0af9e4d385f4195848
Author: sreejasahithi <[email protected]>
AuthorDate: Fri Jul 11 10:16:35 2025 +0530
HDDS-13095. Support sorting by most/least used nodes in ozone admin
datanode list (#8520)
---
.../hdds/scm/cli/datanode/ListInfoSubcommand.java | 85 ++++++++++++++++
.../scm/cli/datanode/TestListInfoSubcommand.java | 108 +++++++++++++++++++++
2 files changed, 193 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 2b7813f4616..56c50456210 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
@@ -17,10 +17,12 @@
package org.apache.hadoop.hdds.scm.cli.datanode;
+import com.fasterxml.jackson.annotation.JsonInclude;
import com.google.common.base.Strings;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -63,6 +65,9 @@ public class ListInfoSubcommand extends ScmSubcommand {
defaultValue = "false")
private boolean json;
+ @CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private UsageSortingOptions usageSortingOptions;
+
@CommandLine.Mixin
private ListLimitOptions listLimitOptions;
@@ -71,6 +76,16 @@ public class ListInfoSubcommand extends ScmSubcommand {
private List<Pipeline> pipelines;
+ static class UsageSortingOptions {
+ @CommandLine.Option(names = {"--most-used"},
+ description = "Show datanodes sorted by Utilization (most to least).")
+ private boolean mostUsed;
+
+ @CommandLine.Option(names = {"--least-used"},
+ description = "Show datanodes sorted by Utilization (least to most).")
+ private boolean leastUsed;
+ }
+
@Override
public void execute(ScmClient scmClient) throws IOException {
pipelines = scmClient.listPipelines();
@@ -123,6 +138,38 @@ public void execute(ScmClient scmClient) throws
IOException {
private List<DatanodeWithAttributes> getAllNodes(ScmClient scmClient)
throws IOException {
+
+ // If sorting is requested
+ if (usageSortingOptions != null && (usageSortingOptions.mostUsed ||
usageSortingOptions.leastUsed)) {
+ boolean sortByMostUsed = usageSortingOptions.mostUsed;
+ List<HddsProtos.DatanodeUsageInfoProto> usageInfos =
scmClient.getDatanodeUsageInfo(sortByMostUsed,
+ Integer.MAX_VALUE);
+
+ return usageInfos.stream()
+ .map(p -> {
+ String uuidStr = p.getNode().getUuid();
+ UUID parsedUuid = UUID.fromString(uuidStr);
+
+ try {
+ HddsProtos.Node node = scmClient.queryNode(parsedUuid);
+ long capacity = p.getCapacity();
+ long used = capacity - p.getRemaining();
+ double percentUsed = (capacity > 0) ? (used * 100.0) / capacity
: 0.0;
+ return new DatanodeWithAttributes(
+ DatanodeDetails.getFromProtoBuf(node.getNodeID()),
+ node.getNodeOperationalStates(0),
+ node.getNodeStates(0),
+ used,
+ capacity,
+ percentUsed);
+ } catch (IOException e) {
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+
List<HddsProtos.Node> nodes = scmClient.queryNode(null,
null, HddsProtos.QueryScope.CLUSTER, "");
@@ -165,12 +212,24 @@ private void printDatanodeInfo(DatanodeWithAttributes
dna) {
System.out.println("Operational State: " + dna.getOpState());
System.out.println("Health State: " + dna.getHealthState());
System.out.println("Related pipelines:\n" + pipelineListInfo);
+
+ if (dna.getUsed() != null && dna.getCapacity() != null && dna.getUsed() >=
0 && dna.getCapacity() > 0) {
+ System.out.println("Capacity: " + dna.getCapacity());
+ System.out.println("Used: " + dna.getUsed());
+ System.out.printf("Percentage Used : %.2f%%%n%n", dna.getPercentUsed());
+ }
}
private static class DatanodeWithAttributes {
private DatanodeDetails datanodeDetails;
private HddsProtos.NodeOperationalState operationalState;
private HddsProtos.NodeState healthState;
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private Long used = null;
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private Long capacity = null;
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private Double percentUsed = null;
DatanodeWithAttributes(DatanodeDetails dn,
HddsProtos.NodeOperationalState opState,
@@ -180,6 +239,20 @@ private static class DatanodeWithAttributes {
this.healthState = healthState;
}
+ DatanodeWithAttributes(DatanodeDetails dn,
+ HddsProtos.NodeOperationalState opState,
+ HddsProtos.NodeState healthState,
+ long used,
+ long capacity,
+ double percentUsed) {
+ this.datanodeDetails = dn;
+ this.operationalState = opState;
+ this.healthState = healthState;
+ this.used = used;
+ this.capacity = capacity;
+ this.percentUsed = percentUsed;
+ }
+
public DatanodeDetails getDatanodeDetails() {
return datanodeDetails;
}
@@ -191,5 +264,17 @@ public HddsProtos.NodeOperationalState getOpState() {
public HddsProtos.NodeState getHealthState() {
return healthState;
}
+
+ public Long getUsed() {
+ return used;
+ }
+
+ public Long getCapacity() {
+ return capacity;
+ }
+
+ public Double getPercentUsed() {
+ return percentUsed;
+ }
}
}
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 0f53d85463d..f24fe6e1b2f 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
@@ -33,6 +33,7 @@
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
@@ -42,6 +43,8 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import picocli.CommandLine;
/**
@@ -212,6 +215,111 @@ public void testDataNodeByUuidOutput()
"Expected UUID " + nodes.get(0).getNodeID().getUuid() + " but got: " +
uuid);
}
+ @ParameterizedTest
+ @CsvSource({
+ "true, --most-used, descending",
+ "false, --least-used, ascending"
+ })
+ public void testUsedOrderingAndOutput(
+ boolean mostUsed,
+ String cliFlag,
+ String orderDirection) throws Exception {
+
+ ScmClient scmClient = mock(ScmClient.class);
+ List<HddsProtos.DatanodeUsageInfoProto> usageList = new ArrayList<>();
+ List<HddsProtos.Node> nodeList = getNodeDetails();
+
+ for (int i = 0; i < 4; i++) {
+ long remaining = 1000 - (100 * i);
+ usageList.add(HddsProtos.DatanodeUsageInfoProto.newBuilder()
+ .setNode(nodeList.get(i).getNodeID())
+ .setRemaining(remaining)
+ .setCapacity(1000)
+ .build());
+
+
when(scmClient.queryNode(UUID.fromString(nodeList.get(i).getNodeID().getUuid())))
+ .thenReturn(nodeList.get(i));
+ }
+
+ if (mostUsed) {
+ Collections.reverse(usageList); // For most-used only
+ }
+
+ when(scmClient.getDatanodeUsageInfo(mostUsed,
Integer.MAX_VALUE)).thenReturn(usageList);
+ when(scmClient.listPipelines()).thenReturn(new ArrayList<>());
+
+ // ----- JSON output test -----
+ CommandLine c = new CommandLine(cmd);
+ c.parseArgs(cliFlag, "--json");
+ cmd.execute(scmClient);
+
+ String jsonOutput = outContent.toString(DEFAULT_ENCODING);
+ JsonNode root = new ObjectMapper().readTree(jsonOutput);
+
+ assertTrue(root.isArray(), "JSON output should be an array");
+ assertEquals(4, root.size(), "Expected 4 nodes in JSON output");
+
+ for (JsonNode node : root) {
+ assertTrue(node.has("used"), "Missing 'used'");
+ assertTrue(node.has("capacity"), "Missing 'capacity'");
+ assertTrue(node.has("percentUsed"), "Missing 'percentUsed'");
+ }
+
+ validateOrdering(root, orderDirection);
+
+ outContent.reset();
+
+ // ----- Text output test -----
+ c = new CommandLine(cmd);
+ c.parseArgs(cliFlag);
+ cmd.execute(scmClient);
+
+ String textOutput = outContent.toString(DEFAULT_ENCODING);
+ validateOrderingFromTextOutput(textOutput, orderDirection);
+ }
+
+ private void validateOrdering(JsonNode root, String orderDirection) {
+ for (int i = 0; i < root.size() - 1; i++) {
+ long usedCurrent = root.get(i).get("used").asLong();
+ long capacityCurrent = root.get(i).get("capacity").asLong();
+ long usedNext = root.get(i + 1).get("used").asLong();
+ long capacityNext = root.get(i + 1).get("capacity").asLong();
+ double ratio1 = (capacityCurrent == 0) ? 0.0 : (double) usedCurrent /
capacityCurrent;
+ double ratio2 = (capacityNext == 0) ? 0.0 : (double) usedNext /
capacityNext;
+
+ if ("ascending".equals(orderDirection)) {
+ assertTrue(ratio1 <= ratio2, "Expected ascending order, got: " +
ratio1 + " > " + ratio2);
+ } else {
+ assertTrue(ratio1 >= ratio2, "Expected descending order, got: " +
ratio1 + " < " + ratio2);
+ }
+ }
+ }
+
+ private void validateOrderingFromTextOutput(String output, String
orderDirection) {
+ Pattern usedPattern = Pattern.compile("Used: (\\d+)");
+ Pattern capacityPattern = Pattern.compile("Capacity: (\\d+)");
+ Matcher usedMatcher = usedPattern.matcher(output);
+ Matcher capacityMatcher = capacityPattern.matcher(output);
+
+ List<Double> usageRatios = new ArrayList<>();
+ while (usedMatcher.find() && capacityMatcher.find()) {
+ long used = Long.parseLong(usedMatcher.group(1));
+ long capacity = Long.parseLong(capacityMatcher.group(1));
+ double usage = (capacity == 0) ? 0.0 : (double) used / capacity;
+ usageRatios.add(usage);
+ }
+
+ for (int i = 0; i < usageRatios.size() - 1; i++) {
+ double ratio1 = usageRatios.get(i);
+ double ratio2 = usageRatios.get(i + 1);
+ if ("ascending".equals(orderDirection)) {
+ assertTrue(ratio1 <= ratio2, "Expected ascending order, got: " +
ratio1 + " > " + ratio2);
+ } else {
+ assertTrue(ratio1 >= ratio2, "Expected descending order, got: " +
ratio1 + " < " + ratio2);
+ }
+ }
+ }
+
private List<HddsProtos.Node> getNodeDetails() {
List<HddsProtos.Node> nodes = new ArrayList<>();
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]