]> git.proxmox.com Git - ceph.git/blame - ceph/src/python-common/ceph/deployment/service_spec.py
Import ceph 15.2.8
[ceph.git] / ceph / src / python-common / ceph / deployment / service_spec.py
CommitLineData
f6b5b4d7 1import errno
9f95a23c
TL
2import fnmatch
3import re
f6b5b4d7 4from collections import namedtuple, OrderedDict
1911f103 5from functools import wraps
f91f0fd5 6from typing import Optional, Dict, Any, List, Union, Callable, Iterable
9f95a23c
TL
7
8import six
f6b5b4d7
TL
9import yaml
10
11from ceph.deployment.hostspec import HostSpec
f91f0fd5 12from ceph.deployment.utils import unwrap_ipv6
9f95a23c
TL
13
14
15class ServiceSpecValidationError(Exception):
16 """
17 Defining an exception here is a bit problematic, cause you cannot properly catch it,
18 if it was raised in a different mgr module.
19 """
f6b5b4d7
TL
20 def __init__(self,
21 msg: str,
22 errno: int = -errno.EINVAL):
9f95a23c 23 super(ServiceSpecValidationError, self).__init__(msg)
f6b5b4d7 24 self.errno = errno
9f95a23c
TL
25
26
27def assert_valid_host(name):
28 p = re.compile('^[a-zA-Z0-9-]+$')
29 try:
30 assert len(name) <= 250, 'name is too long (max 250 chars)'
31 for part in name.split('.'):
32 assert len(part) > 0, '.-delimited name component must not be empty'
33 assert len(part) <= 63, '.-delimited name component must not be more than 63 chars'
34 assert p.match(part), 'name component must include only a-z, 0-9, and -'
35 except AssertionError as e:
36 raise ServiceSpecValidationError(e)
37
38
1911f103
TL
39def handle_type_error(method):
40 @wraps(method)
41 def inner(cls, *args, **kwargs):
42 try:
43 return method(cls, *args, **kwargs)
44 except (TypeError, AttributeError) as e:
45 error_msg = '{}: {}'.format(cls.__name__, e)
f6b5b4d7 46 raise ServiceSpecValidationError(error_msg)
1911f103
TL
47 return inner
48
49
9f95a23c
TL
50class HostPlacementSpec(namedtuple('HostPlacementSpec', ['hostname', 'network', 'name'])):
51 def __str__(self):
52 res = ''
53 res += self.hostname
54 if self.network:
55 res += ':' + self.network
56 if self.name:
57 res += '=' + self.name
58 return res
59
60 @classmethod
1911f103 61 @handle_type_error
9f95a23c 62 def from_json(cls, data):
f91f0fd5
TL
63 if isinstance(data, str):
64 return cls.parse(data)
9f95a23c
TL
65 return cls(**data)
66
f91f0fd5
TL
67 def to_json(self) -> str:
68 return str(self)
9f95a23c
TL
69
70 @classmethod
71 def parse(cls, host, require_network=True):
72 # type: (str, bool) -> HostPlacementSpec
73 """
74 Split host into host, network, and (optional) daemon name parts. The network
75 part can be an IP, CIDR, or ceph addrvec like '[v2:1.2.3.4:3300,v1:1.2.3.4:6789]'.
76 e.g.,
77 "myhost"
78 "myhost=name"
79 "myhost:1.2.3.4"
80 "myhost:1.2.3.4=name"
81 "myhost:1.2.3.0/24"
82 "myhost:1.2.3.0/24=name"
83 "myhost:[v2:1.2.3.4:3000]=name"
84 "myhost:[v2:1.2.3.4:3000,v1:1.2.3.4:6789]=name"
85 """
86 # Matches from start to : or = or until end of string
87 host_re = r'^(.*?)(:|=|$)'
88 # Matches from : to = or until end of string
89 ip_re = r':(.*?)(=|$)'
90 # Matches from = to end of string
91 name_re = r'=(.*?)$'
92
93 # assign defaults
94 host_spec = cls('', '', '')
95
96 match_host = re.search(host_re, host)
97 if match_host:
98 host_spec = host_spec._replace(hostname=match_host.group(1))
99
100 name_match = re.search(name_re, host)
101 if name_match:
102 host_spec = host_spec._replace(name=name_match.group(1))
103
104 ip_match = re.search(ip_re, host)
105 if ip_match:
106 host_spec = host_spec._replace(network=ip_match.group(1))
107
108 if not require_network:
109 return host_spec
110
111 from ipaddress import ip_network, ip_address
112 networks = list() # type: List[str]
113 network = host_spec.network
114 # in case we have [v2:1.2.3.4:3000,v1:1.2.3.4:6478]
115 if ',' in network:
116 networks = [x for x in network.split(',')]
117 else:
1911f103
TL
118 if network != '':
119 networks.append(network)
120
9f95a23c
TL
121 for network in networks:
122 # only if we have versioned network configs
123 if network.startswith('v') or network.startswith('[v'):
f91f0fd5
TL
124 # if this is ipv6 we can't just simply split on ':' so do
125 # a split once and rsplit once to leave us with just ipv6 addr
126 network = network.split(':', 1)[1]
127 network = network.rsplit(':', 1)[0]
9f95a23c
TL
128 try:
129 # if subnets are defined, also verify the validity
130 if '/' in network:
131 ip_network(six.text_type(network))
132 else:
f91f0fd5 133 ip_address(unwrap_ipv6(network))
9f95a23c
TL
134 except ValueError as e:
135 # logging?
136 raise e
137 host_spec.validate()
138 return host_spec
139
140 def validate(self):
141 assert_valid_host(self.hostname)
142
143
144class PlacementSpec(object):
145 """
146 For APIs that need to specify a host subset
147 """
148
149 def __init__(self,
150 label=None, # type: Optional[str]
151 hosts=None, # type: Union[List[str],List[HostPlacementSpec]]
152 count=None, # type: Optional[int]
153 host_pattern=None # type: Optional[str]
154 ):
155 # type: (...) -> None
156 self.label = label
157 self.hosts = [] # type: List[HostPlacementSpec]
158
159 if hosts:
f6b5b4d7 160 self.set_hosts(hosts)
9f95a23c
TL
161
162 self.count = count # type: Optional[int]
163
164 #: fnmatch patterns to select hosts. Can also be a single host.
165 self.host_pattern = host_pattern # type: Optional[str]
166
167 self.validate()
168
169 def is_empty(self):
170 return self.label is None and \
171 not self.hosts and \
172 not self.host_pattern and \
173 self.count is None
174
f6b5b4d7
TL
175 def __eq__(self, other):
176 if isinstance(other, PlacementSpec):
177 return self.label == other.label \
178 and self.hosts == other.hosts \
179 and self.count == other.count \
180 and self.host_pattern == other.host_pattern
181 return NotImplemented
182
9f95a23c
TL
183 def set_hosts(self, hosts):
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
TL
199 return [h.hostname for h in self.hosts if h.hostname in all_hosts]
200 elif self.label:
f6b5b4d7 201 return [hs.hostname for hs in hostspecs if self.label in hs.labels]
e306af50 202 elif self.host_pattern:
f6b5b4d7
TL
203 all_hosts = [hs.hostname for hs in hostspecs]
204 return fnmatch.filter(all_hosts, self.host_pattern)
e306af50
TL
205 else:
206 # This should be caught by the validation but needs to be here for
207 # get_host_selection_size
9f95a23c 208 return []
e306af50 209
f91f0fd5 210 def get_host_selection_size(self, hostspecs: Iterable[HostSpec]):
e306af50
TL
211 if self.count:
212 return self.count
f6b5b4d7 213 return len(self.filter_matching_hostspecs(hostspecs))
9f95a23c
TL
214
215 def pretty_str(self):
f91f0fd5
TL
216 """
217 >>> #doctest: +SKIP
218 ... ps = PlacementSpec(...) # For all placement specs:
219 ... PlacementSpec.from_string(ps.pretty_str()) == ps
220 """
9f95a23c 221 kv = []
f91f0fd5
TL
222 if self.hosts:
223 kv.append(';'.join([str(h) for h in self.hosts]))
9f95a23c
TL
224 if self.count:
225 kv.append('count:%d' % self.count)
226 if self.label:
227 kv.append('label:%s' % self.label)
9f95a23c
TL
228 if self.host_pattern:
229 kv.append(self.host_pattern)
f91f0fd5 230 return ';'.join(kv)
9f95a23c
TL
231
232 def __repr__(self):
233 kv = []
234 if self.count:
235 kv.append('count=%d' % self.count)
236 if self.label:
237 kv.append('label=%s' % repr(self.label))
238 if self.hosts:
239 kv.append('hosts={!r}'.format(self.hosts))
240 if self.host_pattern:
241 kv.append('host_pattern={!r}'.format(self.host_pattern))
242 return "PlacementSpec(%s)" % ', '.join(kv)
243
244 @classmethod
1911f103 245 @handle_type_error
9f95a23c 246 def from_json(cls, data):
1911f103
TL
247 c = data.copy()
248 hosts = c.get('hosts', [])
9f95a23c 249 if hosts:
1911f103
TL
250 c['hosts'] = []
251 for host in hosts:
f91f0fd5 252 c['hosts'].append(HostPlacementSpec.from_json(host))
1911f103 253 _cls = cls(**c)
9f95a23c
TL
254 _cls.validate()
255 return _cls
256
257 def to_json(self):
258 r = {}
259 if self.label:
260 r['label'] = self.label
261 if self.hosts:
262 r['hosts'] = [host.to_json() for host in self.hosts]
263 if self.count:
264 r['count'] = self.count
265 if self.host_pattern:
266 r['host_pattern'] = self.host_pattern
267 return r
268
269 def validate(self):
270 if self.hosts and self.label:
271 # TODO: a less generic Exception
272 raise ServiceSpecValidationError('Host and label are mutually exclusive')
273 if self.count is not None and self.count <= 0:
274 raise ServiceSpecValidationError("num/count must be > 1")
275 if self.host_pattern and self.hosts:
276 raise ServiceSpecValidationError('cannot combine host patterns and hosts')
277 for h in self.hosts:
278 h.validate()
279
280 @classmethod
281 def from_string(cls, arg):
282 # type: (Optional[str]) -> PlacementSpec
283 """
284 A single integer is parsed as a count:
285 >>> PlacementSpec.from_string('3')
286 PlacementSpec(count=3)
287
288 A list of names is parsed as host specifications:
289 >>> PlacementSpec.from_string('host1 host2')
290 PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacemen\
291tSpec(hostname='host2', network='', name='')])
292
293 You can also prefix the hosts with a count as follows:
294 >>> PlacementSpec.from_string('2 host1 host2')
295 PlacementSpec(count=2, hosts=[HostPlacementSpec(hostname='host1', network='', name=''), Hos\
296tPlacementSpec(hostname='host2', network='', name='')])
297
298 You can spefify labels using `label:<label>`
299 >>> PlacementSpec.from_string('label:mon')
300 PlacementSpec(label='mon')
301
302 Labels als support a count:
303 >>> PlacementSpec.from_string('3 label:mon')
304 PlacementSpec(count=3, label='mon')
305
306 fnmatch is also supported:
307 >>> PlacementSpec.from_string('data[1-3]')
308 PlacementSpec(host_pattern='data[1-3]')
309
310 >>> PlacementSpec.from_string(None)
311 PlacementSpec()
312 """
313 if arg is None or not arg:
314 strings = []
315 elif isinstance(arg, str):
316 if ' ' in arg:
317 strings = arg.split(' ')
318 elif ';' in arg:
319 strings = arg.split(';')
320 elif ',' in arg and '[' not in arg:
321 # FIXME: this isn't quite right. we want to avoid breaking
322 # a list of mons with addrvecs... so we're basically allowing
323 # , most of the time, except when addrvecs are used. maybe
324 # ok?
325 strings = arg.split(',')
326 else:
327 strings = [arg]
328 else:
329 raise ServiceSpecValidationError('invalid placement %s' % arg)
330
331 count = None
332 if strings:
333 try:
334 count = int(strings[0])
335 strings = strings[1:]
336 except ValueError:
337 pass
338 for s in strings:
339 if s.startswith('count:'):
340 try:
341 count = int(s[6:])
342 strings.remove(s)
343 break
344 except ValueError:
345 pass
346
347 advanced_hostspecs = [h for h in strings if
348 (':' in h or '=' in h or not any(c in '[]?*:=' for c in h)) and
349 'label:' not in h]
350 for a_h in advanced_hostspecs:
351 strings.remove(a_h)
352
353 labels = [x for x in strings if 'label:' in x]
354 if len(labels) > 1:
355 raise ServiceSpecValidationError('more than one label provided: {}'.format(labels))
356 for l in labels:
357 strings.remove(l)
358 label = labels[0][6:] if labels else None
359
360 host_patterns = strings
361 if len(host_patterns) > 1:
362 raise ServiceSpecValidationError(
363 'more than one host pattern provided: {}'.format(host_patterns))
364
365 ps = PlacementSpec(count=count,
366 hosts=advanced_hostspecs,
367 label=label,
368 host_pattern=host_patterns[0] if host_patterns else None)
369 return ps
370
371
372class ServiceSpec(object):
373 """
374 Details of service creation.
375
376 Request to the orchestrator for a cluster of daemons
377 such as MDS, RGW, iscsi gateway, MONs, MGRs, Prometheus
378
379 This structure is supposed to be enough information to
380 start the services.
381
382 """
1911f103 383 KNOWN_SERVICE_TYPES = 'alertmanager crash grafana iscsi mds mgr mon nfs ' \
f91f0fd5
TL
384 'node-exporter osd prometheus rbd-mirror rgw ' \
385 'container'.split()
386 REQUIRES_SERVICE_ID = 'iscsi mds nfs osd rgw container'.split()
9f95a23c 387
1911f103
TL
388 @classmethod
389 def _cls(cls, service_type):
390 from ceph.deployment.drive_group import DriveGroupSpec
391
392 ret = {
393 'rgw': RGWSpec,
394 'nfs': NFSServiceSpec,
395 'osd': DriveGroupSpec,
396 'iscsi': IscsiServiceSpec,
f91f0fd5
TL
397 'alertmanager': AlertManagerSpec,
398 'container': CustomContainerSpec,
1911f103
TL
399 }.get(service_type, cls)
400 if ret == ServiceSpec and not service_type:
401 raise ServiceSpecValidationError('Spec needs a "service_type" key.')
402 return ret
403
404 def __new__(cls, *args, **kwargs):
405 """
406 Some Python foo to make sure, we don't have an object
407 like `ServiceSpec('rgw')` of type `ServiceSpec`. Now we have:
408
409 >>> type(ServiceSpec('rgw')) == type(RGWSpec('rgw'))
410 True
411
412 """
413 if cls != ServiceSpec:
414 return object.__new__(cls)
415 service_type = kwargs.get('service_type', args[0] if args else None)
416 sub_cls = cls._cls(service_type)
417 return object.__new__(sub_cls)
418
9f95a23c 419 def __init__(self,
e306af50
TL
420 service_type: str,
421 service_id: Optional[str] = None,
422 placement: Optional[PlacementSpec] = None,
423 count: Optional[int] = None,
424 unmanaged: bool = False,
f6b5b4d7 425 preview_only: bool = False,
9f95a23c
TL
426 ):
427 self.placement = PlacementSpec() if placement is None else placement # type: PlacementSpec
428
429 assert service_type in ServiceSpec.KNOWN_SERVICE_TYPES, service_type
430 self.service_type = service_type
f6b5b4d7
TL
431 self.service_id = None
432 if self.service_type in self.REQUIRES_SERVICE_ID:
433 self.service_id = service_id
9f95a23c 434 self.unmanaged = unmanaged
f6b5b4d7 435 self.preview_only = preview_only
9f95a23c
TL
436
437 @classmethod
1911f103 438 @handle_type_error
9f95a23c
TL
439 def from_json(cls, json_spec):
440 # type: (dict) -> Any
441 # Python 3:
442 # >>> ServiceSpecs = TypeVar('Base', bound=ServiceSpec)
443 # then, the real type is: (dict) -> ServiceSpecs
444 """
445 Initialize 'ServiceSpec' object data from a json structure
f6b5b4d7
TL
446
447 There are two valid styles for service specs:
448
449 the "old" style:
450
451 .. code:: yaml
452
453 service_type: nfs
454 service_id: foo
455 pool: mypool
456 namespace: myns
457
458 and the "new" style:
459
460 .. code:: yaml
461
462 service_type: nfs
463 service_id: foo
464 spec:
465 pool: mypool
466 namespace: myns
467
468 In https://tracker.ceph.com/issues/45321 we decided that we'd like to
469 prefer the new style as it is more readable and provides a better
470 understanding of what fields are special for a give service type.
471
472 Note, we'll need to stay compatible with both versions for the
473 the next two major releases (octoups, pacific).
474
9f95a23c
TL
475 :param json_spec: A valid dict with ServiceSpec
476 """
9f95a23c 477
1911f103 478 c = json_spec.copy()
9f95a23c 479
1911f103
TL
480 # kludge to make `from_json` compatible to `Orchestrator.describe_service`
481 # Open question: Remove `service_id` form to_json?
482 if c.get('service_name', ''):
483 service_type_id = c['service_name'].split('.', 1)
484
485 if not c.get('service_type', ''):
486 c['service_type'] = service_type_id[0]
487 if not c.get('service_id', '') and len(service_type_id) > 1:
488 c['service_id'] = service_type_id[1]
489 del c['service_name']
490
491 service_type = c.get('service_type', '')
492 _cls = cls._cls(service_type)
493
494 if 'status' in c:
495 del c['status'] # kludge to make us compatible to `ServiceDescription.to_json()`
9f95a23c 496
1911f103 497 return _cls._from_json_impl(c) # type: ignore
9f95a23c
TL
498
499 @classmethod
500 def _from_json_impl(cls, json_spec):
501 args = {} # type: Dict[str, Dict[Any, Any]]
502 for k, v in json_spec.items():
503 if k == 'placement':
504 v = PlacementSpec.from_json(v)
505 if k == 'spec':
506 args.update(v)
507 continue
508 args.update({k: v})
1911f103
TL
509 _cls = cls(**args)
510 _cls.validate()
511 return _cls
9f95a23c
TL
512
513 def service_name(self):
514 n = self.service_type
515 if self.service_id:
516 n += '.' + self.service_id
517 return n
518
519 def to_json(self):
f6b5b4d7
TL
520 # type: () -> OrderedDict[str, Any]
521 ret: OrderedDict[str, Any] = OrderedDict()
522 ret['service_type'] = self.service_type
523 if self.service_id:
524 ret['service_id'] = self.service_id
525 ret['service_name'] = self.service_name()
526 ret['placement'] = self.placement.to_json()
527 if self.unmanaged:
528 ret['unmanaged'] = self.unmanaged
529
9f95a23c 530 c = {}
f6b5b4d7
TL
531 for key, val in sorted(self.__dict__.items(), key=lambda tpl: tpl[0]):
532 if key in ret:
533 continue
9f95a23c
TL
534 if hasattr(val, 'to_json'):
535 val = val.to_json()
536 if val:
537 c[key] = val
f6b5b4d7
TL
538 if c:
539 ret['spec'] = c
540 return ret
9f95a23c
TL
541
542 def validate(self):
543 if not self.service_type:
544 raise ServiceSpecValidationError('Cannot add Service: type required')
545
f6b5b4d7
TL
546 if self.service_type in self.REQUIRES_SERVICE_ID:
547 if not self.service_id:
548 raise ServiceSpecValidationError('Cannot add Service: id required')
549 elif self.service_id:
550 raise ServiceSpecValidationError(
551 f'Service of type \'{self.service_type}\' should not contain a service id')
552
9f95a23c
TL
553 if self.placement is not None:
554 self.placement.validate()
555
556 def __repr__(self):
557 return "{}({!r})".format(self.__class__.__name__, self.__dict__)
558
f6b5b4d7
TL
559 def __eq__(self, other):
560 return (self.__class__ == other.__class__
561 and
562 self.__dict__ == other.__dict__)
563
9f95a23c
TL
564 def one_line_str(self):
565 return '<{} for service_name={}>'.format(self.__class__.__name__, self.service_name())
566
f6b5b4d7
TL
567 @staticmethod
568 def yaml_representer(dumper: 'yaml.SafeDumper', data: 'ServiceSpec'):
569 return dumper.represent_dict(data.to_json().items())
570
9f95a23c 571
f6b5b4d7 572yaml.add_representer(ServiceSpec, ServiceSpec.yaml_representer)
9f95a23c
TL
573
574
575class NFSServiceSpec(ServiceSpec):
e306af50
TL
576 def __init__(self,
577 service_type: str = 'nfs',
578 service_id: Optional[str] = None,
579 pool: Optional[str] = None,
580 namespace: Optional[str] = None,
581 placement: Optional[PlacementSpec] = None,
582 unmanaged: bool = False,
f6b5b4d7 583 preview_only: bool = False
e306af50 584 ):
9f95a23c
TL
585 assert service_type == 'nfs'
586 super(NFSServiceSpec, self).__init__(
587 'nfs', service_id=service_id,
f6b5b4d7 588 placement=placement, unmanaged=unmanaged, preview_only=preview_only)
9f95a23c
TL
589
590 #: RADOS pool where NFS client recovery data is stored.
591 self.pool = pool
592
593 #: RADOS namespace where NFS client recovery data is stored in the pool.
594 self.namespace = namespace
595
f6b5b4d7
TL
596 def validate(self):
597 super(NFSServiceSpec, self).validate()
9f95a23c
TL
598
599 if not self.pool:
f6b5b4d7
TL
600 raise ServiceSpecValidationError(
601 'Cannot add NFS: No Pool specified')
9f95a23c 602
1911f103
TL
603 def rados_config_name(self):
604 # type: () -> str
605 return 'conf-' + self.service_name()
606
607 def rados_config_location(self):
608 # type: () -> str
f6b5b4d7
TL
609 url = ''
610 if self.pool:
611 url += 'rados://' + self.pool + '/'
612 if self.namespace:
613 url += self.namespace + '/'
614 url += self.rados_config_name()
1911f103
TL
615 return url
616
9f95a23c 617
f6b5b4d7
TL
618yaml.add_representer(NFSServiceSpec, ServiceSpec.yaml_representer)
619
620
9f95a23c
TL
621class RGWSpec(ServiceSpec):
622 """
623 Settings to configure a (multisite) Ceph RGW
624
625 """
626 def __init__(self,
e306af50
TL
627 service_type: str = 'rgw',
628 service_id: Optional[str] = None,
629 placement: Optional[PlacementSpec] = None,
630 rgw_realm: Optional[str] = None,
631 rgw_zone: Optional[str] = None,
632 subcluster: Optional[str] = None,
633 rgw_frontend_port: Optional[int] = None,
634 rgw_frontend_ssl_certificate: Optional[List[str]] = None,
635 rgw_frontend_ssl_key: Optional[List[str]] = None,
636 unmanaged: bool = False,
637 ssl: bool = False,
f6b5b4d7 638 preview_only: bool = False,
9f95a23c 639 ):
1911f103 640 assert service_type == 'rgw', service_type
9f95a23c
TL
641 if service_id:
642 a = service_id.split('.', 2)
643 rgw_realm = a[0]
f6b5b4d7
TL
644 if len(a) > 1:
645 rgw_zone = a[1]
9f95a23c
TL
646 if len(a) > 2:
647 subcluster = a[2]
648 else:
649 if subcluster:
650 service_id = '%s.%s.%s' % (rgw_realm, rgw_zone, subcluster)
651 else:
652 service_id = '%s.%s' % (rgw_realm, rgw_zone)
653 super(RGWSpec, self).__init__(
654 'rgw', service_id=service_id,
f6b5b4d7
TL
655 placement=placement, unmanaged=unmanaged,
656 preview_only=preview_only)
9f95a23c
TL
657
658 self.rgw_realm = rgw_realm
659 self.rgw_zone = rgw_zone
660 self.subcluster = subcluster
661 self.rgw_frontend_port = rgw_frontend_port
1911f103
TL
662 self.rgw_frontend_ssl_certificate = rgw_frontend_ssl_certificate
663 self.rgw_frontend_ssl_key = rgw_frontend_ssl_key
9f95a23c
TL
664 self.ssl = ssl
665
666 def get_port(self):
667 if self.rgw_frontend_port:
668 return self.rgw_frontend_port
669 if self.ssl:
670 return 443
671 else:
672 return 80
1911f103
TL
673
674 def rgw_frontends_config_value(self):
675 ports = []
676 if self.ssl:
677 ports.append(f"ssl_port={self.get_port()}")
678 ports.append(f"ssl_certificate=config://rgw/cert/{self.rgw_realm}/{self.rgw_zone}.crt")
679 ports.append(f"ssl_key=config://rgw/cert/{self.rgw_realm}/{self.rgw_zone}.key")
680 else:
681 ports.append(f"port={self.get_port()}")
682 return f'beast {" ".join(ports)}'
683
f6b5b4d7
TL
684 def validate(self):
685 super(RGWSpec, self).validate()
686
687 if not self.rgw_realm:
688 raise ServiceSpecValidationError(
689 'Cannot add RGW: No realm specified')
690 if not self.rgw_zone:
691 raise ServiceSpecValidationError(
692 'Cannot add RGW: No zone specified')
693
694
695yaml.add_representer(RGWSpec, ServiceSpec.yaml_representer)
696
1911f103
TL
697
698class IscsiServiceSpec(ServiceSpec):
e306af50
TL
699 def __init__(self,
700 service_type: str = 'iscsi',
701 service_id: Optional[str] = None,
702 pool: Optional[str] = None,
703 trusted_ip_list: Optional[str] = None,
704 api_port: Optional[int] = None,
705 api_user: Optional[str] = None,
706 api_password: Optional[str] = None,
707 api_secure: Optional[bool] = None,
708 ssl_cert: Optional[str] = None,
709 ssl_key: Optional[str] = None,
710 placement: Optional[PlacementSpec] = None,
f6b5b4d7
TL
711 unmanaged: bool = False,
712 preview_only: bool = False
e306af50 713 ):
1911f103
TL
714 assert service_type == 'iscsi'
715 super(IscsiServiceSpec, self).__init__('iscsi', service_id=service_id,
f6b5b4d7
TL
716 placement=placement, unmanaged=unmanaged,
717 preview_only=preview_only)
1911f103
TL
718
719 #: RADOS pool where ceph-iscsi config data is stored.
720 self.pool = pool
721 self.trusted_ip_list = trusted_ip_list
1911f103
TL
722 self.api_port = api_port
723 self.api_user = api_user
724 self.api_password = api_password
725 self.api_secure = api_secure
726 self.ssl_cert = ssl_cert
727 self.ssl_key = ssl_key
728
e306af50
TL
729 if not self.api_secure and self.ssl_cert and self.ssl_key:
730 self.api_secure = True
731
732 def validate(self):
733 super(IscsiServiceSpec, self).validate()
1911f103
TL
734
735 if not self.pool:
736 raise ServiceSpecValidationError(
737 'Cannot add ISCSI: No Pool specified')
e306af50
TL
738 if not self.api_user:
739 raise ServiceSpecValidationError(
740 'Cannot add ISCSI: No Api user specified')
741 if not self.api_password:
742 raise ServiceSpecValidationError(
743 'Cannot add ISCSI: No Api password specified')
f6b5b4d7
TL
744
745
746yaml.add_representer(IscsiServiceSpec, ServiceSpec.yaml_representer)
747
748
749class AlertManagerSpec(ServiceSpec):
750 def __init__(self,
751 service_type: str = 'alertmanager',
752 service_id: Optional[str] = None,
753 placement: Optional[PlacementSpec] = None,
754 unmanaged: bool = False,
755 preview_only: bool = False,
756 user_data: Optional[Dict[str, Any]] = None,
757 ):
758 assert service_type == 'alertmanager'
759 super(AlertManagerSpec, self).__init__(
760 'alertmanager', service_id=service_id,
761 placement=placement, unmanaged=unmanaged,
762 preview_only=preview_only)
763
764 # Custom configuration.
765 #
766 # Example:
767 # service_type: alertmanager
768 # service_id: xyz
769 # user_data:
770 # default_webhook_urls:
771 # - "https://foo"
772 # - "https://bar"
773 #
774 # Documentation:
775 # default_webhook_urls - A list of additional URL's that are
776 # added to the default receivers'
777 # <webhook_configs> configuration.
778 self.user_data = user_data or {}
779
780
781yaml.add_representer(AlertManagerSpec, ServiceSpec.yaml_representer)
f91f0fd5
TL
782
783
784class CustomContainerSpec(ServiceSpec):
785 def __init__(self,
786 service_type: str = 'container',
787 service_id: str = None,
788 placement: Optional[PlacementSpec] = None,
789 unmanaged: bool = False,
790 preview_only: bool = False,
791 image: str = None,
792 entrypoint: Optional[str] = None,
793 uid: Optional[int] = None,
794 gid: Optional[int] = None,
795 volume_mounts: Optional[Dict[str, str]] = {},
796 args: Optional[List[str]] = [],
797 envs: Optional[List[str]] = [],
798 privileged: Optional[bool] = False,
799 bind_mounts: Optional[List[List[str]]] = None,
800 ports: Optional[List[int]] = [],
801 dirs: Optional[List[str]] = [],
802 files: Optional[Dict[str, Any]] = {},
803 ):
804 assert service_type == 'container'
805 assert service_id is not None
806 assert image is not None
807
808 super(CustomContainerSpec, self).__init__(
809 service_type, service_id,
810 placement=placement, unmanaged=unmanaged,
811 preview_only=preview_only)
812
813 self.image = image
814 self.entrypoint = entrypoint
815 self.uid = uid
816 self.gid = gid
817 self.volume_mounts = volume_mounts
818 self.args = args
819 self.envs = envs
820 self.privileged = privileged
821 self.bind_mounts = bind_mounts
822 self.ports = ports
823 self.dirs = dirs
824 self.files = files
825
826 def config_json(self) -> Dict[str, Any]:
827 """
828 Helper function to get the value of the `--config-json` cephadm
829 command line option. It will contain all specification properties
830 that haven't a `None` value. Such properties will get default
831 values in cephadm.
832 :return: Returns a dictionary containing all specification
833 properties.
834 """
835 config_json = {}
836 for prop in ['image', 'entrypoint', 'uid', 'gid', 'args',
837 'envs', 'volume_mounts', 'privileged',
838 'bind_mounts', 'ports', 'dirs', 'files']:
839 value = getattr(self, prop)
840 if value is not None:
841 config_json[prop] = value
842 return config_json
843
844
845yaml.add_representer(CustomContainerSpec, ServiceSpec.yaml_representer)