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 cc4f52a9d09 HDDS-15083. Add local filesystem lifecycle implementation 
(#10455)
cc4f52a9d09 is described below

commit cc4f52a9d09197aa7c2da80a1de38f7f9e73d4f3
Author: Chun-Hung Tseng <[email protected]>
AuthorDate: Wed Jun 24 15:44:33 2026 +0200

    HDDS-15083. Add local filesystem lifecycle implementation (#10455)
---
 .../hadoop/ozone/local/LocalOzoneCluster.java      | 486 +++++++++++++++++++++
 .../hadoop/ozone/local/TestLocalOzoneCluster.java  | 356 +++++++++++++++
 2 files changed, 842 insertions(+)

diff --git 
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneCluster.java
 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneCluster.java
new file mode 100644
index 00000000000..03505e20d26
--- /dev/null
+++ 
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/local/LocalOzoneCluster.java
@@ -0,0 +1,486 @@
+/*
+ * 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.ozone.local;
+
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_SAFEMODE_MIN_DATANODE;
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_SAFEMODE_PIPELINE_CREATION;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.HDDS_CONTAINER_RATIS_ENABLED_KEY;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_BLOCK_CLIENT_ADDRESS_KEY;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_BLOCK_CLIENT_BIND_HOST_KEY;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_CLIENT_ADDRESS_KEY;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_CLIENT_BIND_HOST_KEY;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_DATANODE_ADDRESS_KEY;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_DATANODE_BIND_HOST_KEY;
+import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_GRPC_PORT_KEY;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_HTTPS_ADDRESS_KEY;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_HTTPS_BIND_HOST_KEY;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_HTTP_ADDRESS_KEY;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_HTTP_BIND_HOST_KEY;
+import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_NAMES;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_RATIS_PORT_KEY;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_SECURITY_SERVICE_ADDRESS_KEY;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_SECURITY_SERVICE_BIND_HOST_KEY;
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_METADATA_DIRS;
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_REPLICATION;
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_REPLICATION_TYPE;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_ADDRESS_KEY;
+import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_HTTP_ADDRESS_KEY;
+import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_HTTP_BIND_HOST_KEY;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_RATIS_PORT_KEY;
+import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_KEY;
+import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_TYPE_KEY;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Stream;
+import org.apache.hadoop.hdds.client.ReplicationFactor;
+import org.apache.hadoop.hdds.client.ReplicationType;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+
+/**
+ * Prepares local filesystem state and configuration for {@code ozone local}.
+ *
+ * <p>This runtime intentionally stops at filesystem and configuration
+ * preparation. Starting SCM, OM, datanodes, and S3 Gateway belongs to later
+ * HDDS-15084/HDDS-15086 work.</p>
+ */
+public final class LocalOzoneCluster implements LocalOzoneRuntime {
+
+  static final String PORTS_STATE_FILE_NAME = "ports.properties";
+
+  private static final String METADATA_DIR_NAME = "metadata";
+  private static final String SCM_CLIENT_PORT_KEY = "scm.client";
+  private static final String SCM_BLOCK_PORT_KEY = "scm.block";
+  private static final String SCM_DATANODE_PORT_KEY = "scm.datanode";
+  private static final String SCM_SECURITY_PORT_KEY = "scm.security";
+  private static final String SCM_HTTP_PORT_KEY = "scm.http";
+  private static final String SCM_HTTPS_PORT_KEY = "scm.https";
+  private static final String SCM_RATIS_PORT_KEY = "scm.ratis";
+  private static final String SCM_GRPC_PORT_KEY = "scm.grpc";
+  private static final String OM_RPC_PORT_KEY = "om.rpc";
+  private static final String OM_HTTP_PORT_KEY = "om.http";
+  private static final String OM_RATIS_PORT_KEY = "om.ratis";
+
+  private static final String[] REQUIRED_PERSISTED_PORT_KEYS = {
+      SCM_CLIENT_PORT_KEY,
+      SCM_BLOCK_PORT_KEY,
+      SCM_DATANODE_PORT_KEY,
+      SCM_SECURITY_PORT_KEY,
+      SCM_HTTP_PORT_KEY,
+      SCM_HTTPS_PORT_KEY,
+      SCM_RATIS_PORT_KEY,
+      SCM_GRPC_PORT_KEY,
+      OM_RPC_PORT_KEY,
+      OM_HTTP_PORT_KEY,
+      OM_RATIS_PORT_KEY
+  };
+
+  private final LocalOzoneClusterConfig config;
+  private final OzoneConfiguration seedConfiguration;
+
+  private PreparedConfiguration preparedConfiguration;
+
+  public LocalOzoneCluster(LocalOzoneClusterConfig config,
+      OzoneConfiguration seedConfiguration) {
+    this.config = Objects.requireNonNull(config, "config");
+    this.seedConfiguration = new OzoneConfiguration(
+        Objects.requireNonNull(seedConfiguration, "seedConfiguration"));
+  }
+
+  @Override
+  public void start() throws IOException {
+    prepareConfiguration();
+  }
+
+  PreparedConfiguration prepareConfiguration() throws IOException {
+    if (preparedConfiguration != null) {
+      return preparedConfiguration;
+    }
+
+    prepareStorageLayout();
+
+    OzoneConfiguration conf = new OzoneConfiguration(seedConfiguration);
+    configureLocalDefaults(conf);
+
+    PersistedPortState persistedPorts = loadPersistedPortState();
+    PortAllocator portAllocator = new PortAllocator();
+    int scmPort = configureScm(conf, persistedPorts, portAllocator);
+    int omPort = configureOm(conf, persistedPorts, portAllocator);
+
+    persistedPorts.store();
+    preparedConfiguration = new PreparedConfiguration(conf, scmPort, omPort);
+    return preparedConfiguration;
+  }
+
+  @Override
+  public String getDisplayHost() {
+    return LocalOzoneClusterConfig.DEFAULT_BIND_HOST.equals(config.getHost())
+        ? LocalOzoneClusterConfig.DEFAULT_HOST : config.getHost();
+  }
+
+  @Override
+  public int getScmPort() {
+    return preparedConfiguration == null ? config.getScmPort()
+        : preparedConfiguration.getScmPort();
+  }
+
+  @Override
+  public int getOmPort() {
+    return preparedConfiguration == null ? config.getOmPort()
+        : preparedConfiguration.getOmPort();
+  }
+
+  @Override
+  public int getS3gPort() {
+    return -1;
+  }
+
+  @Override
+  public String getS3Endpoint() {
+    return "";
+  }
+
+  @Override
+  public void close() throws IOException {
+    // Ephemeral mode owns the data directory lifecycle for short-lived runs.
+    if (config.isEphemeral()) {
+      deleteDirectory(config.getDataDir());
+    }
+  }
+
+  private void configureLocalDefaults(OzoneConfiguration conf) {
+    conf.set(OZONE_METADATA_DIRS, metadataDir().toString());
+    conf.set(OZONE_REPLICATION, ReplicationFactor.ONE.name());
+    conf.set(OZONE_REPLICATION_TYPE, ReplicationType.STAND_ALONE.name());
+    conf.set(OZONE_SERVER_DEFAULT_REPLICATION_KEY, 
ReplicationFactor.ONE.name());
+    conf.set(OZONE_SERVER_DEFAULT_REPLICATION_TYPE_KEY,
+        ReplicationType.STAND_ALONE.name());
+    conf.setBoolean(HDDS_CONTAINER_RATIS_ENABLED_KEY, false);
+    conf.setBoolean(HDDS_SCM_SAFEMODE_PIPELINE_CREATION, false);
+    conf.setInt(HDDS_SCM_SAFEMODE_MIN_DATANODE,
+        Math.max(1, config.getDatanodes()));
+  }
+
+  private int configureScm(OzoneConfiguration conf,
+      PersistedPortState persistedPorts, PortAllocator portAllocator)
+      throws IOException {
+    int scmClientPort = reservePort(portAllocator, persistedPorts,
+        SCM_CLIENT_PORT_KEY, config.getScmPort());
+    int scmBlockPort = reservePort(portAllocator, persistedPorts,
+        SCM_BLOCK_PORT_KEY, 0);
+    int scmDatanodePort = reservePort(portAllocator, persistedPorts,
+        SCM_DATANODE_PORT_KEY, 0);
+    int scmSecurityPort = reservePort(portAllocator, persistedPorts,
+        SCM_SECURITY_PORT_KEY, 0);
+    int scmHttpPort = reservePort(portAllocator, persistedPorts,
+        SCM_HTTP_PORT_KEY, 0);
+    int scmHttpsPort = reservePort(portAllocator, persistedPorts,
+        SCM_HTTPS_PORT_KEY, 0);
+    int scmRatisPort = reservePort(portAllocator, persistedPorts,
+        SCM_RATIS_PORT_KEY, 0);
+    int scmGrpcPort = reservePort(portAllocator, persistedPorts,
+        SCM_GRPC_PORT_KEY, 0);
+
+    conf.set(OZONE_SCM_CLIENT_ADDRESS_KEY,
+        address(config.getHost(), scmClientPort));
+    conf.set(OZONE_SCM_CLIENT_BIND_HOST_KEY, config.getBindHost());
+    conf.set(OZONE_SCM_BLOCK_CLIENT_ADDRESS_KEY,
+        address(config.getHost(), scmBlockPort));
+    conf.set(OZONE_SCM_BLOCK_CLIENT_BIND_HOST_KEY, config.getBindHost());
+    conf.set(OZONE_SCM_DATANODE_ADDRESS_KEY,
+        address(config.getHost(), scmDatanodePort));
+    conf.set(OZONE_SCM_DATANODE_BIND_HOST_KEY, config.getBindHost());
+    conf.set(OZONE_SCM_SECURITY_SERVICE_ADDRESS_KEY,
+        address(config.getHost(), scmSecurityPort));
+    conf.set(OZONE_SCM_SECURITY_SERVICE_BIND_HOST_KEY, config.getBindHost());
+    conf.set(OZONE_SCM_HTTP_ADDRESS_KEY,
+        address(config.getHost(), scmHttpPort));
+    conf.set(OZONE_SCM_HTTP_BIND_HOST_KEY, config.getBindHost());
+    conf.set(OZONE_SCM_HTTPS_ADDRESS_KEY,
+        address(config.getHost(), scmHttpsPort));
+    conf.set(OZONE_SCM_HTTPS_BIND_HOST_KEY, config.getBindHost());
+    conf.setInt(OZONE_SCM_RATIS_PORT_KEY, scmRatisPort);
+    conf.setInt(OZONE_SCM_GRPC_PORT_KEY, scmGrpcPort);
+    conf.setStrings(OZONE_SCM_NAMES,
+        address(config.getHost(), scmDatanodePort));
+    return scmClientPort;
+  }
+
+  private int configureOm(OzoneConfiguration conf,
+      PersistedPortState persistedPorts, PortAllocator portAllocator)
+      throws IOException {
+    int omRpcPort = reservePort(portAllocator, persistedPorts, OM_RPC_PORT_KEY,
+        config.getOmPort());
+    int omHttpPort = reservePort(portAllocator, persistedPorts,
+        OM_HTTP_PORT_KEY, 0);
+    int omRatisPort = reservePort(portAllocator, persistedPorts,
+        OM_RATIS_PORT_KEY, 0);
+
+    conf.set(OZONE_OM_ADDRESS_KEY, address(config.getHost(), omRpcPort));
+    conf.set(OZONE_OM_HTTP_ADDRESS_KEY, address(config.getHost(), omHttpPort));
+    conf.set(OZONE_OM_HTTP_BIND_HOST_KEY, config.getBindHost());
+    conf.setInt(OZONE_OM_RATIS_PORT_KEY, omRatisPort);
+    return omRpcPort;
+  }
+
+  private void prepareStorageLayout() throws IOException {
+    Path dataDir = config.getDataDir();
+    if (Files.exists(dataDir) && !Files.isDirectory(dataDir)) {
+      throw new IOException("Local Ozone data dir " + dataDir
+          + " is not a directory.");
+    }
+
+    switch (config.getFormatMode()) {
+    case ALWAYS:
+      deleteDirectory(dataDir);
+      createBaseLayout();
+      break;
+    case NEVER:
+      requireExistingLayout();
+      break;
+    case IF_NEEDED:
+      createBaseLayout();
+      break;
+    default:
+      throw new IOException("Unsupported format mode "
+          + config.getFormatMode() + ".");
+    }
+  }
+
+  private void createBaseLayout() throws IOException {
+    Files.createDirectories(config.getDataDir());
+    Files.createDirectories(metadataDir());
+  }
+
+  /**
+   * {@link LocalOzoneClusterConfig.FormatMode#NEVER} is a strict reuse mode:
+   * the command must not initialize missing local state on behalf of the user.
+   */
+  private void requireExistingLayout() throws IOException {
+    Path dataDir = config.getDataDir();
+    if (!Files.exists(dataDir)) {
+      throw new IOException("Local Ozone data dir " + dataDir
+          + " does not exist.");
+    }
+    if (!Files.isDirectory(metadataDir())) {
+      throw new IOException("Local Ozone metadata dir " + metadataDir()
+          + " does not exist.");
+    }
+    if (!Files.isRegularFile(portStateFile())) {
+      throw new IOException("Local Ozone port state file " + portStateFile()
+          + " does not exist.");
+    }
+  }
+
+  private PersistedPortState loadPersistedPortState() throws IOException {
+    PersistedPortState persistedPorts =
+        PersistedPortState.load(portStateFile());
+    if (config.getFormatMode() == LocalOzoneClusterConfig.FormatMode.NEVER) {
+      persistedPorts.requireKeys(REQUIRED_PERSISTED_PORT_KEYS);
+    }
+    return persistedPorts;
+  }
+
+  private int reservePort(PortAllocator allocator,
+      PersistedPortState persistedPorts, String key, int configuredPort)
+      throws IOException {
+    int preferredPort = configuredPort > 0 ? configuredPort
+        : persistedPorts.get(key);
+    int port = allocator.reserve(preferredPort);
+    persistedPorts.set(key, port);
+    return port;
+  }
+
+  private Path metadataDir() {
+    return config.getDataDir().resolve(METADATA_DIR_NAME);
+  }
+
+  private Path portStateFile() {
+    return config.getDataDir().resolve(PORTS_STATE_FILE_NAME);
+  }
+
+  private static String address(String host, int port) {
+    return host + ":" + port;
+  }
+
+  private static void deleteDirectory(Path directory) throws IOException {
+    if (!Files.exists(directory)) {
+      return;
+    }
+    try (Stream<Path> paths = Files.walk(directory)) {
+      Iterable<Path> deleteOrder =
+          () -> paths.sorted(Comparator.reverseOrder()).iterator();
+      for (Path path : deleteOrder) {
+        Files.deleteIfExists(path);
+      }
+    }
+  }
+
+  static final class PreparedConfiguration {
+    private final OzoneConfiguration configuration;
+    private final int scmPort;
+    private final int omPort;
+
+    PreparedConfiguration(OzoneConfiguration configuration, int scmPort,
+        int omPort) {
+      this.configuration = Objects.requireNonNull(configuration,
+          "configuration");
+      this.scmPort = scmPort;
+      this.omPort = omPort;
+    }
+
+    OzoneConfiguration getConfiguration() {
+      return configuration;
+    }
+
+    int getScmPort() {
+      return scmPort;
+    }
+
+    int getOmPort() {
+      return omPort;
+    }
+  }
+
+  /**
+   * Allocates distinct local ports for the configuration being prepared.
+   */
+  static final class PortAllocator {
+    private final Set<Integer> reserved = new HashSet<>();
+
+    int reserve(int preferredPort) throws IOException {
+      if (preferredPort > 0) {
+        return reserveConfiguredPort(preferredPort);
+      }
+
+      while (true) {
+        int candidate = nextFreePort();
+        if (reserved.add(candidate)) {
+          return candidate;
+        }
+      }
+    }
+
+    private int reserveConfiguredPort(int port) throws IOException {
+      if (port > 65_535) {
+        throw new IOException("Port " + port + " is outside the valid range.");
+      }
+      if (!reserved.add(port)) {
+        throw new IOException("Port " + port
+            + " is configured more than once.");
+      }
+      return port;
+    }
+
+    private static int nextFreePort() throws IOException {
+      try (ServerSocket socket = new ServerSocket(0)) {
+        socket.setReuseAddress(false);
+        return socket.getLocalPort();
+      }
+    }
+  }
+
+  /**
+   * Persists dynamic port choices so repeated local starts keep stable
+   * client-facing endpoints until the user explicitly formats storage.
+   */
+  static final class PersistedPortState {
+    private final Path path;
+    private final Properties properties = new Properties();
+    private boolean dirty;
+
+    private PersistedPortState(Path path) {
+      this.path = path;
+    }
+
+    static PersistedPortState load(Path path) throws IOException {
+      PersistedPortState state = new PersistedPortState(path);
+      if (!Files.exists(path)) {
+        return state;
+      }
+      if (!Files.isRegularFile(path)) {
+        throw new IOException("Local Ozone port state file " + path
+            + " is not a regular file.");
+      }
+      try (InputStream input = Files.newInputStream(path)) {
+        state.properties.load(input);
+      }
+      return state;
+    }
+
+    int get(String key) throws IOException {
+      String value = properties.getProperty(key);
+      if (value == null) {
+        return 0;
+      }
+      String trimmedValue = value.trim();
+      if (trimmedValue.isEmpty()) {
+        return 0;
+      }
+      try {
+        int port = Integer.parseInt(trimmedValue);
+        if (port < 0 || port > 65_535) {
+          throw invalidPortValue(key, value);
+        }
+        return port;
+      } catch (NumberFormatException ex) {
+        throw invalidPortValue(key, value);
+      }
+    }
+
+    void requireKeys(String[] keys) throws IOException {
+      for (String key : keys) {
+        if (get(key) <= 0) {
+          throw new IOException("Local Ozone port state file " + path
+              + " is missing required port key " + key + ".");
+        }
+      }
+    }
+
+    void set(String key, int port) {
+      String value = Integer.toString(port);
+      if (!value.equals(properties.getProperty(key))) {
+        properties.setProperty(key, value);
+        dirty = true;
+      }
+    }
+
+    void store() throws IOException {
+      if (!dirty && Files.exists(path)) {
+        return;
+      }
+      try (OutputStream output = Files.newOutputStream(path)) {
+        properties.store(output, "Local Ozone reserved ports");
+      }
+      dirty = false;
+    }
+
+    private static IOException invalidPortValue(String key, String value) {
+      return new IOException("Invalid port value for " + key + ": " + value);
+    }
+  }
+}
diff --git 
a/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneCluster.java
 
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneCluster.java
new file mode 100644
index 00000000000..c62c108ce71
--- /dev/null
+++ 
b/hadoop-ozone/tools/src/test/java/org/apache/hadoop/ozone/local/TestLocalOzoneCluster.java
@@ -0,0 +1,356 @@
+/*
+ * 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.ozone.local;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_SAFEMODE_MIN_DATANODE;
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_SAFEMODE_PIPELINE_CREATION;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.HDDS_CONTAINER_RATIS_ENABLED_KEY;
+import static 
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_CLIENT_ADDRESS_KEY;
+import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_NAMES;
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_METADATA_DIRS;
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_REPLICATION;
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_REPLICATION_TYPE;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_ADDRESS_KEY;
+import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_KEY;
+import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_TYPE_KEY;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Properties;
+import org.apache.hadoop.hdds.client.ReplicationFactor;
+import org.apache.hadoop.hdds.client.ReplicationType;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Tests for {@link LocalOzoneCluster}.
+ */
+class TestLocalOzoneCluster {
+
+  private static final String METADATA_DIR_NAME = "metadata";
+  private static final String PORTS_STATE_FILE_NAME = "ports.properties";
+
+  @TempDir
+  private Path tempDir;
+
+  @Test
+  void startPreparesConfiguration() throws Exception {
+    Path dataDir = tempDir.resolve("local-ozone");
+    LocalOzoneClusterConfig config =
+        LocalOzoneClusterConfig.builder(dataDir).build();
+
+    try (LocalOzoneCluster cluster = newCluster(config)) {
+      cluster.start();
+
+      assertTrue(Files.isDirectory(metadataDir(dataDir)));
+      assertTrue(Files.isRegularFile(portStateFile(dataDir)));
+      assertTrue(cluster.getScmPort() > 0);
+      assertTrue(cluster.getOmPort() > 0);
+      assertEquals(-1, cluster.getS3gPort());
+      assertEquals("", cluster.getS3Endpoint());
+    }
+  }
+
+  @Test
+  void prepareConfigurationCreatesBaseLayoutAndLocalDefaults()
+      throws Exception {
+    Path dataDir = tempDir.resolve("local-ozone");
+    LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(dataDir)
+        .setScmPort(9860)
+        .setOmPort(9862)
+        .setDatanodes(2)
+        .build();
+
+    LocalOzoneCluster.PreparedConfiguration prepared = prepare(config);
+    OzoneConfiguration conf = prepared.getConfiguration();
+
+    assertTrue(Files.isDirectory(metadataDir(dataDir)));
+    assertTrue(Files.isRegularFile(portStateFile(dataDir)));
+    assertEquals(metadataDir(dataDir).toString(), 
conf.get(OZONE_METADATA_DIRS));
+    assertEquals(ReplicationFactor.ONE.name(), conf.get(OZONE_REPLICATION));
+    assertEquals(ReplicationType.STAND_ALONE.name(),
+        conf.get(OZONE_REPLICATION_TYPE));
+    assertEquals(ReplicationFactor.ONE.name(),
+        conf.get(OZONE_SERVER_DEFAULT_REPLICATION_KEY));
+    assertEquals(ReplicationType.STAND_ALONE.name(),
+        conf.get(OZONE_SERVER_DEFAULT_REPLICATION_TYPE_KEY));
+    assertFalse(conf.getBoolean(HDDS_CONTAINER_RATIS_ENABLED_KEY, true));
+    assertFalse(conf.getBoolean(HDDS_SCM_SAFEMODE_PIPELINE_CREATION, true));
+    assertEquals(2, conf.getInt(HDDS_SCM_SAFEMODE_MIN_DATANODE, 0));
+    assertTrue(conf.get(OZONE_SCM_CLIENT_ADDRESS_KEY).endsWith(":9860"));
+    assertTrue(conf.get(OZONE_OM_ADDRESS_KEY).endsWith(":9862"));
+    assertTrue(conf.getTrimmedStringCollection(OZONE_SCM_NAMES).iterator()
+        .next().contains(":"));
+    assertEquals(9860, prepared.getScmPort());
+    assertEquals(9862, prepared.getOmPort());
+  }
+
+  @Test
+  void prepareConfigurationPersistsDynamicPortsAcrossInstances()
+      throws Exception {
+    Path dataDir = tempDir.resolve("local-ozone");
+    LocalOzoneClusterConfig config =
+        LocalOzoneClusterConfig.builder(dataDir).build();
+
+    LocalOzoneCluster.PreparedConfiguration first = prepare(config);
+    LocalOzoneCluster.PreparedConfiguration second = prepare(config);
+
+    assertTrue(first.getScmPort() > 0);
+    assertTrue(first.getOmPort() > 0);
+    assertEquals(first.getScmPort(), second.getScmPort());
+    assertEquals(first.getOmPort(), second.getOmPort());
+  }
+
+  @Test
+  void prepareConfigurationIsIdempotent() throws Exception {
+    LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(
+        tempDir.resolve("local-ozone")).build();
+
+    try (LocalOzoneCluster cluster = newCluster(config)) {
+      LocalOzoneCluster.PreparedConfiguration first =
+          cluster.prepareConfiguration();
+      LocalOzoneCluster.PreparedConfiguration second =
+          cluster.prepareConfiguration();
+
+      assertSame(first, second);
+      assertEquals(first.getScmPort(), second.getScmPort());
+      assertEquals(first.getOmPort(), second.getOmPort());
+    }
+  }
+
+  @Test
+  void prepareConfigurationRejectsDuplicateConfiguredPorts() {
+    LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(
+            tempDir.resolve("local-ozone"))
+        .setScmPort(9860)
+        .setOmPort(9860)
+        .build();
+
+    IOException error = assertPrepareFails(config);
+
+    assertMessageContains(error, "more than once");
+  }
+
+  @Test
+  void formatIfNeededPreservesExistingState() throws Exception {
+    Path dataDir = tempDir.resolve("local-ozone");
+    Path marker = writeMarker(dataDir, "existing");
+    LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(dataDir)
+        .setFormatMode(LocalOzoneClusterConfig.FormatMode.IF_NEEDED)
+        .build();
+
+    prepare(config);
+
+    assertTrue(Files.exists(marker));
+    assertTrue(Files.isDirectory(metadataDir(dataDir)));
+  }
+
+  @Test
+  void formatAlwaysClearsExistingState() throws Exception {
+    Path dataDir = tempDir.resolve("local-ozone");
+    Path marker = writeMarker(dataDir, "stale");
+    LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(dataDir)
+        .setFormatMode(LocalOzoneClusterConfig.FormatMode.ALWAYS)
+        .build();
+
+    prepare(config);
+
+    assertFalse(Files.exists(marker));
+    assertTrue(Files.isDirectory(metadataDir(dataDir)));
+  }
+
+  @Test
+  void formatNeverAcceptsExistingLayout() throws Exception {
+    Path dataDir = tempDir.resolve("local-ozone");
+    LocalOzoneClusterConfig initial =
+        LocalOzoneClusterConfig.builder(dataDir).build();
+    LocalOzoneCluster.PreparedConfiguration initialConfiguration =
+        prepare(initial);
+    LocalOzoneClusterConfig never = LocalOzoneClusterConfig.builder(dataDir)
+        .setFormatMode(LocalOzoneClusterConfig.FormatMode.NEVER)
+        .build();
+
+    LocalOzoneCluster.PreparedConfiguration prepared = prepare(never);
+
+    assertEquals(initialConfiguration.getScmPort(), prepared.getScmPort());
+    assertEquals(initialConfiguration.getOmPort(), prepared.getOmPort());
+  }
+
+  @Test
+  void formatNeverRejectsMissingLayout() {
+    LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(
+            tempDir.resolve("missing-local-ozone"))
+        .setFormatMode(LocalOzoneClusterConfig.FormatMode.NEVER)
+        .build();
+
+    IOException error = assertPrepareFails(config);
+
+    assertMessageContains(error, "does not exist");
+  }
+
+  @Test
+  void formatNeverRejectsInvalidLayout() throws Exception {
+    Path dataDir = tempDir.resolve("local-ozone");
+    Files.createDirectories(dataDir);
+    LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(dataDir)
+        .setFormatMode(LocalOzoneClusterConfig.FormatMode.NEVER)
+        .build();
+
+    IOException error = assertPrepareFails(config);
+
+    assertMessageContains(error, METADATA_DIR_NAME);
+  }
+
+  @Test
+  void formatNeverRejectsMissingPortState() throws Exception {
+    Path dataDir = tempDir.resolve("local-ozone");
+    Files.createDirectories(metadataDir(dataDir));
+    LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(dataDir)
+        .setFormatMode(LocalOzoneClusterConfig.FormatMode.NEVER)
+        .build();
+
+    IOException error = assertPrepareFails(config);
+
+    assertMessageContains(error, PORTS_STATE_FILE_NAME);
+  }
+
+  @Test
+  void closeDeletesEphemeralDataDir() throws Exception {
+    Path dataDir = tempDir.resolve("ephemeral-local-ozone");
+    LocalOzoneClusterConfig config = LocalOzoneClusterConfig.builder(dataDir)
+        .setEphemeral(true)
+        .build();
+
+    try (LocalOzoneCluster cluster = newCluster(config)) {
+      cluster.prepareConfiguration();
+      writeMarker(dataDir, "ephemeral");
+    }
+
+    assertFalse(Files.exists(dataDir));
+  }
+
+  @Test
+  void prepareConfigurationRejectsRegularFileDataDir() throws Exception {
+    Path dataDir = tempDir.resolve("not-a-directory");
+    Files.write(dataDir, "file".getBytes(UTF_8));
+    LocalOzoneClusterConfig config =
+        LocalOzoneClusterConfig.builder(dataDir).build();
+
+    IOException error = assertPrepareFails(config);
+
+    assertMessageContains(error, "not a directory");
+    assertTrue(Files.isRegularFile(dataDir));
+  }
+
+  @Test
+  void prepareConfigurationRejectsCorruptPortsFile() throws Exception {
+    Path dataDir = tempDir.resolve("local-ozone");
+    Files.createDirectories(metadataDir(dataDir));
+    Files.write(portStateFile(dataDir), 
"scm.client=not-a-port\n".getBytes(UTF_8));
+    LocalOzoneClusterConfig config =
+        LocalOzoneClusterConfig.builder(dataDir).build();
+
+    IOException error = assertPrepareFails(config);
+
+    assertMessageContains(error, "Invalid port value");
+  }
+
+  @Test
+  void persistedPortFileContainsDistinctAllocatedPorts() throws Exception {
+    Path dataDir = tempDir.resolve("local-ozone");
+    LocalOzoneClusterConfig config =
+        LocalOzoneClusterConfig.builder(dataDir).build();
+
+    prepare(config);
+
+    Properties properties = loadPortState(dataDir);
+    assertPositivePort(properties, "scm.client");
+    assertPositivePort(properties, "scm.block");
+    assertPositivePort(properties, "scm.datanode");
+    assertPositivePort(properties, "scm.security");
+    assertPositivePort(properties, "scm.http");
+    assertPositivePort(properties, "scm.https");
+    assertPositivePort(properties, "scm.ratis");
+    assertPositivePort(properties, "scm.grpc");
+    assertPositivePort(properties, "om.rpc");
+    assertPositivePort(properties, "om.http");
+    assertPositivePort(properties, "om.ratis");
+    assertNotEquals(properties.getProperty("scm.client"),
+        properties.getProperty("om.rpc"));
+  }
+
+  private LocalOzoneCluster.PreparedConfiguration prepare(
+      LocalOzoneClusterConfig config) throws IOException {
+    try (LocalOzoneCluster cluster = newCluster(config)) {
+      return cluster.prepareConfiguration();
+    }
+  }
+
+  private LocalOzoneCluster newCluster(LocalOzoneClusterConfig config) {
+    return new LocalOzoneCluster(config, new OzoneConfiguration());
+  }
+
+  private IOException assertPrepareFails(LocalOzoneClusterConfig config) {
+    return assertThrows(IOException.class, () -> {
+      try (LocalOzoneCluster cluster = newCluster(config)) {
+        cluster.prepareConfiguration();
+      }
+    });
+  }
+
+  private void assertMessageContains(IOException error, String expectedText) {
+    assertTrue(error.getMessage().contains(expectedText), error.getMessage());
+  }
+
+  private Path writeMarker(Path dataDir, String content) throws IOException {
+    Files.createDirectories(dataDir);
+    Path marker = dataDir.resolve("marker.txt");
+    Files.write(marker, content.getBytes(UTF_8));
+    return marker;
+  }
+
+  private Properties loadPortState(Path dataDir) throws IOException {
+    Properties properties = new Properties();
+    try (InputStream input = Files.newInputStream(portStateFile(dataDir))) {
+      properties.load(input);
+    }
+    return properties;
+  }
+
+  private void assertPositivePort(Properties properties, String key) {
+    assertTrue(Integer.parseInt(properties.getProperty(key)) > 0, key);
+  }
+
+  private Path metadataDir(Path dataDir) {
+    return dataDir.resolve(METADATA_DIR_NAME);
+  }
+
+  private Path portStateFile(Path dataDir) {
+    return dataDir.resolve(PORTS_STATE_FILE_NAME);
+  }
+}


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

Reply via email to