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 <ssodani@redhat.com>
This commit is contained in:
Shweta Sodani 2026-07-15 20:15:43 +05:30
parent ec822b631e
commit a4b10fb820
5 changed files with 165 additions and 45 deletions

View 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()}'

View 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(

View 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')

View 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:

View 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()