]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/smb: add rate limiting support
authorAvan Thakkar <athakkar@redhat.com>
Thu, 31 Jul 2025 14:47:03 +0000 (20:17 +0530)
committerAvan Thakkar <athakkar@redhat.com>
Sun, 8 Feb 2026 14:14:35 +0000 (19:44 +0530)
Introduce a new optional `qos` component under the `cephfs` block
of the Share resource to configure rate limiting options per SMB share.

The new structure supports:
- read_iops_limit
- write_iops_limit
- read_bw_limit
- write_bw_limit
- read_delay_max
- write_delay_max

A new CLI command is added:
  `ceph smb share update cephfs qos <cluster> <share> [options]`

Signed-off-by: Avan Thakkar <athakkar@redhat.com>
src/pybind/mgr/smb/handler.py
src/pybind/mgr/smb/module.py
src/pybind/mgr/smb/resources.py

index 51189a74e34353ad05a00258259e1b77b877cc65..a1fba2194def708434e93a799bc5b2bf73f8000c 100644 (file)
@@ -708,11 +708,19 @@ def _generate_share(
         }[cephfs.provider.expand()]
     except KeyError:
         raise ValueError(f'unsupported provider: {cephfs.provider}')
+    modules = ["acl_xattr", "ceph_snapshots"]
+
+    if qos := cephfs.qos:
+        vfs_rl = "aio_ratelimit"
+        modules.extend([vfs_rl, ceph_vfs])
+    else:
+        modules.append(ceph_vfs)
+
     cfg = {
         # smb.conf options
         'options': {
             'path': path,
-            "vfs objects": f"acl_xattr ceph_snapshots {ceph_vfs}",
+            "vfs objects": " ".join(modules),
             'acl_xattr:security_acl_name': 'user.NTACL',
             f'{ceph_vfs}:config_file': '/etc/ceph/ceph.conf',
             f'{ceph_vfs}:filesystem': cephfs.volume,
@@ -725,6 +733,19 @@ def _generate_share(
             'posix locking': 'no',
         }
     }
+
+    if qos:
+        opts = cfg["options"]
+        for field in (
+            "read_iops_limit",
+            "read_bw_limit",
+            "read_delay_max",
+            "write_iops_limit",
+            "write_bw_limit",
+            "write_delay_max",
+        ):
+            if value := getattr(qos, field):
+                opts[f"{vfs_rl}:{field}"] = str(value)
     if share.comment is not None:
         cfg['options']['comment'] = share.comment
 
index 1983234e2ebd8de3d4ceb18e13aad3ea8a6044fe..4fd73ecc4407c0ac96cfbbe048a4a9a9c9ec1aa2 100644 (file)
@@ -1,6 +1,7 @@
 from typing import TYPE_CHECKING, Any, List, Optional, cast
 
 import logging
+from dataclasses import replace
 
 import orchestrator
 from ceph.deployment.service_spec import PlacementSpec, SMBSpec
@@ -388,7 +389,46 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule):
         )
         return self._apply_res([share]).one()
 
-    @SMBCLICommand("show", perm="r")
+    @cli.SMBCommand('share update cephfs qos', perm='rw')
+    def share_update_qos(
+        self,
+        cluster_id: str,
+        share_id: str,
+        read_iops_limit: Optional[int] = None,
+        write_iops_limit: Optional[int] = None,
+        read_bw_limit: Optional[int] = None,
+        write_bw_limit: Optional[int] = None,
+        read_delay_max: Optional[int] = 30,
+        write_delay_max: Optional[int] = 30,
+    ) -> results.Result:
+        """Update QoS settings for a CephFS share"""
+        try:
+            shares = self._handler.matching_resources(
+                [f'ceph.smb.share.{cluster_id}.{share_id}']
+            )
+            if not shares or not isinstance(shares[0], resources.Share):
+                raise ValueError(f"Share {cluster_id}/{share_id} not found")
+
+            share = shares[0]
+            if not share.cephfs:
+                raise ValueError("Share has no CephFS configuration")
+
+            updated_cephfs = share.cephfs.update_qos(
+                read_iops_limit=read_iops_limit,
+                write_iops_limit=write_iops_limit,
+                read_bw_limit=read_bw_limit,
+                write_bw_limit=write_bw_limit,
+                read_delay_max=read_delay_max,
+                write_delay_max=write_delay_max,
+            )
+
+            updated_share = replace(share, cephfs=updated_cephfs)
+            return self._apply_res([updated_share]).one()
+
+        except resources.InvalidResourceError as err:
+            return results.InvalidResourceResult(err.resource_data, str(err))
+
+    @cli.SMBCommand("show", perm="r")
     def show(
         self,
         resource_names: Optional[List[str]] = None,
index c43655b6cb874c390f9f455495f09304138a6829..03b5d0ed8de959f341db84c7b01c627db3f6fc64 100644 (file)
@@ -4,6 +4,7 @@ import base64
 import dataclasses
 import errno
 import json
+from dataclasses import replace
 
 import yaml
 
@@ -133,6 +134,18 @@ class _RBase:
         return self
 
 
+@resourcelib.component()
+class QoSConfig(_RBase):
+    """Quality of Service configuration for CephFS shares."""
+
+    read_iops_limit: Optional[int] = None
+    write_iops_limit: Optional[int] = None
+    read_bw_limit: Optional[int] = None
+    write_bw_limit: Optional[int] = None
+    read_delay_max: Optional[int] = 30
+    write_delay_max: Optional[int] = 30
+
+
 @resourcelib.component()
 class CephFSStorage(_RBase):
     """Description of where in a CephFS file system a share is located."""
@@ -142,6 +155,12 @@ class CephFSStorage(_RBase):
     subvolumegroup: str = ''
     subvolume: str = ''
     provider: CephFSStorageProvider = CephFSStorageProvider.SAMBA_VFS
+    qos: Optional[QoSConfig] = None
+    DELAY_MAX_LIMIT = 300
+    # Maximal value for iops_limit
+    IOPS_LIMIT_MAX = 1_000_000
+    # Maximal value for bw_limit (1 << 40 = 1 TB)
+    BYTES_LIMIT_MAX = 1 << 40
 
     def __post_init__(self) -> None:
         # Allow a shortcut form of <subvolgroup>/<subvol> in the subvolume
@@ -175,8 +194,53 @@ class CephFSStorage(_RBase):
     def _customize_resource(rc: resourcelib.Resource) -> resourcelib.Resource:
         rc.subvolumegroup.quiet = True
         rc.subvolume.quiet = True
+        rc.qos.quiet = True
         return rc
 
+    def update_qos(
+        self,
+        *,
+        read_iops_limit: Optional[int] = None,
+        write_iops_limit: Optional[int] = None,
+        read_bw_limit: Optional[int] = None,
+        write_bw_limit: Optional[int] = None,
+        read_delay_max: Optional[int] = 30,
+        write_delay_max: Optional[int] = 30,
+    ) -> Self:
+        """Return a new CephFSStorage instance with updated QoS values."""
+
+        qos_updates = {}
+        new_qos: Optional[QoSConfig] = None
+        if read_iops_limit is not None and read_iops_limit > 0:
+            qos_updates["read_iops_limit"] = min(
+                read_iops_limit, self.IOPS_LIMIT_MAX
+            )
+        if write_iops_limit is not None and write_iops_limit > 0:
+            qos_updates["write_iops_limit"] = min(
+                write_iops_limit, self.IOPS_LIMIT_MAX
+            )
+        if read_bw_limit is not None and read_bw_limit > 0:
+            qos_updates["read_bw_limit"] = min(
+                read_bw_limit, self.BYTES_LIMIT_MAX
+            )
+        if write_bw_limit is not None and write_bw_limit > 0:
+            qos_updates["write_bw_limit"] = min(
+                write_bw_limit, self.BYTES_LIMIT_MAX
+            )
+        if read_delay_max is not None and read_delay_max > 0:
+            qos_updates["read_delay_max"] = min(
+                read_delay_max, self.DELAY_MAX_LIMIT
+            )
+        if write_delay_max is not None and write_delay_max > 0:
+            qos_updates["write_delay_max"] = min(
+                write_delay_max, self.DELAY_MAX_LIMIT
+            )
+
+        if qos_updates:
+            new_qos = replace(self.qos or QoSConfig(), **qos_updates)
+
+        return replace(self, qos=new_qos)
+
 
 @resourcelib.component()
 class LoginAccessEntry(_RBase):