This is an automated email from the ASF dual-hosted git repository.
sadanand48 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 b5af13b8dc1 HDDS-16389. Add command to display snapshot counts per
bucket. (#11217)
b5af13b8dc1 is described below
commit b5af13b8dc101adcc5cfa24e6501134b5c46e5d4
Author: Sadanand Shenoy <[email protected]>
AuthorDate: Fri Sep 18 14:01:56 2026 +0530
HDDS-16389. Add command to display snapshot counts per bucket. (#11217)
---
.../ozone/shell/snapshot/CountSnapshotHandler.java | 91 +++++++++
.../ozone/shell/snapshot/SnapshotCommands.java | 1 +
.../shell/snapshot/TestCountSnapshotHandler.java | 122 ++++++++++++
.../apache/hadoop/ozone/client/ObjectStore.java | 11 ++
.../ozone/client/protocol/ClientProtocol.java | 9 +
.../apache/hadoop/ozone/client/rpc/RpcClient.java | 7 +
.../main/java/org/apache/hadoop/ozone/OmUtils.java | 2 +
.../ozone/om/protocol/OzoneManagerProtocol.java | 12 ++
...OzoneManagerProtocolClientSideTranslatorPB.java | 25 +++
.../hadoop/ozone/snapshot/SnapshotBucketCount.java | 57 ++++++
.../ozone/snapshot/SnapshotCountResponse.java} | 48 +++--
.../src/main/proto/OmClientProtocol.proto | 22 +++
.../org/apache/hadoop/ozone/om/OzoneManager.java | 209 +++++++++++++++++++++
.../protocolPB/OzoneManagerRequestHandler.java | 29 +++
.../ozone/om/TestOzoneManagerSnapshotCount.java | 149 +++++++++++++++
.../hadoop/ozone/client/ClientProtocolStub.java | 7 +
16 files changed, 783 insertions(+), 18 deletions(-)
diff --git
a/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/snapshot/CountSnapshotHandler.java
b/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/snapshot/CountSnapshotHandler.java
new file mode 100644
index 00000000000..0f05ab68e44
--- /dev/null
+++
b/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/snapshot/CountSnapshotHandler.java
@@ -0,0 +1,91 @@
+/*
+ * 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.shell.snapshot;
+
+import java.io.IOException;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.shell.Handler;
+import org.apache.hadoop.ozone.shell.OzoneAddress;
+import org.apache.hadoop.ozone.snapshot.SnapshotBucketCount;
+import org.apache.hadoop.ozone.snapshot.SnapshotCountResponse;
+import picocli.CommandLine;
+
+/**
+ * ozone sh snapshot count.
+ * Displays bucket-wise snapshot distribution in the cluster.
+ */
[email protected](name = "count",
+ description = "Display bucket-wise snapshot distribution for the cluster.")
+public class CountSnapshotHandler extends Handler {
+
+ @CommandLine.Option(names = {"-b", "--bucket"},
+ description = "Optional bucket filter. Accepts either <bucket> or
<volume>/<bucket>.")
+ private String bucketFilter;
+
+ @Override
+ protected void execute(OzoneClient client, OzoneAddress address)
+ throws IOException {
+ SnapshotCountResponse response =
client.getObjectStore().snapshotCount(bucketFilter);
+ printObjectAsJson(new ShellSnapshotCountResponse(response));
+ }
+
+ private static final class ShellSnapshotCountResponse {
+ private final Count total;
+ private final java.util.Map<String, Count> buckets = new
java.util.TreeMap<>();
+
+ private ShellSnapshotCountResponse(SnapshotCountResponse response) {
+ this.total = new Count(response.getActive(), response.getDeleted(),
response.getTotal());
+ for (SnapshotBucketCount bucketCount : response.getBuckets()) {
+ buckets.put(bucketCount.getVolumeName() + "/" +
bucketCount.getBucketName(),
+ new Count(bucketCount.getActive(), bucketCount.getDeleted(),
bucketCount.getTotal()));
+ }
+ }
+
+ public Count getTotal() {
+ return total;
+ }
+
+ public java.util.Map<String, Count> getBuckets() {
+ return buckets;
+ }
+ }
+
+ private static final class Count {
+ private final long active;
+ private final long deleted;
+ private final long total;
+
+ private Count(long active, long deleted, long total) {
+ this.active = active;
+ this.deleted = deleted;
+ this.total = total;
+ }
+
+ public long getActive() {
+ return active;
+ }
+
+ public long getDeleted() {
+ return deleted;
+ }
+
+ public long getTotal() {
+ return total;
+ }
+ }
+}
diff --git
a/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/snapshot/SnapshotCommands.java
b/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/snapshot/SnapshotCommands.java
index 743d94a8a46..9bd151e7b6c 100644
---
a/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/snapshot/SnapshotCommands.java
+++
b/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/snapshot/SnapshotCommands.java
@@ -28,6 +28,7 @@
subcommands = {
CreateSnapshotHandler.class,
DeleteSnapshotHandler.class,
+ CountSnapshotHandler.class,
ListSnapshotHandler.class,
SnapshotDiffHandler.class,
ListSnapshotDiffHandler.class,
diff --git
a/hadoop-ozone/cli-shell/src/test/java/org/apache/hadoop/ozone/shell/snapshot/TestCountSnapshotHandler.java
b/hadoop-ozone/cli-shell/src/test/java/org/apache/hadoop/ozone/shell/snapshot/TestCountSnapshotHandler.java
new file mode 100644
index 00000000000..2b6fe18868e
--- /dev/null
+++
b/hadoop-ozone/cli-shell/src/test/java/org/apache/hadoop/ozone/shell/snapshot/TestCountSnapshotHandler.java
@@ -0,0 +1,122 @@
+/*
+ * 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.shell.snapshot;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.shell.OzoneAddress;
+import org.apache.hadoop.ozone.snapshot.SnapshotBucketCount;
+import org.apache.hadoop.ozone.snapshot.SnapshotCountResponse;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link CountSnapshotHandler}.
+ */
+public class TestCountSnapshotHandler {
+
+ private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ private final PrintStream originalOut = System.out;
+ private static final String DEFAULT_ENCODING = StandardCharsets.UTF_8.name();
+
+ @BeforeEach
+ public void setup() throws UnsupportedEncodingException {
+ outContent.reset();
+ System.setOut(new PrintStream(outContent, false, DEFAULT_ENCODING));
+ }
+
+ @AfterEach
+ public void teardown() {
+ System.setOut(originalOut);
+ }
+
+ @Test
+ public void testCountBucketWiseSnapshotDistribution() throws IOException {
+ CountSnapshotHandler handler = new CountSnapshotHandler();
+ OzoneClient client = mock(OzoneClient.class);
+ ObjectStore store = mock(ObjectStore.class);
+ when(client.getObjectStore()).thenReturn(store);
+
+ SnapshotCountResponse countResponse = new SnapshotCountResponse(3, 1, 4,
Arrays.asList(
+ new SnapshotBucketCount("vol1", "bucket1", 1, 1, 2),
+ new SnapshotBucketCount("vol2", "bucket3", 2, 0, 2)));
+ when(store.snapshotCount(null)).thenReturn(countResponse);
+
+ handler.execute(client, new OzoneAddress());
+
+ JsonNode output = new
ObjectMapper().readTree(outContent.toString(DEFAULT_ENCODING));
+ assertEquals(3, output.get("total").get("active").asInt());
+ assertEquals(1, output.get("total").get("deleted").asInt());
+ assertEquals(4, output.get("total").get("total").asInt());
+
+ assertEquals(2, output.get("buckets").size());
+ JsonNode bucket1Count = output.get("buckets").get("vol1/bucket1");
+ assertEquals(1, bucket1Count.get("active").asInt());
+ assertEquals(1, bucket1Count.get("deleted").asInt());
+ assertEquals(2, bucket1Count.get("total").asInt());
+
+ JsonNode bucket3Count = output.get("buckets").get("vol2/bucket3");
+ assertEquals(2, bucket3Count.get("active").asInt());
+ assertEquals(0, bucket3Count.get("deleted").asInt());
+ assertEquals(2, bucket3Count.get("total").asInt());
+ }
+
+ @Test
+ public void testCountBucketFilterIncludesMatchingEmptyBucket() throws
Exception {
+ CountSnapshotHandler handler = new CountSnapshotHandler();
+ setBucketFilter(handler, "vol1/bucket2");
+
+ OzoneClient client = mock(OzoneClient.class);
+ ObjectStore store = mock(ObjectStore.class);
+ when(client.getObjectStore()).thenReturn(store);
+ SnapshotCountResponse emptyBucketResponse = new SnapshotCountResponse(0,
0, 0,
+ Arrays.asList(new SnapshotBucketCount("vol1", "bucket2", 0, 0, 0)));
+ when(store.snapshotCount("vol1/bucket2")).thenReturn(emptyBucketResponse);
+
+ handler.execute(client, new OzoneAddress());
+ verify(store).snapshotCount("vol1/bucket2");
+
+ JsonNode output = new
ObjectMapper().readTree(outContent.toString(DEFAULT_ENCODING));
+ assertEquals(0, output.get("total").get("active").asInt());
+ assertEquals(0, output.get("total").get("deleted").asInt());
+ assertEquals(0, output.get("total").get("total").asInt());
+ assertEquals(1, output.get("buckets").size());
+ assertEquals(0,
output.get("buckets").get("vol1/bucket2").get("total").asInt());
+ }
+
+ private static void setBucketFilter(CountSnapshotHandler handler, String
value) throws Exception {
+ Field field = CountSnapshotHandler.class.getDeclaredField("bucketFilter");
+ field.setAccessible(true);
+ field.set(handler, value);
+ }
+}
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
index 2cc610d4007..cca80cf3825 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
@@ -50,6 +50,7 @@
import org.apache.hadoop.ozone.snapshot.CancelSnapshotDiffResponse;
import org.apache.hadoop.ozone.snapshot.ListSnapshotDiffJobResponse;
import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
+import org.apache.hadoop.ozone.snapshot.SnapshotCountResponse;
import org.apache.hadoop.ozone.snapshot.SnapshotDiffResponse;
import org.apache.hadoop.ozone.snapshot.SubmitSnapshotDiffResponse;
import org.apache.hadoop.security.UserGroupInformation;
@@ -622,6 +623,16 @@ public Iterator<OzoneSnapshot> listSnapshot(String
volumeName,
return new SnapshotIterator(volumeName, bucketName, snapshotPrefix,
prevSnapshot);
}
+ /**
+ * Get bucket-wise snapshot count distribution from snapshotInfo table.
+ * @param bucketFilter optional filter, accepts either bucket or
volume/bucket
+ * @return snapshot counts aggregated by bucket
+ * @throws IOException
+ */
+ public SnapshotCountResponse snapshotCount(String bucketFilter) throws
IOException {
+ return proxy.snapshotCount(bucketFilter);
+ }
+
/**
* An Iterator to iterate over {@link OzoneSnapshot} list.
*/
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
index 3a31a111d6e..c4355581db6 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
@@ -76,6 +76,7 @@
import org.apache.hadoop.ozone.snapshot.CancelSnapshotDiffResponse;
import org.apache.hadoop.ozone.snapshot.ListSnapshotDiffJobResponse;
import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
+import org.apache.hadoop.ozone.snapshot.SnapshotCountResponse;
import org.apache.hadoop.ozone.snapshot.SnapshotDiffResponse;
import org.apache.hadoop.ozone.snapshot.SubmitSnapshotDiffResponse;
import org.apache.hadoop.security.KerberosInfo;
@@ -1498,6 +1499,14 @@ ListSnapshotResponse listSnapshot(
String volumeName, String bucketName, String snapshotPrefix,
String prevSnapshot, int maxListResult) throws IOException;
+ /**
+ * Bucket-wise snapshot count distribution from snapshotInfo table.
+ * @param bucketFilter optional filter, accepts either bucket or
volume/bucket
+ * @return snapshot counts aggregated by bucket
+ * @throws IOException
+ */
+ SnapshotCountResponse snapshotCount(String bucketFilter) throws IOException;
+
/**
* Get the differences between two snapshots.
* @param volumeName Name of the volume to which the snapshotted bucket
belong
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
index 92f4df2b7c9..5b3c4f9e63c 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
@@ -183,6 +183,7 @@
import org.apache.hadoop.ozone.snapshot.CancelSnapshotDiffResponse;
import org.apache.hadoop.ozone.snapshot.ListSnapshotDiffJobResponse;
import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
+import org.apache.hadoop.ozone.snapshot.SnapshotCountResponse;
import org.apache.hadoop.ozone.snapshot.SnapshotDiffResponse;
import org.apache.hadoop.ozone.snapshot.SubmitSnapshotDiffResponse;
import org.apache.hadoop.security.UserGroupInformation;
@@ -1149,6 +1150,12 @@ public ListSnapshotResponse listSnapshot(
return ozoneManagerClient.listSnapshot(volumeName, bucketName,
snapshotPrefix, prevSnapshot, maxListResult);
}
+ @Override
+ public SnapshotCountResponse snapshotCount(String bucketFilter)
+ throws IOException {
+ return ozoneManagerClient.snapshotCount(bucketFilter);
+ }
+
/**
* Assign admin role to an accessId in a tenant.
* @param accessId access ID.
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
index 31530900cf2..dc676300b42 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
@@ -243,6 +243,7 @@ public static boolean isReadOnly(OMRequest omRequest) {
case TenantGetUserInfo:
case TenantListUser:
case ListSnapshot:
+ case SnapshotCount:
case RefetchSecretKey:
case RangerBGSync:
// RangerBGSync is a read operation in the sense that it doesn't directly
@@ -389,6 +390,7 @@ public static boolean shouldSendToFollower(OMRequest
omRequest) {
case TenantGetUserInfo:
case TenantListUser:
case ListSnapshot:
+ case SnapshotCount:
case RefetchSecretKey:
case GetKeyInfo:
case GetSnapshotInfo:
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
index 7cea19d7a90..3eea12d53f8 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
@@ -76,6 +76,7 @@
import org.apache.hadoop.ozone.snapshot.CancelSnapshotDiffResponse;
import org.apache.hadoop.ozone.snapshot.ListSnapshotDiffJobResponse;
import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
+import org.apache.hadoop.ozone.snapshot.SnapshotCountResponse;
import org.apache.hadoop.ozone.snapshot.SnapshotDiffResponse;
import org.apache.hadoop.ozone.snapshot.SubmitSnapshotDiffResponse;
import org.apache.hadoop.ozone.upgrade.UpgradeFinalization;
@@ -789,6 +790,17 @@ default ListSnapshotResponse listSnapshot(
"this to be implemented");
}
+ /**
+ * Bucket-wise snapshot count distribution from snapshotInfo table.
+ * @param bucketFilter optional filter, accepts either bucket or
volume/bucket
+ * @return snapshot counts aggregated by bucket
+ * @throws IOException
+ */
+ default SnapshotCountResponse snapshotCount(String bucketFilter) throws
IOException {
+ throw new UnsupportedOperationException("OzoneManager does not require " +
+ "this to be implemented");
+ }
+
/**
* Get the differences between two snapshots.
* @param volumeName Name of the volume to which the snapshotted bucket
belong
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index b5a9e3a2bbc..bce59dd6d10 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -258,6 +258,8 @@
import org.apache.hadoop.ozone.snapshot.CancelSnapshotDiffResponse;
import org.apache.hadoop.ozone.snapshot.ListSnapshotDiffJobResponse;
import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
+import org.apache.hadoop.ozone.snapshot.SnapshotBucketCount;
+import org.apache.hadoop.ozone.snapshot.SnapshotCountResponse;
import org.apache.hadoop.ozone.snapshot.SnapshotDiffReportOzone;
import org.apache.hadoop.ozone.snapshot.SnapshotDiffResponse;
import org.apache.hadoop.ozone.snapshot.SnapshotDiffResponse.JobStatus;
@@ -1424,6 +1426,29 @@ public ListSnapshotResponse listSnapshot(
return new ListSnapshotResponse(snapshotInfos, lastSnapshot);
}
+ @Override
+ public SnapshotCountResponse snapshotCount(String bucketFilter)
+ throws IOException {
+ final OzoneManagerProtocolProtos.SnapshotCountRequest.Builder
requestBuilder =
+ OzoneManagerProtocolProtos.SnapshotCountRequest.newBuilder();
+ if (bucketFilter != null) {
+ requestBuilder.setBucketFilter(bucketFilter);
+ }
+ final OMRequest omRequest = createOMRequest(Type.SnapshotCount)
+ .setSnapshotCountRequest(requestBuilder)
+ .build();
+ final OMResponse omResponse = submitRequest(omRequest);
+ handleError(omResponse);
+
+ OzoneManagerProtocolProtos.SnapshotCountResponse response =
omResponse.getSnapshotCountResponse();
+ List<SnapshotBucketCount> bucketCounts = response.getBucketsList().stream()
+ .map(count -> new SnapshotBucketCount(count.getVolumeName(),
count.getBucketName(),
+ count.getActive(), count.getDeleted(), count.getTotal()))
+ .collect(Collectors.toList());
+
+ return new SnapshotCountResponse(response.getActive(),
response.getDeleted(), response.getTotal(), bucketCounts);
+ }
+
/**
* {@inheritDoc}
*/
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/snapshot/SnapshotBucketCount.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/snapshot/SnapshotBucketCount.java
new file mode 100644
index 00000000000..a7c67437715
--- /dev/null
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/snapshot/SnapshotBucketCount.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.snapshot;
+
+/**
+ * Bucket-wise snapshot count distribution.
+ */
+public final class SnapshotBucketCount {
+ private final String volumeName;
+ private final String bucketName;
+ private final long active;
+ private final long deleted;
+ private final long total;
+
+ public SnapshotBucketCount(String volumeName, String bucketName, long
active, long deleted, long total) {
+ this.volumeName = volumeName;
+ this.bucketName = bucketName;
+ this.active = active;
+ this.deleted = deleted;
+ this.total = total;
+ }
+
+ public String getVolumeName() {
+ return volumeName;
+ }
+
+ public String getBucketName() {
+ return bucketName;
+ }
+
+ public long getActive() {
+ return active;
+ }
+
+ public long getDeleted() {
+ return deleted;
+ }
+
+ public long getTotal() {
+ return total;
+ }
+}
diff --git
a/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/snapshot/SnapshotCommands.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/snapshot/SnapshotCountResponse.java
similarity index 52%
copy from
hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/snapshot/SnapshotCommands.java
copy to
hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/snapshot/SnapshotCountResponse.java
index 743d94a8a46..b24cc1c4c15 100644
---
a/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/snapshot/SnapshotCommands.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/snapshot/SnapshotCountResponse.java
@@ -15,27 +15,39 @@
* limitations under the License.
*/
-package org.apache.hadoop.ozone.shell.snapshot;
+package org.apache.hadoop.ozone.snapshot;
-import org.apache.hadoop.hdds.cli.HddsVersionProvider;
-import picocli.CommandLine.Command;
+import java.util.List;
/**
- * Subcommands for the snapshot related operations.
+ * POJO for snapshot count API.
*/
-@Command(name = "snapshot",
- description = "Snapshot specific operations",
- subcommands = {
- CreateSnapshotHandler.class,
- DeleteSnapshotHandler.class,
- ListSnapshotHandler.class,
- SnapshotDiffHandler.class,
- ListSnapshotDiffHandler.class,
- InfoSnapshotHandler.class,
- RenameSnapshotHandler.class
- },
- mixinStandardHelpOptions = true,
- versionProvider = HddsVersionProvider.class)
-public class SnapshotCommands {
+public final class SnapshotCountResponse {
+ private final long active;
+ private final long deleted;
+ private final long total;
+ private final List<SnapshotBucketCount> buckets;
+ public SnapshotCountResponse(long active, long deleted, long total,
List<SnapshotBucketCount> buckets) {
+ this.active = active;
+ this.deleted = deleted;
+ this.total = total;
+ this.buckets = buckets;
+ }
+
+ public long getActive() {
+ return active;
+ }
+
+ public long getDeleted() {
+ return deleted;
+ }
+
+ public long getTotal() {
+ return total;
+ }
+
+ public List<SnapshotBucketCount> getBuckets() {
+ return buckets;
+ }
}
diff --git
a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index 3702b021ac2..bec4a40810f 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -170,6 +170,7 @@ enum Type {
RevokeSTSToken = 154;
DeleteRevokedSTSTokens = 155;
GetCallerIdentity = 156;
+ SnapshotCount = 157;
}
enum SafeMode {
@@ -335,6 +336,7 @@ message OMRequest {
optional DeleteRevokedSTSTokensRequest deleteRevokedSTSTokensRequest =
156;
optional UpdateAssumeRoleRequest updateAssumeRoleRequest =
157;
optional GetCallerIdentityRequest getCallerIdentityRequest =
158;
+ optional SnapshotCountRequest snapshotCountRequest = 159;
}
message OMResponse {
@@ -484,6 +486,7 @@ message OMResponse {
optional RevokeSTSTokenResponse revokeSTSTokenResponse =
154;
optional DeleteRevokedSTSTokensResponse deleteRevokedSTSTokensResponse =
155;
optional GetCallerIdentityResponse getCallerIdentityResponse =
156;
+ optional SnapshotCountResponse snapshotCountResponse = 157;
}
enum Status {
@@ -2115,6 +2118,10 @@ message ListSnapshotRequest {
optional uint32 maxListResult = 5;
}
+message SnapshotCountRequest {
+ optional string bucketFilter = 1;
+}
+
message SnapshotDiffRequest {
optional string volumeName = 1;
optional string bucketName = 2;
@@ -2262,6 +2269,21 @@ message ListSnapshotResponse {
optional string lastSnapshot = 2;
}
+message SnapshotBucketCount {
+ optional string volumeName = 1;
+ optional string bucketName = 2;
+ optional uint64 active = 3;
+ optional uint64 deleted = 4;
+ optional uint64 total = 5;
+}
+
+message SnapshotCountResponse {
+ optional uint64 active = 1;
+ optional uint64 deleted = 2;
+ optional uint64 total = 3;
+ repeated SnapshotBucketCount buckets = 4;
+}
+
message SnapshotDiffResponse {
enum JobStatusProto {
QUEUED = 1;
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
index 1960e1fc3bd..fdc6f95b48d 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
@@ -144,6 +144,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -345,6 +346,8 @@
import org.apache.hadoop.ozone.snapshot.CancelSnapshotDiffResponse;
import org.apache.hadoop.ozone.snapshot.ListSnapshotDiffJobResponse;
import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
+import org.apache.hadoop.ozone.snapshot.SnapshotBucketCount;
+import org.apache.hadoop.ozone.snapshot.SnapshotCountResponse;
import org.apache.hadoop.ozone.snapshot.SnapshotDiffResponse;
import org.apache.hadoop.ozone.snapshot.SubmitSnapshotDiffResponse;
import
org.apache.hadoop.ozone.storage.proto.OzoneManagerStorageProtos.PersistedUserVolumeInfo;
@@ -3384,6 +3387,212 @@ public ListSnapshotResponse listSnapshot(
}
}
+ @Override
+ public SnapshotCountResponse snapshotCount(String bucketFilter)
+ throws IOException {
+ Map<String, String> auditMap = new LinkedHashMap<>();
+ auditMap.put("bucketFilter", StringUtils.defaultString(bucketFilter));
+
+ try {
+ BucketFilter parsedFilter = BucketFilter.parse(bucketFilter);
+ Map<String, BucketSnapshotCount> bucketCounts = new LinkedHashMap<>();
+ Map<String, Boolean> bucketAclCache = new LinkedHashMap<>();
+ String tablePrefix = parsedFilter.getSnapshotInfoPrefix();
+ Table<String, SnapshotInfo> snapshotInfoTable =
metadataManager.getSnapshotInfoTable();
+
+ Map<String, SnapshotInfo> cacheEntries = new LinkedHashMap<>();
+ Iterator<Map.Entry<CacheKey<String>, CacheValue<SnapshotInfo>>>
cacheIter = snapshotInfoTable.cacheIterator();
+ while (cacheIter.hasNext()) {
+ Map.Entry<CacheKey<String>, CacheValue<SnapshotInfo>> entry =
cacheIter.next();
+ String key = entry.getKey().getCacheKey();
+ if (key.startsWith(tablePrefix)) {
+ CacheValue<SnapshotInfo> cacheValue = entry.getValue();
+ SnapshotInfo snapshotInfo = cacheValue != null ?
cacheValue.getCacheValue() : null;
+ cacheEntries.put(key, snapshotInfo != null ?
snapshotInfo.copyObject() : null);
+ }
+ }
+
+ try (TableIterator<String, ? extends KeyValue<String, SnapshotInfo>>
keyIter =
+ snapshotInfoTable.iterator(tablePrefix)) {
+ while (keyIter.hasNext()) {
+ KeyValue<String, SnapshotInfo> entry = keyIter.next();
+ if (cacheEntries.containsKey(entry.getKey())) {
+ continue;
+ }
+ addSnapshotToCount(entry.getValue(), parsedFilter, bucketAclCache,
bucketCounts);
+ }
+ }
+
+ for (SnapshotInfo snapshotInfo : cacheEntries.values()) {
+ if (snapshotInfo == null) {
+ continue;
+ }
+ addSnapshotToCount(snapshotInfo, parsedFilter, bucketAclCache,
bucketCounts);
+ }
+
+ if (parsedFilter.isExactBucketPath()) {
+ String volumeName = parsedFilter.getVolumeName();
+ String bucketName = parsedFilter.getBucketName();
+ String bucketDbKey = metadataManager.getBucketKey(volumeName,
bucketName);
+ boolean bucketExists =
metadataManager.getBucketTable().get(bucketDbKey) != null;
+ if (bucketExists && (!getAclsEnabled() || hasListAccess(volumeName,
bucketName, bucketAclCache))) {
+ String bucketKey = volumeName + "/" + bucketName;
+ bucketCounts.computeIfAbsent(bucketKey, unused -> new
BucketSnapshotCount(volumeName, bucketName));
+ }
+ }
+
+ long totalActive = 0;
+ long totalDeleted = 0;
+ long total = 0;
+ List<SnapshotBucketCount> bucketCountList = new
ArrayList<>(bucketCounts.size());
+ for (BucketSnapshotCount bucketCount : bucketCounts.values()) {
+ bucketCountList.add(bucketCount.toResponse());
+ totalActive += bucketCount.active;
+ totalDeleted += bucketCount.deleted;
+ total += bucketCount.total;
+ }
+
+ AUDIT.logReadSuccess(buildAuditMessageForSuccess(OMAction.LIST_SNAPSHOT,
auditMap));
+ return new SnapshotCountResponse(totalActive, totalDeleted, total,
bucketCountList);
+ } catch (Exception ex) {
+ AUDIT.logReadFailure(buildAuditMessageForFailure(OMAction.LIST_SNAPSHOT,
auditMap, ex));
+ throw ex;
+ }
+ }
+
+ private void addSnapshotToCount(SnapshotInfo snapshotInfo, BucketFilter
parsedFilter,
+ Map<String, Boolean> bucketAclCache,
+ Map<String, BucketSnapshotCount>
bucketCounts) throws IOException {
+ String volumeName = snapshotInfo.getVolumeName();
+ String bucketName = snapshotInfo.getBucketName();
+ if (!parsedFilter.matches(volumeName, bucketName)) {
+ return;
+ }
+
+ if (getAclsEnabled() && !hasListAccess(volumeName, bucketName,
bucketAclCache)) {
+ return;
+ }
+
+ String bucketKey = volumeName + "/" + bucketName;
+ BucketSnapshotCount bucketCount =
+ bucketCounts.computeIfAbsent(bucketKey, unused -> new
BucketSnapshotCount(volumeName, bucketName));
+ bucketCount.increment(snapshotInfo.getSnapshotStatus());
+ }
+
+ private boolean hasListAccess(String volumeName, String bucketName,
Map<String, Boolean> bucketAclCache)
+ throws IOException {
+ String bucketKey = volumeName + "/" + bucketName;
+ Boolean hasAccess = bucketAclCache.get(bucketKey);
+ if (hasAccess != null) {
+ return hasAccess;
+ }
+
+ try {
+ omMetadataReader.checkAcls(ResourceType.BUCKET, StoreType.OZONE,
ACLType.LIST, volumeName, bucketName, null);
+ bucketAclCache.put(bucketKey, true);
+ return true;
+ } catch (OMException ex) {
+ if (ex.getResult() == OMException.ResultCodes.PERMISSION_DENIED
+ || ex.getResult() == OMException.ResultCodes.ACCESS_DENIED) {
+ bucketAclCache.put(bucketKey, false);
+ return false;
+ }
+ throw ex;
+ }
+ }
+
+ private static final class BucketFilter {
+ private final String volumeName;
+ private final String bucketName;
+ private final boolean exactBucketPath;
+
+ private BucketFilter(String volumeName, String bucketName, boolean
exactBucketPath) {
+ this.volumeName = volumeName;
+ this.bucketName = bucketName;
+ this.exactBucketPath = exactBucketPath;
+ }
+
+ static BucketFilter parse(String filter) throws OMException {
+ if (StringUtils.isBlank(filter)) {
+ return new BucketFilter(null, null, false);
+ }
+
+ String normalized = StringUtils.strip(filter, "/");
+ int slashIndex = normalized.indexOf('/');
+ if (slashIndex < 0) {
+ return new BucketFilter(null, normalized, false);
+ }
+
+ if (normalized.indexOf('/', slashIndex + 1) >= 0) {
+ throw new OMException("Invalid --bucket filter. Use either <bucket> or
<volume>/<bucket>.",
+ OMException.ResultCodes.INVALID_REQUEST);
+ }
+
+ String volume = normalized.substring(0, slashIndex);
+ String bucket = normalized.substring(slashIndex + 1);
+ if (StringUtils.isBlank(volume) || StringUtils.isBlank(bucket)) {
+ throw new OMException("Invalid --bucket filter. Use either <bucket> or
<volume>/<bucket>.",
+ OMException.ResultCodes.INVALID_REQUEST);
+ }
+ return new BucketFilter(volume, bucket, true);
+ }
+
+ boolean matches(String volume, String bucket) {
+ if (bucketName == null) {
+ return true;
+ }
+ if (volumeName == null) {
+ return bucketName.equals(bucket);
+ }
+ return volumeName.equals(volume) && bucketName.equals(bucket);
+ }
+
+ String getSnapshotInfoPrefix() {
+ if (exactBucketPath) {
+ return OM_KEY_PREFIX + volumeName + OM_KEY_PREFIX + bucketName +
OM_KEY_PREFIX;
+ }
+ return OM_KEY_PREFIX;
+ }
+
+ boolean isExactBucketPath() {
+ return exactBucketPath;
+ }
+
+ String getVolumeName() {
+ return volumeName;
+ }
+
+ String getBucketName() {
+ return bucketName;
+ }
+ }
+
+ private static final class BucketSnapshotCount {
+ private final String volumeName;
+ private final String bucketName;
+ private long active;
+ private long deleted;
+ private long total;
+
+ private BucketSnapshotCount(String volumeName, String bucketName) {
+ this.volumeName = volumeName;
+ this.bucketName = bucketName;
+ }
+
+ private void increment(SnapshotInfo.SnapshotStatus status) {
+ if (status == SnapshotInfo.SnapshotStatus.SNAPSHOT_DELETED) {
+ deleted++;
+ } else if (status == SnapshotInfo.SnapshotStatus.SNAPSHOT_ACTIVE) {
+ active++;
+ }
+ total++;
+ }
+
+ private SnapshotBucketCount toResponse() {
+ return new SnapshotBucketCount(volumeName, bucketName, active, deleted,
total);
+ }
+ }
+
/**
* Gets the lifecycle configuration information.
* @param volumeName - Volume name.
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
index 19e012350d2..327dd9d53dc 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
@@ -177,6 +177,7 @@
import org.apache.hadoop.ozone.security.STSTokenIdentifier;
import org.apache.hadoop.ozone.security.acl.OzoneObjInfo;
import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
+import org.apache.hadoop.ozone.snapshot.SnapshotCountResponse;
import org.apache.hadoop.ozone.upgrade.UpgradeFinalization.StatusAndMessages;
import org.apache.hadoop.ozone.util.PayloadUtils;
import org.apache.hadoop.ozone.util.ProtobufUtils;
@@ -351,6 +352,11 @@ public OMResponse handleReadRequest(OMRequest request) {
getSnapshots(request.getListSnapshotRequest());
responseBuilder.setListSnapshotResponse(listSnapshotResponse);
break;
+ case SnapshotCount:
+ OzoneManagerProtocolProtos.SnapshotCountResponse snapshotCountResponse
=
+ getSnapshotCount(request.getSnapshotCountRequest());
+ responseBuilder.setSnapshotCountResponse(snapshotCountResponse);
+ break;
case SnapshotDiff:
SnapshotDiffResponse snapshotDiffReport = snapshotDiff(
request.getSnapshotDiffRequest());
@@ -1644,6 +1650,29 @@ private OzoneManagerProtocolProtos.ListSnapshotResponse
getSnapshots(
return builder.build();
}
+ @DisallowedUntilLayoutVersion(FILESYSTEM_SNAPSHOT)
+ private OzoneManagerProtocolProtos.SnapshotCountResponse getSnapshotCount(
+ OzoneManagerProtocolProtos.SnapshotCountRequest request) throws
IOException {
+ SnapshotCountResponse implResponse = impl.snapshotCount(
+ request.hasBucketFilter() ? request.getBucketFilter() : null);
+ List<OzoneManagerProtocolProtos.SnapshotBucketCount> bucketCountList =
implResponse.getBuckets().stream()
+ .map(bucketCount ->
OzoneManagerProtocolProtos.SnapshotBucketCount.newBuilder()
+ .setVolumeName(bucketCount.getVolumeName())
+ .setBucketName(bucketCount.getBucketName())
+ .setActive(bucketCount.getActive())
+ .setDeleted(bucketCount.getDeleted())
+ .setTotal(bucketCount.getTotal())
+ .build())
+ .collect(Collectors.toList());
+
+ return OzoneManagerProtocolProtos.SnapshotCountResponse.newBuilder()
+ .setActive(implResponse.getActive())
+ .setDeleted(implResponse.getDeleted())
+ .setTotal(implResponse.getTotal())
+ .addAllBuckets(bucketCountList)
+ .build();
+ }
+
private TransferLeadershipResponseProto transferLeadership(
TransferLeadershipRequestProto req) throws IOException {
String newLeaderId = req.getNewLeaderId();
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOzoneManagerSnapshotCount.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOzoneManagerSnapshotCount.java
new file mode 100644
index 00000000000..43c066c3840
--- /dev/null
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOzoneManagerSnapshotCount.java
@@ -0,0 +1,149 @@
+/*
+ * 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.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.LIST;
+import static
org.apache.hadoop.ozone.security.acl.OzoneObj.ResourceType.BUCKET;
+import static org.apache.hadoop.ozone.security.acl.OzoneObj.StoreType.OZONE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.util.UUID;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.server.ServerUtils;
+import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
+import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
+import org.apache.hadoop.ozone.audit.AuditMessage;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.SnapshotInfo;
+import org.apache.hadoop.ozone.om.helpers.SnapshotInfo.SnapshotStatus;
+import org.apache.hadoop.ozone.om.request.OMRequestTestUtils;
+import org.apache.hadoop.ozone.snapshot.SnapshotCountResponse;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Unit tests for {@link OzoneManager#snapshotCount(String)}.
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class TestOzoneManagerSnapshotCount {
+
+ private OmTestManagers omTestManagers;
+ private OzoneManager omSpy;
+ private OmMetadataReader omMetadataReader;
+ private OMMetadataManager metadataManager;
+
+ @BeforeAll
+ void setup(@TempDir File folder) throws Exception {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ ServerUtils.setOzoneMetaDirPath(conf, folder.toString());
+ omTestManagers = new OmTestManagers(conf);
+ metadataManager = omTestManagers.getMetadataManager();
+ }
+
+ @AfterAll
+ void cleanup() {
+ if (omTestManagers != null) {
+ omTestManagers.stop();
+ }
+ }
+
+ @BeforeEach
+ void init() {
+ omSpy = spy(omTestManagers.getOzoneManager());
+ omMetadataReader = mock(OmMetadataReader.class);
+ org.apache.hadoop.hdds.scm.HddsWhiteboxTestUtils.setInternalState(omSpy,
"omMetadataReader", omMetadataReader);
+ AuditMessage auditMessage = mock(AuditMessage.class);
+ when(auditMessage.getOp()).thenReturn("SNAPSHOT_COUNT");
+ doReturn(auditMessage).when(omSpy).buildAuditMessageForSuccess(any(),
anyMap());
+ doReturn(auditMessage).when(omSpy).buildAuditMessageForFailure(any(),
anyMap(), any(Throwable.class));
+ }
+
+ @Test
+ void testSnapshotCountUsesCacheAndExactBucketFilter() throws Exception {
+ String volume = "vol-" + UUID.randomUUID();
+ String bucket = "bucket-" + UUID.randomUUID();
+ String emptyBucket = "bucket-" + UUID.randomUUID();
+ OMRequestTestUtils.addVolumeToDB(volume, metadataManager);
+ OMRequestTestUtils.addBucketToDB(volume, bucket, metadataManager,
BucketLayout.DEFAULT);
+ OMRequestTestUtils.addBucketToDB(volume, emptyBucket, metadataManager,
BucketLayout.DEFAULT);
+
+ SnapshotInfo dbSnapshot = SnapshotInfo.newInstance(volume, bucket,
"snap1", UUID.randomUUID(), 1L);
+ metadataManager.getSnapshotInfoTable().put(dbSnapshot.getTableKey(),
dbSnapshot);
+
+ SnapshotInfo cachedSnapshot =
dbSnapshot.toBuilder().setSnapshotStatus(SnapshotStatus.SNAPSHOT_DELETED).build();
+ metadataManager.getSnapshotInfoTable().addCacheEntry(
+ new CacheKey<>(cachedSnapshot.getTableKey()), CacheValue.get(2L,
cachedSnapshot));
+
+ when(omSpy.getAclsEnabled()).thenReturn(false);
+ SnapshotCountResponse countResponse = omSpy.snapshotCount(volume + "/" +
bucket);
+ assertEquals(0, countResponse.getActive());
+ assertEquals(1, countResponse.getDeleted());
+ assertEquals(1, countResponse.getTotal());
+ assertEquals(1, countResponse.getBuckets().size());
+
+ SnapshotCountResponse emptyBucketResponse = omSpy.snapshotCount(volume +
"/" + emptyBucket);
+ assertEquals(0, emptyBucketResponse.getActive());
+ assertEquals(0, emptyBucketResponse.getDeleted());
+ assertEquals(0, emptyBucketResponse.getTotal());
+ assertEquals(1, emptyBucketResponse.getBuckets().size());
+ }
+
+ @Test
+ void testSnapshotCountRejectsInvalidBucketFilter() throws Exception {
+ when(omSpy.getAclsEnabled()).thenReturn(false);
+ OMException ex = assertThrows(OMException.class, () ->
omSpy.snapshotCount("vol1/bucket1/extra"));
+ assertEquals(OMException.ResultCodes.INVALID_REQUEST, ex.getResult());
+ }
+
+ @Test
+ void testSnapshotCountSkipsPermissionDeniedBuckets() throws Exception {
+ String volume = "vol-" + UUID.randomUUID();
+ String bucket = "bucket-" + UUID.randomUUID();
+ OMRequestTestUtils.addVolumeToDB(volume, metadataManager);
+ OMRequestTestUtils.addBucketToDB(volume, bucket, metadataManager,
BucketLayout.DEFAULT);
+
+ SnapshotInfo snapshotInfo = SnapshotInfo.newInstance(volume, bucket,
"snap1", UUID.randomUUID(), 1L);
+ metadataManager.getSnapshotInfoTable().put(snapshotInfo.getTableKey(),
snapshotInfo);
+
+ when(omSpy.getAclsEnabled()).thenReturn(true);
+ doThrow(new OMException("denied",
OMException.ResultCodes.PERMISSION_DENIED))
+ .when(omMetadataReader).checkAcls(eq(BUCKET), eq(OZONE), eq(LIST),
anyString(), anyString(), any());
+
+ SnapshotCountResponse countResponse = omSpy.snapshotCount(null);
+ assertEquals(0, countResponse.getActive());
+ assertEquals(0, countResponse.getDeleted());
+ assertEquals(0, countResponse.getTotal());
+ assertEquals(0, countResponse.getBuckets().size());
+ }
+}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
index 816922f5dac..dfb4f1a43ca 100644
---
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
@@ -67,6 +67,7 @@
import org.apache.hadoop.ozone.snapshot.CancelSnapshotDiffResponse;
import org.apache.hadoop.ozone.snapshot.ListSnapshotDiffJobResponse;
import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
+import org.apache.hadoop.ozone.snapshot.SnapshotCountResponse;
import org.apache.hadoop.ozone.snapshot.SnapshotDiffResponse;
import org.apache.hadoop.ozone.snapshot.SubmitSnapshotDiffResponse;
import org.apache.hadoop.security.token.Token;
@@ -836,6 +837,12 @@ public ListSnapshotResponse listSnapshot(
String prevSnapshot, int maxListResult) throws IOException {
return null;
}
+
+ @Override
+ public SnapshotCountResponse snapshotCount(String bucketFilter)
+ throws IOException {
+ return null;
+ }
@Override
public void deleteSnapshot(String volumeName,
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]