mirror of
https://github.com/ceph/ceph
synced 2025-01-20 01:51:34 +00:00
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:
commit
4098fa130a
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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}'
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user