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()}'
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)
'',
)
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
)
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(
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')
# 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,
'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:
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:
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()