From 3b0a090eab0c722d57c0edbbf3706fd234d337ec Mon Sep 17 00:00:00 2001 From: John Mulligan Date: Wed, 6 Dec 2023 15:14:32 -0500 Subject: [PATCH] cephadm: import and enable deployment of SMB daemon class Enable the use of the SMB container daemon form class by importing, and thus registering, it. Note that the only way to invoke this feature is by hand rolling some JSON to feed to the `ceph _orch deploy` command. Connecting this with the cephadm mgr module is left as a future task. Signed-off-by: John Mulligan (cherry picked from commit f86e7106a48bd64a36c67377c780c6cf1521bdb5) --- src/cephadm/cephadmlib/daemons/__init__.py | 2 + src/cephadm/cephadmlib/daemons/smb.py | 442 +++++++++++++++++++++ 2 files changed, 444 insertions(+) create mode 100644 src/cephadm/cephadmlib/daemons/smb.py diff --git a/src/cephadm/cephadmlib/daemons/__init__.py b/src/cephadm/cephadmlib/daemons/__init__.py index 29f15069483..1a9d2d568bc 100644 --- a/src/cephadm/cephadmlib/daemons/__init__.py +++ b/src/cephadm/cephadmlib/daemons/__init__.py @@ -5,6 +5,7 @@ from .iscsi import CephIscsi from .monitoring import Monitoring from .nfs import NFSGanesha from .nvmeof import CephNvmeof +from .smb import SMB from .snmp import SNMPGateway from .tracing import Tracing from .node_proxy import NodeProxy @@ -20,6 +21,7 @@ __all__ = [ 'Monitoring', 'NFSGanesha', 'OSD', + 'SMB', 'SNMPGateway', 'Tracing', 'NodeProxy', diff --git a/src/cephadm/cephadmlib/daemons/smb.py b/src/cephadm/cephadmlib/daemons/smb.py new file mode 100644 index 00000000000..00103ac8e4b --- /dev/null +++ b/src/cephadm/cephadmlib/daemons/smb.py @@ -0,0 +1,442 @@ +import enum +import json +import pathlib +import logging + +from typing import List, Dict, Tuple, Optional, Any + +from .. import context_getters +from .. import daemon_form +from .. import data_utils +from .. import deployment_utils +from .. import file_utils +from ..constants import DEFAULT_SMB_IMAGE +from ..container_daemon_form import ContainerDaemonForm, daemon_to_container +from ..container_engines import Podman +from ..container_types import ( + CephContainer, + InitContainer, + Namespace, + SidecarContainer, + enable_shared_namespaces, +) +from ..context import CephadmContext +from ..daemon_identity import DaemonIdentity, DaemonSubIdentity +from ..deploy import DeploymentType +from ..exceptions import Error +from ..net_utils import EndPoint + + +logger = logging.getLogger() + + +class Features(enum.Enum): + DOMAIN = 'domain' + CLUSTERED = 'clustered' + + @classmethod + def valid(cls, value: str) -> bool: + # workaround for older python versions + try: + cls(value) + return True + except ValueError: + return False + + +class Config: + instance_id: str + source_config: str + samba_debug_level: int + debug_delay: int + domain_member: bool + clustered: bool + join_sources: List[str] + custom_dns: List[str] + smb_port: int + ceph_config_entity: str + + def __init__( + self, + *, + instance_id: str, + source_config: str, + domain_member: bool, + clustered: bool, + samba_debug_level: int = 0, + debug_delay: int = 0, + join_sources: Optional[List[str]] = None, + custom_dns: Optional[List[str]] = None, + smb_port: int = 0, + ceph_config_entity: str = 'client.admin', + ) -> None: + self.instance_id = instance_id + self.source_config = source_config + self.domain_member = domain_member + self.clustered = clustered + self.samba_debug_level = samba_debug_level + self.debug_delay = debug_delay + self.join_sources = join_sources or [] + self.custom_dns = custom_dns or [] + self.smb_port = smb_port + self.ceph_config_entity = ceph_config_entity + + def __str__(self) -> str: + return ( + f'SMB Config[id={self.instance_id},' + f' source_config={self.source_config},' + f' domain_member={self.domain_member},' + f' clustered={self.clustered}]' + ) + + +class SambaContainerCommon: + def __init__( + self, + cfg: Config, + ) -> None: + self.cfg = cfg + + def name(self) -> str: + raise NotImplementedError('samba container name') + + def envs(self) -> Dict[str, str]: + cfg_uris = [self.cfg.source_config] + environ = { + 'SAMBA_CONTAINER_ID': self.cfg.instance_id, + 'SAMBACC_CONFIG': json.dumps(cfg_uris), + } + if self.cfg.ceph_config_entity: + environ['SAMBACC_CEPH_ID'] = f'name={self.cfg.ceph_config_entity}' + return environ + + def envs_list(self) -> List[str]: + return [f'{k}={v}' for (k, v) in self.envs().items()] + + def args(self) -> List[str]: + args = [] + if self.cfg.samba_debug_level: + args.append(f'--samba-debug-level={self.cfg.samba_debug_level}') + if self.cfg.debug_delay: + args.append(f'--debug-delay={self.cfg.debug_delay}') + return args + + def container_args(self) -> List[str]: + return [] + + +class SMBDContainer(SambaContainerCommon): + def name(self) -> str: + return 'smbd' + + def args(self) -> List[str]: + return super().args() + ['run', 'smbd'] + + def container_args(self) -> List[str]: + cargs = [] + if self.cfg.smb_port: + cargs.append(f'--publish={self.cfg.smb_port}:{self.cfg.smb_port}') + return cargs + + +class WinbindContainer(SambaContainerCommon): + def name(self) -> str: + return 'winbindd' + + def args(self) -> List[str]: + return super().args() + ['run', 'winbindd'] + + +class ConfigInitContainer(SambaContainerCommon): + def name(self) -> str: + return 'config' + + def args(self) -> List[str]: + return super().args() + ['init'] + + +class MustJoinContainer(SambaContainerCommon): + def name(self) -> str: + return 'mustjoin' + + def args(self) -> List[str]: + args = super().args() + ['must-join'] + for join_src in self.cfg.join_sources: + args.append(f'-j{join_src}') + return args + + def container_args(self) -> List[str]: + cargs = [] + for dns in self.cfg.custom_dns: + cargs.append(f'--dns={dns}') + return cargs + + +class ConfigWatchContainer(SambaContainerCommon): + def name(self) -> str: + return 'configwatch' + + def args(self) -> List[str]: + return super().args() + ['update-config', '--watch'] + + +class ContainerLayout: + init_containers: List[SambaContainerCommon] + primary: SambaContainerCommon + supplemental: List[SambaContainerCommon] + + def __init__( + self, + init_containers: List[SambaContainerCommon], + primary: SambaContainerCommon, + supplemental: List[SambaContainerCommon], + ) -> None: + self.init_containers = init_containers + self.primary = primary + self.supplemental = supplemental + + +@daemon_form.register +class SMB(ContainerDaemonForm): + """Provides a form for SMB containers.""" + + daemon_type = 'smb' + default_image = DEFAULT_SMB_IMAGE + + @classmethod + def for_daemon_type(cls, daemon_type: str) -> bool: + return cls.daemon_type == daemon_type + + def __init__(self, ctx: CephadmContext, ident: DaemonIdentity): + assert ident.daemon_type == self.daemon_type + self._identity = ident + self._instance_cfg: Optional[Config] = None + self._files: Dict[str, str] = {} + self._raw_configs: Dict[str, Any] = context_getters.fetch_configs(ctx) + self._config_keyring = context_getters.get_config_and_keyring(ctx) + self._cached_layout: Optional[ContainerLayout] = None + self.smb_port = 445 + logger.debug('Created SMB ContainerDaemonForm instance') + + def validate(self) -> None: + if self._instance_cfg is not None: + return + + configs = self._raw_configs + instance_id = configs.get('cluster_id', '') + source_config = configs.get('config_uri', '') + join_sources = configs.get('join_sources', []) + custom_dns = configs.get('custom_dns', []) + instance_features = configs.get('features', []) + files = data_utils.dict_get(configs, 'files', {}) + ceph_config_entity = configs.get('config_auth_entity', '') + + if not instance_id: + raise Error('invalid instance (cluster) id') + if not source_config: + raise Error('invalid configuration source uri') + invalid_features = { + f for f in instance_features if not Features.valid(f) + } + if invalid_features: + raise Error( + f'invalid instance features: {", ".join(invalid_features)}' + ) + if Features.CLUSTERED.value in instance_features: + raise NotImplementedError('clustered instance') + + self._instance_cfg = Config( + instance_id=instance_id, + source_config=source_config, + join_sources=join_sources, + custom_dns=custom_dns, + domain_member=Features.DOMAIN.value in instance_features, + clustered=Features.CLUSTERED.value in instance_features, + samba_debug_level=6, + smb_port=self.smb_port, + ceph_config_entity=ceph_config_entity, + ) + self._files = files + logger.debug('SMB Instance Config: %s', self._instance_cfg) + logger.debug('Configured files: %s', self._files) + + @property + def _cfg(self) -> Config: + self.validate() + assert self._instance_cfg + return self._instance_cfg + + @property + def instance_id(self) -> str: + return self._cfg.instance_id + + @property + def source_config(self) -> str: + return self._cfg.source_config + + @classmethod + def create(cls, ctx: CephadmContext, ident: DaemonIdentity) -> 'SMB': + return cls(ctx, ident) + + @property + def identity(self) -> DaemonIdentity: + return self._identity + + def uid_gid(self, ctx: CephadmContext) -> Tuple[int, int]: + return 0, 0 + + def config_and_keyring( + self, ctx: CephadmContext + ) -> Tuple[Optional[str], Optional[str]]: + return self._config_keyring + + def _layout(self) -> ContainerLayout: + if self._cached_layout: + return self._cached_layout + init_ctrs: List[SambaContainerCommon] = [] + ctrs: List[SambaContainerCommon] = [] + + init_ctrs.append(ConfigInitContainer(self._cfg)) + ctrs.append(ConfigWatchContainer(self._cfg)) + + if self._cfg.domain_member: + init_ctrs.append(MustJoinContainer(self._cfg)) + ctrs.append(WinbindContainer(self._cfg)) + + smbd = SMBDContainer(self._cfg) + self._cached_layout = ContainerLayout(init_ctrs, smbd, ctrs) + return self._cached_layout + + def _to_init_container( + self, ctx: CephadmContext, smb_ctr: SambaContainerCommon + ) -> InitContainer: + volume_mounts: Dict[str, str] = {} + container_args: List[str] = smb_ctr.container_args() + self.customize_container_mounts(ctx, volume_mounts) + # XXX: is this needed? if so, can this be simplified + if isinstance(ctx.container_engine, Podman): + ctx.container_engine.update_mounts(ctx, volume_mounts) + identity = DaemonSubIdentity.from_parent( + self.identity, smb_ctr.name() + ) + return InitContainer( + ctx, + entrypoint='', + image=ctx.image or self.default_image, + identity=identity, + args=smb_ctr.args(), + container_args=container_args, + envs=smb_ctr.envs_list(), + volume_mounts=volume_mounts, + ) + + def _to_sidecar_container( + self, ctx: CephadmContext, smb_ctr: SambaContainerCommon + ) -> SidecarContainer: + volume_mounts: Dict[str, str] = {} + container_args: List[str] = smb_ctr.container_args() + self.customize_container_mounts(ctx, volume_mounts) + shared_ns = { + Namespace.ipc, + Namespace.network, + Namespace.pid, + } + if isinstance(ctx.container_engine, Podman): + # XXX: is this needed? if so, can this be simplified + ctx.container_engine.update_mounts(ctx, volume_mounts) + # docker doesn't support sharing the uts namespace with other + # containers. It may not be entirely needed on podman but it gives + # me warm fuzzies to make sure it gets shared. + shared_ns.add(Namespace.uts) + enable_shared_namespaces( + container_args, self.identity.container_name, shared_ns + ) + identity = DaemonSubIdentity.from_parent( + self.identity, smb_ctr.name() + ) + return SidecarContainer( + ctx, + entrypoint='', + image=ctx.image or self.default_image, + identity=identity, + container_args=container_args, + args=smb_ctr.args(), + envs=smb_ctr.envs_list(), + volume_mounts=volume_mounts, + init=False, + remove=True, + ) + + def container(self, ctx: CephadmContext) -> CephContainer: + ctr = daemon_to_container(ctx, self, host_network=False) + # We want to share the IPC ns between the samba containers for one + # instance. Cephadm's default, host ipc, is not what we want. + # Unsetting it works fine for podman but docker (on ubuntu 22.04) needs + # to be expliclty told that ipc of the primary container must be + # shareable. + ctr.ipc = 'shareable' + return deployment_utils.to_deployment_container(ctx, ctr) + + def init_containers(self, ctx: CephadmContext) -> List[InitContainer]: + return [ + self._to_init_container(ctx, smb_ctr) + for smb_ctr in self._layout().init_containers + ] + + def sidecar_containers( + self, ctx: CephadmContext + ) -> List[SidecarContainer]: + return [ + self._to_sidecar_container(ctx, smb_ctr) + for smb_ctr in self._layout().supplemental + ] + + def customize_container_envs( + self, ctx: CephadmContext, envs: List[str] + ) -> None: + clayout = self._layout() + envs.extend(clayout.primary.envs_list()) + + def customize_process_args( + self, ctx: CephadmContext, args: List[str] + ) -> None: + clayout = self._layout() + args.extend(clayout.primary.args()) + + def customize_container_args( + self, ctx: CephadmContext, args: List[str] + ) -> None: + args.extend(self._layout().primary.container_args()) + + def customize_container_mounts( + self, + ctx: CephadmContext, + mounts: Dict[str, str], + ) -> None: + self.validate() + data_dir = pathlib.Path(self.identity.data_dir(ctx.data_dir)) + etc_samba_ctr = str(data_dir / 'etc-samba-container') + lib_samba = str(data_dir / 'lib-samba') + run_samba = str(data_dir / 'run') + config = str(data_dir / 'config') + keyring = str(data_dir / 'keyring') + mounts[etc_samba_ctr] = '/etc/samba/container:z' + mounts[lib_samba] = '/var/lib/samba:z' + mounts[run_samba] = '/run:z' # TODO: make this a shared tmpfs + mounts[config] = '/etc/ceph/ceph.conf:z' + mounts[keyring] = '/etc/ceph/keyring:z' + + def customize_container_endpoints( + self, endpoints: List[EndPoint], deployment_type: DeploymentType + ) -> None: + if not any(ep.port == self.smb_port for ep in endpoints): + endpoints.append(EndPoint('0.0.0.0', self.smb_port)) + + def prepare_data_dir(self, data_dir: str, uid: int, gid: int) -> None: + self.validate() + ddir = pathlib.Path(data_dir) + file_utils.makedirs(ddir / 'etc-samba-container', uid, gid, 0o770) + file_utils.makedirs(ddir / 'lib-samba', uid, gid, 0o770) + file_utils.makedirs(ddir / 'run', uid, gid, 0o770) + if self._files: + file_utils.populate_files(data_dir, self._files, uid, gid) -- 2.39.5