Skip to content

Commit b3b4f32

Browse files
authored
HDDS-14987. OM Web UI dashboard for Ozone Snapshot (list snapshot) (#10055)
1 parent bee32d5 commit b3b4f32

6 files changed

Lines changed: 240 additions & 1 deletion

File tree

hadoop-hdds/framework/src/main/resources/webapps/static/templates/menu.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,6 @@
5858
</li>
5959
<li ng-show="$ctrl.iostatus"><a ng-href="{{$ctrl.ioLinkHref}}">IO Status</a></li>
6060
<li ng-show="$ctrl.scanner"><a ng-href="{{$ctrl.scannerLinkHref}}">Data Scanner</a></li>
61-
<li ng-show="$ctrl.snapshot"><a ng-href="{{$ctrl.snapshotLinkHref}}">Ozone Snapshot</a></li>
61+
<li ng-show="$ctrl.snapshot"><a ng-href="{{$ctrl.snapshotLinkHref}}">Snapshots</a></li>
6262
</ul>
6363
</div><!--/.nav-collapse -->

hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManagerHttpServer.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public OzoneManagerHttpServer(MutableConfigurationSource conf,
3636
super(conf, "ozoneManager");
3737
addServlet("serviceList", OZONE_OM_SERVICE_LIST_HTTP_ENDPOINT,
3838
ServiceListJSONServlet.class);
39+
addServlet("snapshotList", "/snapshotList",
40+
SnapshotListJSONServlet.class);
3941
addServlet("dbCheckpoint", OZONE_DB_CHECKPOINT_HTTP_ENDPOINT,
4042
OMDBCheckpointServlet.class);
4143
addServlet("dbCheckpointv2", OZONE_DB_CHECKPOINT_HTTP_ENDPOINT_V2,
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.hadoop.ozone.om;
19+
20+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
21+
import com.fasterxml.jackson.databind.ObjectMapper;
22+
import com.fasterxml.jackson.databind.SerializationFeature;
23+
import java.io.IOException;
24+
import java.io.PrintWriter;
25+
import javax.servlet.ServletException;
26+
import javax.servlet.http.HttpServlet;
27+
import javax.servlet.http.HttpServletRequest;
28+
import javax.servlet.http.HttpServletResponse;
29+
import org.apache.commons.lang3.StringUtils;
30+
import org.apache.hadoop.ozone.OzoneConsts;
31+
import org.apache.hadoop.ozone.om.helpers.SnapshotInfo;
32+
import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
33+
import org.slf4j.Logger;
34+
import org.slf4j.LoggerFactory;
35+
36+
/**
37+
* Provides REST access to Ozone Snapshot List.
38+
*/
39+
public class SnapshotListJSONServlet extends HttpServlet {
40+
41+
private static final Logger LOG =
42+
LoggerFactory.getLogger(SnapshotListJSONServlet.class);
43+
private static final long serialVersionUID = 1L;
44+
45+
private transient OzoneManager om;
46+
47+
/**
48+
* Jackson mix-in to ignore protobuf getter in SnapshotInfo.
49+
*/
50+
@JsonIgnoreProperties({"protobuf", "createTransactionInfo", "lastTransactionInfo"})
51+
abstract static class SnapshotInfoMixin {
52+
}
53+
54+
@Override
55+
public void init() throws ServletException {
56+
this.om = (OzoneManager) getServletContext()
57+
.getAttribute(OzoneConsts.OM_CONTEXT_ATTRIBUTE);
58+
}
59+
60+
@Override
61+
public void doGet(HttpServletRequest request, HttpServletResponse response) {
62+
try {
63+
String volume = request.getParameter("volume");
64+
String bucket = request.getParameter("bucket");
65+
66+
if (volume == null || volume.isEmpty() || bucket == null || bucket.isEmpty()) {
67+
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
68+
response.getWriter().write("Both volume and bucket parameters are required.");
69+
return;
70+
}
71+
72+
String prefix = request.getParameter("prefix");
73+
74+
final int maxKeys = 1000;
75+
76+
ObjectMapper objectMapper = new ObjectMapper();
77+
objectMapper.addMixIn(SnapshotInfo.class, SnapshotInfoMixin.class);
78+
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
79+
response.setContentType("application/json; charset=utf8");
80+
try (PrintWriter writer = response.getWriter()) {
81+
String lastSnapshot = null;
82+
do {
83+
ListSnapshotResponse listSnapshotResponse = om.listSnapshot(volume, bucket, prefix, lastSnapshot, maxKeys);
84+
writer.write(objectMapper.writeValueAsString(listSnapshotResponse.getSnapshotInfos()));
85+
lastSnapshot = listSnapshotResponse.getLastSnapshot();
86+
} while (StringUtils.isNotEmpty(lastSnapshot));
87+
}
88+
} catch (IOException e) {
89+
LOG.error("Caught an exception while processing SnapshotList request", e);
90+
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
91+
} catch (Exception e) {
92+
LOG.error("Unexpected error while processing SnapshotList request", e);
93+
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
94+
}
95+
}
96+
}

hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/om-snapshots.html

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,59 @@
1414
See the License for the specific language governing permissions and
1515
limitations under the License.
1616
-->
17+
<h1>Snapshots</h1>
18+
<div class="row">
19+
<div class="col-md-4">
20+
<div class="input-group">
21+
<span class="input-group-addon">Volume</span>
22+
<input type="text" class="form-control" ng-model="searchVolume" placeholder="volume">
23+
</div>
24+
</div>
25+
<div class="col-md-4">
26+
<div class="input-group">
27+
<span class="input-group-addon">Bucket</span>
28+
<input type="text" class="form-control" ng-model="searchBucket" placeholder="bucket">
29+
</div>
30+
</div>
31+
<div class="col-md-2">
32+
<button class="btn btn-primary" ng-click="$ctrl.listSnapshots(searchVolume, searchBucket)" ng-disabled="!searchVolume || !searchBucket">Search</button>
33+
</div>
34+
</div>
35+
36+
<div ng-if="!searchVolume || !searchBucket" style="color: grey; margin-top: 5px;">
37+
<i>Please enter both volume and bucket to search snapshots.</i>
38+
</div>
39+
40+
<div style="margin-bottom: 20px;"></div>
41+
42+
<table class="table table-bordered table-striped" ng-if="$ctrl.snapshots && $ctrl.snapshots.length > 0">
43+
<thead>
44+
<tr>
45+
<th>Name</th>
46+
<th>Status</th>
47+
<th>Creation Time</th>
48+
<th title="Total size of all files and directories visible in this snapshot.">Referenced Size</th>
49+
<th title="Size of data held exclusively by this snapshot. Deleting this snapshot will reclaim this much space.">Exclusive Size</th>
50+
<th>Snapshot ID</th>
51+
<th>Snapshot Path</th>
52+
</tr>
53+
</thead>
54+
<tbody>
55+
<tr ng-repeat="snapshot in $ctrl.snapshots">
56+
<td>{{snapshot.name}}</td>
57+
<td title="{{snapshot.snapshotStatus === 'SNAPSHOT_ACTIVE' ? 'active' : 'deletion in progress'}}">
58+
{{snapshot.snapshotStatus === 'SNAPSHOT_ACTIVE' ? 'active' : 'deletion in progress'}}
59+
</td>
60+
<td>{{snapshot.creationTime | date:'yyyy-MM-dd HH:mm:ss'}}</td>
61+
<td>{{$ctrl.formatBytes(snapshot.referencedSize)}}</td>
62+
<td>{{$ctrl.formatBytes(snapshot.exclusiveSize + snapshot.exclusiveSizeDeltaFromDirDeepCleaning)}}</td>
63+
<td>{{snapshot.snapshotId}}</td>
64+
<td>{{snapshot.snapshotPath}}</td>
65+
</tr>
66+
</tbody>
67+
</table>
1768

69+
<div style="margin-bottom: 20px;"></div>
1870

1971
<h1>Snapshot Diff Jobs</h1>
2072

hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/ozoneManager.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,38 @@
3838
var ctrl = this;
3939
ctrl.snapshotMetrics = [];
4040
ctrl.snapshotDiffJobs = [];
41+
ctrl.snapshots = [];
4142
ctrl.snapshotUsageMetrics = {
4243
'NumSnapshotActive': 0,
4344
'NumSnapshotDeleted': 0,
4445
'NumSnapshotCacheSize': 0
4546
};
47+
48+
ctrl.listSnapshots = function(volume, bucket) {
49+
if (volume && bucket) {
50+
$http.get("snapshotList?volume=" + volume + "&bucket=" + bucket)
51+
.then(function (result) {
52+
ctrl.snapshots = result.data;
53+
})
54+
.catch(function (error) {
55+
console.error("Error fetching snapshots:", error);
56+
ctrl.snapshots = [];
57+
});
58+
} else {
59+
ctrl.snapshots = [];
60+
}
61+
};
62+
63+
ctrl.formatBytes = function(bytes, decimals) {
64+
if (bytes == 0) return '0 Bytes';
65+
if (!bytes) return 'N/A';
66+
var k = 1024,
67+
dm = decimals + 1 || 3,
68+
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
69+
i = Math.floor(Math.log(bytes) / Math.log(k));
70+
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
71+
}
72+
4673
$scope.reverse = false;
4774
$scope.columnName = "jobId";
4875
let snapDiffJobsCopy = [];
@@ -57,6 +84,11 @@
5784
ctrl.snapshotUsageMetrics.NumSnapshotActive = metrics.NumSnapshotActive || 0;
5885
ctrl.snapshotUsageMetrics.NumSnapshotDeleted = metrics.NumSnapshotDeleted || 0;
5986
ctrl.snapshotUsageMetrics.NumSnapshotCacheSize = metrics.NumSnapshotCacheSize || 0;
87+
for (var key in metrics) {
88+
if (key.match(/NumSnapshot|NumCancelSnapshotDiff|NumListSnapshotDiffJob/)) {
89+
ctrl.snapshotMetrics.push({key: key, value: metrics[key]});
90+
}
91+
}
6092
}
6193
});
6294

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.hadoop.ozone.om;
19+
20+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
21+
import static org.junit.jupiter.api.Assertions.assertFalse;
22+
import static org.junit.jupiter.api.Assertions.assertTrue;
23+
24+
import com.fasterxml.jackson.databind.ObjectMapper;
25+
import java.util.Collections;
26+
import java.util.UUID;
27+
import org.apache.hadoop.ozone.om.helpers.SnapshotInfo;
28+
import org.apache.hadoop.util.Time;
29+
import org.junit.jupiter.api.Test;
30+
31+
/**
32+
* Test for SnapshotListJSONServlet.
33+
*/
34+
public class TestSnapshotListJSONServlet {
35+
36+
@Test
37+
public void testSerialization() {
38+
ObjectMapper objectMapper = new ObjectMapper();
39+
objectMapper.addMixIn(SnapshotInfo.class,
40+
SnapshotListJSONServlet.SnapshotInfoMixin.class);
41+
42+
String snapName = "snap1";
43+
SnapshotInfo snapshotInfo = SnapshotInfo.newInstance("vol1", "bucket1",
44+
snapName, UUID.randomUUID(), Time.now());
45+
46+
String json = assertDoesNotThrow(() ->
47+
objectMapper.writeValueAsString(Collections.singletonList(snapshotInfo)));
48+
49+
// Verify that the problematic fields are NOT in the JSON
50+
assertFalse(json.contains("protobuf"), "JSON should not contain protobuf field");
51+
assertFalse(json.contains("createTransactionInfo"), "JSON should not contain createTransactionInfo field");
52+
assertFalse(json.contains("lastTransactionInfo"), "JSON should not contain lastTransactionInfo field");
53+
54+
// Verify that the expected fields ARE in the JSON
55+
assertTrue(json.contains("\"name\":\"" + snapName + "\""), "JSON should contain snapshot name");
56+
}
57+
}

0 commit comments

Comments
 (0)