Merge pull request #57180 from phlogistonjohn/jjm-smb-linked-res

mgr/smb: cluster linked  join auth and users/groups resource types

Reviewed-by: Adam King <adking@redhat.com>
This commit is contained in:
Adam King 2024-06-11 12:20:30 -04:00 committed by GitHub
commit 4098fa130a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 693 additions and 478 deletions

View File

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

View File

@ -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):

View File

@ -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}'

View File

@ -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

View File

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

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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",
},
],
)

View File

@ -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.status['additional_results'][0]['resource']['resource_type']
== 'ceph.smb.join.auth'
)
assert (
result.src.domain_settings.join_sources[0].auth.username
== 'Administrator'
)
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):