mirror of
https://github.com/ceph/ceph
synced 2026-08-02 07:03:18 +00:00
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:
parent
ec822b631e
commit
a4b10fb820
@ -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()}'
|
||||
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user