Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<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();
Expand Down Expand Up @@ -135,6 +150,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, "");

Expand Down Expand Up @@ -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,
Expand All @@ -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;
}
Expand All @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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<>();

Expand Down