This is an automated email from the ASF dual-hosted git repository.

chungen0126 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 c4b2ae1fff4 HDDS-15086. Wire ozone local command (#10758)
c4b2ae1fff4 is described below

commit c4b2ae1fff4091f22f52e649280898743ff244cc
Author: Chun-Hung Tseng <[email protected]>
AuthorDate: Wed Jul 22 11:45:06 2026 +0200

    HDDS-15086. Wire ozone local command (#10758)
---
 hadoop-hdds/docs/content/tools/_index.md           |   2 +
 hadoop-ozone/dist/src/shell/ozone/ozone            |   5 +
 .../org/apache/hadoop/ozone/local/OzoneLocal.java  |  64 +++++++++--
 .../apache/hadoop/ozone/local/TestOzoneLocal.java  | 120 ++++++++++++++++++---
 4 files changed, 168 insertions(+), 23 deletions(-)

diff --git a/hadoop-hdds/docs/content/tools/_index.md 
b/hadoop-hdds/docs/content/tools/_index.md
index aa708e263b8..3a8bf08e826 100644
--- a/hadoop-hdds/docs/content/tools/_index.md
+++ b/hadoop-hdds/docs/content/tools/_index.md
@@ -43,6 +43,8 @@ Client commands:
 
    * **sh** -  Primary command line interface for ozone to manage 
volumes/buckets/keys.
    * **fs** - Runs a command on ozone file system (similar to `hdfs dfs`)
+   * **local** - Runs a single-node local Ozone cluster (SCM, OM, and 
datanodes)
+   in one process for development.
    * **version** - Prints the version of Ozone and HDDS.
 
 
diff --git a/hadoop-ozone/dist/src/shell/ozone/ozone 
b/hadoop-ozone/dist/src/shell/ozone/ozone
index ecddd4f1ecc..8592ef99e2f 100755
--- a/hadoop-ozone/dist/src/shell/ozone/ozone
+++ b/hadoop-ozone/dist/src/shell/ozone/ozone
@@ -66,6 +66,7 @@ function ozone_usage
   ozone_add_subcommand "s3" client "command line interface for s3 related 
operations"
   ozone_add_subcommand "tenant" client "command line interface for 
multi-tenant related operations"
   ozone_add_subcommand "insight" client "tool to get runtime operation 
information"
+  ozone_add_subcommand "local" client "run a single-node local ozone cluster"
   ozone_add_subcommand "version" client "print the version"
   ozone_add_subcommand "dtutil" client "operations related to delegation 
tokens"
   ozone_add_subcommand "interactive" client "interactive shell for ozone 
commands"
@@ -208,6 +209,10 @@ function ozonecmd_case
       OZONE_CLASSNAME=org.apache.hadoop.ozone.insight.Insight
       OZONE_RUN_ARTIFACT_NAME="ozone-insight"
     ;;
+    local)
+      OZONE_CLASSNAME=org.apache.hadoop.ozone.local.OzoneLocal
+      OZONE_RUN_ARTIFACT_NAME="ozone-tools"
+    ;;
     version)
       OZONE_CLASSNAME=org.apache.hadoop.ozone.util.OzoneVersionInfo
       OZONE_RUN_ARTIFACT_NAME="ozone-tools"
diff --git 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/OzoneLocal.java
 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/OzoneLocal.java
index 6d1d4d1394f..0d5e6ec1734 100644
--- 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/OzoneLocal.java
+++ 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/OzoneLocal.java
@@ -17,26 +17,30 @@
 
 package org.apache.hadoop.ozone.local;
 
+import java.io.PrintWriter;
 import java.nio.file.Path;
 import java.time.Duration;
 import java.time.format.DateTimeParseException;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import org.apache.hadoop.hdds.cli.AbstractSubcommand;
 import org.apache.hadoop.hdds.cli.GenericCli;
 import org.apache.hadoop.hdds.cli.HddsVersionProvider;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.conf.TimeDurationUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import picocli.CommandLine;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.ITypeConverter;
 import picocli.CommandLine.Option;
 
 /**
- * Internal CLI entry point for local Ozone cluster commands.
+ * CLI entry point for local single-node Ozone.
  */
 @Command(name = "ozone local",
-    hidden = true,
-    description = "Internal commands for local Ozone cluster runtime",
+    description = "Run a single-node local Ozone cluster",
     versionProvider = HddsVersionProvider.class,
     mixinStandardHelpOptions = true,
     subcommands = {
@@ -44,6 +48,8 @@
     })
 public class OzoneLocal extends GenericCli {
 
+  private static final Logger LOG = LoggerFactory.getLogger(OzoneLocal.class);
+
   static final String ENV_DATA_DIR = "OZONE_LOCAL_DATA_DIR";
   static final String ENV_FORMAT = "OZONE_LOCAL_FORMAT";
   static final String ENV_DATANODES = "OZONE_LOCAL_DATANODES";
@@ -108,8 +114,7 @@ public static void main(String[] args) {
   }
 
   @Command(name = "run",
-      hidden = true,
-      description = "Resolve configuration for local Ozone runtime startup")
+      description = "Start SCM, OM, and datanodes in one local process")
   static class RunCommand extends AbstractSubcommand implements Callable<Void> 
{
 
     @Option(names = "--data-dir",
@@ -189,11 +194,56 @@ static class RunCommand extends AbstractSubcommand 
implements Callable<Void> {
     private String s3Region;
 
     @Override
-    public Void call() {
-      resolveConfig();
+    public Void call() throws Exception {
+      LocalOzoneClusterConfig config = resolveConfig();
+      try (LocalOzoneRuntime runtime = createRuntime(config, getOzoneConf())) {
+        runtime.start();
+        printSummary(runtime, config);
+        awaitShutdown(runtime);
+      }
       return null;
     }
 
+    LocalOzoneRuntime createRuntime(LocalOzoneClusterConfig config, 
OzoneConfiguration seedConfiguration) {
+      return new LocalOzoneCluster(config, seedConfiguration);
+    }
+
+    private void printSummary(LocalOzoneRuntime runtime, 
LocalOzoneClusterConfig config) {
+      PrintWriter writer = out();
+      writer.println("Local Ozone is running from " + config.getDataDir());
+      writer.println("SCM RPC: " + runtime.getDisplayHost() + ":" + 
runtime.getScmPort());
+      writer.println("OM RPC: " + runtime.getDisplayHost() + ":" + 
runtime.getOmPort());
+      writer.println("Press Ctrl+C to stop.");
+      writer.flush();
+    }
+
+    /**
+     * Blocks until the JVM is asked to shut down (for example by Ctrl+C), 
closing the runtime from
+     * the shutdown hook before the JVM exits.
+     */
+    void awaitShutdown(LocalOzoneRuntime runtime) throws InterruptedException {
+      CountDownLatch stopped = new CountDownLatch(1);
+      Thread shutdownHook = new Thread(() -> {
+        try {
+          runtime.close();
+        } catch (Exception ex) {
+          LOG.warn("Failed to close ozone local runtime", ex);
+        } finally {
+          stopped.countDown();
+        }
+      }, "ozone-local-shutdown");
+      Runtime.getRuntime().addShutdownHook(shutdownHook);
+      try {
+        stopped.await();
+      } finally {
+        try {
+          Runtime.getRuntime().removeShutdownHook(shutdownHook);
+        } catch (IllegalStateException ignored) {
+          // JVM shutdown is already in progress.
+        }
+      }
+    }
+
     LocalOzoneClusterConfig resolveConfig() {
       if (datanodes < 1) {
         throw new IllegalArgumentException(
diff --git 
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestOzoneLocal.java
 
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestOzoneLocal.java
index 219b3ae5d54..0f8686e91a2 100644
--- 
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestOzoneLocal.java
+++ 
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestOzoneLocal.java
@@ -30,6 +30,7 @@
 import java.lang.reflect.Field;
 import java.nio.file.Paths;
 import java.time.Duration;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.junit.jupiter.api.Test;
 import picocli.CommandLine;
 import picocli.CommandLine.Command;
@@ -45,21 +46,21 @@
 class TestOzoneLocal {
 
   @Test
-  void localCommandMetadataIsPresentAndHidden() {
+  void localCommandMetadataIsPresentAndPublic() {
     Command command = OzoneLocal.class.getAnnotation(Command.class);
 
     assertNotNull(command);
     assertEquals("ozone local", command.name());
-    assertTrue(command.hidden());
+    assertFalse(command.hidden());
   }
 
   @Test
-  void runCommandMetadataIsPresentAndHidden() {
+  void runCommandMetadataIsPresentAndPublic() {
     Command command = OzoneLocal.RunCommand.class.getAnnotation(Command.class);
 
     assertNotNull(command);
     assertEquals("run", command.name());
-    assertTrue(command.hidden());
+    assertFalse(command.hidden());
   }
 
   @Test
@@ -70,7 +71,7 @@ void genericCliRegistersRunCommand() {
   }
 
   @Test
-  void rootHelpHidesRunCommand() throws Exception {
+  void rootHelpListsRunCommand() throws Exception {
     OzoneLocal local = new OzoneLocal();
     CommandLine commandLine = local.getCmd();
     ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -85,27 +86,41 @@ void rootHelpHidesRunCommand() throws Exception {
     String help = out.toString(UTF_8.name());
     assertEquals(0, exitCode);
     assertTrue(help.contains("Usage: ozone local"));
-    assertFalse(help.matches("(?s).*\\R\\s+run\\b.*"), help);
+    assertTrue(help.matches("(?s).*\\R\\s+run\\b.*"), help);
     assertEquals("", err.toString(UTF_8.name()));
   }
 
   @Test
-  void runCommandResolvesConfigurationQuietlyUntilRuntimeStartup()
-      throws Exception {
-    OzoneLocal local = new OzoneLocal();
-    CommandLine commandLine = local.getCmd();
+  void runCommandStartsRuntimeAndPrintsStartupSummary() throws Exception {
     ByteArrayOutputStream out = new ByteArrayOutputStream();
-    ByteArrayOutputStream err = new ByteArrayOutputStream();
+    StubRuntime runtime = new StubRuntime("localhost", 9860, 9862);
+    TestableRunCommand command = new TestableRunCommand(runtime);
+    CommandLine commandLine = new CommandLine(command);
     commandLine.setOut(new PrintWriter(new OutputStreamWriter(out, UTF_8),
         true));
-    commandLine.setErr(new PrintWriter(new OutputStreamWriter(err, UTF_8),
-        true));
 
-    int exitCode = local.execute(new String[] {"run"});
+    int exitCode = commandLine.execute();
 
     assertEquals(0, exitCode);
-    assertEquals("", out.toString(UTF_8.name()));
-    assertEquals("", err.toString(UTF_8.name()));
+    assertTrue(runtime.started);
+    assertTrue(runtime.closed);
+    String text = out.toString(UTF_8.name());
+    assertTrue(text.contains("Local Ozone is running from"), text);
+    assertTrue(text.contains("SCM RPC: localhost:9860"), text);
+    assertTrue(text.contains("OM RPC: localhost:9862"), text);
+    assertTrue(text.contains("Press Ctrl+C to stop."), text);
+  }
+
+  @Test
+  void runCommandClosesRuntimeWhenStartupFails() {
+    StubRuntime runtime = new StubRuntime("localhost", 9860, 9862);
+    runtime.failStart = true;
+    TestableRunCommand command = new TestableRunCommand(runtime);
+
+    int exitCode = new CommandLine(command).execute();
+
+    assertEquals(1, exitCode);
+    assertTrue(runtime.closed);
   }
 
   @Test
@@ -323,6 +338,79 @@ private static void assertEnvDefault(String fieldName,
         defaultValue);
   }
 
+  private static final class TestableRunCommand extends OzoneLocal.RunCommand {
+
+    private final LocalOzoneRuntime runtime;
+
+    private TestableRunCommand(LocalOzoneRuntime runtime) {
+      this.runtime = runtime;
+    }
+
+    @Override
+    LocalOzoneRuntime createRuntime(LocalOzoneClusterConfig config, 
OzoneConfiguration seedConfiguration) {
+      return runtime;
+    }
+
+    @Override
+    void awaitShutdown(LocalOzoneRuntime localRuntime) {
+      // Return immediately instead of blocking until JVM shutdown.
+    }
+  }
+
+  private static final class StubRuntime implements LocalOzoneRuntime {
+
+    private final String displayHost;
+    private final int scmPort;
+    private final int omPort;
+    private boolean failStart;
+    private boolean started;
+    private boolean closed;
+
+    private StubRuntime(String displayHost, int scmPort, int omPort) {
+      this.displayHost = displayHost;
+      this.scmPort = scmPort;
+      this.omPort = omPort;
+    }
+
+    @Override
+    public void start() {
+      if (failStart) {
+        throw new IllegalStateException("startup failed");
+      }
+      started = true;
+    }
+
+    @Override
+    public String getDisplayHost() {
+      return displayHost;
+    }
+
+    @Override
+    public int getScmPort() {
+      return scmPort;
+    }
+
+    @Override
+    public int getOmPort() {
+      return omPort;
+    }
+
+    @Override
+    public int getS3gPort() {
+      return 0;
+    }
+
+    @Override
+    public String getS3Endpoint() {
+      return "";
+    }
+
+    @Override
+    public void close() {
+      closed = true;
+    }
+  }
+
   private static final class RunCommandFallbackDefaults
       implements IDefaultValueProvider {
 


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

Reply via email to