From: Redouane Kachach Date: Thu, 15 Jan 2026 13:33:46 +0000 (+0100) Subject: mgr/cephadm: adding new API to get nvmeof TLS bundle from certmgr X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=f2a96c95efa5ea8a70f0d38b3098cbcaa057e31e;p=ceph.git mgr/cephadm: adding new API to get nvmeof TLS bundle from certmgr This new API is intended for use by the Ceph Dashboard and keeps cephadm certmgr internals hidden from it. Certmgr populates the bundle based on the service’s configured certificate_source. https://tracker.ceph.com/issues/74377 Signed-off-by: Redouane Kachach --- diff --git a/src/pybind/mgr/cephadm/module.py b/src/pybind/mgr/cephadm/module.py index 347305c64995..590763e8ae60 100644 --- a/src/pybind/mgr/cephadm/module.py +++ b/src/pybind/mgr/cephadm/module.py @@ -84,6 +84,7 @@ from .services.osd import OSDRemovalQueue, OSDService, OSD, NotFoundError from .services.monitoring import AlertmanagerService, PrometheusService from .services.node_proxy import NodeProxy from .services.smb import SMBService +from .services.nvmeof import NvmeofService from .schedule import HostAssignment from .inventory import ( Inventory, @@ -3488,6 +3489,12 @@ Then run the following: def cert_store_key_ls(self, include_cephadm_generated_keys: bool = False) -> Dict[str, Any]: return self.cert_mgr.key_ls(include_cephadm_generated_keys) + @handle_orch_error + def get_nvmeof_tls_bundle(self, service_name: str) -> Dict[str, str]: + nvmeof_svc = cast(NvmeofService, service_registry.get_service('nvmeof')) + tls_bundle = nvmeof_svc.get_nvmeof_tls_bundle(service_name) + return tls_bundle._asdict() if tls_bundle else {} + @handle_orch_error def cert_store_get_cert( self, diff --git a/src/pybind/mgr/cephadm/services/nvmeof.py b/src/pybind/mgr/cephadm/services/nvmeof.py index b811d0118f78..fa8ec1741434 100644 --- a/src/pybind/mgr/cephadm/services/nvmeof.py +++ b/src/pybind/mgr/cephadm/services/nvmeof.py @@ -1,7 +1,7 @@ import errno import logging import json -from typing import List, cast, Optional +from typing import List, cast, Optional, NamedTuple from ipaddress import ip_address, IPv6Address from mgr_module import HandleCommandResult @@ -21,6 +21,14 @@ logger = logging.getLogger(__name__) NVMEOF_CLIENT_CERT_LABEL = 'client' +class NvmeofTLSBundle(NamedTuple): + server_cert: str = '' + server_key: str = '' + client_cert: str = '' + client_key: str = '' + ca_cert: str = '' + + @register_cephadm_service class NvmeofService(CephService): TYPE = 'nvmeof' @@ -359,3 +367,109 @@ class NvmeofService(CephService): for blocking_daemon in blocking_daemons if blocking_daemon.hostname is not None ] return blocking_daemon_hosts + + def _pick_running_daemon_host_for_service(self, service_name: str) -> Optional[str]: + """ + Resolve a deterministic host for a service when HOST-scoped objects are needed. + Picks the first RUNNING daemon host from the orchestrator cache. + Returns None if none found. + """ + try: + dds = self.mgr.cache.get_daemons_by_service(service_name) + except Exception: + return None + + for dd in dds: + # dd.hostname is the short host name used for HOST-scoped certmgr objects + if dd.status == DaemonDescriptionStatus.running and dd.hostname: + return dd.hostname + + # fallback: any host if nothing is RUNNING + for dd in dds: + if dd.hostname: + return dd.hostname + + return None + + def get_nvmeof_tls_bundle(self, service_name: str) -> Optional[NvmeofTLSBundle]: + """ + Deterministic NVMeoF TLS bundle retrieval based on the NVMeoF spec's certificate_source. + + - INLINE: read from spec fields (ssl_cert/ssl_key[/client_cert/client_key/root_ca_cert]). + - REFERENCE: read from certmgr store objects (nvmeof_* names). + - CEPHADM_SIGNED: read from cephadm-signed objects (self_signed_* names) + certmgr root CA. + + """ + spec = cast(NvmeofServiceSpec, self.mgr.spec_store.all_specs.get(service_name, None)) + if spec is None: + return None + + # NVMeoF TLS may be disabled at spec level + if not getattr(spec, 'ssl', False): + return NvmeofTLSBundle() + + cert_source = getattr(spec, 'certificate_source', None) + valid_sources = [source.value for source in CertificateSource] + if cert_source not in valid_sources: + # Unknown / unset certificate_source + logger.error(f"Found unknown/invalid certificate_source='{cert_source}' for service '{service_name}'") + return None + + server_cert = '' + server_key = '' + client_cert = '' + client_key = '' + ca_cert = '' + cert_mgr = self.mgr.cert_mgr + enable_mtls = getattr(spec, 'enable_auth', False) + + # -------- INLINE -------- + if cert_source == CertificateSource.INLINE.value: + server_cert = getattr(spec, 'server_cert', None) or getattr(spec, 'ssl_cert', '') or '' + server_key = getattr(spec, 'server_key', None) or getattr(spec, 'ssl_key', '') or '' + ca_cert = getattr(spec, 'root_ca_cert', '') or '' + if enable_mtls: + client_cert = getattr(spec, 'client_cert', '') or '' + client_key = getattr(spec, 'client_key', '') or '' + + # -------- REFERENCE -------- + elif cert_source == CertificateSource.REFERENCE.value: + server_cert = cert_mgr.get_cert(self.cert_name, service_name=service_name) or '' + server_key = cert_mgr.get_key(self.key_name, service_name=service_name) or '' + ca_cert = cert_mgr.get_cert(self.ca_cert_name, service_name=service_name) or '' + if enable_mtls: + client_cert = cert_mgr.get_cert(self.client_cert_name, service_name=service_name) or '' + client_key = cert_mgr.get_key(self.client_key_name, service_name=service_name) or '' + + # -------- CEPHADM_SIGNED -------- + elif cert_source == CertificateSource.CEPHADM_SIGNED.value: + hostname = self._pick_running_daemon_host_for_service(service_name) + if not hostname: + logger.error(f"certificate_source=cephadm-signed for '{service_name}' but no hostname could be resolved") + return None + + server_creds = cert_mgr.get_self_signed_tls_credentials(service_name, hostname) + server_cert = server_creds.cert + server_key = server_creds.key + ca_cert = server_creds.ca_cert or '' + if enable_mtls: + client_creds = cert_mgr.get_self_signed_tls_credentials(service_name, hostname, NVMEOF_CLIENT_CERT_LABEL) + client_cert = client_creds.cert + client_key = client_creds.key + + # -------- Build bundle -------- + # Return bundle with client creds only if mTLS is enabled + if enable_mtls: + return NvmeofTLSBundle( + client_cert=client_cert, + client_key=client_key, + server_cert=server_cert, + server_key=server_key, + ca_cert=ca_cert + ) + else: + return NvmeofTLSBundle( + server_cert=server_cert, + server_key=server_key, + ca_cert=ca_cert + ) diff --git a/src/pybind/mgr/orchestrator/_interface.py b/src/pybind/mgr/orchestrator/_interface.py index 136fde595ac0..14f108dfc7fc 100644 --- a/src/pybind/mgr/orchestrator/_interface.py +++ b/src/pybind/mgr/orchestrator/_interface.py @@ -539,6 +539,9 @@ class Orchestrator(object): def cert_store_key_ls(self, include_cephadm_generated_keys: bool = False) -> OrchResult[Dict[str, Any]]: raise NotImplementedError() + def get_nvmeof_tls_bundle(self, service_name: str) -> OrchResult[Dict[str, str]]: + raise NotImplementedError() + def cert_store_get_cert( self, cert_name: str,