]>
Commit | Line | Data |
---|---|---|
f67539c2 | 1 | import errno |
f6b5b4d7 | 2 | import json |
e306af50 | 3 | import logging |
f67539c2 | 4 | import re |
33c7a0ef TL |
5 | import socket |
6 | import time | |
f6b5b4d7 | 7 | from abc import ABCMeta, abstractmethod |
f67539c2 TL |
8 | from typing import TYPE_CHECKING, List, Callable, TypeVar, \ |
9 | Optional, Dict, Any, Tuple, NewType, cast | |
e306af50 | 10 | |
f6b5b4d7 | 11 | from mgr_module import HandleCommandResult, MonCommandFailed |
e306af50 | 12 | |
39ae355f | 13 | from ceph.deployment.service_spec import ServiceSpec, RGWSpec, CephExporterSpec |
f91f0fd5 | 14 | from ceph.deployment.utils import is_ipv6, unwrap_ipv6 |
39ae355f | 15 | from mgr_util import build_url, merge_dicts |
f67539c2 TL |
16 | from orchestrator import OrchestratorError, DaemonDescription, DaemonDescriptionStatus |
17 | from orchestrator._interface import daemon_type_to_service | |
e306af50 TL |
18 | from cephadm import utils |
19 | ||
20 | if TYPE_CHECKING: | |
21 | from cephadm.module import CephadmOrchestrator | |
22 | ||
23 | logger = logging.getLogger(__name__) | |
24 | ||
f6b5b4d7 | 25 | ServiceSpecs = TypeVar('ServiceSpecs', bound=ServiceSpec) |
f91f0fd5 | 26 | AuthEntity = NewType('AuthEntity', str) |
e306af50 | 27 | |
f6b5b4d7 | 28 | |
39ae355f TL |
29 | def 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 | 50 | class 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 | |
152 | class 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 | |
486 | class 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 | ||
549 | class 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 | 645 | class 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 | 765 | class 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 | 821 | class 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 | 996 | class 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 | 1030 | class 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 |
1048 | class 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 |
1076 | class 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 | ||
1108 | class 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'))]) |