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]

Reply via email to