Skip to content

Commit 5d42b73

Browse files
committed
mgr/cephadm: renaming cert-store cmds to certmgr, adding new cmds
Signed-off-by: Redouane Kachach <rkachach@ibm.com>
1 parent 3bb6c57 commit 5d42b73

File tree

5 files changed

+343
-62
lines changed

5 files changed

+343
-62
lines changed

src/pybind/mgr/cephadm/cert_mgr.py

Lines changed: 64 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from typing import TYPE_CHECKING, Tuple, Union, List, Dict, Optional, cast, Any
22
import logging
3-
import copy
43

54
from cephadm.ssl_cert_utils import SSLCerts, SSLConfigException
65
from mgr_util import verify_tls, ServerConfigException
@@ -216,47 +215,75 @@ def rm_cert(self, cert_name: str, service_name: Optional[str] = None, host: Opti
216215
def rm_key(self, key_name: str, service_name: Optional[str] = None, host: Optional[str] = None) -> None:
217216
self.key_store.rm_tlsobject(key_name, service_name, host)
218217

219-
def cert_ls(self) -> Dict[str, Union[bool, Dict[str, Dict[str, bool]]]]:
220-
ls: Dict = copy.deepcopy(self.cert_store.get_tlsobjects())
221-
for k, v in ls.items():
222-
if isinstance(v, dict):
223-
tmp: Dict[str, Any] = {key: get_certificate_info(cast(Cert, v[key]).cert) for key in v if isinstance(v[key], Cert)}
224-
ls[k] = tmp if tmp else {}
225-
elif isinstance(v, Cert):
226-
ls[k] = get_certificate_info(cast(Cert, v).cert) if bool(v) else False
218+
def cert_ls(self, include_datails: bool = False) -> Dict:
219+
cert_objects: List = self.cert_store.list_tlsobjects()
220+
ls: Dict = {}
221+
for cert_name, cert_obj, target in cert_objects:
222+
cert_extended_info = get_certificate_info(cert_obj.cert, include_datails)
223+
cert_scope = self.get_cert_scope(cert_name)
224+
if cert_name not in ls:
225+
ls[cert_name] = {'scope': str(cert_scope), 'certificates': {}}
226+
if cert_scope == TLSObjectScope.GLOBAL:
227+
ls[cert_name]['certificates'] = cert_extended_info
228+
else:
229+
ls[cert_name]['certificates'][target] = cert_extended_info
230+
227231
return ls
228232

229-
def key_ls(self) -> Dict[str, Union[bool, Dict[str, bool]]]:
230-
ls: Dict = copy.deepcopy(self.key_store.get_tlsobjects())
231-
if self.CEPHADM_ROOT_CA_KEY in ls:
232-
del ls[self.CEPHADM_ROOT_CA_KEY]
233-
for k, v in ls.items():
234-
if isinstance(v, dict) and v:
235-
tmp: Dict[str, Any] = {key: get_private_key_info(cast(PrivKey, v[key]).key) for key in v if v[key]}
236-
ls[k] = tmp if tmp else {}
237-
elif isinstance(v, PrivKey):
238-
ls[k] = get_private_key_info(cast(PrivKey, v).key)
233+
def key_ls(self) -> Dict:
234+
key_objects: List = self.key_store.list_tlsobjects()
235+
ls: Dict = {}
236+
for key_name, key_obj, target in key_objects:
237+
priv_key_info = get_private_key_info(key_obj.key)
238+
key_scope = self.get_key_scope(key_name)
239+
if key_name not in ls:
240+
ls[key_name] = {'scope': str(key_scope), 'keys': {}}
241+
if key_scope == TLSObjectScope.GLOBAL:
242+
ls[key_name]['keys'] = priv_key_info
243+
else:
244+
ls[key_name]['keys'].update({target: priv_key_info})
245+
246+
# we don't want this key to be leaked
247+
del ls[self.CEPHADM_ROOT_CA_KEY]
248+
239249
return ls
240250

241251
def list_entity_known_certificates(self, entity: str) -> List[str]:
242-
return [cert_name for cert_name, service in self.cert_to_service.items() if service == entity]
252+
"""
253+
Retrieves all certificates associated with a given entity.
243254
244-
def entity_ls(self, get_scope: bool = False) -> List[Union[str, Tuple[str, str]]]:
245-
if get_scope:
246-
return [(entity, self.determine_scope(entity)) for entity in set(self.cert_to_service.values())]
247-
else:
248-
return list(self.cert_to_service.values())
249-
250-
def determine_scope(self, entity: str) -> str:
251-
for cert, service in self.cert_to_service.items():
252-
if service == entity:
253-
if cert in self.known_certs[TLSObjectScope.SERVICE]:
254-
return TLSObjectScope.SERVICE.value
255-
elif cert in self.known_certs[TLSObjectScope.HOST]:
256-
return TLSObjectScope.HOST.value
257-
elif cert in self.known_certs[TLSObjectScope.GLOBAL]:
258-
return TLSObjectScope.GLOBAL.value
259-
return TLSObjectScope.UNKNOWN.value
255+
:param entity: The entity name.
256+
:return: A list of certificate names, or None if the entity is not found.
257+
"""
258+
for scope, entities in self.entities.items():
259+
if entity in entities:
260+
return entities[entity]['certs'] # Return certs for the entity
261+
return []
262+
263+
def get_entities(self, get_scope: bool = False) -> Dict[str, Any]:
264+
return {f'{scope}': entities for scope, entities in self.entities.items()}
265+
266+
def list_entities(self) -> List[str]:
267+
"""
268+
Retrieves a list of all registered entities across all scopes.
269+
:return: A list of entity names.
270+
"""
271+
entities: List[str] = []
272+
for scope_entities in self.entities.values():
273+
entities.extend(scope_entities.keys())
274+
return entities
275+
276+
def get_cert_scope(self, cert_name: str) -> TLSObjectScope:
277+
for scope, certificates in self.known_certs.items():
278+
if cert_name in certificates:
279+
return scope
280+
return TLSObjectScope.UNKNOWN
281+
282+
def get_key_scope(self, key_name: str) -> TLSObjectScope:
283+
for scope, keys in self.known_keys.items():
284+
if key_name in keys:
285+
return scope
286+
return TLSObjectScope.UNKNOWN
260287

261288
def _notify_certificates_health_status(self, problematic_certificates: List[CertInfo]) -> None:
262289

@@ -372,7 +399,7 @@ def get_key(cert_name: str, target: Optional[str]) -> Optional[PrivKey]:
372399
service_name, host = self.cert_store.determine_tlsobject_target(cert_name, target)
373400
key = cast(PrivKey, self.key_store.get_tlsobject(key_name, service_name=service_name, host=host))
374401
return key
375-
except TLSObjectException as e:
402+
except TLSObjectException:
376403
return None
377404

378405
# Filter non-empty entries skipping cephadm root CA cetificate

src/pybind/mgr/cephadm/module.py

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3189,8 +3189,26 @@ def get_alertmanager_access_info(self) -> Dict[str, str]:
31893189
'certificate': self.cert_mgr.get_root_ca()}
31903190

31913191
@handle_orch_error
3192-
def cert_store_cert_ls(self) -> Dict[str, Any]:
3193-
return self.cert_mgr.cert_ls()
3192+
def cert_store_cert_ls(self, show_details: bool = False) -> Dict[str, Any]:
3193+
return self.cert_mgr.cert_ls(show_details)
3194+
3195+
@handle_orch_error
3196+
def cert_store_entity_ls(self) -> Dict[str, Dict[str, List[str]]]:
3197+
return self.cert_mgr.get_entities()
3198+
3199+
@handle_orch_error
3200+
def cert_store_reload(self) -> str:
3201+
self.cert_mgr.load()
3202+
return "OK"
3203+
3204+
@handle_orch_error
3205+
def cert_store_cert_check(self) -> List[str]:
3206+
report = []
3207+
_, certs_with_issues = self.cert_mgr.check_services_certificates(fix_issues=False)
3208+
for cert_info in certs_with_issues:
3209+
if not cert_info.is_operationally_valid():
3210+
report.append(cert_info.get_status_description())
3211+
return report
31943212

31953213
@handle_orch_error
31963214
def cert_store_key_ls(self) -> Dict[str, Any]:
@@ -3199,33 +3217,106 @@ def cert_store_key_ls(self) -> Dict[str, Any]:
31993217
@handle_orch_error
32003218
def cert_store_get_cert(
32013219
self,
3202-
entity: str,
3220+
cert_name: str,
32033221
service_name: Optional[str] = None,
32043222
hostname: Optional[str] = None,
32053223
no_exception_when_missing: bool = False
32063224
) -> str:
3207-
cert = self.cert_mgr.get_cert(entity, service_name or '', hostname or '')
3225+
cert = self.cert_mgr.get_cert(cert_name, service_name or '', hostname or '')
32083226
if not cert:
32093227
if no_exception_when_missing:
32103228
return ''
3211-
raise OrchSecretNotFound(entity=entity, service_name=service_name, hostname=hostname)
3229+
raise OrchSecretNotFound(entity=cert_name, service_name=service_name, hostname=hostname)
32123230
return cert
32133231

32143232
@handle_orch_error
32153233
def cert_store_get_key(
32163234
self,
3217-
entity: str,
3235+
key_name: str,
32183236
service_name: Optional[str] = None,
32193237
hostname: Optional[str] = None,
32203238
no_exception_when_missing: bool = False
32213239
) -> str:
3222-
key = self.cert_mgr.get_key(entity, service_name or '', hostname or '')
3240+
key = self.cert_mgr.get_key(key_name, service_name or '', hostname or '')
32233241
if not key:
32243242
if no_exception_when_missing:
32253243
return ''
3226-
raise OrchSecretNotFound(entity=entity, service_name=service_name, hostname=hostname)
3244+
raise OrchSecretNotFound(entity=key_name, service_name=service_name, hostname=hostname)
32273245
return key
32283246

3247+
@handle_orch_error
3248+
def cert_store_set_pair(
3249+
self,
3250+
cert: str,
3251+
key: str,
3252+
entity: str,
3253+
cert_name: str = "",
3254+
service_name: str = "",
3255+
hostname: str = "",
3256+
force: bool = False
3257+
) -> str:
3258+
3259+
if entity not in self.cert_mgr.list_entities():
3260+
raise OrchestratorError(f"Invalid entity: {entity}. Please use 'ceph orch certmgr entity ls' to list valid entities.")
3261+
3262+
# Check the certificate validity status
3263+
target = service_name or hostname
3264+
cert_info = self.cert_mgr.check_certificate_state(entity, target, cert, key)
3265+
if not force and not cert_info.is_operationally_valid():
3266+
raise OrchestratorError(cert_info.get_status_description())
3267+
3268+
# Obtain the certificate name (from entity)
3269+
cert_names = self.cert_mgr.list_entity_known_certificates(entity)
3270+
if len(cert_names) == 1:
3271+
cert_name = cert_names[0]
3272+
elif len(cert_names) > 1 and not cert_name:
3273+
raise OrchestratorError(f"Entity '{entity}' has many certificates, please use --cert-name argument to specify which one from the list: {cert_names}")
3274+
3275+
# Check the certificate scope
3276+
scope_errors = {
3277+
TLSObjectScope.HOST: "Certificate is bound to a host. Please specify the host using --hostname.",
3278+
TLSObjectScope.SERVICE: "Certificate is bound to a service. Please specify the service using --service-name.",
3279+
TLSObjectScope.UNKNOWN: f"Unknown certificate '{cert_name}'. Use 'ceph orch certmgr cert ls' to list supported certificates.",
3280+
}
3281+
scope = self.cert_mgr.get_cert_scope(cert_name)
3282+
if (scope == TLSObjectScope.HOST and not hostname) or (scope == TLSObjectScope.SERVICE and not service_name):
3283+
raise OrchestratorError(scope_errors[scope])
3284+
3285+
key_name = cert_name.replace('_cert', '_key')
3286+
self.cert_mgr.save_cert(cert_name, cert, service_name, hostname, True)
3287+
self.cert_mgr.save_key(key_name, key, service_name, hostname, True)
3288+
return "Certificate/key pair set correctly"
3289+
3290+
@handle_orch_error
3291+
def cert_store_set_cert(
3292+
self,
3293+
cert: str,
3294+
cert_name: str,
3295+
service_name: Optional[str] = None,
3296+
hostname: Optional[str] = None,
3297+
) -> str:
3298+
3299+
try:
3300+
days_to_expiration = verify_cacrt_content(cert)
3301+
if days_to_expiration < self.certificate_renewal_threshold_days:
3302+
raise OrchestratorError(f'Error: Certificate is about to expire (Remaining days: {days_to_expiration})')
3303+
except ServerConfigException as e:
3304+
raise OrchestratorError(f'Error: Invalid certificate for {cert_name}: {e}')
3305+
3306+
self.cert_mgr.save_cert(cert_name, cert, service_name, hostname, True)
3307+
return f'Certificate for {cert_name} set correctly'
3308+
3309+
@handle_orch_error
3310+
def cert_store_set_key(
3311+
self,
3312+
key: str,
3313+
key_name: str,
3314+
service_name: Optional[str] = None,
3315+
hostname: Optional[str] = None,
3316+
) -> str:
3317+
self.cert_mgr.save_key(key_name, key, service_name, hostname, True)
3318+
return f'Key for {key_name} set correctly'
3319+
32293320
@handle_orch_error
32303321
def apply_mon(self, spec: ServiceSpec) -> str:
32313322
return self._apply(spec)

src/pybind/mgr/mgr_util.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,3 +990,21 @@ def password_hash(password: Optional[str], salt_password: Optional[str] = None)
990990
else:
991991
salt = salt_password.encode('utf8')
992992
return bcrypt.hashpw(password.encode('utf8'), salt).decode('utf8')
993+
994+
def parse_combined_pem_file(pem_data: str) -> Tuple[Optional[str], Optional[str]]:
995+
996+
# Extract the certificate
997+
cert_start = "-----BEGIN CERTIFICATE-----"
998+
cert_end = "-----END CERTIFICATE-----"
999+
cert = None
1000+
if cert_start in pem_data and cert_end in pem_data:
1001+
cert = pem_data[pem_data.index(cert_start):pem_data.index(cert_end) + len(cert_end)]
1002+
1003+
# Extract the private key
1004+
key_start = "-----BEGIN PRIVATE KEY-----"
1005+
key_end = "-----END PRIVATE KEY-----"
1006+
private_key = None
1007+
if key_start in pem_data and key_end in pem_data:
1008+
private_key = pem_data[pem_data.index(key_start):pem_data.index(key_end) + len(key_end)]
1009+
1010+
return cert, private_key

src/pybind/mgr/orchestrator/_interface.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -560,15 +560,24 @@ def list_daemons(self, service_name: Optional[str] = None, daemon_type: Optional
560560
"""
561561
raise NotImplementedError()
562562

563-
def cert_store_cert_ls(self) -> OrchResult[Dict[str, Any]]:
563+
def cert_store_cert_ls(self, show_details: bool = False) -> OrchResult[Dict[str, Any]]:
564+
raise NotImplementedError()
565+
566+
def cert_store_entity_ls(self) -> OrchResult[Dict[Any, Dict[str, List[str]]]]:
567+
raise NotImplementedError()
568+
569+
def cert_store_reload(self) -> OrchResult[str]:
570+
raise NotImplementedError()
571+
572+
def cert_store_cert_check(self) -> OrchResult[List[str]]:
564573
raise NotImplementedError()
565574

566575
def cert_store_key_ls(self) -> OrchResult[Dict[str, Any]]:
567576
raise NotImplementedError()
568577

569578
def cert_store_get_cert(
570579
self,
571-
entity: str,
580+
cert_name: str,
572581
service_name: Optional[str] = None,
573582
hostname: Optional[str] = None,
574583
no_exception_when_missing: bool = False
@@ -577,13 +586,43 @@ def cert_store_get_cert(
577586

578587
def cert_store_get_key(
579588
self,
580-
entity: str,
589+
key_name: str,
581590
service_name: Optional[str] = None,
582591
hostname: Optional[str] = None,
583592
no_exception_when_missing: bool = False
584593
) -> OrchResult[str]:
585594
raise NotImplementedError()
586595

596+
def cert_store_set_pair(
597+
self,
598+
cert: str,
599+
key: str,
600+
entity: str,
601+
cert_name: Optional[str] = None,
602+
service_name: Optional[str] = None,
603+
hostname: Optional[str] = None,
604+
force: Optional[bool] = False
605+
) -> OrchResult[str]:
606+
raise NotImplementedError()
607+
608+
def cert_store_set_cert(
609+
self,
610+
cert: str,
611+
cert_name: str,
612+
service_name: Optional[str] = None,
613+
hostname: Optional[str] = None,
614+
) -> OrchResult[str]:
615+
raise NotImplementedError()
616+
617+
def cert_store_set_key(
618+
self,
619+
key: str,
620+
key_name: str,
621+
service_name: Optional[str] = None,
622+
hostname: Optional[str] = None,
623+
) -> OrchResult[str]:
624+
raise NotImplementedError()
625+
587626
@handle_orch_error
588627
def apply(
589628
self,

0 commit comments

Comments
 (0)