From: Shweta Sodani Date: Wed, 15 Jul 2026 14:45:43 +0000 (+0530) Subject: mgr/smb: add support for RGW shares with external Ceph clusters X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=a4b10fb820d6cdadc1e388805805677716278b67;p=ceph.git mgr/smb: add support for RGW shares with external Ceph clusters Implement support for creating RGW-backed SMB shares on external Ceph clusters with user configuration. Fixes: https://tracker.ceph.com/issues/77784 Signed-off-by: Shweta Sodani --- diff --git a/src/pybind/mgr/cephadm/services/smb.py b/src/pybind/mgr/cephadm/services/smb.py index d2f15c7624f6..6eb3ff04e377 100644 --- a/src/pybind/mgr/cephadm/services/smb.py +++ b/src/pybind/mgr/cephadm/services/smb.py @@ -609,20 +609,23 @@ def _to_conf(ext_cluster: SMBExternalCephCluster) -> str: def _to_keyring(ext_cluster: SMBExternalCephCluster) -> str: - return '\n'.join( - [ - f'[{ext_cluster.user}]', - f'key = {ext_cluster.key}', - '', - ] - ) + entries = [] + if ext_cluster.user and ext_cluster.key: + entries += [f'[{ext_cluster.user}]', f'key = {ext_cluster.key}', ''] + if ext_cluster.rgw_user and ext_cluster.rgw_key: + entries += [f'[{ext_cluster.rgw_user}]', f'key = {ext_cluster.rgw_key}', ''] + return '\n'.join(entries) def _hash_ceph_cluster_config(spec: SMBExternalCephCluster) -> str: - _fields = ['alias', 'fsid', 'mon_host', 'user', 'key'] + _fields = ['alias', 'fsid', 'mon_host', 'user', 'key', 'rgw_user', 'rgw_key'] fdg = hashlib.sha256() for field_name in _fields: - fdg.update(getattr(spec, field_name, '').encode()) + value = getattr(spec, field_name, '') + # Handle None values explicitly + if value is None: + value = '' + fdg.update(value.encode()) return f'sha256:{fdg.hexdigest()}' diff --git a/src/pybind/mgr/smb/handler.py b/src/pybind/mgr/smb/handler.py index 12ea2e06eb33..3f3d50d83748 100644 --- a/src/pybind/mgr/smb/handler.py +++ b/src/pybind/mgr/smb/handler.py @@ -881,21 +881,35 @@ class _ClusterConf: str ] = [] # passed to cephadm service spec ceph_cluster = '' # empty string means local cluster + + has_cephfs_shares = any( + s.cephfs is not None for s in change_group.shares + ) + has_rgw_shares = any(s.rgw is not None for s in change_group.shares) + if extcc: log.debug('external ceph cluster') resolver = ExoResolver(change_group.cluster) ceph_cluster = 'exo' - cephx_entity = checked(extcc.cluster).cephfs_user.name - cephfs_entity = cephx_entity - cephadm_data_entities = [cephx_entity] + + # Handle CephFS user if available and needed + if has_cephfs_shares: + if not extcc.cluster or not extcc.cluster.cephfs_user: + raise ValueError( + 'External cluster must have cephfs_user configured for CephFS shares' + ) + + cephx_entity = extcc.cluster.cephfs_user.name + cephfs_entity = cephx_entity + # Handle RGW user if available and needed + if has_rgw_shares: + if not extcc.cluster or not extcc.cluster.rgw_user: + raise ValueError( + 'External cluster must have rgw_user configured for RGW shares' + ) + log.debug('external ceph cluster with RGW shares') + rgw_entity = extcc.cluster.rgw_user.name elif change_group.shares: - # Check if we have any CephFS shares that need CephX auth - has_cephfs_shares = any( - s.cephfs is not None for s in change_group.shares - ) - has_rgw_shares = any( - s.rgw is not None for s in change_group.shares - ) if has_cephfs_shares: log.debug('local ceph cluster with CephFS shares') cephfs_entity = _cephx_data_entity(change_group.cluster) @@ -1190,10 +1204,28 @@ def _generate_config(conf: _ClusterConf) -> Dict[str, Any]: '', ) if rgw_entity: - cluster_global_opts[ - 'ceph_rgw:config_file' - ] = '/etc/ceph/ceph.conf' - cluster_global_opts['ceph_rgw:keyring_file'] = '/etc/ceph/keyring' + # Check if this is an external cluster + is_external_cluster = any( + share.ceph_cluster == 'exo' + for share in conf.shares + if share.resource.rgw + ) + + if is_external_cluster: + cluster_global_opts[ + 'ceph_rgw:config_file' + ] = '/etc/ceph/exo.ceph.conf' + cluster_global_opts[ + 'ceph_rgw:keyring_file' + ] = '/etc/ceph/exo.ceph.keyring' + else: + cluster_global_opts[ + 'ceph_rgw:config_file' + ] = '/etc/ceph/ceph.conf' + cluster_global_opts[ + 'ceph_rgw:keyring_file' + ] = '/etc/ceph/keyring' + cluster_global_opts['ceph_rgw:id'] = cephx_stripped_entity( rgw_entity ) @@ -1350,13 +1382,28 @@ def _generate_smb_service_spec( ceph_cluster_configs = None if ext_ceph_cluster: exo = checked(ext_ceph_cluster.cluster) + + rgw_user = None + rgw_key = None + cephfs_user = None + cephfs_key = None + + if exo.rgw_user: + rgw_user = exo.rgw_user.name + rgw_key = exo.rgw_user.key + if exo.cephfs_user: + cephfs_user = exo.cephfs_user.name + cephfs_key = exo.cephfs_user.key + ceph_cluster_configs = [ SMBExternalCephCluster( alias='exo', fsid=exo.fsid, mon_host=exo.mon_host, - user=exo.cephfs_user.name, - key=exo.cephfs_user.key, + user=cephfs_user, + key=cephfs_key, + rgw_user=rgw_user, + rgw_key=rgw_key, ) ] return SMBSpec( diff --git a/src/pybind/mgr/smb/resources.py b/src/pybind/mgr/smb/resources.py index 66d2e7585f54..75439d96b2a2 100644 --- a/src/pybind/mgr/smb/resources.py +++ b/src/pybind/mgr/smb/resources.py @@ -1249,7 +1249,18 @@ class ExternalCephClusterValues(_RBase): fsid: str mon_host: str - cephfs_user: CephUserKey + cephfs_user: Optional[CephUserKey] = None + rgw_user: Optional[CephUserKey] = None + + def __post_init__(self) -> None: + self.validate() + + def validate(self) -> None: + """Validate that at least one user (cephfs_user or rgw_user) is configured.""" + if not self.cephfs_user and not self.rgw_user: + raise ValueError( + 'At least one of cephfs_user or rgw_user must be configured' + ) @resourcelib.resource('ceph.smb.ext.cluster') diff --git a/src/pybind/mgr/smb/staging.py b/src/pybind/mgr/smb/staging.py index 87f96890a66c..709d6d190b87 100644 --- a/src/pybind/mgr/smb/staging.py +++ b/src/pybind/mgr/smb/staging.py @@ -376,9 +376,26 @@ def _check_share_resource( # Handle RGW shares if share.rgw is not None: + # Check if cluster uses external Ceph cluster + cluster = staging.get_cluster(share.cluster_id) + is_external_cluster = ( + cluster.external_ceph_cluster is not None + and cluster.external_ceph_cluster.ref + ) + # If credential_ref is not provided, auto-create credential if not share.rgw.credential_ref: - # Fetch credentials from RGW + # For external clusters, require explicit credential_ref + if is_external_cluster: + raise ErrorResult( + share, + msg=( + "RGW shares with external clusters require explicit 'credential_ref'. " + "Create an RGWCredential resource and reference it in the share." + ), + ) + + # Fetch credentials from RGW (LOCAL cluster only) try: ( fetched_user_id, @@ -455,14 +472,17 @@ def _check_share_resource( 'other_cluster_id': cred.linked_to_cluster, }, ) - # Validate bucket exists - if not rgw.validate_rgw_bucket( - staging._tool_execer, share.rgw.bucket - ): - raise ErrorResult( - share, - msg=f"RGW bucket '{share.rgw.bucket}' does not exist or is not accessible", - ) + # Validate bucket exists (skip for external clusters) + if not is_external_cluster: + if not rgw.validate_rgw_bucket( + staging._tool_execer, share.rgw.bucket + ): + raise ErrorResult( + share, + msg=f"RGW bucket '{share.rgw.bucket}' does not exist or is not accessible", + ) + # For external clusters, skip bucket validation + # User must ensure bucket exists on external cluster name_used_by = _share_name_in_use(staging, share) if name_used_by: diff --git a/src/python-common/ceph/deployment/service_spec.py b/src/python-common/ceph/deployment/service_spec.py index ec8688435039..b123b9ced8c6 100644 --- a/src/python-common/ceph/deployment/service_spec.py +++ b/src/python-common/ceph/deployment/service_spec.py @@ -4041,14 +4041,18 @@ class SMBExternalCephCluster: fsid: str, mon_host: str, # default user and key - user: str, - key: str, + user: Optional[str] = None, + key: Optional[str] = None, + rgw_user: Optional[str] = None, + rgw_key: Optional[str] = None, ) -> None: self.alias = alias self.fsid = fsid self.mon_host = mon_host self.user = user self.key = key + self.rgw_user = rgw_user + self.rgw_key = rgw_key self.validate() def validate(self) -> None: @@ -4058,26 +4062,61 @@ class SMBExternalCephCluster: raise SpecValidationError('an fsid value is required') if not self.mon_host: raise SpecValidationError('a mon_host value is required') - if not self.user: - raise SpecValidationError('a default user name is required') - if not self.key: - raise SpecValidationError('a default key is required') + + # Validate CephFS credentials (user+key pair) + has_user = self.user is not None and self.user != '' + has_key = self.key is not None and self.key != '' + if has_user != has_key: + raise SpecValidationError( + 'CephFS credentials must be provided as a complete pair: ' + 'both user and key are required if either is specified' + ) + + # Validate RGW credentials (rgw_user+rgw_key pair) + has_rgw_user = self.rgw_user is not None and self.rgw_user != '' + has_rgw_key = self.rgw_key is not None and self.rgw_key != '' + if has_rgw_user != has_rgw_key: + raise SpecValidationError( + 'RGW credentials must be provided as a complete pair: ' + 'both rgw_user and rgw_key are required if either is specified' + ) + + # Ensure at least one complete credential pair is provided + has_cephfs_creds = has_user and has_key + has_rgw_creds = has_rgw_user and has_rgw_key + if not has_cephfs_creds and not has_rgw_creds: + raise SpecValidationError( + 'at least one complete credential pair is required: ' + 'either (user+key) for CephFS or (rgw_user+rgw_key) for RGW' + ) def __repr__(self) -> str: - _names = ['alias', 'fsid', 'mon_host', 'user', 'key'] + _names = ['alias', 'fsid', 'mon_host', 'user', 'key', 'rgw_user', 'rgw_key'] fields = ', '.join(f'{n}={getattr(self, n, "")!r}' for n in _names) return f'{self.__class__.__name__}({fields})' def to_simplified(self) -> Dict[str, Any]: """Return a serializable representation of SMBExternalCephCluster.""" - return { + result: Dict[str, Any] = { 'alias': self.alias, 'fsid': self.fsid, 'mon_host': self.mon_host, - 'user': self.user, - 'key': self.key, } + # Only include CephFS credentials if they are set + if self.user: + result['user'] = self.user + if self.key: + result['key'] = self.key + + # Only include RGW credentials if they are set + if self.rgw_user: + result['rgw_user'] = self.rgw_user + if self.rgw_key: + result['rgw_key'] = self.rgw_key + + return result + def to_json(self) -> Dict[str, Any]: """Return a JSON-compatible dict.""" return self.to_simplified()