diff options
-rw-r--r-- | doc/mgr/smb.rst | 50 | ||||
-rw-r--r-- | src/pybind/mgr/smb/enums.py | 5 | ||||
-rw-r--r-- | src/pybind/mgr/smb/handler.py | 493 | ||||
-rw-r--r-- | src/pybind/mgr/smb/internal.py | 68 | ||||
-rw-r--r-- | src/pybind/mgr/smb/module.py | 27 | ||||
-rw-r--r-- | src/pybind/mgr/smb/proto.py | 22 | ||||
-rw-r--r-- | src/pybind/mgr/smb/resources.py | 62 | ||||
-rw-r--r-- | src/pybind/mgr/smb/results.py | 17 | ||||
-rw-r--r-- | src/pybind/mgr/smb/tests/test_enums.py | 2 | ||||
-rw-r--r-- | src/pybind/mgr/smb/tests/test_handler.py | 231 | ||||
-rw-r--r-- | src/pybind/mgr/smb/tests/test_resources.py | 127 | ||||
-rw-r--r-- | src/pybind/mgr/smb/tests/test_smb.py | 63 |
12 files changed, 691 insertions, 476 deletions
diff --git a/doc/mgr/smb.rst b/doc/mgr/smb.rst index 814f453632e..05de3be1710 100644 --- a/doc/mgr/smb.rst +++ b/doc/mgr/smb.rst @@ -364,14 +364,7 @@ placement A join source object supports the following fields: source_type - One of ``password`` or ``resource`` -auth - Object. Required for ``source_type: password``. Fields: - - username: - Required string. User with ability to join a system to AD. - password: - Required string. The AD user's password + Optional. Must be ``resource`` if specified. ref String. Required for ``source_type: resource``. Must refer to the ID of a ``ceph.smb.join.auth`` resource @@ -381,26 +374,15 @@ ref A user group source object supports the following fields: source_type - One of ``inline`` or ``resource`` -values - Object. Required for ``source_type: inline``. Fields: - - users - List of objects. Fields: - - username - A user name - password - A password - groups - List of objects. Fields: - - name - The name of the group + Optional. One of ``resource`` (the default) or ``empty`` ref String. Required for ``source_type: resource``. Must refer to the ID of a ``ceph.smb.join.auth`` resource +.. note:: + The ``source_type`` ``empty`` is generally only for debugging and testing + the module and should not be needed in production deployments. + The following is an example of a cluster configured for AD membership: .. code-block:: yaml @@ -427,14 +409,8 @@ The following is an example of a cluster configured for standalone operation: cluster_id: rhumba auth_mode: user user_group_settings: - - source_type: inline - values: - users: - - name: chuckx - password: 3xample101 - - name: steves - password: F00Bar123 - groups: [] + - source_type: resource + ref: ug1 placement: hosts: - node6.mycluster.sink.test @@ -534,6 +510,10 @@ auth Required string. User with ability to join a system to AD password Required string. The AD user's password +linked_to_cluster: + Optional. A string containing a cluster id. If set, the resource may only + be used with the linked cluster and will automatically be removed when the + linked cluster is removed. Example: @@ -564,7 +544,7 @@ values users List of objects. Fields: - username + name A user name password A password @@ -573,6 +553,10 @@ values name The name of the group +linked_to_cluster: + Optional. A string containing a cluster id. If set, the resource may only + be used with the linked cluster and will automatically be removed when the + linked cluster is removed. Example: diff --git a/src/pybind/mgr/smb/enums.py b/src/pybind/mgr/smb/enums.py index 6e19c882dad..92b8705ebba 100644 --- a/src/pybind/mgr/smb/enums.py +++ b/src/pybind/mgr/smb/enums.py @@ -41,15 +41,12 @@ class AuthMode(_StrEnum): class JoinSourceType(_StrEnum): - PASSWORD = 'password' - HTTP_URI = 'http_uri' RESOURCE = 'resource' class UserGroupSourceType(_StrEnum): - INLINE = 'inline' - HTTP_URI = 'http_uri' RESOURCE = 'resource' + EMPTY = 'empty' class ConfigNS(_StrEnum): diff --git a/src/pybind/mgr/smb/handler.py b/src/pybind/mgr/smb/handler.py index 387d0a41283..9931fe0a688 100644 --- a/src/pybind/mgr/smb/handler.py +++ b/src/pybind/mgr/smb/handler.py @@ -3,6 +3,7 @@ from typing import ( Collection, Dict, Iterable, + Iterator, List, Optional, Set, @@ -12,6 +13,8 @@ from typing import ( ) import logging +import random +import string import time from ceph.deployment.service_spec import SMBSpec @@ -28,14 +31,16 @@ from .enums import ( from .internal import ( ClusterEntry, JoinAuthEntry, - ResourceEntry, ShareEntry, UsersAndGroupsEntry, + resource_entry, + resource_key, ) from .proto import ( AccessAuthorizer, ConfigEntry, ConfigStore, + EntryKey, OrchSubmitter, PathResolver, Simplified, @@ -180,6 +185,92 @@ class _Matcher: ) +class _Staging: + def __init__(self, store: ConfigStore) -> None: + self.destination_store = store + self.incoming: Dict[EntryKey, SMBResource] = {} + self.deleted: Dict[EntryKey, SMBResource] = {} + self._keycache: Set[EntryKey] = set() + + def stage(self, resource: SMBResource) -> None: + self._keycache = set() + ekey = resource_key(resource) + if resource.intent == Intent.REMOVED: + self.deleted[ekey] = resource + else: + self.deleted.pop(ekey, None) + self.incoming[ekey] = resource + + def _virtual_keys(self) -> Iterator[EntryKey]: + new = set(self.incoming.keys()) + for ekey in self.destination_store: + if ekey in self.deleted: + continue + yield ekey + new.discard(ekey) + for ekey in new: + yield ekey + + def __iter__(self) -> Iterator[EntryKey]: + self._keycache = set(self._virtual_keys()) + return iter(self._keycache) + + def namespaces(self) -> Collection[str]: + return {k[0] for k in self} + + def contents(self, ns: str) -> Collection[str]: + return {kname for kns, kname in self if kns == ns} + + def get_cluster(self, cluster_id: str) -> resources.Cluster: + ekey = (str(ClusterEntry.namespace), cluster_id) + if ekey in self.incoming: + res = self.incoming[ekey] + assert isinstance(res, resources.Cluster) + return res + return ClusterEntry.from_store( + self.destination_store, cluster_id + ).get_cluster() + + def get_join_auth(self, auth_id: str) -> resources.JoinAuth: + ekey = (str(JoinAuthEntry.namespace), auth_id) + if ekey in self.incoming: + res = self.incoming[ekey] + assert isinstance(res, resources.JoinAuth) + return res + return JoinAuthEntry.from_store( + self.destination_store, auth_id + ).get_join_auth() + + def get_users_and_groups(self, ug_id: str) -> resources.UsersAndGroups: + ekey = (str(UsersAndGroupsEntry.namespace), ug_id) + if ekey in self.incoming: + res = self.incoming[ekey] + assert isinstance(res, resources.UsersAndGroups) + return res + return UsersAndGroupsEntry.from_store( + self.destination_store, ug_id + ).get_users_and_groups() + + def save(self) -> ResultGroup: + results = ResultGroup() + for res in self.deleted.values(): + results.append(self._save(res)) + for res in self.incoming.values(): + results.append(self._save(res)) + return results + + def _save(self, resource: SMBResource) -> Result: + entry = resource_entry(self.destination_store, resource) + if resource.intent == Intent.REMOVED: + removed = entry.remove() + state = State.REMOVED if removed else State.NOT_PRESENT + else: + state = entry.create_or_update(resource) + log.debug('saved resource: %r; state: %s', resource, state) + result = Result(resource, success=True, status={'state': state}) + return result + + class ClusterConfigHandler: """The central class for ingesting and handling smb configuration change requests. @@ -247,20 +338,26 @@ class ClusterConfigHandler: def apply(self, inputs: Iterable[SMBResource]) -> ResultGroup: log.debug('applying changes to internal data store') results = ResultGroup() - for resource in self._order_inputs(inputs): - try: - result = self._update_resource(resource) - except ErrorResult as err: - result = err - except Exception as err: - log.exception("error updating resource") - result = ErrorResult(resource, msg=str(err)) + staging = _Staging(self.internal_store) + try: + incoming = order_resources(inputs) + for resource in incoming: + staging.stage(resource) + for resource in incoming: + results.append(self._check(resource, staging)) + except ErrorResult as err: + results.append(err) + except Exception as err: + log.exception("error updating resource") + result = ErrorResult(resource, msg=str(err)) results.append(result) if results.success: log.debug( 'successfully updated %s resources. syncing changes to public stores', len(list(results)), ) + results = staging.save() + _prune_linked_entries(staging) self._sync_modified(results) return results @@ -324,58 +421,31 @@ class ClusterConfigHandler: log.debug("search found %d resources", len(out)) return out - def _order_inputs( - self, inputs: Iterable[SMBResource] - ) -> List[SMBResource]: - """Sort resource objects by type so that the user can largely input - objects freely but that references map out cleanly. - """ - - def _keyfunc(r: SMBResource) -> int: - if isinstance(r, resources.RemovedShare): - return -2 - if isinstance(r, resources.RemovedCluster): - return -1 - if isinstance(r, resources.Share): - return 2 - if isinstance(r, resources.Cluster): - return 1 - return 0 - - return sorted(inputs, key=_keyfunc) - - def _update_resource(self, resource: SMBResource) -> Result: - """Update the internal store with a new resource object.""" - entry: ResourceEntry - log.debug('updating resource: %r', resource) - if isinstance( - resource, (resources.Cluster, resources.RemovedCluster) - ): - self._check_cluster(resource) - entry = self._cluster_entry(resource.cluster_id) - elif isinstance(resource, (resources.Share, resources.RemovedShare)): - self._check_share(resource) - entry = self._share_entry(resource.cluster_id, resource.share_id) - elif isinstance(resource, resources.JoinAuth): - self._check_join_auths(resource) - entry = self._join_auth_entry(resource.auth_id) - elif isinstance(resource, resources.UsersAndGroups): - self._check_users_and_groups(resource) - entry = self._users_and_groups_entry(resource.users_groups_id) - else: - raise TypeError('not a valid smb resource') - state = self._save(entry, resource) - result = Result(resource, success=True, status={'state': state}) - log.debug('saved resource: %r; state: %s', resource, state) + def _check(self, resource: SMBResource, staging: _Staging) -> Result: + """Check/validate a staged resource.""" + log.debug('staging resource: %r', resource) + try: + if isinstance( + resource, (resources.Cluster, resources.RemovedCluster) + ): + _check_cluster(resource, staging) + elif isinstance( + resource, (resources.Share, resources.RemovedShare) + ): + _check_share(resource, staging, self._path_resolver) + elif isinstance(resource, resources.JoinAuth): + _check_join_auths(resource, staging) + elif isinstance(resource, resources.UsersAndGroups): + _check_users_and_groups(resource, staging) + else: + raise TypeError('not a valid smb resource') + except ErrorResult as err: + log.debug('rejected resource: %r', resource) + return err + log.debug('checked resource: %r', resource) + result = Result(resource, success=True, status={'checked': True}) return result - def _save(self, entry: ResourceEntry, resource: SMBResource) -> State: - # Returns the Intent indicating the previous state. - if resource.intent == Intent.REMOVED: - removed = entry.remove() - return State.REMOVED if removed else State.NOT_PRESENT - return entry.create_or_update(resource) - def _sync_clusters( self, modified_cluster_ids: Optional[Collection[str]] = None ) -> None: @@ -572,92 +642,6 @@ class ClusterConfigHandler: external.rm_cluster(self.priv_store, cluster_id) external.rm_cluster(self.public_store, cluster_id) - def _check_cluster(self, cluster: ClusterRef) -> None: - """Check that the cluster resource can be updated.""" - if cluster.intent == Intent.REMOVED: - share_ids = ShareEntry.ids(self.internal_store) - clusters_used = {cid for cid, _ in share_ids} - if cluster.cluster_id in clusters_used: - raise ErrorResult( - cluster, - msg="cluster in use by shares", - status={ - 'clusters': [ - shid - for cid, shid in share_ids - if cid == cluster.cluster_id - ] - }, - ) - return - assert isinstance(cluster, resources.Cluster) - cluster.validate() - - def _check_share(self, share: ShareRef) -> None: - """Check that the share resource can be updated.""" - if share.intent == Intent.REMOVED: - return - assert isinstance(share, resources.Share) - share.validate() - if share.cluster_id not in ClusterEntry.ids(self.internal_store): - raise ErrorResult( - share, - msg="no matching cluster id", - status={"cluster_id": share.cluster_id}, - ) - assert share.cephfs is not None - try: - self._path_resolver.resolve_exists( - share.cephfs.volume, - share.cephfs.subvolumegroup, - share.cephfs.subvolume, - share.cephfs.path, - ) - except (FileNotFoundError, NotADirectoryError): - raise ErrorResult( - share, msg="path is not a valid directory in volume" - ) - - def _check_join_auths(self, join_auth: resources.JoinAuth) -> None: - """Check that the JoinAuth resource can be updated.""" - if join_auth.intent == Intent.PRESENT: - return # adding is always ok - refs_in_use: Dict[str, List[str]] = {} - for cluster_id in ClusterEntry.ids(self.internal_store): - cluster = self._cluster_entry(cluster_id).get_cluster() - for ref in _auth_refs(cluster): - refs_in_use.setdefault(ref, []).append(cluster_id) - log.debug('refs_in_use: %r', refs_in_use) - if join_auth.auth_id in refs_in_use: - raise ErrorResult( - join_auth, - msg='join auth resource in use by clusters', - status={ - 'clusters': refs_in_use[join_auth.auth_id], - }, - ) - - def _check_users_and_groups( - self, users_and_groups: resources.UsersAndGroups - ) -> None: - """Check that the UsersAndGroups resource can be updated.""" - if users_and_groups.intent == Intent.PRESENT: - return # adding is always ok - refs_in_use: Dict[str, List[str]] = {} - for cluster_id in ClusterEntry.ids(self.internal_store): - cluster = self._cluster_entry(cluster_id).get_cluster() - for ref in _ug_refs(cluster): - refs_in_use.setdefault(ref, []).append(cluster_id) - log.debug('refs_in_use: %r', refs_in_use) - if users_and_groups.users_groups_id in refs_in_use: - raise ErrorResult( - users_and_groups, - msg='users and groups resource in use by clusters', - status={ - 'clusters': refs_in_use[users_and_groups.users_groups_id], - }, - ) - def _cluster_entry(self, cluster_id: str) -> ClusterEntry: return ClusterEntry.from_store(self.internal_store, cluster_id) @@ -716,6 +700,210 @@ class ClusterConfigHandler: ) +def order_resources( + resource_objs: Iterable[SMBResource], +) -> List[SMBResource]: + """Sort resource objects by type so that the user can largely input + objects freely but that references map out cleanly. + """ + + def _keyfunc(r: SMBResource) -> int: + if isinstance(r, resources.RemovedShare): + return -2 + if isinstance(r, resources.RemovedCluster): + return -1 + if isinstance(r, resources.Share): + return 2 + if isinstance(r, resources.Cluster): + return 1 + return 0 + + return sorted(resource_objs, key=_keyfunc) + + +def _check_cluster(cluster: ClusterRef, staging: _Staging) -> None: + """Check that the cluster resource can be updated.""" + if cluster.intent == Intent.REMOVED: + share_ids = ShareEntry.ids(staging) + clusters_used = {cid for cid, _ in share_ids} + if cluster.cluster_id in clusters_used: + raise ErrorResult( + cluster, + msg="cluster in use by shares", + status={ + 'clusters': [ + shid + for cid, shid in share_ids + if cid == cluster.cluster_id + ] + }, + ) + return + assert isinstance(cluster, resources.Cluster) + cluster.validate() + for auth_ref in _auth_refs(cluster): + auth = staging.get_join_auth(auth_ref) + if ( + auth.linked_to_cluster + and auth.linked_to_cluster != cluster.cluster_id + ): + raise ErrorResult( + cluster, + msg="join auth linked to different cluster", + status={ + 'other_cluster_id': auth.linked_to_cluster, + }, + ) + for ug_ref in _ug_refs(cluster): + ug = staging.get_users_and_groups(ug_ref) + if ( + ug.linked_to_cluster + and ug.linked_to_cluster != cluster.cluster_id + ): + raise ErrorResult( + cluster, + msg="users and groups linked to different cluster", + status={ + 'other_cluster_id': ug.linked_to_cluster, + }, + ) + + +def _check_share( + share: ShareRef, staging: _Staging, resolver: PathResolver +) -> None: + """Check that the share resource can be updated.""" + if share.intent == Intent.REMOVED: + return + assert isinstance(share, resources.Share) + share.validate() + if share.cluster_id not in ClusterEntry.ids(staging): + raise ErrorResult( + share, + msg="no matching cluster id", + status={"cluster_id": share.cluster_id}, + ) + assert share.cephfs is not None + try: + resolver.resolve_exists( + share.cephfs.volume, + share.cephfs.subvolumegroup, + share.cephfs.subvolume, + share.cephfs.path, + ) + except (FileNotFoundError, NotADirectoryError): + raise ErrorResult( + share, msg="path is not a valid directory in volume" + ) + + +def _check_join_auths( + join_auth: resources.JoinAuth, staging: _Staging +) -> None: + """Check that the JoinAuth resource can be updated.""" + if join_auth.intent == Intent.PRESENT: + return _check_join_auths_present(join_auth, staging) + return _check_join_auths_removed(join_auth, staging) + + +def _check_join_auths_removed( + join_auth: resources.JoinAuth, staging: _Staging +) -> None: + cids = set(ClusterEntry.ids(staging)) + refs_in_use: Dict[str, List[str]] = {} + for cluster_id in cids: + cluster = staging.get_cluster(cluster_id) + for ref in _auth_refs(cluster): + refs_in_use.setdefault(ref, []).append(cluster_id) + log.debug('refs_in_use: %r', refs_in_use) + if join_auth.auth_id in refs_in_use: + raise ErrorResult( + join_auth, + msg='join auth resource in use by clusters', + status={ + 'clusters': refs_in_use[join_auth.auth_id], + }, + ) + + +def _check_join_auths_present( + join_auth: resources.JoinAuth, staging: _Staging +) -> None: + if join_auth.linked_to_cluster: + cids = set(ClusterEntry.ids(staging)) + if join_auth.linked_to_cluster not in cids: + raise ErrorResult( + join_auth, + msg='linked_to_cluster id not valid', + status={ + 'unknown_id': join_auth.linked_to_cluster, + }, + ) + + +def _check_users_and_groups( + users_and_groups: resources.UsersAndGroups, staging: _Staging +) -> None: + """Check that the UsersAndGroups resource can be updated.""" + if users_and_groups.intent == Intent.PRESENT: + return _check_users_and_groups_present(users_and_groups, staging) + return _check_users_and_groups_removed(users_and_groups, staging) + + +def _check_users_and_groups_removed( + users_and_groups: resources.UsersAndGroups, staging: _Staging +) -> None: + refs_in_use: Dict[str, List[str]] = {} + cids = set(ClusterEntry.ids(staging)) + for cluster_id in cids: + cluster = staging.get_cluster(cluster_id) + for ref in _ug_refs(cluster): + refs_in_use.setdefault(ref, []).append(cluster_id) + log.debug('refs_in_use: %r', refs_in_use) + if users_and_groups.users_groups_id in refs_in_use: + raise ErrorResult( + users_and_groups, + msg='users and groups resource in use by clusters', + status={ + 'clusters': refs_in_use[users_and_groups.users_groups_id], + }, + ) + + +def _check_users_and_groups_present( + users_and_groups: resources.UsersAndGroups, staging: _Staging +) -> None: + if users_and_groups.linked_to_cluster: + cids = set(ClusterEntry.ids(staging)) + if users_and_groups.linked_to_cluster not in cids: + raise ErrorResult( + users_and_groups, + msg='linked_to_cluster id not valid', + status={ + 'unknown_id': users_and_groups.linked_to_cluster, + }, + ) + + +def _prune_linked_entries(staging: _Staging) -> None: + cids = set(ClusterEntry.ids(staging)) + for auth_id in JoinAuthEntry.ids(staging): + join_auth = staging.get_join_auth(auth_id) + if ( + join_auth.linked_to_cluster + and join_auth.linked_to_cluster not in cids + ): + JoinAuthEntry.from_store( + staging.destination_store, auth_id + ).remove() + for ug_id in UsersAndGroupsEntry.ids(staging): + ug = staging.get_users_and_groups(ug_id) + if ug.linked_to_cluster and ug.linked_to_cluster not in cids: + UsersAndGroupsEntry.from_store( + staging.destination_store, ug_id + ).remove() + + def _auth_refs(cluster: resources.Cluster) -> Collection[str]: if cluster.auth_mode != AuthMode.ACTIVE_DIRECTORY: return set() @@ -911,8 +1099,6 @@ def _save_pending_join_auths( for idx, src in enumerate(checked(cluster.domain_settings).join_sources): if src.source_type == JoinSourceType.RESOURCE: javalues = checked(arefs[src.ref].auth) - elif src.source_type == JoinSourceType.PASSWORD: - javalues = checked(src.auth) else: raise ValueError( f'unsupported join source type: {src.source_type}' @@ -936,9 +1122,8 @@ def _save_pending_users_and_groups( if ugsv.source_type == UserGroupSourceType.RESOURCE: ugvalues = augs[ugsv.ref].values assert ugvalues - elif ugsv.source_type == UserGroupSourceType.INLINE: - ugvalues = ugsv.values - assert ugvalues + elif ugsv.source_type == UserGroupSourceType.EMPTY: + continue else: raise ValueError( f'unsupported users/groups source type: {ugsv.source_type}' @@ -985,3 +1170,11 @@ def _cephx_data_entity(cluster_id: str) -> str: use for data access. """ return f'client.smb.fs.cluster.{cluster_id}' + + +def rand_name(prefix: str, max_len: int = 18, suffix_len: int = 8) -> str: + trunc = prefix[: (max_len - suffix_len)] + suffix = ''.join( + random.choice(string.ascii_lowercase) for _ in range(suffix_len) + ) + return f'{trunc}{suffix}' diff --git a/src/pybind/mgr/smb/internal.py b/src/pybind/mgr/smb/internal.py index d40561b08db..1b2554317ec 100644 --- a/src/pybind/mgr/smb/internal.py +++ b/src/pybind/mgr/smb/internal.py @@ -5,13 +5,54 @@ from typing import Collection, Tuple, Type, TypeVar from . import resources from .enums import AuthMode, ConfigNS, State -from .proto import ConfigEntry, ConfigStore, Self, Simplifiable, one +from .proto import ( + ConfigEntry, + ConfigStore, + ConfigStoreListing, + EntryKey, + Self, + Simplifiable, + one, +) from .resources import SMBResource from .results import ErrorResult T = TypeVar('T') +def cluster_key(cluster_id: str) -> EntryKey: + """Return store entry key for a cluster entry.""" + return str(ConfigNS.CLUSTERS), cluster_id + + +def share_key(cluster_id: str, share_id: str) -> EntryKey: + """Return store entry key for a share entry.""" + return str(ConfigNS.SHARES), f'{cluster_id}.{share_id}' + + +def join_auth_key(auth_id: str) -> EntryKey: + """Return store entry key for a join auth entry.""" + return str(ConfigNS.JOIN_AUTHS), auth_id + + +def users_and_groups_key(users_groups_id: str) -> EntryKey: + """Return store entry key for a users-and-groups entry.""" + return str(ConfigNS.USERS_AND_GROUPS), users_groups_id + + +def resource_key(resource: SMBResource) -> EntryKey: + """Return a store entry key for an smb resource object.""" + if isinstance(resource, (resources.Cluster, resources.RemovedCluster)): + return cluster_key(resource.cluster_id) + elif isinstance(resource, (resources.Share, resources.RemovedShare)): + return share_key(resource.cluster_id, resource.share_id) + elif isinstance(resource, resources.JoinAuth): + return join_auth_key(resource.auth_id) + elif isinstance(resource, resources.UsersAndGroups): + return users_and_groups_key(resource.users_groups_id) + raise TypeError('not a valid smb resource') + + class ResourceEntry: """Base class for resource entry getter/setter objects.""" @@ -61,7 +102,7 @@ class ClusterEntry(ResourceEntry): return cls(cluster_id, store[str(cls.namespace), cluster_id]) @classmethod - def ids(cls, store: ConfigStore) -> Collection[str]: + def ids(cls, store: ConfigStoreListing) -> Collection[str]: return store.contents(str(cls.namespace)) def get_cluster(self) -> resources.Cluster: @@ -118,7 +159,7 @@ class ShareEntry(ResourceEntry): return cls(key, store[str(cls.namespace), key]) @classmethod - def ids(cls, store: ConfigStore) -> Collection[Tuple[str, str]]: + def ids(cls, store: ConfigStoreListing) -> Collection[Tuple[str, str]]: return [_split(k) for k in store.contents(str(cls.namespace))] def get_share(self) -> resources.Share: @@ -135,7 +176,7 @@ class JoinAuthEntry(ResourceEntry): return cls(auth_id, store[str(cls.namespace), auth_id]) @classmethod - def ids(cls, store: ConfigStore) -> Collection[str]: + def ids(cls, store: ConfigStoreListing) -> Collection[str]: return store.contents(str(cls.namespace)) def get_join_auth(self) -> resources.JoinAuth: @@ -154,13 +195,30 @@ class UsersAndGroupsEntry(ResourceEntry): return cls(auth_id, store[str(cls.namespace), auth_id]) @classmethod - def ids(cls, store: ConfigStore) -> Collection[str]: + def ids(cls, store: ConfigStoreListing) -> Collection[str]: return store.contents(str(cls.namespace)) def get_users_and_groups(self) -> resources.UsersAndGroups: return self.get_resource_type(resources.UsersAndGroups) +def resource_entry( + store: ConfigStore, resource: SMBResource +) -> ResourceEntry: + """Return a bound store entry object given a resource object.""" + if isinstance(resource, (resources.Cluster, resources.RemovedCluster)): + return ClusterEntry.from_store(store, resource.cluster_id) + elif isinstance(resource, (resources.Share, resources.RemovedShare)): + return ShareEntry.from_store( + store, resource.cluster_id, resource.share_id + ) + elif isinstance(resource, resources.JoinAuth): + return JoinAuthEntry.from_store(store, resource.auth_id) + elif isinstance(resource, resources.UsersAndGroups): + return UsersAndGroupsEntry.from_store(store, resource.users_groups_id) + raise TypeError('not a valid smb resource') + + def _split(share_key: str) -> Tuple[str, str]: cluster_id, share_id = share_key.split('.', 1) return cluster_id, share_id diff --git a/src/pybind/mgr/smb/module.py b/src/pybind/mgr/smb/module.py index fff7fc46925..a6eef0cb4fb 100644 --- a/src/pybind/mgr/smb/module.py +++ b/src/pybind/mgr/smb/module.py @@ -86,6 +86,7 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): """Create an smb cluster""" domain_settings = None user_group_settings = None + to_apply: List[resources.SMBResource] = [] if domain_realm or domain_join_ref or domain_join_user_pass: join_sources: List[resources.JoinSource] = [] @@ -108,13 +109,21 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): 'a domain join username & password value' ' must contain a "%" separator' ) + rname = handler.rand_name(cluster_id) join_sources.append( resources.JoinSource( - source_type=JoinSourceType.PASSWORD, + source_type=JoinSourceType.RESOURCE, + ref=rname, + ) + ) + to_apply.append( + resources.JoinAuth( + auth_id=rname, auth=resources.JoinAuthValues( username=username, password=password, ), + linked_to_cluster=cluster_id, ) ) domain_settings = resources.DomainSettings( @@ -140,15 +149,22 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): for unpw in define_user_pass or []: username, password = unpw.split('%', 1) users.append({'name': username, 'password': password}) - user_group_settings += [ + rname = handler.rand_name(cluster_id) + user_group_settings.append( resources.UserGroupSource( - source_type=UserGroupSourceType.INLINE, + source_type=UserGroupSourceType.RESOURCE, ref=rname + ) + ) + to_apply.append( + resources.UsersAndGroups( + users_groups_id=rname, values=resources.UserGroupSettings( users=users, groups=[], ), + linked_to_cluster=cluster_id, ) - ] + ) pspec = resources.WrappedPlacementSpec.wrap( PlacementSpec.from_string(placement) @@ -161,7 +177,8 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): custom_dns=custom_dns, placement=pspec, ) - return self._handler.apply([cluster]).one() + to_apply.append(cluster) + return self._handler.apply(to_apply).squash(cluster) @cli.SMBCommand('cluster rm', perm='rw') def cluster_rm(self, cluster_id: str) -> handler.Result: diff --git a/src/pybind/mgr/smb/proto.py b/src/pybind/mgr/smb/proto.py index 96aed6c4174..6af80866cff 100644 --- a/src/pybind/mgr/smb/proto.py +++ b/src/pybind/mgr/smb/proto.py @@ -18,7 +18,7 @@ from ceph.deployment.service_spec import SMBSpec # this uses a version check as opposed to a try/except because this # form makes mypy happy and try/except doesn't. -if sys.version_info >= (3, 8): +if sys.version_info >= (3, 8): # pragma: no cover from typing import Protocol elif TYPE_CHECKING: # pragma: no cover # typing_extensions will not be available for the real mgr server @@ -29,7 +29,7 @@ else: # pragma: no cover pass -if sys.version_info >= (3, 11): +if sys.version_info >= (3, 11): # pragma: no cover from typing import Self elif TYPE_CHECKING: # pragma: no cover # typing_extensions will not be available for the real mgr server @@ -78,13 +78,8 @@ class ConfigEntry(Protocol): ... # pragma: no cover -class ConfigStore(Protocol): - """A protocol for describing a configuration data store capable of - retaining and tracking configuration entry objects. - """ - - def __getitem__(self, key: EntryKey) -> ConfigEntry: - ... # pragma: no cover +class ConfigStoreListing(Protocol): + """A protocol for describing the content-listing methods of a config store.""" def namespaces(self) -> Collection[str]: ... # pragma: no cover @@ -95,6 +90,15 @@ class ConfigStore(Protocol): def __iter__(self) -> Iterator[EntryKey]: ... # pragma: no cover + +class ConfigStore(ConfigStoreListing, Protocol): + """A protocol for describing a configuration data store capable of + retaining and tracking configuration entry objects. + """ + + def __getitem__(self, key: EntryKey) -> ConfigEntry: + ... # pragma: no cover + def remove(self, ns: EntryKey) -> bool: ... # pragma: no cover diff --git a/src/pybind/mgr/smb/resources.py b/src/pybind/mgr/smb/resources.py index aad57ff79f0..1a40076538e 100644 --- a/src/pybind/mgr/smb/resources.py +++ b/src/pybind/mgr/smb/resources.py @@ -162,18 +162,17 @@ class JoinAuthValues(_RBase): class JoinSource(_RBase): """Represents data that can be used to join a system to Active Directory.""" - source_type: JoinSourceType - auth: Optional[JoinAuthValues] = None - uri: str = '' + source_type: JoinSourceType = JoinSourceType.RESOURCE ref: str = '' def validate(self) -> None: - if self.ref: + if not self.ref: + raise ValueError('reference value must be specified') + else: validation.check_id(self.ref) @resourcelib.customize def _customize_resource(rc: resourcelib.Resource) -> resourcelib.Resource: - rc.uri.quiet = True rc.ref.quiet = True return rc @@ -190,40 +189,21 @@ class UserGroupSettings(_RBase): class UserGroupSource(_RBase): """Represents data used to set up user/group settings for an instance.""" - source_type: UserGroupSourceType - values: Optional[UserGroupSettings] = None - uri: str = '' + source_type: UserGroupSourceType = UserGroupSourceType.RESOURCE ref: str = '' def validate(self) -> None: - if self.source_type == UserGroupSourceType.INLINE: - pfx = 'inline User/Group configuration' - if self.values is None: - raise ValueError(pfx + ' requires values') - if self.uri: - raise ValueError(pfx + ' does not take a uri') - if self.ref: - raise ValueError(pfx + ' does not take a ref value') - if self.source_type == UserGroupSourceType.HTTP_URI: - pfx = 'http User/Group configuration' - if not self.uri: - raise ValueError(pfx + ' requires a uri') - if self.values: - raise ValueError(pfx + ' does not take inline values') - if self.ref: - raise ValueError(pfx + ' does not take a ref value') if self.source_type == UserGroupSourceType.RESOURCE: - pfx = 'resource reference User/Group configuration' if not self.ref: - raise ValueError(pfx + ' requires a ref value') - if self.uri: - raise ValueError(pfx + ' does not take a uri') - if self.values: - raise ValueError(pfx + ' does not take inline values') + raise ValueError('reference value must be specified') + else: + validation.check_id(self.ref) + else: + if self.ref: + raise ValueError('ref may not be specified') @resourcelib.customize def _customize_resource(rc: resourcelib.Resource) -> resourcelib.Resource: - rc.uri.quiet = True rc.ref.quiet = True return rc @@ -338,11 +318,21 @@ class JoinAuth(_RBase): auth_id: str intent: Intent = Intent.PRESENT auth: Optional[JoinAuthValues] = None + # linked resources can only be used by the resource they are linked to + # and are automatically removed when the "parent" resource is removed + linked_to_cluster: Optional[str] = None def validate(self) -> None: if not self.auth_id: raise ValueError('auth_id requires a value') validation.check_id(self.auth_id) + if self.linked_to_cluster is not None: + validation.check_id(self.linked_to_cluster) + + @resourcelib.customize + def _customize_resource(rc: resourcelib.Resource) -> resourcelib.Resource: + rc.linked_to_cluster.quiet = True + return rc @resourcelib.resource('ceph.smb.usersgroups') @@ -352,11 +342,21 @@ class UsersAndGroups(_RBase): users_groups_id: str intent: Intent = Intent.PRESENT values: Optional[UserGroupSettings] = None + # linked resources can only be used by the resource they are linked to + # and are automatically removed when the "parent" resource is removed + linked_to_cluster: Optional[str] = None def validate(self) -> None: if not self.users_groups_id: raise ValueError('users_groups_id requires a value') validation.check_id(self.users_groups_id) + if self.linked_to_cluster is not None: + validation.check_id(self.linked_to_cluster) + + @resourcelib.customize + def _customize_resource(rc: resourcelib.Resource) -> resourcelib.Resource: + rc.linked_to_cluster.quiet = True + return rc # SMBResource is a union of all valid top-level smb resource types. diff --git a/src/pybind/mgr/smb/results.py b/src/pybind/mgr/smb/results.py index 4b958fd7a5e..0d9f9605e8c 100644 --- a/src/pybind/mgr/smb/results.py +++ b/src/pybind/mgr/smb/results.py @@ -70,6 +70,23 @@ class ResultGroup: def one(self) -> Result: return one(self._contents) + def squash(self, target: SMBResource) -> Result: + match: Optional[Result] = None + others: List[Result] = [] + for result in self._contents: + if result.src == target: + match = result + else: + others.append(result) + if match: + match.success = self.success + match.status = {} if match.status is None else match.status + match.status['additional_results'] = [ + r.to_simplified() for r in others + ] + return match + raise ValueError('no matching result for resource found') + def __iter__(self) -> Iterator[Result]: return iter(self._contents) diff --git a/src/pybind/mgr/smb/tests/test_enums.py b/src/pybind/mgr/smb/tests/test_enums.py index f3f0f4eeb8b..ef0edf87acb 100644 --- a/src/pybind/mgr/smb/tests/test_enums.py +++ b/src/pybind/mgr/smb/tests/test_enums.py @@ -18,8 +18,6 @@ import smb.enums (smb.enums.State.UPDATED, "updated"), (smb.enums.AuthMode.USER, "user"), (smb.enums.AuthMode.ACTIVE_DIRECTORY, "active-directory"), - (smb.enums.JoinSourceType.PASSWORD, "password"), - (smb.enums.UserGroupSourceType.INLINE, "inline"), ], ) def test_stringified(value, strval): diff --git a/src/pybind/mgr/smb/tests/test_handler.py b/src/pybind/mgr/smb/tests/test_handler.py index 270f3e72bf9..ceaf044744d 100644 --- a/src/pybind/mgr/smb/tests/test_handler.py +++ b/src/pybind/mgr/smb/tests/test_handler.py @@ -31,11 +31,7 @@ def test_internal_apply_cluster(thandler): auth_mode=smb.enums.AuthMode.USER, user_group_settings=[ smb.resources.UserGroupSource( - source_type=smb.resources.UserGroupSourceType.INLINE, - values=smb.resources.UserGroupSettings( - users=[], - groups=[], - ), + source_type=smb.resources.UserGroupSourceType.EMPTY, ), ], ) @@ -50,11 +46,7 @@ def test_cluster_add(thandler): auth_mode=smb.enums.AuthMode.USER, user_group_settings=[ smb.resources.UserGroupSource( - source_type=smb.resources.UserGroupSourceType.INLINE, - values=smb.resources.UserGroupSettings( - users=[], - groups=[], - ), + source_type=smb.resources.UserGroupSourceType.EMPTY, ), ], ) @@ -72,11 +64,7 @@ def test_internal_apply_cluster_and_share(thandler): auth_mode=smb.enums.AuthMode.USER, user_group_settings=[ smb.resources.UserGroupSource( - source_type=smb.resources.UserGroupSourceType.INLINE, - values=smb.resources.UserGroupSettings( - users=[], - groups=[], - ), + source_type=smb.resources.UserGroupSourceType.EMPTY, ), ], ) @@ -109,8 +97,7 @@ def test_internal_apply_remove_cluster(thandler): 'intent': 'present', 'user_group_settings': [ { - 'source_type': 'inline', - 'values': {'users': [], 'groups': []}, + 'source_type': 'empty', } ], } @@ -141,8 +128,7 @@ def test_internal_apply_remove_shares(thandler): 'intent': 'present', 'user_group_settings': [ { - 'source_type': 'inline', - 'values': {'users': [], 'groups': []}, + 'source_type': 'empty', } ], }, @@ -222,8 +208,7 @@ def test_internal_apply_add_joinauth(thandler): 'intent': 'present', 'user_group_settings': [ { - 'source_type': 'inline', - 'values': {'users': [], 'groups': []}, + 'source_type': 'empty', } ], } @@ -254,8 +239,7 @@ def test_internal_apply_add_usergroups(thandler): 'intent': 'present', 'user_group_settings': [ { - 'source_type': 'inline', - 'values': {'users': [], 'groups': []}, + 'source_type': 'empty', } ], } @@ -286,8 +270,7 @@ def test_generate_config_basic(thandler): 'intent': 'present', 'user_group_settings': [ { - 'source_type': 'inline', - 'values': {'users': [], 'groups': []}, + 'source_type': 'empty', } ], }, @@ -338,15 +321,21 @@ def test_generate_config_ad(thandler): 'realm': 'dom1.example.com', 'join_sources': [ { - 'source_type': 'password', - 'auth': { - 'username': 'testadmin', - 'password': 'Passw0rd', - }, + 'source_type': 'resource', + 'ref': 'foo1', } ], }, }, + 'join_auths.foo1': { + 'resource_type': 'ceph.smb.join.auth', + 'auth_id': 'foo1', + 'intent': 'present', + 'auth': { + 'username': 'testadmin', + 'password': 'Passw0rd', + }, + }, 'shares.foo.s1': { 'resource_type': 'ceph.smb.share', 'cluster_id': 'foo', @@ -566,52 +555,6 @@ def test_apply_update_password(thandler): assert jdata == {'username': 'testadmin', 'password': 'Zm9vYmFyCg'} -def test_apply_update_cluster_inline_pw(thandler): - test_apply_full_cluster_create(thandler) - to_apply = [ - smb.resources.Cluster( - cluster_id='mycluster1', - auth_mode=smb.enums.AuthMode.ACTIVE_DIRECTORY, - domain_settings=smb.resources.DomainSettings( - realm='MYDOMAIN.EXAMPLE.ORG', - join_sources=[ - smb.resources.JoinSource( - source_type=smb.enums.JoinSourceType.RESOURCE, - ref='join1', - ), - smb.resources.JoinSource( - source_type=smb.enums.JoinSourceType.PASSWORD, - auth=smb.resources.JoinAuthValues( - username='Jimmy', - password='j4mb0ree!', - ), - ), - ], - ), - ), - ] - - results = thandler.apply(to_apply) - assert results.success, results.to_simplified() - assert len(list(results)) == 1 - - assert 'mycluster1' in thandler.public_store.namespaces() - ekeys = list(thandler.public_store.contents('mycluster1')) - assert len(ekeys) == 5 - assert 'cluster-info' in ekeys - assert 'config.smb' in ekeys - assert 'spec.smb' in ekeys - assert 'join.0.json' in ekeys - assert 'join.1.json' in ekeys - - # we changed the password value. the store should reflect that - jdata = thandler.public_store['mycluster1', 'join.0.json'].get() - assert jdata == {'username': 'testadmin', 'password': 'Passw0rd'} - # we changed the password value. the store should reflect that - jdata2 = thandler.public_store['mycluster1', 'join.1.json'].get() - assert jdata2 == {'username': 'Jimmy', 'password': 'j4mb0ree!'} - - def test_apply_add_second_cluster(thandler): test_apply_full_cluster_create(thandler) to_apply = [ @@ -622,15 +565,20 @@ def test_apply_add_second_cluster(thandler): realm='YOURDOMAIN.EXAMPLE.ORG', join_sources=[ smb.resources.JoinSource( - source_type=smb.enums.JoinSourceType.PASSWORD, - auth=smb.resources.JoinAuthValues( - username='Jimmy', - password='j4mb0ree!', - ), + source_type=smb.enums.JoinSourceType.RESOURCE, + ref='coolcluster', ), ], ), ), + smb.resources.JoinAuth( + auth_id='coolcluster', + auth=smb.resources.JoinAuthValues( + username='Jimmy', + password='j4mb0ree!', + ), + linked_to_cluster='coolcluster', + ), smb.resources.Share( cluster_id='coolcluster', share_id='images', @@ -643,7 +591,7 @@ def test_apply_add_second_cluster(thandler): results = thandler.apply(to_apply) assert results.success, results.to_simplified() - assert len(list(results)) == 2 + assert len(list(results)) == 3 assert 'mycluster1' in thandler.public_store.namespaces() ekeys = list(thandler.public_store.contents('mycluster1')) @@ -865,13 +813,14 @@ def test_apply_remove_all_clusters(thandler): def test_all_resources(thandler): test_apply_add_second_cluster(thandler) rall = thandler.all_resources() - assert len(rall) == 6 + assert len(rall) == 7 assert rall[0].resource_type == 'ceph.smb.cluster' assert rall[1].resource_type == 'ceph.smb.share' assert rall[2].resource_type == 'ceph.smb.share' assert rall[3].resource_type == 'ceph.smb.cluster' assert rall[4].resource_type == 'ceph.smb.share' assert rall[5].resource_type == 'ceph.smb.join.auth' + assert rall[6].resource_type == 'ceph.smb.join.auth' @pytest.mark.parametrize( @@ -962,6 +911,10 @@ def test_all_resources(thandler): 'resource_type': 'ceph.smb.join.auth', 'auth_id': 'join1', }, + { + 'resource_type': 'ceph.smb.join.auth', + 'auth_id': 'coolcluster', + }, ], ), # cluster with id @@ -1051,3 +1004,115 @@ def test_matching_resources(thandler, params): def test_invalid_resource_match_strs(thandler, txt): with pytest.raises(ValueError): thandler.matching_resources([txt]) + + +def test_apply_cluster_linked_auth(thandler): + to_apply = [ + smb.resources.JoinAuth( + auth_id='join1', + auth=smb.resources.JoinAuthValues( + username='testadmin', + password='Passw0rd', + ), + linked_to_cluster='mycluster1', + ), + smb.resources.Cluster( + cluster_id='mycluster1', + auth_mode=smb.enums.AuthMode.ACTIVE_DIRECTORY, + domain_settings=smb.resources.DomainSettings( + realm='MYDOMAIN.EXAMPLE.ORG', + join_sources=[ + smb.resources.JoinSource( + source_type=smb.enums.JoinSourceType.RESOURCE, + ref='join1', + ), + ], + ), + custom_dns=['192.168.76.204'], + ), + smb.resources.Share( + cluster_id='mycluster1', + share_id='homedirs', + name='Home Directries', + cephfs=smb.resources.CephFSStorage( + volume='cephfs', + subvolume='homedirs', + path='/', + ), + ), + ] + results = thandler.apply(to_apply) + assert results.success, results.to_simplified() + assert len(list(results)) == 3 + assert ('clusters', 'mycluster1') in thandler.internal_store.data + assert ('shares', 'mycluster1.homedirs') in thandler.internal_store.data + assert ('join_auths', 'join1') in thandler.internal_store.data + + to_apply = [ + smb.resources.RemovedCluster( + cluster_id='mycluster1', + ), + smb.resources.RemovedShare( + cluster_id='mycluster1', + share_id='homedirs', + ), + ] + results = thandler.apply(to_apply) + assert results.success, results.to_simplified() + assert len(list(results)) == 2 + assert ('clusters', 'mycluster1') not in thandler.internal_store.data + assert ( + 'shares', + 'mycluster1.homedirs', + ) not in thandler.internal_store.data + assert ('join_auths', 'join1') not in thandler.internal_store.data + + +def test_apply_cluster_bad_linked_auth(thandler): + to_apply = [ + smb.resources.JoinAuth( + auth_id='join1', + auth=smb.resources.JoinAuthValues( + username='testadmin', + password='Passw0rd', + ), + linked_to_cluster='mycluster2', + ), + smb.resources.Cluster( + cluster_id='mycluster1', + auth_mode=smb.enums.AuthMode.ACTIVE_DIRECTORY, + domain_settings=smb.resources.DomainSettings( + realm='MYDOMAIN.EXAMPLE.ORG', + join_sources=[ + smb.resources.JoinSource( + source_type=smb.enums.JoinSourceType.RESOURCE, + ref='join1', + ), + ], + ), + custom_dns=['192.168.76.204'], + ), + ] + results = thandler.apply(to_apply) + assert not results.success + rs = results.to_simplified() + assert len(rs['results']) == 2 + assert rs['results'][0]['msg'] == 'linked_to_cluster id not valid' + assert rs['results'][1]['msg'] == 'join auth linked to different cluster' + + +def test_rand_name(): + name = smb.handler.rand_name('bob') + assert name.startswith('bob') + assert len(name) == 11 + name = smb.handler.rand_name('carla') + assert name.startswith('carla') + assert len(name) == 13 + name = smb.handler.rand_name('dangeresque') + assert name.startswith('dangeresqu') + assert len(name) == 18 + name = smb.handler.rand_name('fhqwhgadsfhqwhgadsfhqwhgads') + assert name.startswith('fhqwhgadsf') + assert len(name) == 18 + name = smb.handler.rand_name('') + assert len(name) == 8 diff --git a/src/pybind/mgr/smb/tests/test_resources.py b/src/pybind/mgr/smb/tests/test_resources.py index 6fce09c2698..82446876a7c 100644 --- a/src/pybind/mgr/smb/tests/test_resources.py +++ b/src/pybind/mgr/smb/tests/test_resources.py @@ -117,10 +117,6 @@ domain_settings: join_sources: - source_type: resource ref: bob - - source_type: password - auth: - username: Administrator - password: fallb4kP4ssw0rd --- resource_type: ceph.smb.share cluster_id: chacha @@ -168,13 +164,10 @@ def test_load_yaml_resource_yaml1(): assert cluster.intent == enums.Intent.PRESENT assert cluster.auth_mode == enums.AuthMode.ACTIVE_DIRECTORY assert cluster.domain_settings.realm == 'CEPH.SINK.TEST' - assert len(cluster.domain_settings.join_sources) == 2 + assert len(cluster.domain_settings.join_sources) == 1 jsrc = cluster.domain_settings.join_sources assert jsrc[0].source_type == enums.JoinSourceType.RESOURCE assert jsrc[0].ref == 'bob' - assert jsrc[1].source_type == enums.JoinSourceType.PASSWORD - assert jsrc[1].auth.username == 'Administrator' - assert jsrc[1].auth.password == 'fallb4kP4ssw0rd' assert isinstance(loaded[1], smb.resources.Share) assert isinstance(loaded[2], smb.resources.Share) @@ -427,7 +420,7 @@ domain_settings: "exc_type": ValueError, "error": "not supported", }, - # u/g inline missing + # u/g empty with extra ref { "yaml": """ resource_type: ceph.smb.cluster @@ -435,89 +428,11 @@ cluster_id: randolph intent: present auth_mode: user user_group_settings: - - source_type: inline -""", - "exc_type": ValueError, - "error": "requires values", - }, - # u/g inline extra uri - { - "yaml": """ -resource_type: ceph.smb.cluster -cluster_id: randolph -intent: present -auth_mode: user -user_group_settings: - - source_type: inline - values: - users: [] - groups: [] - uri: http://foo.bar.example.com/baz.txt -""", - "exc_type": ValueError, - "error": "does not take", - }, - # u/g inline extra ref - { - "yaml": """ -resource_type: ceph.smb.cluster -cluster_id: randolph -intent: present -auth_mode: user -user_group_settings: - - source_type: inline - values: - users: [] - groups: [] + - source_type: empty ref: xyz """, "exc_type": ValueError, - "error": "does not take", - }, - # u/g uri missing - { - "yaml": """ -resource_type: ceph.smb.cluster -cluster_id: randolph -intent: present -auth_mode: user -user_group_settings: - - source_type: http_uri -""", - "exc_type": ValueError, - "error": "requires", - }, - # u/g uri extra values - { - "yaml": """ -resource_type: ceph.smb.cluster -cluster_id: randolph -intent: present -auth_mode: user -user_group_settings: - - source_type: http_uri - values: - users: [] - groups: [] - uri: http://foo.bar.example.com/baz.txt -""", - "exc_type": ValueError, - "error": "does not take", - }, - # u/g uri extra ref - { - "yaml": """ -resource_type: ceph.smb.cluster -cluster_id: randolph -intent: present -auth_mode: user -user_group_settings: - - source_type: http_uri - uri: http://boop.example.net - ref: xyz -""", - "exc_type": ValueError, - "error": "does not take", + "error": "ref may not be", }, # u/g resource missing { @@ -530,39 +445,7 @@ user_group_settings: - source_type: resource """, "exc_type": ValueError, - "error": "requires", - }, - # u/g resource extra values - { - "yaml": """ -resource_type: ceph.smb.cluster -cluster_id: randolph -intent: present -auth_mode: user -user_group_settings: - - source_type: resource - ref: xyz - uri: http://example.net/foo -""", - "exc_type": ValueError, - "error": "does not take", - }, - # u/g resource extra resource - { - "yaml": """ -resource_type: ceph.smb.cluster -cluster_id: randolph -intent: present -auth_mode: user -user_group_settings: - - source_type: resource - ref: xyz - values: - users: [] - groups: [] -""", - "exc_type": ValueError, - "error": "does not take", + "error": "reference value must be", }, ], ) diff --git a/src/pybind/mgr/smb/tests/test_smb.py b/src/pybind/mgr/smb/tests/test_smb.py index 03648750360..2e6ec2f96ff 100644 --- a/src/pybind/mgr/smb/tests/test_smb.py +++ b/src/pybind/mgr/smb/tests/test_smb.py @@ -39,11 +39,7 @@ def test_internal_apply_cluster(tmodule): auth_mode=smb.enums.AuthMode.USER, user_group_settings=[ smb.resources.UserGroupSource( - source_type=smb.resources.UserGroupSourceType.INLINE, - values=smb.resources.UserGroupSettings( - users=[], - groups=[], - ), + source_type=smb.resources.UserGroupSourceType.EMPTY, ), ], ) @@ -58,11 +54,7 @@ def test_cluster_add_cluster_ls(tmodule): auth_mode=smb.enums.AuthMode.USER, user_group_settings=[ smb.resources.UserGroupSource( - source_type=smb.resources.UserGroupSourceType.INLINE, - values=smb.resources.UserGroupSettings( - users=[], - groups=[], - ), + source_type=smb.resources.UserGroupSourceType.EMPTY, ), ], ) @@ -80,11 +72,7 @@ def test_internal_apply_cluster_and_share(tmodule): auth_mode=smb.enums.AuthMode.USER, user_group_settings=[ smb.resources.UserGroupSource( - source_type=smb.resources.UserGroupSourceType.INLINE, - values=smb.resources.UserGroupSettings( - users=[], - groups=[], - ), + source_type=smb.resources.UserGroupSourceType.EMPTY, ), ], ) @@ -117,8 +105,7 @@ def test_internal_apply_remove_cluster(tmodule): 'intent': 'present', 'user_group_settings': [ { - 'source_type': 'inline', - 'values': {'users': [], 'groups': []}, + 'source_type': 'empty', } ], } @@ -149,8 +136,7 @@ def test_internal_apply_remove_shares(tmodule): 'intent': 'present', 'user_group_settings': [ { - 'source_type': 'inline', - 'values': {'users': [], 'groups': []}, + 'source_type': 'empty', } ], }, @@ -230,8 +216,7 @@ def test_internal_apply_add_joinauth(tmodule): 'intent': 'present', 'user_group_settings': [ { - 'source_type': 'inline', - 'values': {'users': [], 'groups': []}, + 'source_type': 'empty', } ], } @@ -262,8 +247,7 @@ def test_internal_apply_add_usergroups(tmodule): 'intent': 'present', 'user_group_settings': [ { - 'source_type': 'inline', - 'values': {'users': [], 'groups': []}, + 'source_type': 'empty', } ], } @@ -296,15 +280,21 @@ def _example_cfg_1(tmodule): 'realm': 'dom1.example.com', 'join_sources': [ { - 'source_type': 'password', - 'auth': { - 'username': 'testadmin', - 'password': 'Passw0rd', - }, + 'source_type': 'resource', + 'ref': 'foo', } ], }, }, + 'join_auths.foo': { + 'resource_type': 'ceph.smb.join.auth', + 'auth_id': 'foo', + 'intent': 'present', + 'auth': { + 'username': 'testadmin', + 'password': 'Passw0rd', + }, + }, 'shares.foo.s1': { 'resource_type': 'ceph.smb.share', 'cluster_id': 'foo', @@ -490,15 +480,24 @@ def test_cluster_create_ad1(tmodule): assert len(result.src.domain_settings.join_sources) == 1 assert ( result.src.domain_settings.join_sources[0].source_type - == smb.enums.JoinSourceType.PASSWORD + == smb.enums.JoinSourceType.RESOURCE ) + assert result.src.domain_settings.join_sources[0].ref.startswith('fizzle') + assert 'additional_results' in result.status + assert len(result.status['additional_results']) == 1 assert ( - result.src.domain_settings.join_sources[0].auth.username - == 'Administrator' + result.status['additional_results'][0]['resource']['resource_type'] + == 'ceph.smb.join.auth' ) assert ( - result.src.domain_settings.join_sources[0].auth.password == 'Passw0rd' + result.status['additional_results'][0]['resource'][ + 'linked_to_cluster' + ] + == 'fizzle' ) + assert result.status['additional_results'][0]['resource'][ + 'auth_id' + ].startswith('fizzle') def test_cluster_create_ad2(tmodule): |