Skip to content

Commit 2b80fa0

Browse files
committed
OM Snapshot UI -- backend code.
Change-Id: Iea21514269d37a233c816a2cc55be44fe788382a
1 parent e407268 commit 2b80fa0

6 files changed

Lines changed: 245 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: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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.hadoop.ozone.OzoneConsts;
30+
import org.apache.hadoop.ozone.om.helpers.SnapshotInfo;
31+
import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
32+
import org.slf4j.Logger;
33+
import org.slf4j.LoggerFactory;
34+
35+
/**
36+
* Provides REST access to Ozone Snapshot List.
37+
*/
38+
public class SnapshotListJSONServlet extends HttpServlet {
39+
40+
private static final Logger LOG =
41+
LoggerFactory.getLogger(SnapshotListJSONServlet.class);
42+
private static final long serialVersionUID = 1L;
43+
44+
private transient OzoneManager om;
45+
46+
/**
47+
* Jackson mix-in to ignore protobuf getter in SnapshotInfo.
48+
*/
49+
@JsonIgnoreProperties({"protobuf", "createTransactionInfo", "lastTransactionInfo"})
50+
abstract static class SnapshotInfoMixin {
51+
}
52+
53+
@Override
54+
public void init() throws ServletException {
55+
this.om = (OzoneManager) getServletContext()
56+
.getAttribute(OzoneConsts.OM_CONTEXT_ATTRIBUTE);
57+
}
58+
59+
@Override
60+
public void doGet(HttpServletRequest request, HttpServletResponse response) {
61+
try {
62+
String volume = request.getParameter("volume");
63+
String bucket = request.getParameter("bucket");
64+
65+
if (volume == null || volume.isEmpty() || bucket == null || bucket.isEmpty()) {
66+
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
67+
response.getWriter().write("Both volume and bucket parameters are required.");
68+
return;
69+
}
70+
71+
String prefix = request.getParameter("prefix");
72+
String startItem = request.getParameter("startItem");
73+
String maxKeysStr = request.getParameter("maxKeys");
74+
75+
int maxKeys = 100;
76+
if (maxKeysStr != null) {
77+
maxKeys = Integer.parseInt(maxKeysStr);
78+
}
79+
80+
ObjectMapper objectMapper = new ObjectMapper();
81+
objectMapper.addMixIn(SnapshotInfo.class, SnapshotInfoMixin.class);
82+
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
83+
response.setContentType("application/json; charset=utf8");
84+
PrintWriter writer = response.getWriter();
85+
try {
86+
ListSnapshotResponse listSnapshotResponse = om.listSnapshot(volume, bucket, prefix, startItem, maxKeys);
87+
writer.write(objectMapper.writeValueAsString(listSnapshotResponse.getSnapshotInfos()));
88+
} finally {
89+
if (writer != null) {
90+
writer.close();
91+
}
92+
}
93+
} catch (IOException e) {
94+
LOG.error("Caught an exception while processing SnapshotList request", e);
95+
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
96+
} catch (Exception e) {
97+
LOG.error("Unexpected error while processing SnapshotList request", e);
98+
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
99+
}
100+
}
101+
}

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)}}</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 com.fasterxml.jackson.databind.ObjectMapper;
21+
import java.util.Collections;
22+
import java.util.UUID;
23+
import org.apache.hadoop.ozone.om.helpers.SnapshotInfo;
24+
import org.apache.hadoop.util.Time;
25+
import org.junit.jupiter.api.Test;
26+
27+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
28+
import static org.junit.jupiter.api.Assertions.assertFalse;
29+
import static org.junit.jupiter.api.Assertions.assertTrue;
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)