diff --git a/hadoop-hdds/framework/src/main/resources/webapps/static/templates/menu.html b/hadoop-hdds/framework/src/main/resources/webapps/static/templates/menu.html
index 1963a654383..d0a6907e586 100644
--- a/hadoop-hdds/framework/src/main/resources/webapps/static/templates/menu.html
+++ b/hadoop-hdds/framework/src/main/resources/webapps/static/templates/menu.html
@@ -58,6 +58,6 @@
IO Status
Data Scanner
- Ozone Snapshot
+ Snapshots
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManagerHttpServer.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManagerHttpServer.java
index 9c2688de812..fd79f153de3 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManagerHttpServer.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManagerHttpServer.java
@@ -36,6 +36,8 @@ public OzoneManagerHttpServer(MutableConfigurationSource conf,
super(conf, "ozoneManager");
addServlet("serviceList", OZONE_OM_SERVICE_LIST_HTTP_ENDPOINT,
ServiceListJSONServlet.class);
+ addServlet("snapshotList", "/snapshotList",
+ SnapshotListJSONServlet.class);
addServlet("dbCheckpoint", OZONE_DB_CHECKPOINT_HTTP_ENDPOINT,
OMDBCheckpointServlet.class);
addServlet("dbCheckpointv2", OZONE_DB_CHECKPOINT_HTTP_ENDPOINT_V2,
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/SnapshotListJSONServlet.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/SnapshotListJSONServlet.java
new file mode 100644
index 00000000000..5f0091be313
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/SnapshotListJSONServlet.java
@@ -0,0 +1,96 @@
+/*
+ * 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.om;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import java.io.IOException;
+import java.io.PrintWriter;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.om.helpers.SnapshotInfo;
+import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides REST access to Ozone Snapshot List.
+ */
+public class SnapshotListJSONServlet extends HttpServlet {
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(SnapshotListJSONServlet.class);
+ private static final long serialVersionUID = 1L;
+
+ private transient OzoneManager om;
+
+ /**
+ * Jackson mix-in to ignore protobuf getter in SnapshotInfo.
+ */
+ @JsonIgnoreProperties({"protobuf", "createTransactionInfo", "lastTransactionInfo"})
+ abstract static class SnapshotInfoMixin {
+ }
+
+ @Override
+ public void init() throws ServletException {
+ this.om = (OzoneManager) getServletContext()
+ .getAttribute(OzoneConsts.OM_CONTEXT_ATTRIBUTE);
+ }
+
+ @Override
+ public void doGet(HttpServletRequest request, HttpServletResponse response) {
+ try {
+ String volume = request.getParameter("volume");
+ String bucket = request.getParameter("bucket");
+
+ if (volume == null || volume.isEmpty() || bucket == null || bucket.isEmpty()) {
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ response.getWriter().write("Both volume and bucket parameters are required.");
+ return;
+ }
+
+ String prefix = request.getParameter("prefix");
+
+ final int maxKeys = 100;
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ objectMapper.addMixIn(SnapshotInfo.class, SnapshotInfoMixin.class);
+ objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
+ response.setContentType("application/json; charset=utf8");
+ try (PrintWriter writer = response.getWriter()) {
+ String lastSnapshot = null;
+ do {
+ ListSnapshotResponse listSnapshotResponse = om.listSnapshot(volume, bucket, prefix, lastSnapshot, maxKeys);
+ writer.write(objectMapper.writeValueAsString(listSnapshotResponse.getSnapshotInfos()));
+ lastSnapshot = listSnapshotResponse.getLastSnapshot();
+ } while (StringUtils.isNotEmpty(lastSnapshot));
+ }
+ } catch (IOException e) {
+ LOG.error("Caught an exception while processing SnapshotList request", e);
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ } catch (Exception e) {
+ LOG.error("Unexpected error while processing SnapshotList request", e);
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+}
diff --git a/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/om-snapshots.html b/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/om-snapshots.html
index ef718260ce0..eafb3f17941 100644
--- a/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/om-snapshots.html
+++ b/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/om-snapshots.html
@@ -14,7 +14,59 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
+Snapshots
+
+
+
+ Please enter both volume and bucket to search snapshots.
+
+
+
+
+
+
+
+ | Name |
+ Status |
+ Creation Time |
+ Referenced Size |
+ Exclusive Size |
+ Snapshot ID |
+ Snapshot Path |
+
+
+
+
+ | {{snapshot.name}} |
+
+ {{snapshot.snapshotStatus === 'SNAPSHOT_ACTIVE' ? 'active' : 'deletion in progress'}}
+ |
+ {{snapshot.creationTime | date:'yyyy-MM-dd HH:mm:ss'}} |
+ {{$ctrl.formatBytes(snapshot.referencedSize)}} |
+ {{$ctrl.formatBytes(snapshot.exclusiveSize + snapshot.exclusiveSizeDeltaFromDirDeepCleaning)}} |
+ {{snapshot.snapshotId}} |
+ {{snapshot.snapshotPath}} |
+
+
+
+
Snapshot Diff Jobs
diff --git a/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/ozoneManager.js b/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/ozoneManager.js
index e98d3f7ba3a..8ad270e8304 100644
--- a/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/ozoneManager.js
+++ b/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/ozoneManager.js
@@ -38,11 +38,38 @@
var ctrl = this;
ctrl.snapshotMetrics = [];
ctrl.snapshotDiffJobs = [];
+ ctrl.snapshots = [];
ctrl.snapshotUsageMetrics = {
'NumSnapshotActive': 0,
'NumSnapshotDeleted': 0,
'NumSnapshotCacheSize': 0
};
+
+ ctrl.listSnapshots = function(volume, bucket) {
+ if (volume && bucket) {
+ $http.get("snapshotList?volume=" + volume + "&bucket=" + bucket)
+ .then(function (result) {
+ ctrl.snapshots = result.data;
+ })
+ .catch(function (error) {
+ console.error("Error fetching snapshots:", error);
+ ctrl.snapshots = [];
+ });
+ } else {
+ ctrl.snapshots = [];
+ }
+ };
+
+ ctrl.formatBytes = function(bytes, decimals) {
+ if (bytes == 0) return '0 Bytes';
+ if (!bytes) return 'N/A';
+ var k = 1024,
+ dm = decimals + 1 || 3,
+ sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
+ i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+ }
+
$scope.reverse = false;
$scope.columnName = "jobId";
let snapDiffJobsCopy = [];
@@ -57,6 +84,11 @@
ctrl.snapshotUsageMetrics.NumSnapshotActive = metrics.NumSnapshotActive || 0;
ctrl.snapshotUsageMetrics.NumSnapshotDeleted = metrics.NumSnapshotDeleted || 0;
ctrl.snapshotUsageMetrics.NumSnapshotCacheSize = metrics.NumSnapshotCacheSize || 0;
+ for (var key in metrics) {
+ if (key.match(/NumSnapshot|NumCancelSnapshotDiff|NumListSnapshotDiffJob/)) {
+ ctrl.snapshotMetrics.push({key: key, value: metrics[key]});
+ }
+ }
}
});
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestSnapshotListJSONServlet.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestSnapshotListJSONServlet.java
new file mode 100644
index 00000000000..7458feb9d5b
--- /dev/null
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestSnapshotListJSONServlet.java
@@ -0,0 +1,57 @@
+/*
+ * 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.om;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.Collections;
+import java.util.UUID;
+import org.apache.hadoop.ozone.om.helpers.SnapshotInfo;
+import org.apache.hadoop.util.Time;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for SnapshotListJSONServlet.
+ */
+public class TestSnapshotListJSONServlet {
+
+ @Test
+ public void testSerialization() {
+ ObjectMapper objectMapper = new ObjectMapper();
+ objectMapper.addMixIn(SnapshotInfo.class,
+ SnapshotListJSONServlet.SnapshotInfoMixin.class);
+
+ String snapName = "snap1";
+ SnapshotInfo snapshotInfo = SnapshotInfo.newInstance("vol1", "bucket1",
+ snapName, UUID.randomUUID(), Time.now());
+
+ String json = assertDoesNotThrow(() ->
+ objectMapper.writeValueAsString(Collections.singletonList(snapshotInfo)));
+
+ // Verify that the problematic fields are NOT in the JSON
+ assertFalse(json.contains("protobuf"), "JSON should not contain protobuf field");
+ assertFalse(json.contains("createTransactionInfo"), "JSON should not contain createTransactionInfo field");
+ assertFalse(json.contains("lastTransactionInfo"), "JSON should not contain lastTransactionInfo field");
+
+ // Verify that the expected fields ARE in the JSON
+ assertTrue(json.contains("\"name\":\"" + snapName + "\""), "JSON should contain snapshot name");
+ }
+}