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 bca868865e HDDS-13098. Create PicoCLI mixin to standardize datanode 
selection options (#8674)
bca868865e is described below

commit bca868865e22175148573aecbfb922c72fe10d3f
Author: sreejasahithi <[email protected]>
AuthorDate: Tue Jul 8 17:59:10 2025 +0530

    HDDS-13098. Create PicoCLI mixin to standardize datanode selection options 
(#8674)
---
 .../cli/datanode/DecommissionStatusSubCommand.java | 33 +++++------
 .../hdds/scm/cli/datanode/ListInfoSubcommand.java  | 30 +++-------
 .../hdds/scm/cli/datanode/NodeSelectionMixin.java  | 66 ++++++++++++++++++++++
 .../hdds/scm/cli/datanode/UsageInfoSubcommand.java | 25 ++++----
 .../datanode/TestDecommissionStatusSubCommand.java | 35 ++++++++++--
 .../src/main/smoketest/admincli/datanode.robot     | 10 ++--
 6 files changed, 140 insertions(+), 59 deletions(-)

diff --git 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/DecommissionStatusSubCommand.java
 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/DecommissionStatusSubCommand.java
index 75a58ceb4b..fd7aaec1bf 100644
--- 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/DecommissionStatusSubCommand.java
+++ 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/DecommissionStatusSubCommand.java
@@ -51,35 +51,36 @@ public class DecommissionStatusSubCommand extends 
ScmSubcommand {
 
   private String errorMessage = "Error getting pipeline and container metrics 
for ";
 
-  @CommandLine.Option(names = { "--id" },
-      description = "Show info by datanode UUID",
-      defaultValue = "")
-  private String uuid;
-
-  @CommandLine.Option(names = { "--ip" },
-      description = "Show info by datanode ipAddress",
-      defaultValue = "")
-  private String ipAddress;
-
   @CommandLine.Option(names = { "--json" },
       description = "Show output in json format",
       defaultValue = "false")
   private boolean json;
 
+  @CommandLine.Mixin
+  private NodeSelectionMixin nodeSelectionMixin;
+
+  @CommandLine.Spec 
+  private CommandLine.Model.CommandSpec spec;
+  
   @Override
   public void execute(ScmClient scmClient) throws IOException {
+    if (!nodeSelectionMixin.getHostname().isEmpty()) {
+      throw new CommandLine.ParameterException(spec.commandLine(),
+          "--hostname option not supported for this command");
+    }
+
     Stream<HddsProtos.Node> allNodes = scmClient.queryNode(DECOMMISSIONING,
         null, HddsProtos.QueryScope.CLUSTER, "").stream();
-    List<HddsProtos.Node> decommissioningNodes =
-        DecommissionUtils.getDecommissioningNodesList(allNodes, uuid, 
ipAddress);
-    if (!Strings.isNullOrEmpty(uuid)) {
+    List<HddsProtos.Node> decommissioningNodes = 
DecommissionUtils.getDecommissioningNodesList(allNodes,
+            nodeSelectionMixin.getNodeId(), nodeSelectionMixin.getIp());
+    if (!Strings.isNullOrEmpty(nodeSelectionMixin.getNodeId())) {
       if (decommissioningNodes.isEmpty()) {
-        System.err.println("Datanode: " + uuid + " is not in DECOMMISSIONING");
+        System.err.println("Datanode: " + nodeSelectionMixin.getNodeId() + " 
is not in DECOMMISSIONING");
         return;
       }
-    } else if (!Strings.isNullOrEmpty(ipAddress)) {
+    } else if (!Strings.isNullOrEmpty(nodeSelectionMixin.getIp())) {
       if (decommissioningNodes.isEmpty()) {
-        System.err.println("Datanode: " + ipAddress + " is not in " +
+        System.err.println("Datanode: " + nodeSelectionMixin.getIp() + " is 
not in " +
             "DECOMMISSIONING");
         return;
       }
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 ad0f0a7ef9..2b7813f461 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
@@ -44,21 +44,6 @@
     versionProvider = HddsVersionProvider.class)
 public class ListInfoSubcommand extends ScmSubcommand {
 
-  @CommandLine.Option(names = {"--ip"},
-      description = "Show info by ip address.",
-      defaultValue = "")
-  private String ipaddress;
-
-  @CommandLine.Option(names = {"--id"},
-      description = "Show info by datanode UUID.",
-      defaultValue = "")
-  private String uuid;
-
-  @CommandLine.Option(names = {"--hostname"},
-      description = "Show info by datanode hostname.",
-      defaultValue = "")
-  private String hostname;
-
   @CommandLine.Option(names = {"--operational-state"},
       description = "Show info by datanode NodeOperationalState(" +
           "IN_SERVICE, " +
@@ -81,13 +66,16 @@ public class ListInfoSubcommand extends ScmSubcommand {
   @CommandLine.Mixin
   private ListLimitOptions listLimitOptions;
 
+  @CommandLine.Mixin
+  private NodeSelectionMixin nodeSelectionMixin;
+
   private List<Pipeline> pipelines;
 
   @Override
   public void execute(ScmClient scmClient) throws IOException {
     pipelines = scmClient.listPipelines();
-    if (!Strings.isNullOrEmpty(uuid)) {
-      HddsProtos.Node node = scmClient.queryNode(UUID.fromString(uuid));
+    if (!Strings.isNullOrEmpty(nodeSelectionMixin.getNodeId())) {
+      HddsProtos.Node node = 
scmClient.queryNode(UUID.fromString(nodeSelectionMixin.getNodeId()));
       DatanodeWithAttributes dwa = new DatanodeWithAttributes(DatanodeDetails
           .getFromProtoBuf(node.getNodeID()),
           node.getNodeOperationalStates(0),
@@ -102,13 +90,13 @@ public void execute(ScmClient scmClient) throws 
IOException {
       return;
     }
     Stream<DatanodeWithAttributes> allNodes = getAllNodes(scmClient).stream();
-    if (!Strings.isNullOrEmpty(ipaddress)) {
+    if (!Strings.isNullOrEmpty(nodeSelectionMixin.getIp())) {
       allNodes = allNodes.filter(p -> p.getDatanodeDetails().getIpAddress()
-          .compareToIgnoreCase(ipaddress) == 0);
+          .compareToIgnoreCase(nodeSelectionMixin.getIp()) == 0);
     }
-    if (!Strings.isNullOrEmpty(hostname)) {
+    if (!Strings.isNullOrEmpty(nodeSelectionMixin.getHostname())) {
       allNodes = allNodes.filter(p -> p.getDatanodeDetails().getHostName()
-          .compareToIgnoreCase(hostname) == 0);
+          .compareToIgnoreCase(nodeSelectionMixin.getHostname()) == 0);
     }
     if (!Strings.isNullOrEmpty(nodeOperationalState)) {
       allNodes = allNodes.filter(p -> p.getOpState().toString()
diff --git 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/NodeSelectionMixin.java
 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/NodeSelectionMixin.java
new file mode 100644
index 0000000000..255ff83fb0
--- /dev/null
+++ 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/NodeSelectionMixin.java
@@ -0,0 +1,66 @@
+/*
+ * 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.datanode;
+
+import com.google.common.base.Strings;
+import picocli.CommandLine;
+
+/**
+ * Picocli mixin providing standardized datanode selection options for 
consistent CLI usage across commands.
+ */
+public class NodeSelectionMixin {
+
+  @CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+  private Selection selection = new Selection();
+
+  //Precedence order: --node-id > --id (deprecated) > --uuid (deprecated).
+  public String getNodeId() {
+    return !Strings.isNullOrEmpty(selection.nodeId) ? selection.nodeId :
+        !Strings.isNullOrEmpty(selection.id) ? selection.id :
+            !Strings.isNullOrEmpty(selection.uuid) ? selection.uuid : "";
+  }
+
+  public String getHostname() {
+    return selection.hostname;
+  }
+
+  public String getIp() {
+    return selection.ip;
+  }
+
+  static class Selection {
+
+    @CommandLine.Option(names = "--node-id", description = "UUID of the 
datanode.", defaultValue = "")
+    private String nodeId;
+
+    @Deprecated
+    @CommandLine.Option(names = "--id", description = "UUID of the datanode.", 
defaultValue = "", hidden = true)
+    private String id;
+
+    @Deprecated
+    @CommandLine.Option(names = "--uuid", description = "UUID of the 
datanode.", defaultValue = "", hidden = true)
+    private String uuid;
+
+    @CommandLine.Option(names = "--hostname", description = "Hostname of the 
datanode. " +
+        "Note: not supported for decommission status command", defaultValue = 
"")
+    private String hostname;
+
+    @CommandLine.Option(names = "--ip", description = "IP address of the 
datanode", defaultValue = "")
+    private String ip;
+  }
+}
diff --git 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/UsageInfoSubcommand.java
 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/UsageInfoSubcommand.java
index 2d486dab49..cc14f5d2b2 100644
--- 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/UsageInfoSubcommand.java
+++ 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/datanode/UsageInfoSubcommand.java
@@ -58,7 +58,7 @@ public class UsageInfoSubcommand extends ScmSubcommand {
   }
 
   @CommandLine.ArgGroup(multiplicity = "1")
-  private ExclusiveArguments exclusiveArguments;
+  private NodeSelectionArguments exclusiveArguments;
 
   @CommandLine.Option(names = {"-c", "--count"}, description = "Number of " +
       "datanodes to display (Default: ${DEFAULT-VALUE}).",
@@ -72,16 +72,21 @@ public class UsageInfoSubcommand extends ScmSubcommand {
 
   @Override
   public void execute(ScmClient scmClient) throws IOException {
+    String hostnameOrIp =
+        !Strings.isNullOrEmpty(exclusiveArguments.getIp()) ? 
exclusiveArguments.getIp()
+            : !Strings.isNullOrEmpty(exclusiveArguments.getHostname()) ? 
exclusiveArguments.getHostname()
+            : exclusiveArguments.address; //Fallback to deprecated --address 
for backward compatibility with older CLI.
+    
     List<HddsProtos.DatanodeUsageInfoProto> infoList;
     if (count < 1) {
       throw new IOException("Count must be an integer greater than 0.");
     }
 
     // fetch info by ip or hostname or uuid
-    if (!Strings.isNullOrEmpty(exclusiveArguments.address) ||
-        !Strings.isNullOrEmpty(exclusiveArguments.uuid)) {
-      infoList = scmClient.getDatanodeUsageInfo(exclusiveArguments.address,
-          exclusiveArguments.uuid);
+    if (!Strings.isNullOrEmpty(hostnameOrIp) ||
+        !Strings.isNullOrEmpty(exclusiveArguments.getNodeId())) {
+      infoList = scmClient.getDatanodeUsageInfo(hostnameOrIp,
+          exclusiveArguments.getNodeId());
     } else { // get info of most used or least used nodes
       infoList = scmClient.getDatanodeUsageInfo(exclusiveArguments.mostUsed,
           count);
@@ -268,16 +273,14 @@ public long getPipelineCount() {
     }
   }
 
-  private static class ExclusiveArguments {
+  private static class NodeSelectionArguments extends NodeSelectionMixin {
+    @Deprecated
     @CommandLine.Option(names = {"--address"}, paramLabel = "ADDRESS",
         description = "Show info by datanode ip or hostname address.",
-        defaultValue = "")
+        defaultValue = "",
+        hidden = true)
     private String address;
 
-    @CommandLine.Option(names = {"--uuid"}, paramLabel = "UUID", description =
-        "Show info by datanode UUID.", defaultValue = "")
-    private String uuid;
-
     @CommandLine.Option(names = {"-m", "--most-used"},
         description = "Show the most used datanodes.",
         defaultValue = "false")
diff --git 
a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestDecommissionStatusSubCommand.java
 
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestDecommissionStatusSubCommand.java
index f7660ab100..08371d16fa 100644
--- 
a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestDecommissionStatusSubCommand.java
+++ 
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestDecommissionStatusSubCommand.java
@@ -18,6 +18,7 @@
 package org.apache.hadoop.hdds.scm.cli.datanode;
 
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.mock;
@@ -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.ValueSource;
 import picocli.CommandLine;
 
 /**
@@ -81,6 +84,8 @@ public void testSuccessWhenDecommissionStatus() throws 
IOException {
     
when(scmClient.getContainersOnDecomNode(any())).thenReturn(containerOnDecom);
     when(scmClient.getMetrics(any())).thenReturn(metrics.get(1));
 
+    CommandLine cmdLine = new CommandLine(cmd);
+    cmdLine.parseArgs();
     cmd.execute(scmClient);
     Pattern p = Pattern.compile("Decommission\\sStatus:\\s" +
             "DECOMMISSIONING\\s-\\s2\\snode\\(s\\)\n");
@@ -112,6 +117,8 @@ public void testNoNodesWhenDecommissionStatus() throws 
IOException {
         .thenReturn(new ArrayList<>());
     when(scmClient.getContainersOnDecomNode(any())).thenReturn(new 
HashMap<>());
     when(scmClient.getMetrics(any())).thenReturn(metrics.get(0));
+    CommandLine cmdLine = new CommandLine(cmd);
+    cmdLine.parseArgs();
     cmd.execute(scmClient);
 
     Pattern p = Pattern.compile("Decommission\\sStatus:\\s" +
@@ -128,8 +135,9 @@ public void testNoNodesWhenDecommissionStatus() throws 
IOException {
     assertFalse(m.find());
   }
 
-  @Test
-  public void testIdOptionDecommissionStatusSuccess() throws IOException {
+  @ParameterizedTest
+  @ValueSource(strings = {"--id", "--node-id"})
+  public void testIdOptionDecommissionStatusSuccess(String flag) throws 
IOException {
     ScmClient scmClient = mock(ScmClient.class);
     when(scmClient.queryNode(any(), any(), any(), any()))
         .thenAnswer(invocation -> nodes); // 2 nodes decommissioning
@@ -137,7 +145,7 @@ public void testIdOptionDecommissionStatusSuccess() throws 
IOException {
     when(scmClient.getMetrics(any())).thenReturn(metrics.get(1));
 
     CommandLine c = new CommandLine(cmd);
-    c.parseArgs("--id", nodes.get(0).getNodeID().getUuid());
+    c.parseArgs(flag, nodes.get(0).getNodeID().getUuid());
     cmd.execute(scmClient); // check status of host0
 
     Pattern p = Pattern.compile("Datanode:\\s.*host0\\)");
@@ -153,8 +161,9 @@ public void testIdOptionDecommissionStatusSuccess() throws 
IOException {
     assertFalse(m.find());
   }
 
-  @Test
-  public void testIdOptionDecommissionStatusFail() throws IOException {
+  @ParameterizedTest
+  @ValueSource(strings = {"--id", "--node-id"})
+  public void testIdOptionDecommissionStatusFail(String flag) throws 
IOException {
     ScmClient scmClient = mock(ScmClient.class);
     when(scmClient.queryNode(any(), any(), any(), any()))
         .thenAnswer(invocation -> nodes.subList(0, 1)); // host0 
decommissioning
@@ -165,7 +174,7 @@ public void testIdOptionDecommissionStatusFail() throws 
IOException {
     when(scmClient.getMetrics(any())).thenReturn(metrics.get(2));
 
     CommandLine c = new CommandLine(cmd);
-    c.parseArgs("--id", nodes.get(1).getNodeID().getUuid());
+    c.parseArgs(flag, nodes.get(1).getNodeID().getUuid());
     cmd.execute(scmClient); // check status of host1
 
     Pattern p = Pattern.compile("Datanode:\\s(.*)\\sis\\snot\\sin" +
@@ -236,6 +245,20 @@ public void testIpOptionDecommissionStatusFail() throws 
IOException {
     assertFalse(m.find());
   }
 
+  @Test
+  public void testHostnameOptionThrowsParameterException() throws IOException {
+    ScmClient scmClient = mock(ScmClient.class);
+    CommandLine cmdLine = new CommandLine(cmd);
+    cmdLine.parseArgs("--hostname", "some-host");
+
+    CommandLine.ParameterException ex = assertThrows(
+        CommandLine.ParameterException.class,
+        () -> cmd.execute(scmClient)
+    );
+
+    assertTrue(ex.getMessage().contains("--hostname option not supported for 
this command"));
+  }
+
   private List<HddsProtos.Node> getNodeDetails(int n) {
     List<HddsProtos.Node> nodesList = new ArrayList<>();
 
diff --git a/hadoop-ozone/dist/src/main/smoketest/admincli/datanode.robot 
b/hadoop-ozone/dist/src/main/smoketest/admincli/datanode.robot
index b3e14c9bbb..d4e7e7a2b4 100644
--- a/hadoop-ozone/dist/src/main/smoketest/admincli/datanode.robot
+++ b/hadoop-ozone/dist/src/main/smoketest/admincli/datanode.robot
@@ -41,7 +41,7 @@ List datanodes
 
 Filter list by UUID
     ${uuid} =           Execute      grep '^Datanode:' ${LIST_FILE} | head -1 
| awk '{ print \$2 }'
-    ${output} =         Execute      ozone admin datanode list --id "${uuid}"
+    ${output} =         Execute      ozone admin datanode list --node-id 
"${uuid}"
     Assert Output       ${output}    1    ${uuid}
 
 Filter list by Ip address
@@ -70,22 +70,22 @@ Filter list by NodeState
 
 Get usage info by UUID
     ${uuid} =           Execute      grep '^Datanode:' ${LIST_FILE} | head -1 
| awk '{ print \$2 }'
-    ${output} =         Execute      ozone admin datanode usageinfo --uuid 
"${uuid}"
+    ${output} =         Execute      ozone admin datanode usageinfo --node-id 
"${uuid}"
     Should contain      ${output}    Usage Information (1 Datanodes)
 
 Get usage info by Ip address
     ${ip} =             Execute      grep '^Datanode:' ${LIST_FILE} | head -1 
| awk '{ print \$3 }' | awk -F '[/]' '{ print \$3 }'
-    ${output} =         Execute      ozone admin datanode usageinfo --address 
"${ip}"
+    ${output} =         Execute      ozone admin datanode usageinfo --ip 
"${ip}"
     Should contain      ${output}    Usage Information (1 Datanodes)
 
 Get usage info by Hostname
     ${hostname} =       Execute      grep '^Datanode:' ${LIST_FILE} | head -1 
| awk '{ print \$3 }' | awk -F '[/]' '{ print \$4 }'
-    ${output} =         Execute      ozone admin datanode usageinfo --address 
"${hostname}"
+    ${output} =         Execute      ozone admin datanode usageinfo --hostname 
"${hostname}"
     Should contain      ${output}    Usage Information (1 Datanodes)
 
 Get usage info with invalid address
     ${uuid} =           Execute      grep '^Datanode:' ${LIST_FILE} | head -1 
| awk '{ print \$2 }'
-    ${output} =         Execute      ozone admin datanode usageinfo --address 
"${uuid}"
+    ${output} =         Execute      ozone admin datanode usageinfo --ip 
"${uuid}"
     Should contain      ${output}    Usage Information (0 Datanodes)
 
 Incomplete command


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to