]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/smb: add support for RGW shares with external Ceph clusters
authorShweta Sodani <Shweta.Sodani@ibm.com>
Wed, 15 Jul 2026 14:45:43 +0000 (20:15 +0530)
committerShweta Sodani <Shweta.Sodani@ibm.com>
Mon, 20 Jul 2026 08:44:14 +0000 (14:14 +0530)
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 <ssodani@redhat.com>
src/pybind/mgr/cephadm/services/smb.py
src/pybind/mgr/smb/handler.py
src/pybind/mgr/smb/resources.py
src/pybind/mgr/smb/staging.py
src/python-common/ceph/deployment/service_spec.py

index d2f15c7624f67addf6c79f45443a545fbea53b07..6eb3ff04e3770b0b94694e1877635b2ae5384c5a 100644 (file)
@@ -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()}'
 
 
index 12ea2e06eb3381e91124e6635649492fd01a2b73..3f3d50d8374876db09c3b8932de316755bdc0fa9 100644 (file)
@@ -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(
index 66d2e7585f54adec0bb61460d438c15a353ee401..75439d96b2a2f39416f011d0cb6e306f63561da7 100644 (file)
@@ -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')
index 87f96890a66c474eaaa21c53619d5758de50bed5..709d6d190b873271fa70242d33a2cf829078110a 100644 (file)
@@ -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:
index ec8688435039541b0738ca755f59c8eba0a2a9d1..b123b9ced8c62c5a0bfb59acdf10d47d57bb7ba9 100644 (file)
@@ -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()