]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/cephadm/services/cephadmservice.py
import ceph quincy 17.2.6
[ceph.git] / ceph / src / pybind / mgr / cephadm / services / cephadmservice.py
CommitLineData
f67539c2 1import errno
f6b5b4d7 2import json
e306af50 3import logging
f67539c2 4import re
33c7a0ef
TL
5import socket
6import time
f6b5b4d7 7from abc import ABCMeta, abstractmethod
f67539c2
TL
8from typing import TYPE_CHECKING, List, Callable, TypeVar, \
9 Optional, Dict, Any, Tuple, NewType, cast
e306af50 10
f6b5b4d7 11from mgr_module import HandleCommandResult, MonCommandFailed
e306af50 12
39ae355f 13from ceph.deployment.service_spec import ServiceSpec, RGWSpec, CephExporterSpec
f91f0fd5 14from ceph.deployment.utils import is_ipv6, unwrap_ipv6
39ae355f 15from mgr_util import build_url, merge_dicts
f67539c2
TL
16from orchestrator import OrchestratorError, DaemonDescription, DaemonDescriptionStatus
17from orchestrator._interface import daemon_type_to_service
e306af50
TL
18from cephadm import utils
19
20if TYPE_CHECKING:
21 from cephadm.module import CephadmOrchestrator
22
23logger = logging.getLogger(__name__)
24
f6b5b4d7 25ServiceSpecs = TypeVar('ServiceSpecs', bound=ServiceSpec)
f91f0fd5 26AuthEntity = NewType('AuthEntity', str)
e306af50 27
f6b5b4d7 28
39ae355f
TL
29def get_auth_entity(daemon_type: str, daemon_id: str, host: str = "") -> AuthEntity:
30 """
31 Map the daemon id to a cephx keyring entity name
32 """
33 # despite this mapping entity names to daemons, self.TYPE within
34 # the CephService class refers to service types, not daemon types
35 if daemon_type in ['rgw', 'rbd-mirror', 'cephfs-mirror', 'nfs', "iscsi", 'ingress', 'ceph-exporter']:
36 return AuthEntity(f'client.{daemon_type}.{daemon_id}')
37 elif daemon_type in ['crash', 'agent']:
38 if host == "":
39 raise OrchestratorError(
40 f'Host not provided to generate <{daemon_type}> auth entity name')
41 return AuthEntity(f'client.{daemon_type}.{host}')
42 elif daemon_type == 'mon':
43 return AuthEntity('mon.')
44 elif daemon_type in ['mgr', 'osd', 'mds']:
45 return AuthEntity(f'{daemon_type}.{daemon_id}')
46 else:
47 raise OrchestratorError(f"unknown daemon type {daemon_type}")
48
49
f67539c2 50class CephadmDaemonDeploySpec:
f6b5b4d7 51 # typing.NamedTuple + Generic is broken in py36
f91f0fd5 52 def __init__(self, host: str, daemon_id: str,
f67539c2 53 service_name: str,
f91f0fd5
TL
54 network: Optional[str] = None,
55 keyring: Optional[str] = None,
56 extra_args: Optional[List[str]] = None,
57 ceph_conf: str = '',
58 extra_files: Optional[Dict[str, Any]] = None,
59 daemon_type: Optional[str] = None,
f67539c2 60 ip: Optional[str] = None,
b3b6e05e
TL
61 ports: Optional[List[int]] = None,
62 rank: Optional[int] = None,
20effc67 63 rank_generation: Optional[int] = None,
2a845540 64 extra_container_args: Optional[List[str]] = None,
39ae355f 65 extra_entrypoint_args: Optional[List[str]] = None,
2a845540 66 ):
f6b5b4d7 67 """
f67539c2 68 A data struction to encapsulate `cephadm deploy ...
f6b5b4d7
TL
69 """
70 self.host: str = host
71 self.daemon_id = daemon_id
f67539c2
TL
72 self.service_name = service_name
73 daemon_type = daemon_type or (service_name.split('.')[0])
f6b5b4d7
TL
74 assert daemon_type is not None
75 self.daemon_type: str = daemon_type
76
f6b5b4d7
TL
77 # mons
78 self.network = network
79
80 # for run_cephadm.
81 self.keyring: Optional[str] = keyring
82
83 # For run_cephadm. Would be great to have more expressive names.
84 self.extra_args: List[str] = extra_args or []
f91f0fd5
TL
85
86 self.ceph_conf = ceph_conf
87 self.extra_files = extra_files or {}
f6b5b4d7
TL
88
89 # TCP ports used by the daemon
f67539c2
TL
90 self.ports: List[int] = ports or []
91 self.ip: Optional[str] = ip
92
93 # values to be populated during generate_config calls
94 # and then used in _run_cephadm
95 self.final_config: Dict[str, Any] = {}
96 self.deps: List[str] = []
f6b5b4d7 97
b3b6e05e
TL
98 self.rank: Optional[int] = rank
99 self.rank_generation: Optional[int] = rank_generation
100
20effc67 101 self.extra_container_args = extra_container_args
39ae355f 102 self.extra_entrypoint_args = extra_entrypoint_args
20effc67 103
f6b5b4d7
TL
104 def name(self) -> str:
105 return '%s.%s' % (self.daemon_type, self.daemon_id)
106
39ae355f
TL
107 def entity_name(self) -> str:
108 return get_auth_entity(self.daemon_type, self.daemon_id, host=self.host)
109
f91f0fd5
TL
110 def config_get_files(self) -> Dict[str, Any]:
111 files = self.extra_files
112 if self.ceph_conf:
113 files['config'] = self.ceph_conf
114
115 return files
116
f67539c2
TL
117 @staticmethod
118 def from_daemon_description(dd: DaemonDescription) -> 'CephadmDaemonDeploySpec':
119 assert dd.hostname
120 assert dd.daemon_id
121 assert dd.daemon_type
122 return CephadmDaemonDeploySpec(
123 host=dd.hostname,
124 daemon_id=dd.daemon_id,
125 daemon_type=dd.daemon_type,
126 service_name=dd.service_name(),
127 ip=dd.ip,
128 ports=dd.ports,
b3b6e05e
TL
129 rank=dd.rank,
130 rank_generation=dd.rank_generation,
20effc67 131 extra_container_args=dd.extra_container_args,
39ae355f 132 extra_entrypoint_args=dd.extra_entrypoint_args,
f67539c2
TL
133 )
134
135 def to_daemon_description(self, status: DaemonDescriptionStatus, status_desc: str) -> DaemonDescription:
136 return DaemonDescription(
137 daemon_type=self.daemon_type,
138 daemon_id=self.daemon_id,
b3b6e05e 139 service_name=self.service_name,
f67539c2
TL
140 hostname=self.host,
141 status=status,
142 status_desc=status_desc,
143 ip=self.ip,
144 ports=self.ports,
b3b6e05e
TL
145 rank=self.rank,
146 rank_generation=self.rank_generation,
20effc67 147 extra_container_args=self.extra_container_args,
39ae355f 148 extra_entrypoint_args=self.extra_entrypoint_args,
f67539c2
TL
149 )
150
f6b5b4d7
TL
151
152class CephadmService(metaclass=ABCMeta):
e306af50
TL
153 """
154 Base class for service types. Often providing a create() and config() fn.
155 """
f6b5b4d7
TL
156
157 @property
158 @abstractmethod
f91f0fd5 159 def TYPE(self) -> str:
f6b5b4d7
TL
160 pass
161
e306af50
TL
162 def __init__(self, mgr: "CephadmOrchestrator"):
163 self.mgr: "CephadmOrchestrator" = mgr
164
f67539c2 165 def allow_colo(self) -> bool:
b3b6e05e
TL
166 """
167 Return True if multiple daemons of the same type can colocate on
168 the same host.
169 """
f67539c2
TL
170 return False
171
b3b6e05e
TL
172 def primary_daemon_type(self) -> str:
173 """
174 This is the type of the primary (usually only) daemon to be deployed.
175 """
176 return self.TYPE
177
f67539c2 178 def per_host_daemon_type(self) -> Optional[str]:
b3b6e05e
TL
179 """
180 If defined, this type of daemon will be deployed once for each host
181 containing one or more daemons of the primary type.
182 """
f67539c2
TL
183 return None
184
b3b6e05e
TL
185 def ranked(self) -> bool:
186 """
187 If True, we will assign a stable rank (0, 1, ...) and monotonically increasing
188 generation (0, 1, ...) to each daemon we create/deploy.
189 """
190 return False
191
192 def fence_old_ranks(self,
193 spec: ServiceSpec,
194 rank_map: Dict[int, Dict[int, Optional[str]]],
195 num_ranks: int) -> None:
196 assert False
f67539c2
TL
197
198 def make_daemon_spec(
b3b6e05e
TL
199 self,
200 host: str,
f67539c2
TL
201 daemon_id: str,
202 network: str,
203 spec: ServiceSpecs,
204 daemon_type: Optional[str] = None,
205 ports: Optional[List[int]] = None,
206 ip: Optional[str] = None,
b3b6e05e
TL
207 rank: Optional[int] = None,
208 rank_generation: Optional[int] = None,
f67539c2
TL
209 ) -> CephadmDaemonDeploySpec:
210 return CephadmDaemonDeploySpec(
f6b5b4d7
TL
211 host=host,
212 daemon_id=daemon_id,
f67539c2
TL
213 service_name=spec.service_name(),
214 network=network,
215 daemon_type=daemon_type,
216 ports=ports,
217 ip=ip,
b3b6e05e
TL
218 rank=rank,
219 rank_generation=rank_generation,
2a845540
TL
220 extra_container_args=spec.extra_container_args if hasattr(
221 spec, 'extra_container_args') else None,
39ae355f
TL
222 extra_entrypoint_args=spec.extra_entrypoint_args if hasattr(
223 spec, 'extra_entrypoint_args') else None,
f6b5b4d7
TL
224 )
225
f67539c2 226 def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
f6b5b4d7
TL
227 raise NotImplementedError()
228
f67539c2 229 def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
f91f0fd5 230 raise NotImplementedError()
f6b5b4d7 231
522d829b 232 def config(self, spec: ServiceSpec) -> None:
f67539c2
TL
233 """
234 Configure the cluster for this service. Only called *once* per
235 service apply. Not for every daemon.
236 """
237 pass
238
f91f0fd5 239 def daemon_check_post(self, daemon_descrs: List[DaemonDescription]) -> None:
e306af50 240 """The post actions needed to be done after daemons are checked"""
f6b5b4d7
TL
241 if self.mgr.config_dashboard:
242 if 'dashboard' in self.mgr.get('mgr_map')['modules']:
243 self.config_dashboard(daemon_descrs)
244 else:
245 logger.debug('Dashboard is not enabled. Skip configuration.')
246
f91f0fd5 247 def config_dashboard(self, daemon_descrs: List[DaemonDescription]) -> None:
f6b5b4d7 248 """Config dashboard settings."""
e306af50
TL
249 raise NotImplementedError()
250
251 def get_active_daemon(self, daemon_descrs: List[DaemonDescription]) -> DaemonDescription:
f6b5b4d7
TL
252 # if this is called for a service type where it hasn't explcitly been
253 # defined, return empty Daemon Desc
254 return DaemonDescription()
e306af50 255
f67539c2
TL
256 def get_keyring_with_caps(self, entity: AuthEntity, caps: List[str]) -> str:
257 ret, keyring, err = self.mgr.mon_command({
258 'prefix': 'auth get-or-create',
259 'entity': entity,
260 'caps': caps,
261 })
262 if err:
263 ret, out, err = self.mgr.mon_command({
264 'prefix': 'auth caps',
265 'entity': entity,
266 'caps': caps,
267 })
268 if err:
269 self.mgr.log.warning(f"Unable to update caps for {entity}")
39ae355f
TL
270
271 # get keyring anyway
272 ret, keyring, err = self.mgr.mon_command({
273 'prefix': 'auth get',
274 'entity': entity,
275 })
276 if err:
277 raise OrchestratorError(f"Unable to fetch keyring for {entity}: {err}")
278
279 # strip down keyring
280 # - don't include caps (auth get includes them; get-or-create does not)
281 # - use pending key if present
282 key = None
283 for line in keyring.splitlines():
284 if ' = ' not in line:
285 continue
286 line = line.strip()
287 (ls, rs) = line.split(' = ', 1)
288 if ls == 'key' and not key:
289 key = rs
290 if ls == 'pending key':
291 key = rs
292 keyring = f'[{entity}]\nkey = {key}\n'
f67539c2
TL
293 return keyring
294
33c7a0ef
TL
295 def _inventory_get_fqdn(self, hostname: str) -> str:
296 """Get a host's FQDN with its hostname.
297
298 If the FQDN can't be resolved, the address from the inventory will
299 be returned instead.
300 """
301 addr = self.mgr.inventory.get_addr(hostname)
302 return socket.getfqdn(addr)
e306af50
TL
303
304 def _set_service_url_on_dashboard(self,
305 service_name: str,
306 get_mon_cmd: str,
307 set_mon_cmd: str,
f91f0fd5 308 service_url: str) -> None:
f6b5b4d7
TL
309 """A helper to get and set service_url via Dashboard's MON command.
310
311 If result of get_mon_cmd differs from service_url, set_mon_cmd will
312 be sent to set the service_url.
313 """
314 def get_set_cmd_dicts(out: str) -> List[dict]:
315 cmd_dict = {
316 'prefix': set_mon_cmd,
317 'value': service_url
318 }
319 return [cmd_dict] if service_url != out else []
320
321 self._check_and_set_dashboard(
322 service_name=service_name,
323 get_cmd=get_mon_cmd,
324 get_set_cmd_dicts=get_set_cmd_dicts
325 )
326
327 def _check_and_set_dashboard(self,
328 service_name: str,
329 get_cmd: str,
f91f0fd5 330 get_set_cmd_dicts: Callable[[str], List[dict]]) -> None:
f6b5b4d7
TL
331 """A helper to set configs in the Dashboard.
332
333 The method is useful for the pattern:
334 - Getting a config from Dashboard by using a Dashboard command. e.g. current iSCSI
335 gateways.
336 - Parse or deserialize previous output. e.g. Dashboard command returns a JSON string.
337 - Determine if the config need to be update. NOTE: This step is important because if a
338 Dashboard command modified Ceph config, cephadm's config_notify() is called. Which
339 kicks the serve() loop and the logic using this method is likely to be called again.
340 A config should be updated only when needed.
341 - Update a config in Dashboard by using a Dashboard command.
342
343 :param service_name: the service name to be used for logging
344 :type service_name: str
345 :param get_cmd: Dashboard command prefix to get config. e.g. dashboard get-grafana-api-url
346 :type get_cmd: str
347 :param get_set_cmd_dicts: function to create a list, and each item is a command dictionary.
348 e.g.
349 [
350 {
351 'prefix': 'dashboard iscsi-gateway-add',
352 'service_url': 'http://admin:admin@aaa:5000',
353 'name': 'aaa'
354 },
355 {
356 'prefix': 'dashboard iscsi-gateway-add',
357 'service_url': 'http://admin:admin@bbb:5000',
358 'name': 'bbb'
359 }
360 ]
361 The function should return empty list if no command need to be sent.
362 :type get_set_cmd_dicts: Callable[[str], List[dict]]
363 """
364
e306af50
TL
365 try:
366 _, out, _ = self.mgr.check_mon_command({
f6b5b4d7 367 'prefix': get_cmd
e306af50
TL
368 })
369 except MonCommandFailed as e:
f6b5b4d7 370 logger.warning('Failed to get Dashboard config for %s: %s', service_name, e)
e306af50 371 return
f6b5b4d7
TL
372 cmd_dicts = get_set_cmd_dicts(out.strip())
373 for cmd_dict in list(cmd_dicts):
e306af50 374 try:
cd265ab1
TL
375 inbuf = cmd_dict.pop('inbuf', None)
376 _, out, _ = self.mgr.check_mon_command(cmd_dict, inbuf)
e306af50 377 except MonCommandFailed as e:
f6b5b4d7
TL
378 logger.warning('Failed to set Dashboard config for %s: %s', service_name, e)
379
f67539c2
TL
380 def ok_to_stop_osd(
381 self,
382 osds: List[str],
383 known: Optional[List[str]] = None, # output argument
384 force: bool = False) -> HandleCommandResult:
385 r = HandleCommandResult(*self.mgr.mon_command({
386 'prefix': "osd ok-to-stop",
387 'ids': osds,
388 'max': 16,
389 }))
390 j = None
391 try:
392 j = json.loads(r.stdout)
393 except json.decoder.JSONDecodeError:
394 self.mgr.log.warning("osd ok-to-stop didn't return structured result")
395 raise
396 if r.retval:
397 return r
398 if known is not None and j and j.get('ok_to_stop'):
399 self.mgr.log.debug(f"got {j}")
400 known.extend([f'osd.{x}' for x in j.get('osds', [])])
401 return HandleCommandResult(
402 0,
403 f'{",".join(["osd.%s" % o for o in osds])} {"is" if len(osds) == 1 else "are"} safe to restart',
404 ''
405 )
406
407 def ok_to_stop(
408 self,
409 daemon_ids: List[str],
410 force: bool = False,
411 known: Optional[List[str]] = None # output argument
412 ) -> HandleCommandResult:
f6b5b4d7 413 names = [f'{self.TYPE}.{d_id}' for d_id in daemon_ids]
f67539c2
TL
414 out = f'It appears safe to stop {",".join(names)}'
415 err = f'It is NOT safe to stop {",".join(names)} at this time'
f6b5b4d7
TL
416
417 if self.TYPE not in ['mon', 'osd', 'mds']:
f67539c2
TL
418 logger.debug(out)
419 return HandleCommandResult(0, out)
420
421 if self.TYPE == 'osd':
422 return self.ok_to_stop_osd(daemon_ids, known, force)
f6b5b4d7
TL
423
424 r = HandleCommandResult(*self.mgr.mon_command({
425 'prefix': f'{self.TYPE} ok-to-stop',
426 'ids': daemon_ids,
427 }))
428
429 if r.retval:
430 err = f'{err}: {r.stderr}' if r.stderr else err
f67539c2 431 logger.debug(err)
f6b5b4d7 432 return HandleCommandResult(r.retval, r.stdout, err)
e306af50 433
f6b5b4d7 434 out = f'{out}: {r.stdout}' if r.stdout else out
f67539c2 435 logger.debug(out)
f6b5b4d7
TL
436 return HandleCommandResult(r.retval, out, r.stderr)
437
f67539c2
TL
438 def _enough_daemons_to_stop(self, daemon_type: str, daemon_ids: List[str], service: str, low_limit: int, alert: bool = False) -> Tuple[bool, str]:
439 # Provides a warning about if it possible or not to stop <n> daemons in a service
440 names = [f'{daemon_type}.{d_id}' for d_id in daemon_ids]
441 number_of_running_daemons = len(
442 [daemon
443 for daemon in self.mgr.cache.get_daemons_by_type(daemon_type)
444 if daemon.status == DaemonDescriptionStatus.running])
445 if (number_of_running_daemons - len(daemon_ids)) >= low_limit:
446 return False, f'It is presumed safe to stop {names}'
447
448 num_daemons_left = number_of_running_daemons - len(daemon_ids)
449
450 def plural(count: int) -> str:
451 return 'daemon' if count == 1 else 'daemons'
452
453 left_count = "no" if num_daemons_left == 0 else num_daemons_left
454
455 if alert:
456 out = (f'ALERT: Cannot stop {names} in {service} service. '
457 f'Not enough remaining {service} daemons. '
458 f'Please deploy at least {low_limit + 1} {service} daemons before stopping {names}. ')
459 else:
460 out = (f'WARNING: Stopping {len(daemon_ids)} out of {number_of_running_daemons} daemons in {service} service. '
461 f'Service will not be operational with {left_count} {plural(num_daemons_left)} left. '
462 f'At least {low_limit} {plural(low_limit)} must be running to guarantee service. ')
463 return True, out
464
f91f0fd5 465 def pre_remove(self, daemon: DaemonDescription) -> None:
f6b5b4d7
TL
466 """
467 Called before the daemon is removed.
468 """
f67539c2
TL
469 assert daemon.daemon_type is not None
470 assert self.TYPE == daemon_type_to_service(daemon.daemon_type)
f91f0fd5
TL
471 logger.debug(f'Pre remove daemon {self.TYPE}.{daemon.daemon_id}')
472
a4b75251 473 def post_remove(self, daemon: DaemonDescription, is_failed_deploy: bool) -> None:
f91f0fd5
TL
474 """
475 Called after the daemon is removed.
476 """
f67539c2
TL
477 assert daemon.daemon_type is not None
478 assert self.TYPE == daemon_type_to_service(daemon.daemon_type)
f91f0fd5
TL
479 logger.debug(f'Post remove daemon {self.TYPE}.{daemon.daemon_id}')
480
f67539c2
TL
481 def purge(self, service_name: str) -> None:
482 """Called to carry out any purge tasks following service removal"""
483 logger.debug(f'Purge called for {self.TYPE} - no action taken')
484
f91f0fd5
TL
485
486class CephService(CephadmService):
f67539c2 487 def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
f91f0fd5
TL
488 # Ceph.daemons (mon, mgr, mds, osd, etc)
489 cephadm_config = self.get_config_and_keyring(
490 daemon_spec.daemon_type,
491 daemon_spec.daemon_id,
492 host=daemon_spec.host,
493 keyring=daemon_spec.keyring,
494 extra_ceph_config=daemon_spec.ceph_conf)
495
496 if daemon_spec.config_get_files():
497 cephadm_config.update({'files': daemon_spec.config_get_files()})
498
499 return cephadm_config, []
500
a4b75251
TL
501 def post_remove(self, daemon: DaemonDescription, is_failed_deploy: bool) -> None:
502 super().post_remove(daemon, is_failed_deploy=is_failed_deploy)
f91f0fd5 503 self.remove_keyring(daemon)
e306af50 504
f91f0fd5 505 def get_auth_entity(self, daemon_id: str, host: str = "") -> AuthEntity:
39ae355f 506 return get_auth_entity(self.TYPE, daemon_id, host=host)
f91f0fd5
TL
507
508 def get_config_and_keyring(self,
509 daemon_type: str,
510 daemon_id: str,
511 host: str,
512 keyring: Optional[str] = None,
513 extra_ceph_config: Optional[str] = None
514 ) -> Dict[str, Any]:
515 # keyring
516 if not keyring:
517 entity: AuthEntity = self.get_auth_entity(daemon_id, host=host)
518 ret, keyring, err = self.mgr.check_mon_command({
519 'prefix': 'auth get',
520 'entity': entity,
521 })
f91f0fd5
TL
522 config = self.mgr.get_minimal_ceph_conf()
523
524 if extra_ceph_config:
525 config += extra_ceph_config
526
527 return {
528 'config': config,
529 'keyring': keyring,
530 }
531
532 def remove_keyring(self, daemon: DaemonDescription) -> None:
f67539c2
TL
533 assert daemon.daemon_id is not None
534 assert daemon.hostname is not None
f91f0fd5
TL
535 daemon_id: str = daemon.daemon_id
536 host: str = daemon.hostname
537
a4b75251 538 assert daemon.daemon_type != 'mon'
f91f0fd5
TL
539
540 entity = self.get_auth_entity(daemon_id, host=host)
541
f67539c2
TL
542 logger.info(f'Removing key for {entity}')
543 ret, out, err = self.mgr.mon_command({
f91f0fd5
TL
544 'prefix': 'auth rm',
545 'entity': entity,
546 })
547
548
549class MonService(CephService):
f6b5b4d7
TL
550 TYPE = 'mon'
551
f67539c2 552 def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
e306af50
TL
553 """
554 Create a new monitor on the given host.
555 """
f6b5b4d7 556 assert self.TYPE == daemon_spec.daemon_type
f67539c2 557 name, _, network = daemon_spec.daemon_id, daemon_spec.host, daemon_spec.network
f6b5b4d7 558
e306af50
TL
559 # get mon. key
560 ret, keyring, err = self.mgr.check_mon_command({
561 'prefix': 'auth get',
39ae355f 562 'entity': daemon_spec.entity_name(),
e306af50
TL
563 })
564
565 extra_config = '[mon.%s]\n' % name
566 if network:
567 # infer whether this is a CIDR network, addrvec, or plain IP
568 if '/' in network:
569 extra_config += 'public network = %s\n' % network
570 elif network.startswith('[v') and network.endswith(']'):
571 extra_config += 'public addrv = %s\n' % network
f91f0fd5
TL
572 elif is_ipv6(network):
573 extra_config += 'public addr = %s\n' % unwrap_ipv6(network)
e306af50
TL
574 elif ':' not in network:
575 extra_config += 'public addr = %s\n' % network
576 else:
f91f0fd5
TL
577 raise OrchestratorError(
578 'Must specify a CIDR network, ceph addrvec, or plain IP: \'%s\'' % network)
e306af50
TL
579 else:
580 # try to get the public_network from the config
581 ret, network, err = self.mgr.check_mon_command({
582 'prefix': 'config get',
583 'who': 'mon',
584 'key': 'public_network',
585 })
f6b5b4d7 586 network = network.strip() if network else network
e306af50 587 if not network:
f91f0fd5
TL
588 raise OrchestratorError(
589 'Must set public_network config option or specify a CIDR network, ceph addrvec, or plain IP')
e306af50 590 if '/' not in network:
f91f0fd5
TL
591 raise OrchestratorError(
592 'public_network is set but does not look like a CIDR network: \'%s\'' % network)
e306af50
TL
593 extra_config += 'public network = %s\n' % network
594
f91f0fd5
TL
595 daemon_spec.ceph_conf = extra_config
596 daemon_spec.keyring = keyring
f6b5b4d7 597
f67539c2
TL
598 daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
599
f91f0fd5 600 return daemon_spec
f6b5b4d7
TL
601
602 def _check_safe_to_destroy(self, mon_id: str) -> None:
603 ret, out, err = self.mgr.check_mon_command({
604 'prefix': 'quorum_status',
605 })
606 try:
607 j = json.loads(out)
f67539c2 608 except Exception:
f6b5b4d7
TL
609 raise OrchestratorError('failed to parse quorum status')
610
611 mons = [m['name'] for m in j['monmap']['mons']]
612 if mon_id not in mons:
613 logger.info('Safe to remove mon.%s: not in monmap (%s)' % (
614 mon_id, mons))
615 return
616 new_mons = [m for m in mons if m != mon_id]
617 new_quorum = [m for m in j['quorum_names'] if m != mon_id]
618 if len(new_quorum) > len(new_mons) / 2:
f91f0fd5
TL
619 logger.info('Safe to remove mon.%s: new quorum should be %s (from %s)' %
620 (mon_id, new_quorum, new_mons))
f6b5b4d7 621 return
f91f0fd5
TL
622 raise OrchestratorError(
623 'Removing %s would break mon quorum (new quorum %s, new mons %s)' % (mon_id, new_quorum, new_mons))
f6b5b4d7 624
f91f0fd5
TL
625 def pre_remove(self, daemon: DaemonDescription) -> None:
626 super().pre_remove(daemon)
f6b5b4d7 627
f67539c2 628 assert daemon.daemon_id is not None
f91f0fd5 629 daemon_id: str = daemon.daemon_id
f6b5b4d7
TL
630 self._check_safe_to_destroy(daemon_id)
631
632 # remove mon from quorum before we destroy the daemon
633 logger.info('Removing monitor %s from monmap...' % daemon_id)
634 ret, out, err = self.mgr.check_mon_command({
635 'prefix': 'mon rm',
636 'name': daemon_id,
637 })
e306af50 638
a4b75251
TL
639 def post_remove(self, daemon: DaemonDescription, is_failed_deploy: bool) -> None:
640 # Do not remove the mon keyring.
641 # super().post_remove(daemon)
642 pass
643
e306af50 644
f91f0fd5 645class MgrService(CephService):
f6b5b4d7
TL
646 TYPE = 'mgr'
647
b3b6e05e
TL
648 def allow_colo(self) -> bool:
649 if self.mgr.get_ceph_option('mgr_standby_modules'):
650 # traditional mgr mode: standby daemons' modules listen on
651 # ports and redirect to the primary. we must not schedule
652 # multiple mgrs on the same host or else ports will
653 # conflict.
654 return False
655 else:
656 # standby daemons do nothing, and therefore port conflicts
657 # are not a concern.
658 return True
659
f67539c2 660 def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
e306af50
TL
661 """
662 Create a new manager instance on a host.
663 """
f6b5b4d7 664 assert self.TYPE == daemon_spec.daemon_type
f67539c2 665 mgr_id, _ = daemon_spec.daemon_id, daemon_spec.host
f6b5b4d7 666
e306af50 667 # get mgr. key
f67539c2
TL
668 keyring = self.get_keyring_with_caps(self.get_auth_entity(mgr_id),
669 ['mon', 'profile mgr',
670 'osd', 'allow *',
671 'mds', 'allow *'])
e306af50 672
f6b5b4d7
TL
673 # Retrieve ports used by manager modules
674 # In the case of the dashboard port and with several manager daemons
675 # running in different hosts, it exists the possibility that the
676 # user has decided to use different dashboard ports in each server
677 # If this is the case then the dashboard port opened will be only the used
678 # as default.
679 ports = []
f6b5b4d7 680 ret, mgr_services, err = self.mgr.check_mon_command({
f91f0fd5 681 'prefix': 'mgr services',
f6b5b4d7
TL
682 })
683 if mgr_services:
684 mgr_endpoints = json.loads(mgr_services)
685 for end_point in mgr_endpoints.values():
f91f0fd5 686 port = re.search(r'\:\d+\/', end_point)
f6b5b4d7
TL
687 if port:
688 ports.append(int(port[0][1:-1]))
689
690 if ports:
691 daemon_spec.ports = ports
692
693 daemon_spec.keyring = keyring
694
f67539c2
TL
695 daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
696
f91f0fd5
TL
697 return daemon_spec
698
f6b5b4d7 699 def get_active_daemon(self, daemon_descrs: List[DaemonDescription]) -> DaemonDescription:
f6b5b4d7 700 for daemon in daemon_descrs:
f67539c2
TL
701 assert daemon.daemon_type is not None
702 assert daemon.daemon_id is not None
f91f0fd5 703 if self.mgr.daemon_is_self(daemon.daemon_type, daemon.daemon_id):
f6b5b4d7
TL
704 return daemon
705 # if no active mgr found, return empty Daemon Desc
706 return DaemonDescription()
e306af50 707
f91f0fd5 708 def fail_over(self) -> None:
33c7a0ef
TL
709 # this has been seen to sometimes transiently fail even when there are multiple
710 # mgr daemons. As long as there are multiple known mgr daemons, we should retry.
711 class NoStandbyError(OrchestratorError):
712 pass
713 no_standby_exc = NoStandbyError('Need standby mgr daemon', event_kind_subject=(
714 'daemon', 'mgr' + self.mgr.get_mgr_id()))
715 for sleep_secs in [2, 8, 15]:
716 try:
717 if not self.mgr_map_has_standby():
718 raise no_standby_exc
719 self.mgr.events.for_daemon('mgr' + self.mgr.get_mgr_id(),
720 'INFO', 'Failing over to other MGR')
721 logger.info('Failing over to other MGR')
722
723 # fail over
724 ret, out, err = self.mgr.check_mon_command({
725 'prefix': 'mgr fail',
726 'who': self.mgr.get_mgr_id(),
727 })
728 return
729 except NoStandbyError:
730 logger.info(
731 f'Failed to find standby mgr for failover. Retrying in {sleep_secs} seconds')
732 time.sleep(sleep_secs)
733 raise no_standby_exc
f91f0fd5
TL
734
735 def mgr_map_has_standby(self) -> bool:
736 """
737 This is a bit safer than asking our inventory. If the mgr joined the mgr map,
738 we know it joined the cluster
739 """
740 mgr_map = self.mgr.get('mgr_map')
741 num = len(mgr_map.get('standbys'))
742 return bool(num)
743
f67539c2
TL
744 def ok_to_stop(
745 self,
746 daemon_ids: List[str],
747 force: bool = False,
748 known: Optional[List[str]] = None # output argument
749 ) -> HandleCommandResult:
750 # ok to stop if there is more than 1 mgr and not trying to stop the active mgr
751
752 warn, warn_message = self._enough_daemons_to_stop(self.TYPE, daemon_ids, 'Mgr', 1, True)
753 if warn:
754 return HandleCommandResult(-errno.EBUSY, '', warn_message)
755
756 mgr_daemons = self.mgr.cache.get_daemons_by_type(self.TYPE)
757 active = self.get_active_daemon(mgr_daemons).daemon_id
758 if active in daemon_ids:
759 warn_message = 'ALERT: Cannot stop active Mgr daemon, Please switch active Mgrs with \'ceph mgr fail %s\'' % active
760 return HandleCommandResult(-errno.EBUSY, '', warn_message)
761
762 return HandleCommandResult(0, warn_message, '')
763
e306af50 764
f91f0fd5 765class MdsService(CephService):
f6b5b4d7
TL
766 TYPE = 'mds'
767
f67539c2
TL
768 def allow_colo(self) -> bool:
769 return True
770
522d829b 771 def config(self, spec: ServiceSpec) -> None:
f6b5b4d7 772 assert self.TYPE == spec.service_type
e306af50 773 assert spec.service_id
f6b5b4d7
TL
774
775 # ensure mds_join_fs is set for these daemons
e306af50
TL
776 ret, out, err = self.mgr.check_mon_command({
777 'prefix': 'config set',
778 'who': 'mds.' + spec.service_id,
779 'name': 'mds_join_fs',
780 'value': spec.service_id,
781 })
782
f67539c2 783 def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
f6b5b4d7 784 assert self.TYPE == daemon_spec.daemon_type
f67539c2 785 mds_id, _ = daemon_spec.daemon_id, daemon_spec.host
f6b5b4d7 786
f67539c2
TL
787 # get mds. key
788 keyring = self.get_keyring_with_caps(self.get_auth_entity(mds_id),
789 ['mon', 'profile mds',
790 'osd', 'allow rw tag cephfs *=*',
791 'mds', 'allow'])
f6b5b4d7
TL
792 daemon_spec.keyring = keyring
793
f67539c2
TL
794 daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
795
f91f0fd5
TL
796 return daemon_spec
797
f6b5b4d7
TL
798 def get_active_daemon(self, daemon_descrs: List[DaemonDescription]) -> DaemonDescription:
799 active_mds_strs = list()
800 for fs in self.mgr.get('fs_map')['filesystems']:
801 mds_map = fs['mdsmap']
802 if mds_map is not None:
803 for mds_id, mds_status in mds_map['info'].items():
804 if mds_status['state'] == 'up:active':
805 active_mds_strs.append(mds_status['name'])
806 if len(active_mds_strs) != 0:
807 for daemon in daemon_descrs:
808 if daemon.daemon_id in active_mds_strs:
809 return daemon
810 # if no mds found, return empty Daemon Desc
811 return DaemonDescription()
e306af50 812
f67539c2
TL
813 def purge(self, service_name: str) -> None:
814 self.mgr.check_mon_command({
815 'prefix': 'config rm',
816 'who': service_name,
817 'name': 'mds_join_fs',
818 })
819
e306af50 820
f91f0fd5 821class RgwService(CephService):
f6b5b4d7
TL
822 TYPE = 'rgw'
823
f67539c2
TL
824 def allow_colo(self) -> bool:
825 return True
f6b5b4d7 826
522d829b 827 def config(self, spec: RGWSpec) -> None: # type: ignore
f67539c2 828 assert self.TYPE == spec.service_type
f6b5b4d7 829
f67539c2
TL
830 # set rgw_realm and rgw_zone, if present
831 if spec.rgw_realm:
832 ret, out, err = self.mgr.check_mon_command({
833 'prefix': 'config set',
834 'who': f"{utils.name_to_config_section('rgw')}.{spec.service_id}",
835 'name': 'rgw_realm',
836 'value': spec.rgw_realm,
837 })
838 if spec.rgw_zone:
839 ret, out, err = self.mgr.check_mon_command({
840 'prefix': 'config set',
841 'who': f"{utils.name_to_config_section('rgw')}.{spec.service_id}",
842 'name': 'rgw_zone',
843 'value': spec.rgw_zone,
844 })
e306af50
TL
845
846 if spec.rgw_frontend_ssl_certificate:
847 if isinstance(spec.rgw_frontend_ssl_certificate, list):
848 cert_data = '\n'.join(spec.rgw_frontend_ssl_certificate)
849 elif isinstance(spec.rgw_frontend_ssl_certificate, str):
850 cert_data = spec.rgw_frontend_ssl_certificate
851 else:
852 raise OrchestratorError(
f91f0fd5
TL
853 'Invalid rgw_frontend_ssl_certificate: %s'
854 % spec.rgw_frontend_ssl_certificate)
e306af50
TL
855 ret, out, err = self.mgr.check_mon_command({
856 'prefix': 'config-key set',
f67539c2 857 'key': f'rgw/cert/{spec.service_name()}',
e306af50
TL
858 'val': cert_data,
859 })
860
f67539c2 861 # TODO: fail, if we don't have a spec
e306af50
TL
862 logger.info('Saving service %s spec with placement %s' % (
863 spec.service_name(), spec.placement.pretty_str()))
864 self.mgr.spec_store.save(spec)
522d829b 865 self.mgr.trigger_connect_dashboard_rgw()
e306af50 866
f67539c2 867 def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
f6b5b4d7 868 assert self.TYPE == daemon_spec.daemon_type
f67539c2
TL
869 rgw_id, _ = daemon_spec.daemon_id, daemon_spec.host
870 spec = cast(RGWSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
f6b5b4d7
TL
871
872 keyring = self.get_keyring(rgw_id)
873
f67539c2
TL
874 if daemon_spec.ports:
875 port = daemon_spec.ports[0]
876 else:
877 # this is a redeploy of older instance that doesn't have an explicitly
878 # assigned port, in which case we can assume there is only 1 per host
879 # and it matches the spec.
880 port = spec.get_port()
881
882 # configure frontend
883 args = []
884 ftype = spec.rgw_frontend_type or "beast"
885 if ftype == 'beast':
886 if spec.ssl:
887 if daemon_spec.ip:
a4b75251
TL
888 args.append(
889 f"ssl_endpoint={build_url(host=daemon_spec.ip, port=port).lstrip('/')}")
f67539c2
TL
890 else:
891 args.append(f"ssl_port={port}")
892 args.append(f"ssl_certificate=config://rgw/cert/{spec.service_name()}")
893 else:
894 if daemon_spec.ip:
a4b75251 895 args.append(f"endpoint={build_url(host=daemon_spec.ip, port=port).lstrip('/')}")
f67539c2
TL
896 else:
897 args.append(f"port={port}")
898 elif ftype == 'civetweb':
899 if spec.ssl:
900 if daemon_spec.ip:
a4b75251
TL
901 # note the 's' suffix on port
902 args.append(f"port={build_url(host=daemon_spec.ip, port=port).lstrip('/')}s")
f67539c2
TL
903 else:
904 args.append(f"port={port}s") # note the 's' suffix on port
905 args.append(f"ssl_certificate=config://rgw/cert/{spec.service_name()}")
906 else:
907 if daemon_spec.ip:
a4b75251 908 args.append(f"port={build_url(host=daemon_spec.ip, port=port).lstrip('/')}")
f67539c2
TL
909 else:
910 args.append(f"port={port}")
911 frontend = f'{ftype} {" ".join(args)}'
912
913 ret, out, err = self.mgr.check_mon_command({
914 'prefix': 'config set',
915 'who': utils.name_to_config_section(daemon_spec.name()),
916 'name': 'rgw_frontends',
917 'value': frontend
918 })
919
f6b5b4d7 920 daemon_spec.keyring = keyring
f67539c2 921 daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
f6b5b4d7 922
f91f0fd5 923 return daemon_spec
f6b5b4d7 924
f91f0fd5 925 def get_keyring(self, rgw_id: str) -> str:
f67539c2
TL
926 keyring = self.get_keyring_with_caps(self.get_auth_entity(rgw_id),
927 ['mon', 'allow *',
928 'mgr', 'allow rw',
929 'osd', 'allow rwx tag rgw *=*'])
f6b5b4d7
TL
930 return keyring
931
f67539c2
TL
932 def purge(self, service_name: str) -> None:
933 self.mgr.check_mon_command({
934 'prefix': 'config rm',
935 'who': utils.name_to_config_section(service_name),
936 'name': 'rgw_realm',
937 })
938 self.mgr.check_mon_command({
939 'prefix': 'config rm',
940 'who': utils.name_to_config_section(service_name),
941 'name': 'rgw_zone',
942 })
943 self.mgr.check_mon_command({
944 'prefix': 'config-key rm',
945 'key': f'rgw/cert/{service_name}',
946 })
522d829b 947 self.mgr.trigger_connect_dashboard_rgw()
f67539c2 948
a4b75251
TL
949 def post_remove(self, daemon: DaemonDescription, is_failed_deploy: bool) -> None:
950 super().post_remove(daemon, is_failed_deploy=is_failed_deploy)
f67539c2
TL
951 self.mgr.check_mon_command({
952 'prefix': 'config rm',
953 'who': utils.name_to_config_section(daemon.name()),
954 'name': 'rgw_frontends',
955 })
956
957 def ok_to_stop(
958 self,
959 daemon_ids: List[str],
960 force: bool = False,
961 known: Optional[List[str]] = None # output argument
962 ) -> HandleCommandResult:
963 # if load balancer (ingress) is present block if only 1 daemon up otherwise ok
964 # if no load balancer, warn if > 1 daemon, block if only 1 daemon
965 def ingress_present() -> bool:
966 running_ingress_daemons = [
967 daemon for daemon in self.mgr.cache.get_daemons_by_type('ingress') if daemon.status == 1]
968 running_haproxy_daemons = [
969 daemon for daemon in running_ingress_daemons if daemon.daemon_type == 'haproxy']
970 running_keepalived_daemons = [
971 daemon for daemon in running_ingress_daemons if daemon.daemon_type == 'keepalived']
972 # check that there is at least one haproxy and keepalived daemon running
973 if running_haproxy_daemons and running_keepalived_daemons:
974 return True
975 return False
976
977 # if only 1 rgw, alert user (this is not passable with --force)
978 warn, warn_message = self._enough_daemons_to_stop(self.TYPE, daemon_ids, 'RGW', 1, True)
979 if warn:
980 return HandleCommandResult(-errno.EBUSY, '', warn_message)
981
982 # if reached here, there is > 1 rgw daemon.
983 # Say okay if load balancer present or force flag set
984 if ingress_present() or force:
985 return HandleCommandResult(0, warn_message, '')
986
987 # if reached here, > 1 RGW daemon, no load balancer and no force flag.
988 # Provide warning
989 warn_message = "WARNING: Removing RGW daemons can cause clients to lose connectivity. "
990 return HandleCommandResult(-errno.EBUSY, '', warn_message)
e306af50 991
522d829b
TL
992 def config_dashboard(self, daemon_descrs: List[DaemonDescription]) -> None:
993 self.mgr.trigger_connect_dashboard_rgw()
994
e306af50 995
f91f0fd5 996class RbdMirrorService(CephService):
f6b5b4d7
TL
997 TYPE = 'rbd-mirror'
998
f67539c2
TL
999 def allow_colo(self) -> bool:
1000 return True
1001
1002 def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
f6b5b4d7 1003 assert self.TYPE == daemon_spec.daemon_type
f67539c2 1004 daemon_id, _ = daemon_spec.daemon_id, daemon_spec.host
f6b5b4d7 1005
f67539c2
TL
1006 keyring = self.get_keyring_with_caps(self.get_auth_entity(daemon_id),
1007 ['mon', 'profile rbd-mirror',
1008 'osd', 'profile rbd'])
f6b5b4d7
TL
1009
1010 daemon_spec.keyring = keyring
1011
f67539c2
TL
1012 daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
1013
f91f0fd5 1014 return daemon_spec
e306af50 1015
f67539c2
TL
1016 def ok_to_stop(
1017 self,
1018 daemon_ids: List[str],
1019 force: bool = False,
1020 known: Optional[List[str]] = None # output argument
1021 ) -> HandleCommandResult:
1022 # if only 1 rbd-mirror, alert user (this is not passable with --force)
1023 warn, warn_message = self._enough_daemons_to_stop(
1024 self.TYPE, daemon_ids, 'Rbdmirror', 1, True)
1025 if warn:
1026 return HandleCommandResult(-errno.EBUSY, '', warn_message)
1027 return HandleCommandResult(0, warn_message, '')
1028
e306af50 1029
f91f0fd5 1030class CrashService(CephService):
f6b5b4d7
TL
1031 TYPE = 'crash'
1032
f67539c2 1033 def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
f6b5b4d7
TL
1034 assert self.TYPE == daemon_spec.daemon_type
1035 daemon_id, host = daemon_spec.daemon_id, daemon_spec.host
1036
f67539c2
TL
1037 keyring = self.get_keyring_with_caps(self.get_auth_entity(daemon_id, host=host),
1038 ['mon', 'profile crash',
1039 'mgr', 'profile crash'])
1040
1041 daemon_spec.keyring = keyring
1042
1043 daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
1044
1045 return daemon_spec
1046
1047
39ae355f
TL
1048class CephExporterService(CephService):
1049 TYPE = 'ceph-exporter'
1050 DEFAULT_SERVICE_PORT = 9926
1051
1052 def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
1053 assert self.TYPE == daemon_spec.daemon_type
1054 spec = cast(CephExporterSpec, self.mgr.spec_store[daemon_spec.service_name].spec)
1055 keyring = self.get_keyring_with_caps(self.get_auth_entity(daemon_spec.daemon_id),
1056 ['mon', 'profile ceph-exporter',
1057 'mon', 'allow r',
1058 'mgr', 'allow r',
1059 'osd', 'allow r'])
1060 exporter_config = {}
1061 if spec.sock_dir:
1062 exporter_config.update({'sock-dir': spec.sock_dir})
1063 if spec.port:
1064 exporter_config.update({'port': f'{spec.port}'})
1065 if spec.prio_limit is not None:
1066 exporter_config.update({'prio-limit': f'{spec.prio_limit}'})
1067 if spec.stats_period:
1068 exporter_config.update({'stats-period': f'{spec.stats_period}'})
1069
1070 daemon_spec.keyring = keyring
1071 daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
1072 daemon_spec.final_config = merge_dicts(daemon_spec.final_config, exporter_config)
1073 return daemon_spec
1074
1075
f67539c2
TL
1076class CephfsMirrorService(CephService):
1077 TYPE = 'cephfs-mirror'
1078
20effc67
TL
1079 def config(self, spec: ServiceSpec) -> None:
1080 # make sure mirroring module is enabled
1081 mgr_map = self.mgr.get('mgr_map')
1082 mod_name = 'mirroring'
1083 if mod_name not in mgr_map.get('services', {}):
1084 self.mgr.check_mon_command({
1085 'prefix': 'mgr module enable',
1086 'module': mod_name
1087 })
1088 # we shouldn't get here (mon will tell the mgr to respawn), but no
1089 # harm done if we do.
1090
f67539c2
TL
1091 def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
1092 assert self.TYPE == daemon_spec.daemon_type
1093
e306af50
TL
1094 ret, keyring, err = self.mgr.check_mon_command({
1095 'prefix': 'auth get-or-create',
39ae355f 1096 'entity': daemon_spec.entity_name(),
b3b6e05e 1097 'caps': ['mon', 'profile cephfs-mirror',
f67539c2
TL
1098 'mds', 'allow r',
1099 'osd', 'allow rw tag cephfs metadata=*, allow r tag cephfs data=*',
1100 'mgr', 'allow r'],
e306af50 1101 })
f6b5b4d7
TL
1102
1103 daemon_spec.keyring = keyring
f67539c2 1104 daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
f91f0fd5 1105 return daemon_spec
20effc67
TL
1106
1107
1108class CephadmAgent(CephService):
1109 TYPE = 'agent'
1110
1111 def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec:
1112 assert self.TYPE == daemon_spec.daemon_type
1113 daemon_id, host = daemon_spec.daemon_id, daemon_spec.host
1114
1115 if not self.mgr.cherrypy_thread:
1116 raise OrchestratorError('Cannot deploy agent before creating cephadm endpoint')
1117
1118 keyring = self.get_keyring_with_caps(self.get_auth_entity(daemon_id, host=host), [])
1119 daemon_spec.keyring = keyring
1120 self.mgr.agent_cache.agent_keys[host] = keyring
1121
1122 daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec)
1123
1124 return daemon_spec
1125
1126 def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]:
1127 try:
1128 assert self.mgr.cherrypy_thread
1129 assert self.mgr.cherrypy_thread.ssl_certs.get_root_cert()
1130 assert self.mgr.cherrypy_thread.server_port
1131 except Exception:
1132 raise OrchestratorError(
1133 'Cannot deploy agent daemons until cephadm endpoint has finished generating certs')
1134
1135 cfg = {'target_ip': self.mgr.get_mgr_ip(),
1136 'target_port': self.mgr.cherrypy_thread.server_port,
1137 'refresh_period': self.mgr.agent_refresh_rate,
1138 'listener_port': self.mgr.agent_starting_port,
1139 'host': daemon_spec.host,
1140 'device_enhanced_scan': str(self.mgr.device_enhanced_scan)}
1141
1142 listener_cert, listener_key = self.mgr.cherrypy_thread.ssl_certs.generate_cert(
1143 self.mgr.inventory.get_addr(daemon_spec.host))
1144 config = {
1145 'agent.json': json.dumps(cfg),
1146 'keyring': daemon_spec.keyring,
1147 'root_cert.pem': self.mgr.cherrypy_thread.ssl_certs.get_root_cert(),
1148 'listener.crt': listener_cert,
1149 'listener.key': listener_key,
1150 }
1151
1152 return config, sorted([str(self.mgr.get_mgr_ip()), str(self.mgr.cherrypy_thread.server_port),
1153 self.mgr.cherrypy_thread.ssl_certs.get_root_cert(),
1154 str(self.mgr.get_module_option('device_enhanced_scan'))])