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 ad0f0a7ef9cb..d73163ee4c59 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; @@ -78,11 +80,24 @@ 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; private List 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(); @@ -135,6 +150,38 @@ public void execute(ScmClient scmClient) throws IOException { private List getAllNodes(ScmClient scmClient) throws IOException { + + // If sorting is requested + if (usageSortingOptions != null && (usageSortingOptions.mostUsed || usageSortingOptions.leastUsed)) { + boolean sortByMostUsed = usageSortingOptions.mostUsed; + List 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 nodes = scmClient.queryNode(null, null, HddsProtos.QueryScope.CLUSTER, ""); @@ -177,12 +224,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, @@ -192,6 +251,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; } @@ -203,5 +276,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 0f53d85463d9..f24fe6e1b2f0 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 usageList = new ArrayList<>(); + List 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 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 getNodeDetails() { List nodes = new ArrayList<>();