This is an automated email from the ASF dual-hosted git repository.
jojochuang 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 402e459b87e HDDS-16171. Tool to download OM metadata with snapshots.
(#11017)
402e459b87e is described below
commit 402e459b87e06aaecf0b976e0e5110cada94d115
Author: Sadanand Shenoy <[email protected]>
AuthorDate: Tue Aug 25 02:54:35 2026 +0530
HDDS-16171. Tool to download OM metadata with snapshots. (#11017)
Co-authored-by: Wei-Chiu Chuang <[email protected]>
Generated-by: Cursor <[email protected]>
---
.../hadoop/ozone/repair/om/DownloadOMDB.java | 205 ++++++++++++++++
.../apache/hadoop/ozone/repair/om/OMRepair.java | 3 +-
.../ozone/repair/om/TestDownloadOMDBTool.java | 260 +++++++++++++++++++++
3 files changed, 467 insertions(+), 1 deletion(-)
diff --git
a/hadoop-ozone/cli-repair/src/main/java/org/apache/hadoop/ozone/repair/om/DownloadOMDB.java
b/hadoop-ozone/cli-repair/src/main/java/org/apache/hadoop/ozone/repair/om/DownloadOMDB.java
new file mode 100644
index 00000000000..d9455859bc1
--- /dev/null
+++
b/hadoop-ozone/cli-repair/src/main/java/org/apache/hadoop/ozone/repair/om/DownloadOMDB.java
@@ -0,0 +1,205 @@
+/*
+ * 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.repair.om;
+
+import static
org.apache.hadoop.hdds.utils.HddsServerUtil.OZONE_RATIS_SNAPSHOT_COMPLETE_FLAG_NAME;
+import static org.apache.hadoop.ozone.OzoneConsts.OM_DB_NAME;
+import static org.apache.hadoop.ozone.OzoneConsts.OM_SNAPSHOT_DIR;
+import static
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_DB_CHECKPOINT_USE_INODE_BASED_KEY;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_NODES_KEY;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.apache.commons.io.FileUtils;
+import org.apache.hadoop.hdds.cli.HddsVersionProvider;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.utils.db.DBCheckpoint;
+import org.apache.hadoop.ozone.OmUtils;
+import org.apache.hadoop.ozone.ha.ConfUtils;
+import org.apache.hadoop.ozone.om.helpers.OMNodeDetails;
+import org.apache.hadoop.ozone.om.ratis_snapshot.OmRatisSnapshotProvider;
+import org.apache.hadoop.ozone.repair.RepairTool;
+import picocli.CommandLine;
+
+/**
+ * Tool to download OM metadata using the follower bootstrap checkpoint flow.
+ */
[email protected](
+ name = "download",
+ description = "Downloads OM metadata (om.db and db.snapshots) from an OM
node using the same "
+ + "checkpoint transfer flow as follower bootstrap.",
+ mixinStandardHelpOptions = true,
+ versionProvider = HddsVersionProvider.class
+)
+public class DownloadOMDB extends RepairTool {
+
+ @CommandLine.Option(
+ names = {"--service-id", "--om-service-id"},
+ description = "Ozone Manager Service ID",
+ required = false
+ )
+ private String omServiceId;
+
+ @CommandLine.Option(
+ names = {"--node-id"},
+ description = "Node ID of the OM to download om.db from. Required when
OM HA is configured.",
+ required = false
+ )
+ private String nodeId;
+
+ @CommandLine.Option(
+ names = {"--output-dir"},
+ description = "Path where the downloaded OM metadata directory will be
written. "
+ + "The output matches follower bootstrap layout: om.db and
db.snapshots.",
+ required = true
+ )
+ private Path outputDir;
+
+ @CommandLine.Option(
+ names = {"--overwrite"},
+ description = "Overwrite output directory if it already exists."
+ )
+ private boolean overwrite;
+
+ @Override
+ public void execute() throws Exception {
+ OzoneConfiguration conf = getOzoneConf();
+ String effectiveServiceId = resolveServiceId(conf);
+
+ boolean outputExists = Files.exists(outputDir);
+ if (outputExists && !overwrite) {
+ fatal("Output directory already exists: %s. Use --overwrite to replace
it.",
+ outputDir.toAbsolutePath());
+ }
+
+ if (outputExists && !Files.isDirectory(outputDir)) {
+ fatal("Output path is not a directory: %s", outputDir.toAbsolutePath());
+ }
+
+ if (isDryRun()) {
+ info("Would download OM metadata at %s (using follower bootstrap flow).",
+ outputDir.toAbsolutePath());
+ return;
+ }
+
+ if (outputExists) {
+ FileUtils.forceDelete(outputDir.toFile());
+ }
+ Files.createDirectories(outputDir);
+
+ // This tool intentionally follows the inode-based follower bootstrap
transfer.
+ conf.setBoolean(OZONE_OM_DB_CHECKPOINT_USE_INODE_BASED_KEY, true);
+
+ Path snapshotWorkDir = Files.createTempDirectory(outputDir,
".omdb-bootstrap-");
+ DBCheckpoint checkpoint = null;
+ try {
+ checkpoint = downloadCheckpoint(conf, effectiveServiceId,
snapshotWorkDir);
+ Path checkpointRoot = checkpoint.getCheckpointLocation();
+ Path omDbPath = checkpointRoot.resolve(OM_DB_NAME);
+ if (!Files.isDirectory(omDbPath)) {
+ throw new IOException("Constructed OM DB directory not found in
checkpoint: " + omDbPath);
+ }
+ Path completionMarker =
checkpointRoot.resolve(OZONE_RATIS_SNAPSHOT_COMPLETE_FLAG_NAME);
+ if (Files.exists(completionMarker)) {
+ Files.delete(completionMarker);
+ }
+ moveDirectoryContents(checkpointRoot, outputDir);
+ checkpoint = null;
+ info("Successfully downloaded OM metadata at: %s (includes %s and %s if
present on the leader).",
+ outputDir.toAbsolutePath(), OM_DB_NAME, OM_SNAPSHOT_DIR);
+ } finally {
+ if (checkpoint != null) {
+ checkpoint.cleanupCheckpoint();
+ }
+ FileUtils.deleteQuietly(snapshotWorkDir.toFile());
+ }
+ }
+
+ private DBCheckpoint downloadCheckpoint(OzoneConfiguration conf,
+ String serviceId, Path snapshotWorkDir) throws Exception {
+ List<String> nodeIds = getCandidateNodeIds(conf, serviceId);
+ Exception lastFailure = null;
+ for (String candidateNodeId : nodeIds) {
+ OMNodeDetails omNodeDetails =
+ OMNodeDetails.getOMNodeDetailsFromConf(conf, serviceId,
candidateNodeId);
+ if (omNodeDetails == null) {
+ if (nodeId != null) {
+ fatal("Couldn't determine OM node from the given service-id: %s and
node-id: %s.",
+ serviceId, nodeId);
+ }
+ continue;
+ }
+ String providerNodeId = omNodeDetails.getNodeId();
+ if (providerNodeId == null) {
+ providerNodeId = "non-ha";
+ }
+ try (OmRatisSnapshotProvider provider = new OmRatisSnapshotProvider(
+ conf, snapshotWorkDir.toFile(),
+ Collections.singletonMap(providerNodeId, omNodeDetails))) {
+ return provider.downloadDBSnapshotFromLeader(providerNodeId);
+ } catch (Exception ex) {
+ lastFailure = ex;
+ if (nodeId != null) {
+ throw ex;
+ }
+ }
+ }
+ if (lastFailure != null) {
+ throw lastFailure;
+ }
+ fatal("Couldn't determine OM node from the given service-id: %s and
node-id: %s.",
+ serviceId, nodeId);
+ return null;
+ }
+
+ private List<String> getCandidateNodeIds(OzoneConfiguration conf,
+ String serviceId) {
+ if (nodeId != null) {
+ return Collections.singletonList(nodeId);
+ }
+ if (!OmUtils.isServiceIdsDefined(conf)) {
+ return Collections.singletonList(null);
+ }
+ String omNodesKey = ConfUtils.addKeySuffixes(OZONE_OM_NODES_KEY,
serviceId);
+ Collection<String> omNodeIds = conf.getTrimmedStringCollection(omNodesKey);
+ return new ArrayList<>(omNodeIds);
+ }
+
+ private static void moveDirectoryContents(Path sourceDir, Path targetDir)
+ throws IOException {
+ try (java.util.stream.Stream<Path> entries = Files.list(sourceDir)) {
+ for (Path entry : (Iterable<Path>) entries::iterator) {
+ Files.move(entry, targetDir.resolve(entry.getFileName()),
+ StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+ }
+
+ private String resolveServiceId(OzoneConfiguration conf) throws IOException {
+ if (omServiceId != null && !omServiceId.isEmpty()) {
+ return omServiceId;
+ }
+ return OmUtils.getOzoneManagerServiceId(conf);
+ }
+}
diff --git
a/hadoop-ozone/cli-repair/src/main/java/org/apache/hadoop/ozone/repair/om/OMRepair.java
b/hadoop-ozone/cli-repair/src/main/java/org/apache/hadoop/ozone/repair/om/OMRepair.java
index 7232ccf2aae..6965fcf6b25 100644
---
a/hadoop-ozone/cli-repair/src/main/java/org/apache/hadoop/ozone/repair/om/OMRepair.java
+++
b/hadoop-ozone/cli-repair/src/main/java/org/apache/hadoop/ozone/repair/om/OMRepair.java
@@ -33,7 +33,8 @@
TransactionInfoRepair.class,
QuotaRepair.class,
CompactOMDB.class,
- OMRatisLogRepair.class
+ OMRatisLogRepair.class,
+ DownloadOMDB.class
},
description = "Operational tool to repair OM.")
@MetaInfServices(RepairSubcommand.class)
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/repair/om/TestDownloadOMDBTool.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/repair/om/TestDownloadOMDBTool.java
new file mode 100644
index 00000000000..a31e21c2a2a
--- /dev/null
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/repair/om/TestDownloadOMDBTool.java
@@ -0,0 +1,260 @@
+/*
+ * 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.repair.om;
+
+import static org.apache.hadoop.ozone.OzoneConsts.OM_DB_NAME;
+import static org.apache.hadoop.ozone.OzoneConsts.OM_SNAPSHOT_DIR;
+import static
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_HTTP_ENDPOINT_V2;
+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_NODES_KEY;
+import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_SERVICE_IDS_KEY;
+import static org.apache.ozone.test.OzoneTestBase.uniqueObjectName;
+import static org.assertj.core.api.Assertions.assertThat;
+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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.utils.IOUtils;
+import org.apache.hadoop.ozone.DataTestUtil;
+import org.apache.hadoop.ozone.MiniOzoneCluster;
+import org.apache.hadoop.ozone.MiniOzoneHAClusterImpl;
+import org.apache.hadoop.ozone.OzoneConfigKeys;
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneBucket;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.OzoneSnapshot;
+import org.apache.hadoop.ozone.ha.ConfUtils;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.ratis_snapshot.OmRatisSnapshotProvider;
+import org.apache.hadoop.ozone.repair.OzoneRepair;
+import org.apache.ozone.test.GenericTestUtils;
+import org.apache.ozone.test.GenericTestUtils.LogCapturer;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Integration tests for `ozone repair om download`.
+ */
+public class TestDownloadOMDBTool {
+
+ private static final String OM_SERVICE_ID = "om-service-download-test";
+ private static MiniOzoneHAClusterImpl cluster;
+ private static OzoneConfiguration conf;
+
+ private GenericTestUtils.PrintStreamCapturer out;
+ private GenericTestUtils.PrintStreamCapturer err;
+
+ @BeforeAll
+ public static void init() throws Exception {
+ conf = new OzoneConfiguration();
+ cluster = MiniOzoneCluster.newHABuilder(conf)
+ .setOMServiceId(OM_SERVICE_ID)
+ .setNumOfOzoneManagers(3)
+ .setNumOfActiveOMs(3)
+ .build();
+ cluster.waitForClusterToBeReady();
+ }
+
+ @AfterAll
+ public static void cleanup() {
+ IOUtils.closeQuietly(cluster);
+ }
+
+ @BeforeEach
+ public void setup() {
+ out = GenericTestUtils.captureOut();
+ err = GenericTestUtils.captureErr();
+ }
+
+ @AfterEach
+ public void reset() {
+ IOUtils.closeQuietly(out, err);
+ }
+
+ @Test
+ public void testDownloadConstructsOmDbUsingV2Transfer(@TempDir Path tempDir)
throws Exception {
+ Path outputDir = tempDir.resolve("downloaded-metadata");
+
+ LogCapturer providerLog =
LogCapturer.captureLogs(OmRatisSnapshotProvider.class);
+ try {
+ int exitCode = new OzoneRepair().getCmd().execute(withHAConf(new
String[] {
+ "om", "download",
+ "--service-id", OM_SERVICE_ID,
+ "--output-dir", outputDir.toString()
+ }));
+
+ assertEquals(0, exitCode, err.getOutput());
+ Path omDbDir = outputDir.resolve(OM_DB_NAME);
+ assertTrue(Files.isDirectory(omDbDir), "Expected downloaded om.db
directory to exist.");
+ assertTrue(Files.exists(omDbDir.resolve("CURRENT")), "Expected RocksDB
CURRENT file in downloaded om.db.");
+
assertThat(providerLog.getOutput()).contains(OZONE_DB_CHECKPOINT_HTTP_ENDPOINT_V2);
+ } finally {
+ providerLog.stopCapturing();
+ }
+ }
+
+ @Test
+ public void testDownloadPreservesDbSnapshots(@TempDir Path tempDir) throws
Exception {
+ String volumeName = uniqueObjectName("vol");
+ String bucketName = uniqueObjectName("buck");
+ String snapshotName = uniqueObjectName("snap");
+ String keyName = uniqueObjectName("key");
+
+ OzoneClient client = cluster.newClient();
+ try {
+ ObjectStore store = client.getObjectStore();
+ OzoneBucket bucket = DataTestUtil.createVolumeAndBucket(client,
volumeName, bucketName);
+ DataTestUtil.createKey(bucket, keyName,
"snapshot-payload".getBytes(StandardCharsets.UTF_8));
+
+ store.createSnapshot(volumeName, bucketName, snapshotName);
+
+ OzoneSnapshot createdSnapshot = null;
+ Iterator<OzoneSnapshot> snapshots =
+ store.listSnapshot(volumeName, bucketName, "", null);
+ while (snapshots.hasNext()) {
+ OzoneSnapshot snapshot = snapshots.next();
+ if (snapshotName.equals(snapshot.getName())) {
+ createdSnapshot = snapshot;
+ break;
+ }
+ }
+ assertNotNull(createdSnapshot, "Expected Ozone snapshot to exist before
download.");
+ UUID snapshotId = createdSnapshot.getSnapshotId();
+
+ OzoneManager leader = cluster.getOMLeader();
+ String leaderNodeId = leader.getOMNodeId();
+ Path outputDir = tempDir.resolve("downloaded-metadata-with-snapshots");
+
+ int exitCode = new OzoneRepair().getCmd().execute(withHAConf(new
String[] {
+ "om", "download",
+ "--service-id", OM_SERVICE_ID,
+ "--node-id", leaderNodeId,
+ "--output-dir", outputDir.toString()
+ }));
+
+ assertEquals(0, exitCode, err.getOutput());
+ Path omDbDir = outputDir.resolve(OM_DB_NAME);
+ Path snapshotsDir = outputDir.resolve(OM_SNAPSHOT_DIR);
+ assertTrue(Files.isDirectory(omDbDir));
+ assertTrue(Files.exists(omDbDir.resolve("CURRENT")));
+ assertTrue(Files.isDirectory(snapshotsDir),
+ "Expected db.snapshots in downloaded metadata layout.");
+ Path checkpointState = snapshotsDir.resolve("checkpointState");
+ assertTrue(Files.isDirectory(checkpointState),
+ "Expected db.snapshots/checkpointState in downloaded metadata.");
+
+ String snapshotCheckpointPrefix = OM_DB_NAME + "-" + snapshotId;
+ Path snapshotYaml = checkpointState.resolve(snapshotCheckpointPrefix +
".yaml");
+ Path snapshotCheckpointDir =
checkpointState.resolve(snapshotCheckpointPrefix);
+ assertTrue(Files.exists(snapshotYaml),
+ "Expected snapshot checkpoint YAML for created Ozone snapshot.");
+ assertTrue(Files.isDirectory(snapshotCheckpointDir),
+ "Expected snapshot RocksDB checkpoint dir for created Ozone
snapshot.");
+ } finally {
+ IOUtils.closeQuietly(client);
+ }
+ }
+
+ @Test
+ public void testOverwriteReplacesExistingOutput(@TempDir Path tempDir)
throws Exception {
+ OzoneManager leader = cluster.getOMLeader();
+ String leaderNodeId = leader.getOMNodeId();
+ Path outputDir = tempDir.resolve("downloaded-metadata");
+ Files.createDirectories(outputDir);
+ Files.write(outputDir.resolve("OLD-MARKER"),
"stale".getBytes(StandardCharsets.UTF_8));
+
+ int firstRunExitCode = new OzoneRepair().getCmd().execute(withHAConf(new
String[] {
+ "om", "download",
+ "--service-id", OM_SERVICE_ID,
+ "--node-id", leaderNodeId,
+ "--output-dir", outputDir.toString()
+ }));
+ assertNotEquals(0, firstRunExitCode);
+ assertTrue(Files.exists(outputDir.resolve("OLD-MARKER")), "Marker should
remain without overwrite.");
+ assertThat(err.getOutput()).contains("Output directory already exists");
+
+ out.reset();
+ err.reset();
+ int secondRunExitCode = new OzoneRepair().getCmd().execute(withHAConf(new
String[] {
+ "om", "download",
+ "--service-id", OM_SERVICE_ID,
+ "--node-id", leaderNodeId,
+ "--output-dir", outputDir.toString(),
+ "--overwrite"
+ }));
+ assertEquals(0, secondRunExitCode, err.getOutput());
+ assertFalse(Files.exists(outputDir.resolve("OLD-MARKER")), "Overwrite
should remove stale output.");
+ assertTrue(Files.exists(outputDir.resolve(OM_DB_NAME).resolve("CURRENT")),
+ "Downloaded om.db should exist after overwrite.");
+ }
+
+ private String[] withHAConf(String[] existingArgs) throws IOException {
+ List<String> args = new ArrayList<>();
+ addConf(args, OZONE_OM_SERVICE_IDS_KEY);
+
+ String omNodesKey = ConfUtils.addKeySuffixes(OZONE_OM_NODES_KEY,
OM_SERVICE_ID);
+ addConf(args, omNodesKey);
+
+ Collection<String> omNodes = conf.getTrimmedStringCollection(omNodesKey);
+ for (String omNodeId : omNodes) {
+ addConf(args, ConfUtils.addKeySuffixes(OZONE_OM_ADDRESS_KEY,
OM_SERVICE_ID, omNodeId));
+ addOptionalConf(args,
ConfUtils.addKeySuffixes(OZONE_OM_HTTP_ADDRESS_KEY, OM_SERVICE_ID, omNodeId));
+ }
+
+ addOptionalConf(args, OzoneConfigKeys.OZONE_HTTP_POLICY_KEY);
+
+ args.addAll(Arrays.asList(existingArgs));
+ return args.toArray(new String[0]);
+ }
+
+ private void addConf(List<String> args, String key) throws IOException {
+ String value = conf.get(key);
+ if (value == null || value.isEmpty()) {
+ throw new IOException("Missing required config key for CLI test: " +
key);
+ }
+ args.add("-D");
+ args.add(key + "=" + value);
+ }
+
+ private void addOptionalConf(List<String> args, String key) {
+ String value = conf.get(key);
+ if (value == null || value.isEmpty()) {
+ return;
+ }
+ args.add("-D");
+ args.add(key + "=" + value);
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]