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

    +
    +
    +
    + Volume + +
    +
    +
    +
    + Bucket + +
    +
    +
    + +
    +
    + +
    + Please enter both volume and bucket to search snapshots. +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    NameStatusCreation TimeReferenced SizeExclusive SizeSnapshot IDSnapshot 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"); + } +}