Skip to content

Commit 73b599d

Browse files
authored
Merge pull request ceph#57294 from phlogistonjohn/jjm-smb-free-customize
mgr/smb: add custom config options to share and cluster resources Reviewed-by: Adam King <[email protected]>
2 parents 9939850 + afd7cee commit 73b599d

File tree

8 files changed

+229
-22
lines changed

8 files changed

+229
-22
lines changed

doc/mgr/smb.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,21 @@ custom_dns
357357
placement
358358
Optional. A Ceph Orchestration :ref:`placement specifier
359359
<orchestrator-cli-placement-spec>`. Defaults to one host if not provided
360+
custom_smb_share_options
361+
Optional mapping. Specify key-value pairs that will be directly added to
362+
the global ``smb.conf`` options (or equivalent) of a Samba server. Do
363+
*not* use this option unless you are prepared to debug the Samba instances
364+
yourself.
365+
366+
This option is meant for developers, feature investigators, and other
367+
advanced users to take more direct control of a share's options without
368+
needing to make changes to the Ceph codebase. Entries in this map should
369+
match parameters in ``smb.conf`` and their values. A special key
370+
``_allow_customization`` must appear somewhere in the mapping with the
371+
value of ``i-take-responsibility-for-all-samba-configuration-errors`` as an
372+
indicator that the user is aware that using this option can easily break
373+
things in ways that the Ceph team can not help with. This special key will
374+
automatically be removed from the list of options passed to Samba.
360375

361376

362377
.. _join-source-fields:
@@ -465,6 +480,20 @@ cephfs
465480
provider
466481
Optional. One of ``samba-vfs`` or ``kcephfs`` (``kcephfs`` is not yet
467482
supported) . Selects how CephFS storage should be provided to the share
483+
custom_smb_share_options
484+
Optional mapping. Specify key-value pairs that will be directly added to
485+
the ``smb.conf`` (or equivalent) of a Samba server. Do *not* use this
486+
option unless you are prepared to debug the Samba instances yourself.
487+
488+
This option is meant for developers, feature investigators, and other
489+
advanced users to take more direct control of a share's options without
490+
needing to make changes to the Ceph codebase. Entries in this map should
491+
match parameters in ``smb.conf`` and their values. A special key
492+
``_allow_customization`` must appear somewhere in the mapping with the
493+
value of ``i-take-responsibility-for-all-samba-configuration-errors`` as an
494+
indicator that the user is aware that using this option can easily break
495+
things in ways that the Ceph team can not help with. This special key will
496+
automatically be removed from the list of options passed to Samba.
468497

469498
The following is an example of a share:
470499

src/pybind/mgr/smb/handler.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -978,7 +978,7 @@ def _generate_share(
978978
share.cephfs.subvolume,
979979
share.cephfs.path,
980980
)
981-
return {
981+
cfg = {
982982
# smb.conf options
983983
'options': {
984984
'path': path,
@@ -992,6 +992,12 @@ def _generate_share(
992992
'x:ceph:id': f'{share.cluster_id}.{share.share_id}',
993993
}
994994
}
995+
# extend share with custom options
996+
custom_opts = share.cleaned_custom_smb_share_options
997+
if custom_opts:
998+
cfg['options'].update(custom_opts)
999+
cfg['options']['x:ceph:has_custom_options'] = 'yes'
1000+
return cfg
9951001

9961002

9971003
def _generate_config(
@@ -1016,7 +1022,7 @@ def _generate_config(
10161022
for share in shares
10171023
}
10181024

1019-
return {
1025+
cfg: Dict[str, Any] = {
10201026
'samba-container-config': 'v0',
10211027
'configs': {
10221028
cluster.cluster_id: {
@@ -1042,6 +1048,14 @@ def _generate_config(
10421048
},
10431049
'shares': share_configs,
10441050
}
1051+
# insert global custom options
1052+
custom_opts = cluster.cleaned_custom_smb_global_options
1053+
if custom_opts:
1054+
# isolate custom config opts into a section for cleanliness
1055+
gname = f'{cluster.cluster_id}_custom'
1056+
cfg['configs'][cluster.cluster_id]['globals'].append(gname)
1057+
cfg['globals'][gname] = {'options': dict(custom_opts)}
1058+
return cfg
10451059

10461060

10471061
def _generate_smb_service_spec(

src/pybind/mgr/smb/module.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ceph.deployment.service_spec import PlacementSpec, SMBSpec
77
from mgr_module import MgrModule, Option
88

9-
from . import cli, fs, handler, mon_store, rados_store, resources
9+
from . import cli, fs, handler, mon_store, rados_store, resources, results
1010
from .enums import AuthMode, JoinSourceType, UserGroupSourceType
1111
from .proto import AccessAuthorizer, Simplified
1212

@@ -59,11 +59,18 @@ def __init__(self, *args: str, **kwargs: Any) -> None:
5959
)
6060

6161
@cli.SMBCommand('apply', perm='rw')
62-
def apply_resources(self, inbuf: str) -> handler.ResultGroup:
62+
def apply_resources(self, inbuf: str) -> results.ResultGroup:
6363
"""Create, update, or remove smb configuration resources based on YAML
6464
or JSON specs
6565
"""
66-
return self._handler.apply(resources.load_text(inbuf))
66+
try:
67+
return self._handler.apply(resources.load_text(inbuf))
68+
except resources.InvalidResourceError as err:
69+
# convert the exception into a result and return it as the only
70+
# item in the result group
71+
return results.ResultGroup(
72+
[results.InvalidResourceResult(err.resource_data, str(err))]
73+
)
6774

6875
@cli.SMBCommand('cluster ls', perm='r')
6976
def cluster_ls(self) -> List[str]:
@@ -82,7 +89,7 @@ def cluster_create(
8289
define_user_pass: Optional[List[str]] = None,
8390
custom_dns: Optional[List[str]] = None,
8491
placement: Optional[str] = None,
85-
) -> handler.Result:
92+
) -> results.Result:
8693
"""Create an smb cluster"""
8794
domain_settings = None
8895
user_group_settings = None
@@ -181,7 +188,7 @@ def cluster_create(
181188
return self._handler.apply(to_apply, create_only=True).squash(cluster)
182189

183190
@cli.SMBCommand('cluster rm', perm='rw')
184-
def cluster_rm(self, cluster_id: str) -> handler.Result:
191+
def cluster_rm(self, cluster_id: str) -> results.Result:
185192
"""Remove an smb cluster"""
186193
cluster = resources.RemovedCluster(cluster_id=cluster_id)
187194
return self._handler.apply([cluster]).one()
@@ -207,7 +214,7 @@ def share_create(
207214
share_name: str = '',
208215
subvolume: str = '',
209216
readonly: bool = False,
210-
) -> handler.Result:
217+
) -> results.Result:
211218
"""Create an smb share"""
212219
share = resources.Share(
213220
cluster_id=cluster_id,
@@ -223,7 +230,7 @@ def share_create(
223230
return self._handler.apply([share], create_only=True).one()
224231

225232
@cli.SMBCommand('share rm', perm='rw')
226-
def share_rm(self, cluster_id: str, share_id: str) -> handler.Result:
233+
def share_rm(self, cluster_id: str, share_id: str) -> results.Result:
227234
"""Remove an smb share"""
228235
share = resources.RemovedShare(
229236
cluster_id=cluster_id, share_id=share_id

src/pybind/mgr/smb/resourcelib.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
Callable,
8484
Dict,
8585
Hashable,
86+
Iterator,
8687
List,
8788
Optional,
8889
Tuple,
@@ -91,6 +92,7 @@
9192
import dataclasses
9293
import logging
9394
import sys
95+
from contextlib import contextmanager
9496
from itertools import chain
9597

9698
from .proto import Self, Simplified
@@ -304,6 +306,7 @@ def __init__(self, cls: Any) -> None:
304306
self.resource_cls = cls
305307
self.fields: Dict[str, Field] = {}
306308
self._on_condition: Optional[Callable[..., bool]] = None
309+
self._on_construction_error: Optional[Callable[..., Exception]] = None
307310

308311
for fld in dataclasses.fields(self.resource_cls):
309312
self.fields[fld.name] = Field.create(fld)
@@ -317,6 +320,12 @@ def on_condition(self, cond: Callable[..., bool]) -> None:
317320
"""Set a condition function."""
318321
self._on_condition = cond
319322

323+
def on_construction_error(self, cond: Callable[..., Exception]) -> None:
324+
"""Set a function to handle/convert exceptions that occur while
325+
constructing objects from simplified data.
326+
"""
327+
self._on_construction_error = cond
328+
320329
def type_name(self) -> str:
321330
"""Return the name of the type managed by this resource."""
322331
return self.resource_cls.__name__
@@ -330,16 +339,29 @@ def object_from_simplified(self, data: Simplified) -> Any:
330339
"""Given a dict-based unstructured data object return the structured
331340
object-based equivalent.
332341
"""
333-
kw = {}
334-
for fld in self.fields.values():
335-
value = self._object_field_from_simplified(fld, data)
336-
if value is not _unset:
337-
kw[fld.name] = value
338-
obj = self.resource_cls(**kw)
339-
validate = getattr(obj, 'validate', None)
340-
if validate:
341-
validate()
342-
return obj
342+
with self._structuring_error_hook(self.resource_cls, data):
343+
kw = {}
344+
for fld in self.fields.values():
345+
value = self._object_field_from_simplified(fld, data)
346+
if value is not _unset:
347+
kw[fld.name] = value
348+
obj = self.resource_cls(**kw)
349+
validate = getattr(obj, 'validate', None)
350+
if validate:
351+
validate()
352+
return obj
353+
354+
@contextmanager
355+
@_xt
356+
def _structuring_error_hook(
357+
self, resource_cls: Any, data: Simplified
358+
) -> Iterator[None]:
359+
try:
360+
yield
361+
except Exception as err:
362+
if self._on_construction_error:
363+
raise self._on_construction_error(err, data) from err
364+
raise
343365

344366
@_xt
345367
def _object_field_from_simplified(

src/pybind/mgr/smb/resources.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ def _present(data: Simplified) -> bool:
3232
return _get_intent(data) == Intent.PRESENT
3333

3434

35+
class InvalidResourceError(ValueError):
36+
def __init__(self, msg: str, data: Simplified) -> None:
37+
super().__init__(msg)
38+
self.resource_data = data
39+
40+
@classmethod
41+
def wrap(cls, err: Exception, data: Simplified) -> Exception:
42+
if isinstance(err, ValueError) and not isinstance(
43+
err, resourcelib.ResourceTypeError
44+
):
45+
return cls(str(err), data)
46+
return err
47+
48+
3549
class _RBase:
3650
# mypy doesn't currently (well?) support class decorators adding methods
3751
# so we use a base class to add this method to all our resource classes.
@@ -104,6 +118,7 @@ def validate(self) -> None:
104118
@resourcelib.customize
105119
def _customize_resource(rc: resourcelib.Resource) -> resourcelib.Resource:
106120
rc.on_condition(_removed)
121+
rc.on_construction_error(InvalidResourceError.wrap)
107122
return rc
108123

109124

@@ -119,6 +134,7 @@ class Share(_RBase):
119134
readonly: bool = False
120135
browseable: bool = True
121136
cephfs: Optional[CephFSStorage] = None
137+
custom_smb_share_options: Optional[Dict[str, str]] = None
122138

123139
def __post_init__(self) -> None:
124140
# if name is not given explicitly, take it from the share_id
@@ -138,6 +154,7 @@ def validate(self) -> None:
138154
# currently only cephfs is supported
139155
if self.cephfs is None:
140156
raise ValueError('a cephfs configuration is required')
157+
validation.check_custom_options(self.custom_smb_share_options)
141158

142159
@property
143160
def checked_cephfs(self) -> CephFSStorage:
@@ -147,8 +164,13 @@ def checked_cephfs(self) -> CephFSStorage:
147164
@resourcelib.customize
148165
def _customize_resource(rc: resourcelib.Resource) -> resourcelib.Resource:
149166
rc.on_condition(_present)
167+
rc.on_construction_error(InvalidResourceError.wrap)
150168
return rc
151169

170+
@property
171+
def cleaned_custom_smb_share_options(self) -> Optional[Dict[str, str]]:
172+
return validation.clean_custom_options(self.custom_smb_share_options)
173+
152174

153175
@resourcelib.component()
154176
class JoinAuthValues(_RBase):
@@ -226,6 +248,7 @@ class RemovedCluster(_RBase):
226248
@resourcelib.customize
227249
def _customize_resource(rc: resourcelib.Resource) -> resourcelib.Resource:
228250
rc.on_condition(_removed)
251+
rc.on_construction_error(InvalidResourceError.wrap)
229252
return rc
230253

231254
def validate(self) -> None:
@@ -277,6 +300,7 @@ class Cluster(_RBase):
277300
domain_settings: Optional[DomainSettings] = None
278301
user_group_settings: Optional[List[UserGroupSource]] = None
279302
custom_dns: Optional[List[str]] = None
303+
custom_smb_global_options: Optional[Dict[str, str]] = None
280304
# embedded orchestration placement spec
281305
placement: Optional[WrappedPlacementSpec] = None
282306

@@ -304,12 +328,18 @@ def validate(self) -> None:
304328
raise ValueError(
305329
'domain settings not supported for user auth mode'
306330
)
331+
validation.check_custom_options(self.custom_smb_global_options)
307332

308333
@resourcelib.customize
309334
def _customize_resource(rc: resourcelib.Resource) -> resourcelib.Resource:
310335
rc.on_condition(_present)
336+
rc.on_construction_error(InvalidResourceError.wrap)
311337
return rc
312338

339+
@property
340+
def cleaned_custom_smb_global_options(self) -> Optional[Dict[str, str]]:
341+
return validation.clean_custom_options(self.custom_smb_global_options)
342+
313343

314344
@resourcelib.resource('ceph.smb.join.auth')
315345
class JoinAuth(_RBase):
@@ -332,6 +362,7 @@ def validate(self) -> None:
332362
@resourcelib.customize
333363
def _customize_resource(rc: resourcelib.Resource) -> resourcelib.Resource:
334364
rc.linked_to_cluster.quiet = True
365+
rc.on_construction_error(InvalidResourceError.wrap)
335366
return rc
336367

337368

@@ -356,6 +387,7 @@ def validate(self) -> None:
356387
@resourcelib.customize
357388
def _customize_resource(rc: resourcelib.Resource) -> resourcelib.Resource:
358389
rc.linked_to_cluster.quiet = True
390+
rc.on_construction_error(InvalidResourceError.wrap)
359391
return rc
360392

361393

src/pybind/mgr/smb/results.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Iterator, List, Optional
1+
from typing import Iterable, Iterator, List, Optional
22

33
import errno
44

@@ -56,13 +56,38 @@ def __init__(
5656
super().__init__(src, success=False, msg=msg, status=status)
5757

5858

59+
class InvalidResourceResult(Result):
60+
def __init__(
61+
self,
62+
resource_data: Simplified,
63+
msg: str = '',
64+
status: Optional[Simplified] = None,
65+
) -> None:
66+
self.resource_data = resource_data
67+
self.success = False
68+
self.msg = msg
69+
self.status = status
70+
71+
def to_simplified(self) -> Simplified:
72+
ds: Simplified = {}
73+
ds['resource'] = self.resource_data
74+
ds['success'] = self.success
75+
if self.msg:
76+
ds['msg'] = self.msg
77+
if self.status:
78+
ds.update(self.status)
79+
return ds
80+
81+
5982
class ResultGroup:
6083
"""Result of applying multiple smb resource updates to the system."""
6184

6285
# Compatible with object formatter, thus suitable for being returned
6386
# directly to mgr module.
64-
def __init__(self) -> None:
65-
self._contents: List[Result] = []
87+
def __init__(
88+
self, initial_results: Optional[Iterable[Result]] = None
89+
) -> None:
90+
self._contents: List[Result] = list(initial_results or [])
6691

6792
def append(self, result: Result) -> None:
6893
self._contents.append(result)

0 commit comments

Comments
 (0)