]>
Commit | Line | Data |
---|---|---|
9f95a23c TL |
1 | import fnmatch |
2 | import re | |
f67539c2 | 3 | from collections import OrderedDict |
1911f103 | 4 | from functools import wraps |
f67539c2 TL |
5 | from ipaddress import ip_network, ip_address |
6 | from typing import Optional, Dict, Any, List, Union, Callable, Iterable, Type, TypeVar, cast, \ | |
522d829b | 7 | NamedTuple, Mapping |
9f95a23c | 8 | |
f6b5b4d7 TL |
9 | import yaml |
10 | ||
f67539c2 | 11 | from ceph.deployment.hostspec import HostSpec, SpecValidationError |
f91f0fd5 | 12 | from ceph.deployment.utils import unwrap_ipv6 |
9f95a23c | 13 | |
f67539c2 TL |
14 | ServiceSpecT = TypeVar('ServiceSpecT', bound='ServiceSpec') |
15 | FuncT = TypeVar('FuncT', bound=Callable) | |
9f95a23c | 16 | |
9f95a23c | 17 | |
f67539c2 | 18 | def assert_valid_host(name: str) -> None: |
9f95a23c TL |
19 | p = re.compile('^[a-zA-Z0-9-]+$') |
20 | try: | |
21 | assert len(name) <= 250, 'name is too long (max 250 chars)' | |
22 | for part in name.split('.'): | |
23 | assert len(part) > 0, '.-delimited name component must not be empty' | |
24 | assert len(part) <= 63, '.-delimited name component must not be more than 63 chars' | |
25 | assert p.match(part), 'name component must include only a-z, 0-9, and -' | |
26 | except AssertionError as e: | |
f67539c2 | 27 | raise SpecValidationError(str(e)) |
9f95a23c TL |
28 | |
29 | ||
f67539c2 | 30 | def handle_type_error(method: FuncT) -> FuncT: |
1911f103 | 31 | @wraps(method) |
f67539c2 | 32 | def inner(cls: Any, *args: Any, **kwargs: Any) -> Any: |
1911f103 TL |
33 | try: |
34 | return method(cls, *args, **kwargs) | |
35 | except (TypeError, AttributeError) as e: | |
36 | error_msg = '{}: {}'.format(cls.__name__, e) | |
f67539c2 TL |
37 | raise SpecValidationError(error_msg) |
38 | return cast(FuncT, inner) | |
39 | ||
1911f103 | 40 | |
f67539c2 TL |
41 | class HostPlacementSpec(NamedTuple): |
42 | hostname: str | |
43 | network: str | |
44 | name: str | |
1911f103 | 45 | |
f67539c2 | 46 | def __str__(self) -> str: |
9f95a23c TL |
47 | res = '' |
48 | res += self.hostname | |
49 | if self.network: | |
50 | res += ':' + self.network | |
51 | if self.name: | |
52 | res += '=' + self.name | |
53 | return res | |
54 | ||
55 | @classmethod | |
1911f103 | 56 | @handle_type_error |
f67539c2 | 57 | def from_json(cls, data: Union[dict, str]) -> 'HostPlacementSpec': |
f91f0fd5 TL |
58 | if isinstance(data, str): |
59 | return cls.parse(data) | |
9f95a23c TL |
60 | return cls(**data) |
61 | ||
f91f0fd5 TL |
62 | def to_json(self) -> str: |
63 | return str(self) | |
9f95a23c TL |
64 | |
65 | @classmethod | |
66 | def parse(cls, host, require_network=True): | |
67 | # type: (str, bool) -> HostPlacementSpec | |
68 | """ | |
69 | Split host into host, network, and (optional) daemon name parts. The network | |
70 | part can be an IP, CIDR, or ceph addrvec like '[v2:1.2.3.4:3300,v1:1.2.3.4:6789]'. | |
71 | e.g., | |
72 | "myhost" | |
73 | "myhost=name" | |
74 | "myhost:1.2.3.4" | |
75 | "myhost:1.2.3.4=name" | |
76 | "myhost:1.2.3.0/24" | |
77 | "myhost:1.2.3.0/24=name" | |
78 | "myhost:[v2:1.2.3.4:3000]=name" | |
79 | "myhost:[v2:1.2.3.4:3000,v1:1.2.3.4:6789]=name" | |
80 | """ | |
81 | # Matches from start to : or = or until end of string | |
82 | host_re = r'^(.*?)(:|=|$)' | |
83 | # Matches from : to = or until end of string | |
84 | ip_re = r':(.*?)(=|$)' | |
85 | # Matches from = to end of string | |
86 | name_re = r'=(.*?)$' | |
87 | ||
88 | # assign defaults | |
89 | host_spec = cls('', '', '') | |
90 | ||
91 | match_host = re.search(host_re, host) | |
92 | if match_host: | |
93 | host_spec = host_spec._replace(hostname=match_host.group(1)) | |
94 | ||
95 | name_match = re.search(name_re, host) | |
96 | if name_match: | |
97 | host_spec = host_spec._replace(name=name_match.group(1)) | |
98 | ||
99 | ip_match = re.search(ip_re, host) | |
100 | if ip_match: | |
101 | host_spec = host_spec._replace(network=ip_match.group(1)) | |
102 | ||
103 | if not require_network: | |
104 | return host_spec | |
105 | ||
9f95a23c TL |
106 | networks = list() # type: List[str] |
107 | network = host_spec.network | |
108 | # in case we have [v2:1.2.3.4:3000,v1:1.2.3.4:6478] | |
109 | if ',' in network: | |
110 | networks = [x for x in network.split(',')] | |
111 | else: | |
1911f103 TL |
112 | if network != '': |
113 | networks.append(network) | |
114 | ||
9f95a23c TL |
115 | for network in networks: |
116 | # only if we have versioned network configs | |
117 | if network.startswith('v') or network.startswith('[v'): | |
f91f0fd5 TL |
118 | # if this is ipv6 we can't just simply split on ':' so do |
119 | # a split once and rsplit once to leave us with just ipv6 addr | |
120 | network = network.split(':', 1)[1] | |
121 | network = network.rsplit(':', 1)[0] | |
9f95a23c TL |
122 | try: |
123 | # if subnets are defined, also verify the validity | |
124 | if '/' in network: | |
f67539c2 | 125 | ip_network(network) |
9f95a23c | 126 | else: |
f91f0fd5 | 127 | ip_address(unwrap_ipv6(network)) |
9f95a23c TL |
128 | except ValueError as e: |
129 | # logging? | |
130 | raise e | |
131 | host_spec.validate() | |
132 | return host_spec | |
133 | ||
f67539c2 | 134 | def validate(self) -> None: |
9f95a23c TL |
135 | assert_valid_host(self.hostname) |
136 | ||
137 | ||
138 | class PlacementSpec(object): | |
139 | """ | |
140 | For APIs that need to specify a host subset | |
141 | """ | |
142 | ||
143 | def __init__(self, | |
144 | label=None, # type: Optional[str] | |
f67539c2 | 145 | hosts=None, # type: Union[List[str],List[HostPlacementSpec], None] |
9f95a23c | 146 | count=None, # type: Optional[int] |
f67539c2 TL |
147 | count_per_host=None, # type: Optional[int] |
148 | host_pattern=None, # type: Optional[str] | |
9f95a23c TL |
149 | ): |
150 | # type: (...) -> None | |
151 | self.label = label | |
152 | self.hosts = [] # type: List[HostPlacementSpec] | |
153 | ||
154 | if hosts: | |
f6b5b4d7 | 155 | self.set_hosts(hosts) |
9f95a23c TL |
156 | |
157 | self.count = count # type: Optional[int] | |
f67539c2 | 158 | self.count_per_host = count_per_host # type: Optional[int] |
9f95a23c TL |
159 | |
160 | #: fnmatch patterns to select hosts. Can also be a single host. | |
161 | self.host_pattern = host_pattern # type: Optional[str] | |
162 | ||
163 | self.validate() | |
164 | ||
f67539c2 TL |
165 | def is_empty(self) -> bool: |
166 | return ( | |
167 | self.label is None | |
168 | and not self.hosts | |
169 | and not self.host_pattern | |
170 | and self.count is None | |
171 | and self.count_per_host is None | |
172 | ) | |
9f95a23c | 173 | |
f67539c2 | 174 | def __eq__(self, other: Any) -> bool: |
f6b5b4d7 TL |
175 | if isinstance(other, PlacementSpec): |
176 | return self.label == other.label \ | |
177 | and self.hosts == other.hosts \ | |
178 | and self.count == other.count \ | |
f67539c2 TL |
179 | and self.host_pattern == other.host_pattern \ |
180 | and self.count_per_host == other.count_per_host | |
f6b5b4d7 TL |
181 | return NotImplemented |
182 | ||
f67539c2 | 183 | def set_hosts(self, hosts: Union[List[str], List[HostPlacementSpec]]) -> None: |
9f95a23c TL |
184 | # To backpopulate the .hosts attribute when using labels or count |
185 | # in the orchestrator backend. | |
f6b5b4d7 TL |
186 | if all([isinstance(host, HostPlacementSpec) for host in hosts]): |
187 | self.hosts = hosts # type: ignore | |
188 | else: | |
189 | self.hosts = [HostPlacementSpec.parse(x, require_network=False) # type: ignore | |
190 | for x in hosts if x] | |
9f95a23c | 191 | |
f91f0fd5 | 192 | # deprecated |
e306af50 | 193 | def filter_matching_hosts(self, _get_hosts_func: Callable) -> List[str]: |
f6b5b4d7 TL |
194 | return self.filter_matching_hostspecs(_get_hosts_func(as_hostspec=True)) |
195 | ||
f91f0fd5 | 196 | def filter_matching_hostspecs(self, hostspecs: Iterable[HostSpec]) -> List[str]: |
e306af50 | 197 | if self.hosts: |
f6b5b4d7 | 198 | all_hosts = [hs.hostname for hs in hostspecs] |
e306af50 | 199 | return [h.hostname for h in self.hosts if h.hostname in all_hosts] |
f67539c2 | 200 | if self.label: |
f6b5b4d7 | 201 | return [hs.hostname for hs in hostspecs if self.label in hs.labels] |
f67539c2 TL |
202 | all_hosts = [hs.hostname for hs in hostspecs] |
203 | if self.host_pattern: | |
f6b5b4d7 | 204 | return fnmatch.filter(all_hosts, self.host_pattern) |
f67539c2 | 205 | return all_hosts |
e306af50 | 206 | |
f67539c2 | 207 | def get_target_count(self, hostspecs: Iterable[HostSpec]) -> int: |
e306af50 TL |
208 | if self.count: |
209 | return self.count | |
f67539c2 | 210 | return len(self.filter_matching_hostspecs(hostspecs)) * (self.count_per_host or 1) |
9f95a23c | 211 | |
f67539c2 | 212 | def pretty_str(self) -> str: |
f91f0fd5 TL |
213 | """ |
214 | >>> #doctest: +SKIP | |
215 | ... ps = PlacementSpec(...) # For all placement specs: | |
216 | ... PlacementSpec.from_string(ps.pretty_str()) == ps | |
217 | """ | |
9f95a23c | 218 | kv = [] |
f91f0fd5 TL |
219 | if self.hosts: |
220 | kv.append(';'.join([str(h) for h in self.hosts])) | |
9f95a23c TL |
221 | if self.count: |
222 | kv.append('count:%d' % self.count) | |
f67539c2 TL |
223 | if self.count_per_host: |
224 | kv.append('count-per-host:%d' % self.count_per_host) | |
9f95a23c TL |
225 | if self.label: |
226 | kv.append('label:%s' % self.label) | |
9f95a23c TL |
227 | if self.host_pattern: |
228 | kv.append(self.host_pattern) | |
f91f0fd5 | 229 | return ';'.join(kv) |
9f95a23c | 230 | |
f67539c2 | 231 | def __repr__(self) -> str: |
9f95a23c TL |
232 | kv = [] |
233 | if self.count: | |
234 | kv.append('count=%d' % self.count) | |
f67539c2 TL |
235 | if self.count_per_host: |
236 | kv.append('count_per_host=%d' % self.count_per_host) | |
9f95a23c TL |
237 | if self.label: |
238 | kv.append('label=%s' % repr(self.label)) | |
239 | if self.hosts: | |
240 | kv.append('hosts={!r}'.format(self.hosts)) | |
241 | if self.host_pattern: | |
242 | kv.append('host_pattern={!r}'.format(self.host_pattern)) | |
243 | return "PlacementSpec(%s)" % ', '.join(kv) | |
244 | ||
245 | @classmethod | |
1911f103 | 246 | @handle_type_error |
f67539c2 | 247 | def from_json(cls, data: dict) -> 'PlacementSpec': |
1911f103 TL |
248 | c = data.copy() |
249 | hosts = c.get('hosts', []) | |
9f95a23c | 250 | if hosts: |
1911f103 TL |
251 | c['hosts'] = [] |
252 | for host in hosts: | |
f91f0fd5 | 253 | c['hosts'].append(HostPlacementSpec.from_json(host)) |
1911f103 | 254 | _cls = cls(**c) |
9f95a23c TL |
255 | _cls.validate() |
256 | return _cls | |
257 | ||
f67539c2 TL |
258 | def to_json(self) -> dict: |
259 | r: Dict[str, Any] = {} | |
9f95a23c TL |
260 | if self.label: |
261 | r['label'] = self.label | |
262 | if self.hosts: | |
263 | r['hosts'] = [host.to_json() for host in self.hosts] | |
264 | if self.count: | |
265 | r['count'] = self.count | |
f67539c2 TL |
266 | if self.count_per_host: |
267 | r['count_per_host'] = self.count_per_host | |
9f95a23c TL |
268 | if self.host_pattern: |
269 | r['host_pattern'] = self.host_pattern | |
270 | return r | |
271 | ||
f67539c2 | 272 | def validate(self) -> None: |
9f95a23c TL |
273 | if self.hosts and self.label: |
274 | # TODO: a less generic Exception | |
f67539c2 | 275 | raise SpecValidationError('Host and label are mutually exclusive') |
9f95a23c | 276 | if self.count is not None and self.count <= 0: |
f67539c2 TL |
277 | raise SpecValidationError("num/count must be > 1") |
278 | if self.count_per_host is not None and self.count_per_host < 1: | |
279 | raise SpecValidationError("count-per-host must be >= 1") | |
280 | if self.count_per_host is not None and not ( | |
281 | self.label | |
282 | or self.hosts | |
283 | or self.host_pattern | |
284 | ): | |
285 | raise SpecValidationError( | |
286 | "count-per-host must be combined with label or hosts or host_pattern" | |
287 | ) | |
288 | if self.count is not None and self.count_per_host is not None: | |
289 | raise SpecValidationError("cannot combine count and count-per-host") | |
290 | if ( | |
291 | self.count_per_host is not None | |
292 | and self.hosts | |
293 | and any([hs.network or hs.name for hs in self.hosts]) | |
294 | ): | |
295 | raise SpecValidationError( | |
296 | "count-per-host cannot be combined explicit placement with names or networks" | |
297 | ) | |
9f95a23c | 298 | if self.host_pattern and self.hosts: |
f67539c2 | 299 | raise SpecValidationError('cannot combine host patterns and hosts') |
9f95a23c TL |
300 | for h in self.hosts: |
301 | h.validate() | |
302 | ||
303 | @classmethod | |
304 | def from_string(cls, arg): | |
305 | # type: (Optional[str]) -> PlacementSpec | |
306 | """ | |
307 | A single integer is parsed as a count: | |
308 | >>> PlacementSpec.from_string('3') | |
309 | PlacementSpec(count=3) | |
310 | ||
311 | A list of names is parsed as host specifications: | |
312 | >>> PlacementSpec.from_string('host1 host2') | |
313 | PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacemen\ | |
314 | tSpec(hostname='host2', network='', name='')]) | |
315 | ||
316 | You can also prefix the hosts with a count as follows: | |
317 | >>> PlacementSpec.from_string('2 host1 host2') | |
318 | PlacementSpec(count=2, hosts=[HostPlacementSpec(hostname='host1', network='', name=''), Hos\ | |
319 | tPlacementSpec(hostname='host2', network='', name='')]) | |
320 | ||
321 | You can spefify labels using `label:<label>` | |
322 | >>> PlacementSpec.from_string('label:mon') | |
323 | PlacementSpec(label='mon') | |
324 | ||
325 | Labels als support a count: | |
326 | >>> PlacementSpec.from_string('3 label:mon') | |
327 | PlacementSpec(count=3, label='mon') | |
328 | ||
329 | fnmatch is also supported: | |
330 | >>> PlacementSpec.from_string('data[1-3]') | |
331 | PlacementSpec(host_pattern='data[1-3]') | |
332 | ||
333 | >>> PlacementSpec.from_string(None) | |
334 | PlacementSpec() | |
335 | """ | |
336 | if arg is None or not arg: | |
337 | strings = [] | |
338 | elif isinstance(arg, str): | |
339 | if ' ' in arg: | |
340 | strings = arg.split(' ') | |
341 | elif ';' in arg: | |
342 | strings = arg.split(';') | |
343 | elif ',' in arg and '[' not in arg: | |
344 | # FIXME: this isn't quite right. we want to avoid breaking | |
345 | # a list of mons with addrvecs... so we're basically allowing | |
346 | # , most of the time, except when addrvecs are used. maybe | |
347 | # ok? | |
348 | strings = arg.split(',') | |
349 | else: | |
350 | strings = [arg] | |
351 | else: | |
f67539c2 | 352 | raise SpecValidationError('invalid placement %s' % arg) |
9f95a23c TL |
353 | |
354 | count = None | |
f67539c2 | 355 | count_per_host = None |
9f95a23c TL |
356 | if strings: |
357 | try: | |
358 | count = int(strings[0]) | |
359 | strings = strings[1:] | |
360 | except ValueError: | |
361 | pass | |
362 | for s in strings: | |
363 | if s.startswith('count:'): | |
364 | try: | |
f67539c2 TL |
365 | count = int(s[len('count:'):]) |
366 | strings.remove(s) | |
367 | break | |
368 | except ValueError: | |
369 | pass | |
370 | for s in strings: | |
371 | if s.startswith('count-per-host:'): | |
372 | try: | |
373 | count_per_host = int(s[len('count-per-host:'):]) | |
9f95a23c TL |
374 | strings.remove(s) |
375 | break | |
376 | except ValueError: | |
377 | pass | |
378 | ||
379 | advanced_hostspecs = [h for h in strings if | |
380 | (':' in h or '=' in h or not any(c in '[]?*:=' for c in h)) and | |
381 | 'label:' not in h] | |
382 | for a_h in advanced_hostspecs: | |
383 | strings.remove(a_h) | |
384 | ||
385 | labels = [x for x in strings if 'label:' in x] | |
386 | if len(labels) > 1: | |
f67539c2 | 387 | raise SpecValidationError('more than one label provided: {}'.format(labels)) |
9f95a23c TL |
388 | for l in labels: |
389 | strings.remove(l) | |
390 | label = labels[0][6:] if labels else None | |
391 | ||
392 | host_patterns = strings | |
393 | if len(host_patterns) > 1: | |
f67539c2 | 394 | raise SpecValidationError( |
9f95a23c TL |
395 | 'more than one host pattern provided: {}'.format(host_patterns)) |
396 | ||
397 | ps = PlacementSpec(count=count, | |
f67539c2 | 398 | count_per_host=count_per_host, |
9f95a23c TL |
399 | hosts=advanced_hostspecs, |
400 | label=label, | |
401 | host_pattern=host_patterns[0] if host_patterns else None) | |
402 | return ps | |
403 | ||
404 | ||
405 | class ServiceSpec(object): | |
406 | """ | |
407 | Details of service creation. | |
408 | ||
409 | Request to the orchestrator for a cluster of daemons | |
410 | such as MDS, RGW, iscsi gateway, MONs, MGRs, Prometheus | |
411 | ||
412 | This structure is supposed to be enough information to | |
413 | start the services. | |
9f95a23c | 414 | """ |
1911f103 | 415 | KNOWN_SERVICE_TYPES = 'alertmanager crash grafana iscsi mds mgr mon nfs ' \ |
f91f0fd5 | 416 | 'node-exporter osd prometheus rbd-mirror rgw ' \ |
f67539c2 TL |
417 | 'container cephadm-exporter ingress cephfs-mirror'.split() |
418 | REQUIRES_SERVICE_ID = 'iscsi mds nfs osd rgw container ingress '.split() | |
419 | MANAGED_CONFIG_OPTIONS = [ | |
420 | 'mds_join_fs', | |
421 | ] | |
9f95a23c | 422 | |
1911f103 | 423 | @classmethod |
f67539c2 | 424 | def _cls(cls: Type[ServiceSpecT], service_type: str) -> Type[ServiceSpecT]: |
1911f103 TL |
425 | from ceph.deployment.drive_group import DriveGroupSpec |
426 | ||
427 | ret = { | |
428 | 'rgw': RGWSpec, | |
429 | 'nfs': NFSServiceSpec, | |
430 | 'osd': DriveGroupSpec, | |
431 | 'iscsi': IscsiServiceSpec, | |
f91f0fd5 | 432 | 'alertmanager': AlertManagerSpec, |
f67539c2 | 433 | 'ingress': IngressSpec, |
f91f0fd5 | 434 | 'container': CustomContainerSpec, |
b3b6e05e TL |
435 | 'grafana': MonitoringSpec, |
436 | 'node-exporter': MonitoringSpec, | |
437 | 'prometheus': MonitoringSpec, | |
1911f103 TL |
438 | }.get(service_type, cls) |
439 | if ret == ServiceSpec and not service_type: | |
f67539c2 | 440 | raise SpecValidationError('Spec needs a "service_type" key.') |
1911f103 TL |
441 | return ret |
442 | ||
f67539c2 | 443 | def __new__(cls: Type[ServiceSpecT], *args: Any, **kwargs: Any) -> ServiceSpecT: |
1911f103 TL |
444 | """ |
445 | Some Python foo to make sure, we don't have an object | |
446 | like `ServiceSpec('rgw')` of type `ServiceSpec`. Now we have: | |
447 | ||
448 | >>> type(ServiceSpec('rgw')) == type(RGWSpec('rgw')) | |
449 | True | |
450 | ||
451 | """ | |
452 | if cls != ServiceSpec: | |
453 | return object.__new__(cls) | |
454 | service_type = kwargs.get('service_type', args[0] if args else None) | |
f67539c2 | 455 | sub_cls: Any = cls._cls(service_type) |
1911f103 TL |
456 | return object.__new__(sub_cls) |
457 | ||
9f95a23c | 458 | def __init__(self, |
e306af50 TL |
459 | service_type: str, |
460 | service_id: Optional[str] = None, | |
461 | placement: Optional[PlacementSpec] = None, | |
462 | count: Optional[int] = None, | |
f67539c2 | 463 | config: Optional[Dict[str, str]] = None, |
e306af50 | 464 | unmanaged: bool = False, |
f6b5b4d7 | 465 | preview_only: bool = False, |
f67539c2 | 466 | networks: Optional[List[str]] = None, |
9f95a23c | 467 | ): |
a4b75251 TL |
468 | |
469 | #: See :ref:`orchestrator-cli-placement-spec`. | |
9f95a23c TL |
470 | self.placement = PlacementSpec() if placement is None else placement # type: PlacementSpec |
471 | ||
472 | assert service_type in ServiceSpec.KNOWN_SERVICE_TYPES, service_type | |
a4b75251 TL |
473 | #: The type of the service. Needs to be either a Ceph |
474 | #: service (``mon``, ``crash``, ``mds``, ``mgr``, ``osd`` or | |
475 | #: ``rbd-mirror``), a gateway (``nfs`` or ``rgw``), part of the | |
476 | #: monitoring stack (``alertmanager``, ``grafana``, ``node-exporter`` or | |
477 | #: ``prometheus``) or (``container``) for custom containers. | |
9f95a23c | 478 | self.service_type = service_type |
a4b75251 TL |
479 | |
480 | #: The name of the service. Required for ``iscsi``, ``mds``, ``nfs``, ``osd``, ``rgw``, | |
481 | #: ``container``, ``ingress`` | |
f6b5b4d7 | 482 | self.service_id = None |
a4b75251 | 483 | |
f6b5b4d7 TL |
484 | if self.service_type in self.REQUIRES_SERVICE_ID: |
485 | self.service_id = service_id | |
a4b75251 TL |
486 | |
487 | #: If set to ``true``, the orchestrator will not deploy nor remove | |
488 | #: any daemon associated with this service. Placement and all other properties | |
489 | #: will be ignored. This is useful, if you do not want this service to be | |
490 | #: managed temporarily. For cephadm, See :ref:`cephadm-spec-unmanaged` | |
9f95a23c | 491 | self.unmanaged = unmanaged |
f6b5b4d7 | 492 | self.preview_only = preview_only |
a4b75251 TL |
493 | |
494 | #: A list of network identities instructing the daemons to only bind | |
495 | #: on the particular networks in that list. In case the cluster is distributed | |
496 | #: across multiple networks, you can add multiple networks. See | |
497 | #: :ref:`cephadm-monitoring-networks-ports`, | |
498 | #: :ref:`cephadm-rgw-networks` and :ref:`cephadm-mgr-networks`. | |
f67539c2 TL |
499 | self.networks: List[str] = networks or [] |
500 | ||
501 | self.config: Optional[Dict[str, str]] = None | |
502 | if config: | |
503 | self.config = {k.replace(' ', '_'): v for k, v in config.items()} | |
9f95a23c TL |
504 | |
505 | @classmethod | |
1911f103 | 506 | @handle_type_error |
f67539c2 | 507 | def from_json(cls: Type[ServiceSpecT], json_spec: Dict) -> ServiceSpecT: |
9f95a23c TL |
508 | """ |
509 | Initialize 'ServiceSpec' object data from a json structure | |
f6b5b4d7 TL |
510 | |
511 | There are two valid styles for service specs: | |
512 | ||
513 | the "old" style: | |
514 | ||
515 | .. code:: yaml | |
516 | ||
517 | service_type: nfs | |
518 | service_id: foo | |
519 | pool: mypool | |
520 | namespace: myns | |
521 | ||
522 | and the "new" style: | |
523 | ||
524 | .. code:: yaml | |
525 | ||
526 | service_type: nfs | |
527 | service_id: foo | |
f67539c2 TL |
528 | config: |
529 | some_option: the_value | |
530 | networks: [10.10.0.0/16] | |
f6b5b4d7 TL |
531 | spec: |
532 | pool: mypool | |
533 | namespace: myns | |
534 | ||
535 | In https://tracker.ceph.com/issues/45321 we decided that we'd like to | |
536 | prefer the new style as it is more readable and provides a better | |
537 | understanding of what fields are special for a give service type. | |
538 | ||
539 | Note, we'll need to stay compatible with both versions for the | |
540 | the next two major releases (octoups, pacific). | |
541 | ||
9f95a23c | 542 | :param json_spec: A valid dict with ServiceSpec |
a4b75251 TL |
543 | |
544 | :meta private: | |
9f95a23c | 545 | """ |
9f95a23c | 546 | |
f67539c2 TL |
547 | if not isinstance(json_spec, dict): |
548 | raise SpecValidationError( | |
549 | f'Service Spec is not an (JSON or YAML) object. got "{str(json_spec)}"') | |
550 | ||
551 | json_spec = cls.normalize_json(json_spec) | |
552 | ||
1911f103 | 553 | c = json_spec.copy() |
9f95a23c | 554 | |
1911f103 TL |
555 | # kludge to make `from_json` compatible to `Orchestrator.describe_service` |
556 | # Open question: Remove `service_id` form to_json? | |
557 | if c.get('service_name', ''): | |
558 | service_type_id = c['service_name'].split('.', 1) | |
559 | ||
560 | if not c.get('service_type', ''): | |
561 | c['service_type'] = service_type_id[0] | |
562 | if not c.get('service_id', '') and len(service_type_id) > 1: | |
563 | c['service_id'] = service_type_id[1] | |
564 | del c['service_name'] | |
565 | ||
566 | service_type = c.get('service_type', '') | |
567 | _cls = cls._cls(service_type) | |
568 | ||
569 | if 'status' in c: | |
570 | del c['status'] # kludge to make us compatible to `ServiceDescription.to_json()` | |
9f95a23c | 571 | |
1911f103 | 572 | return _cls._from_json_impl(c) # type: ignore |
9f95a23c | 573 | |
f67539c2 TL |
574 | @staticmethod |
575 | def normalize_json(json_spec: dict) -> dict: | |
576 | networks = json_spec.get('networks') | |
577 | if networks is None: | |
578 | return json_spec | |
579 | if isinstance(networks, list): | |
580 | return json_spec | |
581 | if not isinstance(networks, str): | |
582 | raise SpecValidationError(f'Networks ({networks}) must be a string or list of strings') | |
583 | json_spec['networks'] = [networks] | |
584 | return json_spec | |
585 | ||
9f95a23c | 586 | @classmethod |
f67539c2 TL |
587 | def _from_json_impl(cls: Type[ServiceSpecT], json_spec: dict) -> ServiceSpecT: |
588 | args = {} # type: Dict[str, Any] | |
9f95a23c TL |
589 | for k, v in json_spec.items(): |
590 | if k == 'placement': | |
591 | v = PlacementSpec.from_json(v) | |
592 | if k == 'spec': | |
593 | args.update(v) | |
594 | continue | |
595 | args.update({k: v}) | |
1911f103 TL |
596 | _cls = cls(**args) |
597 | _cls.validate() | |
598 | return _cls | |
9f95a23c | 599 | |
f67539c2 | 600 | def service_name(self) -> str: |
9f95a23c TL |
601 | n = self.service_type |
602 | if self.service_id: | |
603 | n += '.' + self.service_id | |
604 | return n | |
605 | ||
f67539c2 TL |
606 | def get_port_start(self) -> List[int]: |
607 | # If defined, we will allocate and number ports starting at this | |
608 | # point. | |
609 | return [] | |
610 | ||
611 | def get_virtual_ip(self) -> Optional[str]: | |
612 | return None | |
613 | ||
9f95a23c | 614 | def to_json(self): |
f6b5b4d7 TL |
615 | # type: () -> OrderedDict[str, Any] |
616 | ret: OrderedDict[str, Any] = OrderedDict() | |
617 | ret['service_type'] = self.service_type | |
618 | if self.service_id: | |
619 | ret['service_id'] = self.service_id | |
620 | ret['service_name'] = self.service_name() | |
621 | ret['placement'] = self.placement.to_json() | |
622 | if self.unmanaged: | |
623 | ret['unmanaged'] = self.unmanaged | |
f67539c2 TL |
624 | if self.networks: |
625 | ret['networks'] = self.networks | |
f6b5b4d7 | 626 | |
9f95a23c | 627 | c = {} |
f6b5b4d7 TL |
628 | for key, val in sorted(self.__dict__.items(), key=lambda tpl: tpl[0]): |
629 | if key in ret: | |
630 | continue | |
9f95a23c TL |
631 | if hasattr(val, 'to_json'): |
632 | val = val.to_json() | |
633 | if val: | |
634 | c[key] = val | |
f6b5b4d7 TL |
635 | if c: |
636 | ret['spec'] = c | |
637 | return ret | |
9f95a23c | 638 | |
f67539c2 | 639 | def validate(self) -> None: |
9f95a23c | 640 | if not self.service_type: |
f67539c2 | 641 | raise SpecValidationError('Cannot add Service: type required') |
9f95a23c | 642 | |
f6b5b4d7 TL |
643 | if self.service_type in self.REQUIRES_SERVICE_ID: |
644 | if not self.service_id: | |
f67539c2 TL |
645 | raise SpecValidationError('Cannot add Service: id required') |
646 | if not re.match('^[a-zA-Z0-9_.-]+$', self.service_id): | |
647 | raise SpecValidationError('Service id contains invalid characters, ' | |
648 | 'only [a-zA-Z0-9_.-] allowed') | |
f6b5b4d7 | 649 | elif self.service_id: |
f67539c2 | 650 | raise SpecValidationError( |
f6b5b4d7 TL |
651 | f'Service of type \'{self.service_type}\' should not contain a service id') |
652 | ||
9f95a23c TL |
653 | if self.placement is not None: |
654 | self.placement.validate() | |
f67539c2 TL |
655 | if self.config: |
656 | for k, v in self.config.items(): | |
657 | if k in self.MANAGED_CONFIG_OPTIONS: | |
658 | raise SpecValidationError( | |
659 | f'Cannot set config option {k} in spec: it is managed by cephadm' | |
660 | ) | |
661 | for network in self.networks or []: | |
662 | try: | |
663 | ip_network(network) | |
664 | except ValueError as e: | |
665 | raise SpecValidationError( | |
666 | f'Cannot parse network {network}: {e}' | |
667 | ) | |
9f95a23c | 668 | |
f67539c2 | 669 | def __repr__(self) -> str: |
9f95a23c TL |
670 | return "{}({!r})".format(self.__class__.__name__, self.__dict__) |
671 | ||
f67539c2 | 672 | def __eq__(self, other: Any) -> bool: |
f6b5b4d7 TL |
673 | return (self.__class__ == other.__class__ |
674 | and | |
675 | self.__dict__ == other.__dict__) | |
676 | ||
f67539c2 | 677 | def one_line_str(self) -> str: |
9f95a23c TL |
678 | return '<{} for service_name={}>'.format(self.__class__.__name__, self.service_name()) |
679 | ||
f6b5b4d7 | 680 | @staticmethod |
f67539c2 | 681 | def yaml_representer(dumper: 'yaml.SafeDumper', data: 'ServiceSpec') -> Any: |
522d829b | 682 | return dumper.represent_dict(cast(Mapping, data.to_json().items())) |
f6b5b4d7 | 683 | |
9f95a23c | 684 | |
f6b5b4d7 | 685 | yaml.add_representer(ServiceSpec, ServiceSpec.yaml_representer) |
9f95a23c TL |
686 | |
687 | ||
688 | class NFSServiceSpec(ServiceSpec): | |
e306af50 TL |
689 | def __init__(self, |
690 | service_type: str = 'nfs', | |
691 | service_id: Optional[str] = None, | |
e306af50 TL |
692 | placement: Optional[PlacementSpec] = None, |
693 | unmanaged: bool = False, | |
f67539c2 TL |
694 | preview_only: bool = False, |
695 | config: Optional[Dict[str, str]] = None, | |
696 | networks: Optional[List[str]] = None, | |
b3b6e05e | 697 | port: Optional[int] = None, |
e306af50 | 698 | ): |
9f95a23c TL |
699 | assert service_type == 'nfs' |
700 | super(NFSServiceSpec, self).__init__( | |
701 | 'nfs', service_id=service_id, | |
f67539c2 TL |
702 | placement=placement, unmanaged=unmanaged, preview_only=preview_only, |
703 | config=config, networks=networks) | |
9f95a23c | 704 | |
b3b6e05e TL |
705 | self.port = port |
706 | ||
707 | def get_port_start(self) -> List[int]: | |
708 | if self.port: | |
709 | return [self.port] | |
710 | return [] | |
9f95a23c | 711 | |
1911f103 TL |
712 | def rados_config_name(self): |
713 | # type: () -> str | |
714 | return 'conf-' + self.service_name() | |
715 | ||
9f95a23c | 716 | |
f6b5b4d7 TL |
717 | yaml.add_representer(NFSServiceSpec, ServiceSpec.yaml_representer) |
718 | ||
719 | ||
9f95a23c TL |
720 | class RGWSpec(ServiceSpec): |
721 | """ | |
722 | Settings to configure a (multisite) Ceph RGW | |
723 | ||
a4b75251 TL |
724 | .. code-block:: yaml |
725 | ||
726 | service_type: rgw | |
727 | service_id: myrealm.myzone | |
728 | spec: | |
729 | rgw_realm: myrealm | |
730 | rgw_zone: myzone | |
731 | ssl: true | |
732 | rgw_frontend_port: 1234 | |
733 | rgw_frontend_type: beast | |
734 | rgw_frontend_ssl_certificate: ... | |
735 | ||
736 | See also: :ref:`orchestrator-cli-service-spec` | |
9f95a23c | 737 | """ |
a4b75251 | 738 | |
f67539c2 TL |
739 | MANAGED_CONFIG_OPTIONS = ServiceSpec.MANAGED_CONFIG_OPTIONS + [ |
740 | 'rgw_zone', | |
741 | 'rgw_realm', | |
742 | 'rgw_frontends', | |
743 | ] | |
744 | ||
9f95a23c | 745 | def __init__(self, |
e306af50 TL |
746 | service_type: str = 'rgw', |
747 | service_id: Optional[str] = None, | |
748 | placement: Optional[PlacementSpec] = None, | |
749 | rgw_realm: Optional[str] = None, | |
750 | rgw_zone: Optional[str] = None, | |
e306af50 TL |
751 | rgw_frontend_port: Optional[int] = None, |
752 | rgw_frontend_ssl_certificate: Optional[List[str]] = None, | |
f67539c2 | 753 | rgw_frontend_type: Optional[str] = None, |
e306af50 TL |
754 | unmanaged: bool = False, |
755 | ssl: bool = False, | |
f6b5b4d7 | 756 | preview_only: bool = False, |
f67539c2 TL |
757 | config: Optional[Dict[str, str]] = None, |
758 | networks: Optional[List[str]] = None, | |
759 | subcluster: Optional[str] = None, # legacy, only for from_json on upgrade | |
9f95a23c | 760 | ): |
1911f103 | 761 | assert service_type == 'rgw', service_type |
f67539c2 TL |
762 | |
763 | # for backward compatibility with octopus spec files, | |
764 | if not service_id and (rgw_realm and rgw_zone): | |
765 | service_id = rgw_realm + '.' + rgw_zone | |
766 | ||
9f95a23c TL |
767 | super(RGWSpec, self).__init__( |
768 | 'rgw', service_id=service_id, | |
f6b5b4d7 | 769 | placement=placement, unmanaged=unmanaged, |
f67539c2 | 770 | preview_only=preview_only, config=config, networks=networks) |
9f95a23c | 771 | |
a4b75251 TL |
772 | #: The RGW realm associated with this service. Needs to be manually created |
773 | self.rgw_realm: Optional[str] = rgw_realm | |
774 | #: The RGW zone associated with this service. Needs to be manually created | |
775 | self.rgw_zone: Optional[str] = rgw_zone | |
776 | #: Port of the RGW daemons | |
777 | self.rgw_frontend_port: Optional[int] = rgw_frontend_port | |
778 | #: List of SSL certificates | |
779 | self.rgw_frontend_ssl_certificate: Optional[List[str]] = rgw_frontend_ssl_certificate | |
780 | #: civetweb or beast (default: beast). See :ref:`rgw_frontends` | |
781 | self.rgw_frontend_type: Optional[str] = rgw_frontend_type | |
782 | #: enable SSL | |
9f95a23c TL |
783 | self.ssl = ssl |
784 | ||
f67539c2 TL |
785 | def get_port_start(self) -> List[int]: |
786 | return [self.get_port()] | |
787 | ||
788 | def get_port(self) -> int: | |
9f95a23c TL |
789 | if self.rgw_frontend_port: |
790 | return self.rgw_frontend_port | |
791 | if self.ssl: | |
792 | return 443 | |
793 | else: | |
794 | return 80 | |
1911f103 | 795 | |
f67539c2 | 796 | def validate(self) -> None: |
f6b5b4d7 TL |
797 | super(RGWSpec, self).validate() |
798 | ||
f67539c2 TL |
799 | if self.rgw_realm and not self.rgw_zone: |
800 | raise SpecValidationError( | |
801 | 'Cannot add RGW: Realm specified but no zone specified') | |
802 | if self.rgw_zone and not self.rgw_realm: | |
803 | raise SpecValidationError( | |
804 | 'Cannot add RGW: Zone specified but no realm specified') | |
f6b5b4d7 TL |
805 | |
806 | ||
807 | yaml.add_representer(RGWSpec, ServiceSpec.yaml_representer) | |
808 | ||
1911f103 TL |
809 | |
810 | class IscsiServiceSpec(ServiceSpec): | |
e306af50 TL |
811 | def __init__(self, |
812 | service_type: str = 'iscsi', | |
813 | service_id: Optional[str] = None, | |
814 | pool: Optional[str] = None, | |
815 | trusted_ip_list: Optional[str] = None, | |
816 | api_port: Optional[int] = None, | |
817 | api_user: Optional[str] = None, | |
818 | api_password: Optional[str] = None, | |
819 | api_secure: Optional[bool] = None, | |
820 | ssl_cert: Optional[str] = None, | |
821 | ssl_key: Optional[str] = None, | |
822 | placement: Optional[PlacementSpec] = None, | |
f6b5b4d7 | 823 | unmanaged: bool = False, |
f67539c2 TL |
824 | preview_only: bool = False, |
825 | config: Optional[Dict[str, str]] = None, | |
826 | networks: Optional[List[str]] = None, | |
e306af50 | 827 | ): |
1911f103 TL |
828 | assert service_type == 'iscsi' |
829 | super(IscsiServiceSpec, self).__init__('iscsi', service_id=service_id, | |
f6b5b4d7 | 830 | placement=placement, unmanaged=unmanaged, |
f67539c2 TL |
831 | preview_only=preview_only, |
832 | config=config, networks=networks) | |
1911f103 TL |
833 | |
834 | #: RADOS pool where ceph-iscsi config data is stored. | |
835 | self.pool = pool | |
a4b75251 | 836 | #: list of trusted IP addresses |
1911f103 | 837 | self.trusted_ip_list = trusted_ip_list |
a4b75251 | 838 | #: ``api_port`` as defined in the ``iscsi-gateway.cfg`` |
1911f103 | 839 | self.api_port = api_port |
a4b75251 | 840 | #: ``api_user`` as defined in the ``iscsi-gateway.cfg`` |
1911f103 | 841 | self.api_user = api_user |
a4b75251 | 842 | #: ``api_password`` as defined in the ``iscsi-gateway.cfg`` |
1911f103 | 843 | self.api_password = api_password |
a4b75251 | 844 | #: ``api_secure`` as defined in the ``iscsi-gateway.cfg`` |
1911f103 | 845 | self.api_secure = api_secure |
a4b75251 | 846 | #: SSL certificate |
1911f103 | 847 | self.ssl_cert = ssl_cert |
a4b75251 | 848 | #: SSL private key |
1911f103 TL |
849 | self.ssl_key = ssl_key |
850 | ||
e306af50 TL |
851 | if not self.api_secure and self.ssl_cert and self.ssl_key: |
852 | self.api_secure = True | |
853 | ||
f67539c2 | 854 | def validate(self) -> None: |
e306af50 | 855 | super(IscsiServiceSpec, self).validate() |
1911f103 TL |
856 | |
857 | if not self.pool: | |
f67539c2 | 858 | raise SpecValidationError( |
1911f103 | 859 | 'Cannot add ISCSI: No Pool specified') |
adb31ebb TL |
860 | |
861 | # Do not need to check for api_user and api_password as they | |
862 | # now default to 'admin' when setting up the gateway url. Older | |
863 | # iSCSI specs from before this change should be fine as they will | |
864 | # have been required to have an api_user and api_password set and | |
865 | # will be unaffected by the new default value. | |
f6b5b4d7 TL |
866 | |
867 | ||
868 | yaml.add_representer(IscsiServiceSpec, ServiceSpec.yaml_representer) | |
869 | ||
870 | ||
871 | class AlertManagerSpec(ServiceSpec): | |
872 | def __init__(self, | |
873 | service_type: str = 'alertmanager', | |
874 | service_id: Optional[str] = None, | |
875 | placement: Optional[PlacementSpec] = None, | |
876 | unmanaged: bool = False, | |
877 | preview_only: bool = False, | |
878 | user_data: Optional[Dict[str, Any]] = None, | |
f67539c2 TL |
879 | config: Optional[Dict[str, str]] = None, |
880 | networks: Optional[List[str]] = None, | |
b3b6e05e | 881 | port: Optional[int] = None, |
f6b5b4d7 TL |
882 | ): |
883 | assert service_type == 'alertmanager' | |
884 | super(AlertManagerSpec, self).__init__( | |
885 | 'alertmanager', service_id=service_id, | |
886 | placement=placement, unmanaged=unmanaged, | |
f67539c2 | 887 | preview_only=preview_only, config=config, networks=networks) |
f6b5b4d7 TL |
888 | |
889 | # Custom configuration. | |
890 | # | |
891 | # Example: | |
892 | # service_type: alertmanager | |
893 | # service_id: xyz | |
894 | # user_data: | |
895 | # default_webhook_urls: | |
896 | # - "https://foo" | |
897 | # - "https://bar" | |
898 | # | |
899 | # Documentation: | |
900 | # default_webhook_urls - A list of additional URL's that are | |
901 | # added to the default receivers' | |
902 | # <webhook_configs> configuration. | |
903 | self.user_data = user_data or {} | |
b3b6e05e TL |
904 | self.port = port |
905 | ||
906 | def get_port_start(self) -> List[int]: | |
907 | return [self.get_port(), 9094] | |
908 | ||
909 | def get_port(self) -> int: | |
910 | if self.port: | |
911 | return self.port | |
912 | else: | |
913 | return 9093 | |
914 | ||
915 | def validate(self) -> None: | |
916 | super(AlertManagerSpec, self).validate() | |
917 | ||
918 | if self.port == 9094: | |
919 | raise SpecValidationError( | |
920 | 'Port 9094 is reserved for AlertManager cluster listen address') | |
f6b5b4d7 TL |
921 | |
922 | ||
923 | yaml.add_representer(AlertManagerSpec, ServiceSpec.yaml_representer) | |
f91f0fd5 TL |
924 | |
925 | ||
f67539c2 TL |
926 | class IngressSpec(ServiceSpec): |
927 | def __init__(self, | |
928 | service_type: str = 'ingress', | |
929 | service_id: Optional[str] = None, | |
930 | config: Optional[Dict[str, str]] = None, | |
931 | networks: Optional[List[str]] = None, | |
932 | placement: Optional[PlacementSpec] = None, | |
933 | backend_service: Optional[str] = None, | |
934 | frontend_port: Optional[int] = None, | |
935 | ssl_cert: Optional[str] = None, | |
b3b6e05e | 936 | ssl_key: Optional[str] = None, |
f67539c2 TL |
937 | ssl_dh_param: Optional[str] = None, |
938 | ssl_ciphers: Optional[List[str]] = None, | |
939 | ssl_options: Optional[List[str]] = None, | |
940 | monitor_port: Optional[int] = None, | |
941 | monitor_user: Optional[str] = None, | |
942 | monitor_password: Optional[str] = None, | |
943 | enable_stats: Optional[bool] = None, | |
944 | keepalived_password: Optional[str] = None, | |
945 | virtual_ip: Optional[str] = None, | |
946 | virtual_interface_networks: Optional[List[str]] = [], | |
b3b6e05e TL |
947 | unmanaged: bool = False, |
948 | ssl: bool = False | |
f67539c2 TL |
949 | ): |
950 | assert service_type == 'ingress' | |
951 | super(IngressSpec, self).__init__( | |
952 | 'ingress', service_id=service_id, | |
953 | placement=placement, config=config, | |
954 | networks=networks | |
955 | ) | |
956 | self.backend_service = backend_service | |
957 | self.frontend_port = frontend_port | |
958 | self.ssl_cert = ssl_cert | |
b3b6e05e | 959 | self.ssl_key = ssl_key |
f67539c2 TL |
960 | self.ssl_dh_param = ssl_dh_param |
961 | self.ssl_ciphers = ssl_ciphers | |
962 | self.ssl_options = ssl_options | |
963 | self.monitor_port = monitor_port | |
964 | self.monitor_user = monitor_user | |
965 | self.monitor_password = monitor_password | |
966 | self.keepalived_password = keepalived_password | |
967 | self.virtual_ip = virtual_ip | |
968 | self.virtual_interface_networks = virtual_interface_networks or [] | |
b3b6e05e TL |
969 | self.unmanaged = unmanaged |
970 | self.ssl = ssl | |
f67539c2 TL |
971 | |
972 | def get_port_start(self) -> List[int]: | |
973 | return [cast(int, self.frontend_port), | |
974 | cast(int, self.monitor_port)] | |
975 | ||
976 | def get_virtual_ip(self) -> Optional[str]: | |
977 | return self.virtual_ip | |
978 | ||
979 | def validate(self) -> None: | |
980 | super(IngressSpec, self).validate() | |
981 | ||
982 | if not self.backend_service: | |
983 | raise SpecValidationError( | |
984 | 'Cannot add ingress: No backend_service specified') | |
985 | if not self.frontend_port: | |
986 | raise SpecValidationError( | |
987 | 'Cannot add ingress: No frontend_port specified') | |
988 | if not self.monitor_port: | |
989 | raise SpecValidationError( | |
990 | 'Cannot add ingress: No monitor_port specified') | |
991 | if not self.virtual_ip: | |
992 | raise SpecValidationError( | |
993 | 'Cannot add ingress: No virtual_ip provided') | |
994 | ||
995 | ||
b3b6e05e TL |
996 | yaml.add_representer(IngressSpec, ServiceSpec.yaml_representer) |
997 | ||
998 | ||
f91f0fd5 TL |
999 | class CustomContainerSpec(ServiceSpec): |
1000 | def __init__(self, | |
1001 | service_type: str = 'container', | |
f67539c2 TL |
1002 | service_id: Optional[str] = None, |
1003 | config: Optional[Dict[str, str]] = None, | |
1004 | networks: Optional[List[str]] = None, | |
f91f0fd5 TL |
1005 | placement: Optional[PlacementSpec] = None, |
1006 | unmanaged: bool = False, | |
1007 | preview_only: bool = False, | |
f67539c2 | 1008 | image: Optional[str] = None, |
f91f0fd5 TL |
1009 | entrypoint: Optional[str] = None, |
1010 | uid: Optional[int] = None, | |
1011 | gid: Optional[int] = None, | |
1012 | volume_mounts: Optional[Dict[str, str]] = {}, | |
1013 | args: Optional[List[str]] = [], | |
1014 | envs: Optional[List[str]] = [], | |
1015 | privileged: Optional[bool] = False, | |
1016 | bind_mounts: Optional[List[List[str]]] = None, | |
1017 | ports: Optional[List[int]] = [], | |
1018 | dirs: Optional[List[str]] = [], | |
1019 | files: Optional[Dict[str, Any]] = {}, | |
1020 | ): | |
1021 | assert service_type == 'container' | |
1022 | assert service_id is not None | |
1023 | assert image is not None | |
1024 | ||
1025 | super(CustomContainerSpec, self).__init__( | |
1026 | service_type, service_id, | |
1027 | placement=placement, unmanaged=unmanaged, | |
f67539c2 TL |
1028 | preview_only=preview_only, config=config, |
1029 | networks=networks) | |
f91f0fd5 TL |
1030 | |
1031 | self.image = image | |
1032 | self.entrypoint = entrypoint | |
1033 | self.uid = uid | |
1034 | self.gid = gid | |
1035 | self.volume_mounts = volume_mounts | |
1036 | self.args = args | |
1037 | self.envs = envs | |
1038 | self.privileged = privileged | |
1039 | self.bind_mounts = bind_mounts | |
1040 | self.ports = ports | |
1041 | self.dirs = dirs | |
1042 | self.files = files | |
1043 | ||
1044 | def config_json(self) -> Dict[str, Any]: | |
1045 | """ | |
1046 | Helper function to get the value of the `--config-json` cephadm | |
1047 | command line option. It will contain all specification properties | |
1048 | that haven't a `None` value. Such properties will get default | |
1049 | values in cephadm. | |
1050 | :return: Returns a dictionary containing all specification | |
1051 | properties. | |
1052 | """ | |
1053 | config_json = {} | |
1054 | for prop in ['image', 'entrypoint', 'uid', 'gid', 'args', | |
1055 | 'envs', 'volume_mounts', 'privileged', | |
1056 | 'bind_mounts', 'ports', 'dirs', 'files']: | |
1057 | value = getattr(self, prop) | |
1058 | if value is not None: | |
1059 | config_json[prop] = value | |
1060 | return config_json | |
1061 | ||
1062 | ||
1063 | yaml.add_representer(CustomContainerSpec, ServiceSpec.yaml_representer) | |
b3b6e05e TL |
1064 | |
1065 | ||
1066 | class MonitoringSpec(ServiceSpec): | |
1067 | def __init__(self, | |
1068 | service_type: str, | |
1069 | service_id: Optional[str] = None, | |
1070 | config: Optional[Dict[str, str]] = None, | |
1071 | networks: Optional[List[str]] = None, | |
1072 | placement: Optional[PlacementSpec] = None, | |
1073 | unmanaged: bool = False, | |
1074 | preview_only: bool = False, | |
1075 | port: Optional[int] = None, | |
1076 | ): | |
1077 | assert service_type in ['grafana', 'node-exporter', 'prometheus'] | |
1078 | ||
1079 | super(MonitoringSpec, self).__init__( | |
1080 | service_type, service_id, | |
1081 | placement=placement, unmanaged=unmanaged, | |
1082 | preview_only=preview_only, config=config, | |
1083 | networks=networks) | |
1084 | ||
1085 | self.service_type = service_type | |
1086 | self.port = port | |
1087 | ||
1088 | def get_port_start(self) -> List[int]: | |
1089 | return [self.get_port()] | |
1090 | ||
1091 | def get_port(self) -> int: | |
1092 | if self.port: | |
1093 | return self.port | |
1094 | else: | |
1095 | return {'prometheus': 9095, | |
1096 | 'node-exporter': 9100, | |
1097 | 'grafana': 3000}[self.service_type] |