summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/mgr/smb.rst50
-rw-r--r--src/pybind/mgr/smb/enums.py5
-rw-r--r--src/pybind/mgr/smb/handler.py493
-rw-r--r--src/pybind/mgr/smb/internal.py68
-rw-r--r--src/pybind/mgr/smb/module.py27
-rw-r--r--src/pybind/mgr/smb/proto.py22
-rw-r--r--src/pybind/mgr/smb/resources.py62
-rw-r--r--src/pybind/mgr/smb/results.py17
-rw-r--r--src/pybind/mgr/smb/tests/test_enums.py2
-rw-r--r--src/pybind/mgr/smb/tests/test_handler.py231
-rw-r--r--src/pybind/mgr/smb/tests/test_resources.py127
-rw-r--r--src/pybind/mgr/smb/tests/test_smb.py63
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):