Skip to content

Commit 14f033f

Browse files
authored
Merge pull request ceph#64372 from phlogistonjohn/jjm-smb-remotectl
smb: add remote control server Reviewed-by: Adam King <[email protected]> Reviewed-by: Anoop C S <[email protected]>
2 parents 52c80fb + 3e7009d commit 14f033f

File tree

18 files changed

+853
-83
lines changed

18 files changed

+853
-83
lines changed

doc/mgr/smb.rst

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Create Cluster
5757

5858
ceph smb cluster create <cluster_id> {user|active-directory} [--domain-realm=<domain_realm>] [--domain-join-user-pass=<domain_join_user_pass>] [--define-user-pass=<define_user_pass>] [--custom-dns=<custom_dns>] [--placement=<placement>] [--clustering=<clustering>] [--password-filter=<password_filter>] [--password-filter-out=<password_filter_out>]
5959

60-
Create a new logical cluster, identified by the cluster id value. The cluster
60+
Create a new logical cluster, identified by the cluster ID value. The cluster
6161
create command must specify the authentication mode the cluster will use. This
6262
may either be one of:
6363

@@ -278,7 +278,7 @@ password_filter
278278
``resource_name`` arguments can take the following forms:
279279

280280
- ``ceph.smb.cluster``: show all cluster resources
281-
- ``ceph.smb.cluster.<cluster_id>``: show specific cluster with given cluster id
281+
- ``ceph.smb.cluster.<cluster_id>``: show specific cluster with given cluster ID
282282
- ``ceph.smb.share``: show all share resources
283283
- ``ceph.smb.share.<cluster_id>``: show all share resources part of the given
284284
cluster
@@ -450,10 +450,10 @@ custom_dns
450450
custom_ports
451451
Optional. A mapping of service names to port numbers that will override the
452452
default ports used for those services. The service names are:
453-
``smb``, ``smbmetrics``, and ``ctdb``. If a service name is not
454-
present in the mapping the default port will be used.
453+
``smb``, ``smbmetrics``, ``ctdb``, and ``remote-control``. If a service
454+
name is not present in the mapping the default port will be used.
455455
For example, ``{"smb": 4455, "smbmetrics": 9009}`` will change the
456-
ports used by smb for client access and the metrics exporter, but
456+
ports used by SMB for client access and the metrics exporter, but
457457
not change the port used by the CTDB clustering daemon.
458458
Note - not all SMB clients are able to use alternate port numbers.
459459
bind_addrs
@@ -506,6 +506,32 @@ public_addrs
506506
host. Run ``cephadm list-networks`` for an example of these mappings.
507507
If destination is not supplied the network is automatically determined
508508
using the address value supplied and taken as the destination.
509+
remote_control
510+
Optional object. This object configures an SMB cluster to deploy an extra
511+
``remote control`` service. This service provides a gRPC server that
512+
can be used to enumerate connected clients and disconnect clients from
513+
shares. This service uses mTLS for authentication. By default, this service
514+
uses port 54445. The port can be configured using the ``custom_ports``
515+
parameter in the cluster resource. If the service is enabled and any of the
516+
``cert``, ``key``, or ``ca_cert`` fields are not populated mTLS will be
517+
disabled and the service will operate in a read-only mode. Running the
518+
service with mTLS disabled is not recommended.
519+
Fields:
520+
521+
enabled
522+
Optional boolean. If explicitly set to ``true`` or ``false`` this
523+
field will enable or disable the remote control service. If left
524+
unset the TLS fields will be checked - if the TLS fields are filled
525+
automatically enable the service.
526+
cert
527+
Optional object. The fields are described in :ref:`tls source
528+
fields<tls-source-fields>`
529+
key
530+
Optional object. The fields are described in :ref:`tls source
531+
fields<tls-source-fields>`
532+
ca_cert
533+
Optional object. The fields are described in :ref:`tls source
534+
fields<tls-source-fields>`
509535
custom_smb_global_options
510536
Optional mapping. Specify key-value pairs that will be directly added to
511537
the global ``smb.conf`` options (or equivalent) of a Samba server. Do
@@ -561,6 +587,16 @@ ref
561587
String. Required for ``source_type: resource``. Must refer to the ID of a
562588
``ceph.smb.join.auth`` resource
563589

590+
.. _tls-source-fields:
591+
592+
A TLS source object supports the following fields:
593+
594+
source_type
595+
Optional. Must be ``resource`` if specified.
596+
ref
597+
String. Required for ``source_type: resource``. Must refer to the ID of a
598+
``ceph.smb.tls.credential`` resource
599+
564600
.. note::
565601
The ``source_type`` ``empty`` is generally only for debugging and testing
566602
the module and should not be needed in production deployments.
@@ -741,7 +777,7 @@ auth
741777
password
742778
Required string. The AD user's password
743779
linked_to_cluster:
744-
Optional. A string containing a cluster id. If set, the resource may only
780+
Optional. A string containing a cluster ID. If set, the resource may only
745781
be used with the linked cluster and will automatically be removed when the
746782
linked cluster is removed.
747783

@@ -784,7 +820,7 @@ values
784820
name
785821
The name of the group
786822
linked_to_cluster:
787-
Optional. A string containing a cluster id. If set, the resource may only
823+
Optional. A string containing a cluster ID. If set, the resource may only
788824
be used with the linked cluster and will automatically be removed when the
789825
linked cluster is removed.
790826

@@ -804,6 +840,52 @@ Example:
804840
groups: []
805841
806842
843+
TLS Credential Resource
844+
------------------------
845+
846+
TLS credential resources store copies of TLS files such as Certificates, Keys,
847+
or CA Certificates.
848+
A TLS credential resource supports the following fields:
849+
850+
resource_type
851+
A literal string ``ceph.smb.tls.credential``
852+
tls_credential_id
853+
A short string identifying the TLS credential resource
854+
intent
855+
One of ``present`` or ``removed``. If not provided, ``present`` is assumed.
856+
If ``removed`` all following fields are optional
857+
credential_type
858+
Required string. The value may be one of ``cert``, ``key``, or ``ca-cert``.
859+
This value indicates what type of TLS credential the value field holds.
860+
value:
861+
A string containing the TLS certificate or key value in PEM encoding.
862+
linked_to_cluster:
863+
Optional. A string containing a cluster ID. If set, the resource may only
864+
be used with the linked cluster and will automatically be removed when the
865+
linked cluster is removed.
866+
867+
Example:
868+
869+
.. code-block:: yaml
870+
871+
resource_type: ceph.smb.tls.credential
872+
tls_credential_id: mycert1
873+
credential_type: cert
874+
# NOTE: The value below is truncated to make the documentation more
875+
# consise. A real embedded certificate is expected to be valid and
876+
# will be longer than this example.
877+
value: |
878+
-----BEGIN CERTIFICATE-----
879+
MIIFDjCCA/agAwIBAgISBtFQfoXc4RmyVabbv28RClKdMA0GCSqGSIb3DQEBCwUA
880+
MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD
881+
EwNSMTAwHhcNMjUwNTE5MTAyNzUyWhcNMjUwODE3MTAyNzUxWjASMRAwDgYDVQQD
882+
EwdjZXBoLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx6fif6PQ
883+
LOTdnO8d1JHcF7D+oB/mQlplFz4vwq/GB6Y4oWK3uCQ4PPz/qyvE4wyvc5EPhjfg
884+
d8XNc4ajEBcSUoRj3UwWwiA4oht0SyoJIfwVGp/kF5jxHhVCLdoaaqAxv7nAghWM
885+
6Dg=
886+
-----END CERTIFICATE-----
887+
888+
807889
A Declarative Configuration Example
808890
-----------------------------------
809891

src/cephadm/cephadmlib/daemons/smb.py

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,14 @@
4848
_SCC = '/usr/bin/samba-container'
4949
_NODES_SUBCMD = [_SCC, 'ctdb-list-nodes']
5050
_MUTEX_SUBCMD = [_SCC, 'ctdb-rados-mutex'] # requires rados uri
51+
_ETC_SAMBA_TLS = '/etc/samba/tls'
5152

5253

5354
class Features(enum.Enum):
5455
DOMAIN = 'domain'
5556
CLUSTERED = 'clustered'
5657
CEPHFS_PROXY = 'cephfs-proxy'
58+
REMOTE_CONTROL = 'remote-control'
5759

5860
@classmethod
5961
def valid(cls, value: str) -> bool:
@@ -105,6 +107,7 @@ class Ports(enum.Enum):
105107
SMB = 445
106108
SMBMETRICS = 9922
107109
CTDB = 4379
110+
REMOTE_CONTROL = 54445
108111

109112
def customized(self, service_ports: Dict[str, int]) -> int:
110113
"""Return a custom port value if it is present in service_ports or the
@@ -116,6 +119,53 @@ def customized(self, service_ports: Dict[str, int]) -> int:
116119
return int(self.value)
117120

118121

122+
@dataclasses.dataclass(frozen=True)
123+
class TLSFiles:
124+
cert: str = ''
125+
key: str = ''
126+
ca_cert: str = ''
127+
128+
def __bool__(self) -> bool:
129+
return bool(self.cert or self.key or self.ca_cert)
130+
131+
def _interior_path(self, value: str) -> str:
132+
if not value:
133+
return value
134+
return f'{_ETC_SAMBA_TLS}/{value}'
135+
136+
@property
137+
def cert_interior_path(self) -> str:
138+
return self._interior_path(self.cert)
139+
140+
@property
141+
def key_interior_path(self) -> str:
142+
return self._interior_path(self.key)
143+
144+
@property
145+
def ca_cert_interior_path(self) -> str:
146+
return self._interior_path(self.ca_cert)
147+
148+
@classmethod
149+
def match(cls, files: Iterable[str], service: str) -> 'TLSFiles':
150+
kwargs: Dict[str, str] = {}
151+
for filename in files:
152+
if not filename.startswith(f'{service}.'):
153+
continue
154+
if filename.endswith('.ca.crt'):
155+
kwargs['ca_cert'] = filename
156+
elif filename.endswith('.crt'):
157+
kwargs['cert'] = filename
158+
elif filename.endswith('.key'):
159+
kwargs['key'] = filename
160+
return cls(**kwargs)
161+
162+
163+
@dataclasses.dataclass(frozen=True)
164+
class RemoteControlConfig:
165+
port: int
166+
tls_files: TLSFiles
167+
168+
119169
@dataclasses.dataclass(frozen=True)
120170
class Config:
121171
identity: DaemonIdentity
@@ -145,6 +195,7 @@ class Config:
145195
)
146196
bind_to: List[BindInterface] = dataclasses.field(default_factory=list)
147197
proxy_image: str = ''
198+
remote_control: Optional[RemoteControlConfig] = None
148199

149200
def config_uris(self) -> List[str]:
150201
uris = [self.source_config]
@@ -275,6 +326,9 @@ def container_args(self) -> List[str]:
275326
if self.cfg.metrics_port:
276327
metrics_port = self.cfg.metrics_port
277328
cargs.extend(self._publish(metrics_port, metrics_port))
329+
if self.cfg.remote_control:
330+
rc_port = self.cfg.remote_control.port
331+
cargs.extend(self._publish(rc_port, rc_port))
278332
cargs.extend(_container_dns_args(self.cfg))
279333
return cargs
280334

@@ -339,6 +393,38 @@ def args(self) -> List[str]:
339393
return args
340394

341395

396+
class RemoteControlContainer(SambaContainerCommon):
397+
def name(self) -> str:
398+
return 'remotectl'
399+
400+
def args(self) -> List[str]:
401+
args = super().args()
402+
assert self.cfg.remote_control, 'remote_control is not configured'
403+
args.append('serve')
404+
args.append('--grpc')
405+
address = self.cfg.bind_to[0].address if self.cfg.bind_to else '*'
406+
port = self.cfg.remote_control.port
407+
args.append(f'--address={address}:{port}')
408+
if not self.cfg.remote_control.tls_files:
409+
args.append('--insecure')
410+
else:
411+
cert_path = self.cfg.remote_control.tls_files.cert_interior_path
412+
key_path = self.cfg.remote_control.tls_files.key_interior_path
413+
ca_cert = self.cfg.remote_control.tls_files.ca_cert_interior_path
414+
assert cert_path
415+
assert key_path
416+
args.append(f'--tls-cert={cert_path}')
417+
args.append(f'--tls-key={key_path}')
418+
if ca_cert:
419+
args.append(f'--tls-ca-cert={ca_cert}')
420+
return args
421+
422+
def container_args(self) -> List[str]:
423+
return super().container_args() + [
424+
'--entrypoint=samba-remote-control'
425+
]
426+
427+
342428
class CephFSProxyContainer(ContainerCommon):
343429
def name(self) -> str:
344430
return 'proxy'
@@ -462,6 +548,7 @@ def __init__(self, ctx: CephadmContext, ident: DaemonIdentity):
462548
self._identity = ident
463549
self._instance_cfg: Optional[Config] = None
464550
self._files: Dict[str, str] = {}
551+
self._tls_files: Dict[str, str] = {}
465552
self._raw_configs: Dict[str, Any] = context_getters.fetch_configs(ctx)
466553
self._config_keyring = context_getters.get_config_and_keyring(ctx)
467554
self._cached_layout: Optional[ContainerLayout] = None
@@ -542,16 +629,29 @@ def validate(self) -> None:
542629
# cache the cephadm networks->devices mapping for later
543630
self._network_mapper.load()
544631

632+
self._organize_files(files)
633+
634+
if Features.REMOTE_CONTROL.value in instance_features:
635+
remote_control_cfg = RemoteControlConfig(
636+
port=Ports.REMOTE_CONTROL.customized(service_ports),
637+
tls_files=TLSFiles.match(self._tls_files, 'remote_control'),
638+
)
639+
else:
640+
remote_control_cfg = None
641+
545642
rank, rank_gen = self._rank_info
546643
self._instance_cfg = Config(
644+
# core configuration
547645
identity=self._identity,
548646
instance_id=instance_id,
549647
source_config=source_config,
550648
join_sources=join_sources,
551649
user_sources=user_sources,
552650
custom_dns=custom_dns,
651+
# major features
553652
domain_member=Features.DOMAIN.value in instance_features,
554653
clustered=Features.CLUSTERED.value in instance_features,
654+
# config details
555655
smb_port=Ports.SMB.customized(service_ports),
556656
ctdb_port=Ports.CTDB.customized(service_ports),
557657
ceph_config_entity=ceph_config_entity,
@@ -565,10 +665,13 @@ def validate(self) -> None:
565665
cluster_public_addrs=_public_addrs,
566666
proxy_image=proxy_image,
567667
bind_to=self._network_mapper.bind_interfaces(bind_networks),
668+
remote_control=remote_control_cfg,
568669
)
569-
self._files = files
570670
logger.debug('SMB Instance Config: %s', self._instance_cfg)
571671
logger.debug('Configured files: %s', self._files)
672+
logger.debug(
673+
'Configured TLS/SSL files: %s', list(self._tls_files.keys())
674+
)
572675

573676
@property
574677
def _cfg(self) -> Config:
@@ -622,6 +725,8 @@ def _layout(self) -> ContainerLayout:
622725
ctrs.append(
623726
CephFSProxyContainer(self._cfg, self._cfg.proxy_image)
624727
)
728+
if self._cfg.remote_control:
729+
ctrs.append(RemoteControlContainer(self._cfg))
625730

626731
if self._cfg.clustered:
627732
init_ctrs += [
@@ -756,6 +861,9 @@ def customize_container_mounts(
756861
mounts[run_samba] = '/run:z' # TODO: make this a shared tmpfs
757862
mounts[config] = '/etc/ceph/ceph.conf:z'
758863
mounts[keyring] = '/etc/ceph/keyring:z'
864+
if self._tls_files:
865+
tls_dir = str(data_dir / 'tls')
866+
mounts[tls_dir] = f'{_ETC_SAMBA_TLS}:z'
759867
if self._cfg.clustered:
760868
ctdb_persistent = str(data_dir / 'ctdb/persistent')
761869
ctdb_run = str(data_dir / 'ctdb/run') # TODO: tmpfs too!
@@ -802,6 +910,15 @@ def customize_container_endpoints(
802910
for addr in addrs:
803911
endpoints.append(EndPoint(addr, self._cfg.metrics_port))
804912

913+
def _organize_files(self, files: Dict[str, str]) -> None:
914+
# this separation is similar to how ceph services are set up
915+
# regarding certs and keys
916+
for key, value in files.items():
917+
if key.endswith(('.crt', '.key')):
918+
self._tls_files[key] = value
919+
else:
920+
self._files[key] = value
921+
805922
def prepare_data_dir(self, data_dir: str, uid: int, gid: int) -> None:
806923
self.validate()
807924
ddir = pathlib.Path(data_dir)
@@ -811,6 +928,10 @@ def prepare_data_dir(self, data_dir: str, uid: int, gid: int) -> None:
811928
file_utils.makedirs(ddir / 'run', uid, gid, 0o770)
812929
if self._files:
813930
file_utils.populate_files(data_dir, self._files, uid, gid)
931+
if self._tls_files:
932+
tls_dir = ddir / 'tls'
933+
file_utils.makedirs(tls_dir, uid, gid, 0o700)
934+
file_utils.populate_files(tls_dir, self._tls_files, uid, gid)
814935
if self._cfg.clustered:
815936
file_utils.makedirs(ddir / 'ctdb/persistent', uid, gid, 0o770)
816937
file_utils.makedirs(ddir / 'ctdb/run', uid, gid, 0o770)

src/cephadm/cephadmlib/file_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def write_new(
5454

5555

5656
def populate_files(
57-
config_dir: str, config_files: Dict, uid: int, gid: int
57+
config_dir: Union[str, Path], config_files: Dict, uid: int, gid: int
5858
) -> None:
5959
"""create config files for different services"""
6060
for fname in config_files:

0 commit comments

Comments
 (0)