From 9078b91df6b6948ad471c5896960674f122898da Mon Sep 17 00:00:00 2001 From: Avan Thakkar Date: Thu, 31 Jul 2025 20:17:03 +0530 Subject: [PATCH] mgr/smb: add rate limiting support 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 [options]` Signed-off-by: Avan Thakkar --- src/pybind/mgr/smb/handler.py | 23 +++++++++++- src/pybind/mgr/smb/module.py | 40 +++++++++++++++++++++ src/pybind/mgr/smb/resources.py | 64 +++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) diff --git a/src/pybind/mgr/smb/handler.py b/src/pybind/mgr/smb/handler.py index ee0616573ff..35c371652bd 100644 --- a/src/pybind/mgr/smb/handler.py +++ b/src/pybind/mgr/smb/handler.py @@ -814,11 +814,19 @@ def _generate_share(conf: _ShareConf) -> Dict[str, Dict[str, str]]: if conf.ceph_cluster else '/etc/ceph/ceph.conf' ) + 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': ceph_config_file, f'{ceph_vfs}:filesystem': cephfs.volume, @@ -831,6 +839,19 @@ def _generate_share(conf: _ShareConf) -> Dict[str, Dict[str, str]]: '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 diff --git a/src/pybind/mgr/smb/module.py b/src/pybind/mgr/smb/module.py index 1983234e2eb..70f4e3aea2d 100644 --- a/src/pybind/mgr/smb/module.py +++ b/src/pybind/mgr/smb/module.py @@ -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,6 +389,45 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): ) return self._apply_res([share]).one() + @SMBCLICommand('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)) + @SMBCLICommand("show", perm="r") def show( self, diff --git a/src/pybind/mgr/smb/resources.py b/src/pybind/mgr/smb/resources.py index beeb279e741..20064c5e5f4 100644 --- a/src/pybind/mgr/smb/resources.py +++ b/src/pybind/mgr/smb/resources.py @@ -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 / 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): -- 2.47.3