This is an automated email from the ASF dual-hosted git repository.
sarvekshayr 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 ba33ee9ac70 HDDS-16174. Add container balancer assessment command to
report cluster imbalance (#11256)
ba33ee9ac70 is described below
commit ba33ee9ac702a7cfe3210455a71a33ed0b842922
Author: sravani <[email protected]>
AuthorDate: Mon Sep 28 16:25:28 2026 +0530
HDDS-16174. Add container balancer assessment command to report cluster
imbalance (#11256)
---
.../balancer/ContainerBalancerClusterAnalyzer.java | 29 +-
.../cli/ContainerBalancerAssessmentSubcommand.java | 316 +++++++++++++++++++++
.../hdds/scm/cli/ContainerBalancerCommands.java | 6 +
.../TestContainerBalancerAssessmentSubcommand.java | 176 ++++++++++++
.../TestContainerBalancerSubCommand.java | 6 +-
5 files changed, 524 insertions(+), 9 deletions(-)
diff --git
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerClusterAnalyzer.java
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerClusterAnalyzer.java
index 56a5675b18d..683a2eb44bf 100644
---
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerClusterAnalyzer.java
+++
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerClusterAnalyzer.java
@@ -120,6 +120,27 @@ public static ContainerBalancerClusterSnapshot analyze(
double thresholdRatio,
Set<String> includeNodes,
Set<String> excludeNodes) {
+ return analyze(nodes, thresholdRatio, includeNodes, excludeNodes,
TOP_NODE_LIMIT);
+ }
+
+ /**
+ * Builds a cluster snapshot after applying include/exclude filters.
+ *
+ * @param nodes all nodes from getDatanodeUsageInfo (typically healthy
IN_SERVICE)
+ * @param thresholdRatio threshold as ratio, e.g. 0.10 for 10%
+ * @param includeNodes empty = all included; non-empty = allow-list
+ * @param excludeNodes nodes to skip
+ * @param topNodeLimit max source/target nodes to include in the snapshot
lists
+ */
+ public static ContainerBalancerClusterSnapshot analyze(
+ List<DatanodeUsageInfoProto> nodes,
+ double thresholdRatio,
+ Set<String> includeNodes,
+ Set<String> excludeNodes,
+ int topNodeLimit) {
+ if (topNodeLimit < 1) {
+ throw new IllegalArgumentException("topNodeLimit must be at least 1.");
+ }
List<DatanodeUsageInfoProto> eligible = filterEligibleNodes(nodes,
includeNodes, excludeNodes);
if (eligible.isEmpty()) {
@@ -174,8 +195,8 @@ public static ContainerBalancerClusterSnapshot analyze(
.setTotalOverUtilizedBytes(totalOverUtilizedBytes)
.setTotalUnderUtilizedBytes(totalUnderUtilizedBytes)
.setImbalance(imbalance)
- .setTopSourceNodes(topNodes(sources))
- .setBottomTargetNodes(topNodes(targets))
+ .setTopSourceNodes(topNodes(sources, topNodeLimit))
+ .setBottomTargetNodes(topNodes(targets, topNodeLimit))
.build();
}
@@ -242,8 +263,8 @@ private static String
getDisplayHostname(DatanodeUsageInfoProto node) {
return datanode.getIpAddress();
}
- private static List<NodeUtilization> topNodes(List<NodeUtilization> nodes) {
- int limit = Math.min(TOP_NODE_LIMIT, nodes.size());
+ private static List<NodeUtilization> topNodes(List<NodeUtilization> nodes,
int topNodeLimit) {
+ int limit = Math.min(topNodeLimit, nodes.size());
List<NodeUtilization> result = new ArrayList<>(limit);
for (int i = 0; i < limit; i++) {
result.add(nodes.get(i));
diff --git
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerAssessmentSubcommand.java
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerAssessmentSubcommand.java
new file mode 100644
index 00000000000..2b529ee603b
--- /dev/null
+++
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerAssessmentSubcommand.java
@@ -0,0 +1,316 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.hdds.scm.cli;
+
+import static org.apache.hadoop.util.StringUtils.byteDesc;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.hadoop.hdds.cli.HddsVersionProvider;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos.DatanodeUsageInfoProto;
+import org.apache.hadoop.hdds.scm.client.ScmClient;
+import
org.apache.hadoop.hdds.scm.container.balancer.ContainerBalancerClusterAnalyzer;
+import
org.apache.hadoop.hdds.scm.container.balancer.ContainerBalancerClusterSnapshot;
+import
org.apache.hadoop.hdds.scm.container.balancer.ContainerBalancerConfiguration;
+import org.apache.hadoop.hdds.server.JsonUtils;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+
+/** Reports cluster imbalance without starting the container balancer. */
+@Command(
+ name = "assessment",
+ description = "Report cluster imbalance before starting ContainerBalancer",
+ mixinStandardHelpOptions = true,
+ versionProvider = HddsVersionProvider.class)
+public class ContainerBalancerAssessmentSubcommand extends ScmSubcommand {
+
+ @Option(names = {"-t", "--threshold"},
+ description = "Percentage deviation from average utilization of " +
+ "the cluster after which a datanode is considered over- or
under-utilized. " +
+ "The value should be in the range [0.0, 100.0), with a default of 10
" +
+ "(specify '10' for 10%%).")
+ private Optional<Double> threshold;
+
+ @Option(names = {"-n", "--limit"},
+ description = "Maximum number of over- and under-utilized datanodes to
list in the report. Default: 5.")
+ private int nodeLimit = 5;
+
+ @Option(names = {"--include-datanodes"},
+ description = "A list of Datanode hostnames or ip addresses separated by
commas. " +
+ "Only the Datanodes specified in this list are included in the
assessment.")
+ private Optional<String> includeNodes;
+
+ @Option(names = {"--exclude-datanodes"},
+ description = "A list of Datanode hostnames or ip addresses separated by
commas. " +
+ "The Datanodes specified in this list are excluded from the
assessment.")
+ private Optional<String> excludeNodes;
+
+ @Option(names = {"--json"},
+ defaultValue = "false",
+ description = "Format output as JSON")
+ private boolean json;
+
+ @Override
+ public void execute(ScmClient scmClient) throws IOException {
+ ContainerBalancerConfiguration config =
+ getOzoneConf().getObject(ContainerBalancerConfiguration.class);
+
+ if (nodeLimit < 1) {
+ throw new IllegalArgumentException("limit must be at least 1.");
+ }
+
+ double thresholdRatio = threshold
+ .map(t -> {
+ if (t < 0d || t >= 100d) {
+ throw new IllegalArgumentException(
+ "Threshold must be a percentage in the range [0.0, 100.0).");
+ }
+ return t / 100.0;
+ })
+ .orElseGet(config::getThresholdAsRatio);
+
+ Set<String> include = includeNodes
+ .map(ContainerBalancerAssessmentSubcommand::parseNodeList)
+ .orElseGet(config::getIncludeNodes);
+ Set<String> exclude = excludeNodes
+ .map(ContainerBalancerAssessmentSubcommand::parseNodeList)
+ .orElseGet(config::getExcludeNodes);
+
+ List<DatanodeUsageInfoProto> nodes =
+ scmClient.getDatanodeUsageInfo(true, Integer.MAX_VALUE);
+
+ ContainerBalancerClusterSnapshot snapshot =
+ ContainerBalancerClusterAnalyzer.analyze(nodes, thresholdRatio,
include, exclude, nodeLimit);
+
+ if (json) {
+ printJsonReport(snapshot, thresholdRatio);
+ } else {
+ printReport(snapshot, thresholdRatio, nodeLimit);
+ }
+ }
+
+ private static void printReport(ContainerBalancerClusterSnapshot snapshot,
double thresholdRatio, int nodeLimit) {
+ System.out.println("CLUSTER BALANCE ASSESSMENT");
+
+ if (snapshot.getTotalEligibleDatanodes() == 0) {
+ System.out.println(getSummaryPrettyString(snapshot, thresholdRatio));
+ System.out.println();
+ System.out.println("No eligible datanodes found for assessment.");
+ return;
+ }
+
+ System.out.println(getSummaryPrettyString(snapshot, thresholdRatio));
+ System.out.println();
+ System.out.println(getSourceNodesPrettyString(snapshot, nodeLimit));
+ System.out.println();
+ System.out.println(getTargetNodesPrettyString(snapshot, nodeLimit));
+ System.out.println();
+ System.out.println(getMovementSummaryPrettyString(snapshot));
+ }
+
+ private static String
getSummaryPrettyString(ContainerBalancerClusterSnapshot snapshot,
+ double thresholdRatio) {
+ if (snapshot.getTotalEligibleDatanodes() == 0) {
+ return String.format("%-50s %s%n" +
+ "%-50s %s%n" +
+ "%-50s %s%n" +
+ "%-50s %s%n" +
+ "%-50s %s%n", "Key", "Value",
+ "Threshold", formatPercent(thresholdRatio),
+ "Upper utilization limit", formatPercent(snapshot.getUpperLimit()),
+ "Lower utilization limit", formatPercent(snapshot.getLowerLimit()),
+ "Eligible datanodes", "0 datanodes");
+ }
+ return String.format("%-50s %s%n" +
+ "%-50s %s%n" +
+ "%-50s %s%n" +
+ "%-50s %s%n" +
+ "%-50s %s%n" +
+ "%-50s %s%n" +
+ "%-50s %s%n" +
+ "%-50s %s%n", "Key", "Value",
+ "Threshold", formatPercent(thresholdRatio),
+ "Upper utilization limit", formatPercent(snapshot.getUpperLimit()),
+ "Lower utilization limit", formatPercent(snapshot.getLowerLimit()),
+ "Drift", String.format(Locale.US, "%s (max utilization %s - min
utilization %s)",
+ formatPercent(snapshot.getImbalance()),
+ formatPercent(snapshot.getMaxUtilization()),
+ formatPercent(snapshot.getMinUtilization())),
+ "Mean Utilization", formatPercent(snapshot.getClusterAvgUtilization()),
+ "Eligible datanodes", snapshot.getTotalEligibleDatanodes() + "
datanode"
+ + (snapshot.getTotalEligibleDatanodes() == 1 ? "" : "s"),
+ "Category", formatCategory(snapshot));
+ }
+
+ private static String
getSourceNodesPrettyString(ContainerBalancerClusterSnapshot snapshot, int
nodeLimit) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Source Nodes
(over-utilized):").append(System.lineSeparator())
+ .append(String.format("%-50s %s%n", "Datanodes above threshold",
snapshot.getSourceCount()));
+ appendNodeList(builder, snapshot.getTopSourceNodes(),
snapshot.getClusterAvgUtilization(), true, nodeLimit);
+ return builder.toString();
+ }
+
+ private static String
getTargetNodesPrettyString(ContainerBalancerClusterSnapshot snapshot, int
nodeLimit) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Target Nodes
(under-utilized):").append(System.lineSeparator())
+ .append(String.format("%-50s %s%n", "Datanodes below threshold",
snapshot.getTargetCount()));
+ appendNodeList(builder, snapshot.getBottomTargetNodes(),
snapshot.getClusterAvgUtilization(), false, nodeLimit);
+ return builder.toString();
+ }
+
+ private static String
getMovementSummaryPrettyString(ContainerBalancerClusterSnapshot snapshot) {
+ double movementRatio = snapshot.getClusterCapacityBytes() == 0 ? 0
+ : (double) snapshot.getBytesToMove() /
snapshot.getClusterCapacityBytes();
+ return String.format("Movement Summary:%n" +
+ "%-50s %s%n" +
+ "%-50s %s%n", "Total bytes to move",
byteDesc(snapshot.getBytesToMove()),
+ "Movement ratio",
+ formatPercent(movementRatio) + " of cluster capacity");
+ }
+
+ private static void appendNodeList(StringBuilder builder,
+
List<ContainerBalancerClusterSnapshot.NodeUtilization> nodes,
+ double clusterAvgUtilization, boolean
aboveMean, int nodeLimit) {
+ if (nodes.isEmpty()) {
+ builder.append(System.lineSeparator())
+ .append("(none)")
+ .append(System.lineSeparator());
+ return;
+ }
+ int headerCount = Math.min(nodeLimit, nodes.size());
+ builder.append(System.lineSeparator())
+ .append(aboveMean ? "Top " + headerCount + ":" : "Bottom " +
headerCount + ":")
+ .append(System.lineSeparator());
+ for (ContainerBalancerClusterSnapshot.NodeUtilization node : nodes) {
+ double deltaFromMean = (node.getUtilization() - clusterAvgUtilization) *
100;
+ builder.append(String.format(Locale.US, "%-50s %s%n", node.getHostname(),
+ String.format(Locale.US, "%s (%+.1f%% %s mean)",
+ formatPercent(node.getUtilization()), deltaFromMean, aboveMean ?
"above" : "below")));
+ }
+ }
+
+ private static String formatPercent(double ratio) {
+ return String.format(Locale.US, "%.1f%%", ratio * 100);
+ }
+
+ private static double toJsonPercent(double ratio) {
+ return Double.parseDouble(String.format(Locale.US, "%.2f", ratio * 100));
+ }
+
+ private static String formatCategory(ContainerBalancerClusterSnapshot
snapshot) {
+ return formatClusterSize(snapshot.getTotalEligibleDatanodes()) + ", "
+ + formatImbalance(snapshot.getImbalance()) + ", "
+ + formatMovementRatio(snapshot);
+ }
+
+ private static String formatClusterSize(int eligibleDatanodes) {
+ if (eligibleDatanodes >= 100) {
+ return "Large cluster";
+ }
+ if (eligibleDatanodes >= 20) {
+ return "Medium cluster";
+ }
+ return "Small cluster";
+ }
+
+ private static String formatImbalance(double imbalance) {
+ if (imbalance >= 0.20) {
+ return "High imbalance";
+ }
+ if (imbalance >= 0.10) {
+ return "Medium imbalance";
+ }
+ return "Low imbalance";
+ }
+
+ private static String formatMovementRatio(ContainerBalancerClusterSnapshot
snapshot) {
+ if (snapshot.getClusterCapacityBytes() == 0) {
+ return "Low movement ratio";
+ }
+ double ratio = (double) snapshot.getBytesToMove() /
snapshot.getClusterCapacityBytes();
+ if (ratio >= 0.05) {
+ return "High movement ratio";
+ }
+ if (ratio >= 0.01) {
+ return "Medium movement ratio";
+ }
+ return "Low movement ratio";
+ }
+
+ private static void printJsonReport(ContainerBalancerClusterSnapshot
snapshot, double thresholdRatio)
+ throws IOException {
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("totalEligibleDatanodes", snapshot.getTotalEligibleDatanodes());
+ result.put("thresholdPercentage", toJsonPercent(thresholdRatio));
+ result.put("clusterAvgUtilizationPercentage",
toJsonPercent(snapshot.getClusterAvgUtilization()));
+ result.put("driftPercentage", toJsonPercent(snapshot.getImbalance()));
+ result.put("maxUtilizationPercentage",
toJsonPercent(snapshot.getMaxUtilization()));
+ result.put("minUtilizationPercentage",
toJsonPercent(snapshot.getMinUtilization()));
+ result.put("upperLimitPercentage",
toJsonPercent(snapshot.getUpperLimit()));
+ result.put("lowerLimitPercentage",
toJsonPercent(snapshot.getLowerLimit()));
+
+ Map<String, Object> sourceNodes = new LinkedHashMap<>();
+ sourceNodes.put("count", snapshot.getSourceCount());
+ sourceNodes.put("nodes", snapshot.getTopSourceNodes().stream()
+ .map(node -> {
+ Map<String, Object> nodeMap = new LinkedHashMap<>();
+ nodeMap.put("hostname", node.getHostname());
+ nodeMap.put("utilizationPercentage",
toJsonPercent(node.getUtilization()));
+ return nodeMap;
+ })
+ .collect(Collectors.toList()));
+ result.put("sourceNodes", sourceNodes);
+
+ Map<String, Object> targetNodes = new LinkedHashMap<>();
+ targetNodes.put("count", snapshot.getTargetCount());
+ targetNodes.put("nodes", snapshot.getBottomTargetNodes().stream()
+ .map(node -> {
+ Map<String, Object> nodeMap = new LinkedHashMap<>();
+ nodeMap.put("hostname", node.getHostname());
+ nodeMap.put("utilizationPercentage",
toJsonPercent(node.getUtilization()));
+ return nodeMap;
+ })
+ .collect(Collectors.toList()));
+ result.put("targetNodes", targetNodes);
+
+ double movementRatio = snapshot.getClusterCapacityBytes() == 0 ? 0
+ : (double) snapshot.getBytesToMove() /
snapshot.getClusterCapacityBytes();
+ result.put("bytesToMove", snapshot.getBytesToMove());
+ result.put("movementRatioPercentage", toJsonPercent(movementRatio));
+
+ System.out.println(JsonUtils.toJsonStringWithDefaultPrettyPrinter(result));
+ }
+
+ private static Set<String> parseNodeList(String commaSeparated) {
+ if (commaSeparated == null || commaSeparated.isEmpty()) {
+ return Collections.emptySet();
+ }
+ return Arrays.stream(commaSeparated.split(","))
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .collect(Collectors.toSet());
+ }
+}
diff --git
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerCommands.java
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerCommands.java
index 1c2cd364a51..c65cb1f4ca1 100644
---
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerCommands.java
+++
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerCommands.java
@@ -80,6 +80,11 @@
* estimate FAST profile with a 5% threshold
* To stop:
* ozone admin containerbalancer stop
+ * To assess:
+ * ozone admin containerbalancer assessment
+ * [ -t/--threshold {@literal <threshold>}]
+ * [ --include-datanodes {@literal <hostnames>}]
+ * [ --exclude-datanodes {@literal <hostnames>}]
* </pre>
*
* <p>DESCRIPTION
@@ -112,6 +117,7 @@
ContainerBalancerStartSubcommand.class,
ContainerBalancerStopSubcommand.class,
ContainerBalancerStatusSubcommand.class,
+ ContainerBalancerAssessmentSubcommand.class,
ContainerBalancerEstimateSubcommand.class
})
@MetaInfServices(AdminSubcommand.class)
diff --git
a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/TestContainerBalancerAssessmentSubcommand.java
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/TestContainerBalancerAssessmentSubcommand.java
new file mode 100644
index 00000000000..34cb66f0dde
--- /dev/null
+++
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/TestContainerBalancerAssessmentSubcommand.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.hdds.scm.cli;
+
+import static org.apache.hadoop.ozone.ClientVersion.DEFAULT_VERSION;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos.DatanodeUsageInfoProto;
+import org.apache.hadoop.hdds.scm.client.ScmClient;
+import org.apache.hadoop.hdds.utils.IOUtils;
+import org.apache.ozone.test.GenericTestUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+/** Tests for {@link ContainerBalancerAssessmentSubcommand}. */
+class TestContainerBalancerAssessmentSubcommand {
+
+ private ContainerBalancerAssessmentSubcommand cmd;
+ private GenericTestUtils.PrintStreamCapturer out;
+
+ @BeforeEach
+ void setup() {
+ cmd = new ContainerBalancerAssessmentSubcommand();
+ out = GenericTestUtils.captureOut();
+ }
+
+ @AfterEach
+ void tearDown() {
+ IOUtils.closeQuietly(out);
+ }
+
+ @Test
+ void testDefaultAssessmentOutput() {
+ ScmClient scmClient = mock(ScmClient.class);
+ mockUsageInfo(scmClient, sampleNodes());
+
+ runAssessment(scmClient);
+
+ String output = out.get();
+ assertThat(output).contains("CLUSTER BALANCE ASSESSMENT");
+ assertThat(output).contains("Drift");
+ assertThat(output).contains("Source Nodes (over-utilized):");
+ assertThat(output).contains("Target Nodes (under-utilized):");
+ assertThat(output).contains("Movement Summary:");
+ assertThat(output).contains("source-1");
+ assertThat(output).contains("target-1");
+ assertThat(output).contains("10 B");
+ }
+
+ @Test
+ void testThresholdOverride() {
+ ScmClient scmClient = mock(ScmClient.class);
+ mockUsageInfo(scmClient, sampleNodes());
+
+ runAssessment(scmClient, "-t", "5");
+
+ String output = out.get();
+ assertThat(output).contains("15 B");
+ assertThat(output).doesNotContain("10 B");
+ }
+
+ @Test
+ void testIncludeDatanodes() {
+ ScmClient scmClient = mock(ScmClient.class);
+ mockUsageInfo(scmClient, Arrays.asList(
+ proto("keep-source", 100, 10),
+ proto("keep-target", 100, 50),
+ proto("drop-me", 100, 5)));
+
+ runAssessment(scmClient, "--include-datanodes", "keep-source,keep-target");
+
+ String output = out.get();
+ assertThat(output).contains("keep-source");
+ assertThat(output).contains("keep-target");
+ assertThat(output).doesNotContain("drop-me");
+ assertThat(output).contains("2 datanodes");
+ }
+
+ @Test
+ void testExcludeDatanodes() {
+ ScmClient scmClient = mock(ScmClient.class);
+ mockUsageInfo(scmClient, Arrays.asList(
+ proto("source-1", 100, 10),
+ proto("target-1", 100, 50),
+ proto("extra-node", 100, 90)));
+
+ runAssessment(scmClient, "--exclude-datanodes", "extra-node");
+
+ String output = out.get();
+ assertThat(output).doesNotContain("extra-node");
+ assertThat(output).contains("source-1");
+ assertThat(output).contains("target-1");
+ assertThat(output).contains("2 datanodes");
+ }
+
+ @Test
+ void testNodeLimit() {
+ ScmClient scmClient = mock(ScmClient.class);
+ mockUsageInfo(scmClient, Arrays.asList(
+ proto("source-high", 100, 5),
+ proto("source-mid", 100, 20),
+ proto("target-low", 100, 80)));
+
+ runAssessment(scmClient, "-n", "1");
+
+ String output = out.get();
+ assertThat(output).contains("Top 1:");
+ assertThat(output).contains("source-high");
+ assertThat(output).doesNotContain("source-mid");
+ }
+
+ private static void mockUsageInfo(ScmClient scmClient,
List<DatanodeUsageInfoProto> nodes) {
+ try {
+ when(scmClient.getDatanodeUsageInfo(anyBoolean(),
anyInt())).thenReturn(nodes);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void runAssessment(ScmClient scmClient, String... args) {
+ try {
+ CommandLine cli = new CommandLine(cmd);
+ cli.parseArgs(args);
+ cmd.execute(scmClient);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static List<DatanodeUsageInfoProto> sampleNodes() {
+ return Arrays.asList(
+ proto("source-1", 100, 10),
+ proto("target-1", 100, 50));
+ }
+
+ private static DatanodeUsageInfoProto proto(String hostname, long capacity,
long remaining) {
+ DatanodeDetails datanode = DatanodeDetails.newBuilder()
+ .setHostName(hostname)
+ .setIpAddress("127.0.0.1")
+ .setUuid(UUID.randomUUID())
+ .build();
+ long used = capacity - remaining;
+ return DatanodeUsageInfoProto.newBuilder()
+ .setNode(datanode.toProto(DEFAULT_VERSION.toProtoValue()))
+ .setCapacity(capacity)
+ .setRemaining(remaining)
+ .setUsed(used)
+ .build();
+ }
+}
diff --git
a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestContainerBalancerSubCommand.java
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/TestContainerBalancerSubCommand.java
similarity index 99%
rename from
hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestContainerBalancerSubCommand.java
rename to
hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/TestContainerBalancerSubCommand.java
index 92aae28b2f3..70cce33a3e3 100644
---
a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestContainerBalancerSubCommand.java
+++
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/TestContainerBalancerSubCommand.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hadoop.hdds.scm.cli.datanode;
+package org.apache.hadoop.hdds.scm.cli;
import static org.apache.hadoop.hdds.DatanodeVersion.DEFAULT_VERSION;
import static org.apache.hadoop.ozone.OzoneConsts.GB;
@@ -40,10 +40,6 @@
import
org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos;
import
org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ContainerBalancerStatusInfoProto;
import
org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ContainerBalancerStatusInfoResponseProto;
-import org.apache.hadoop.hdds.scm.cli.ContainerBalancerEstimateSubcommand;
-import org.apache.hadoop.hdds.scm.cli.ContainerBalancerStartSubcommand;
-import org.apache.hadoop.hdds.scm.cli.ContainerBalancerStatusSubcommand;
-import org.apache.hadoop.hdds.scm.cli.ContainerBalancerStopSubcommand;
import org.apache.hadoop.hdds.scm.client.ScmClient;
import
org.apache.hadoop.hdds.scm.container.balancer.ContainerBalancerConfiguration;
import org.apache.hadoop.hdds.utils.IOUtils;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]