4 from collections
import OrderedDict
5 from contextlib
import contextmanager
6 from functools
import wraps
7 from ipaddress
import ip_network
, ip_address
8 from typing
import Optional
, Dict
, Any
, List
, Union
, Callable
, Iterable
, Type
, TypeVar
, cast
, \
9 NamedTuple
, Mapping
, Iterator
13 from ceph
.deployment
.hostspec
import HostSpec
, SpecValidationError
, assert_valid_host
14 from ceph
.deployment
.utils
import unwrap_ipv6
, valid_addr
15 from ceph
.utils
import is_hex
17 ServiceSpecT
= TypeVar('ServiceSpecT', bound
='ServiceSpec')
18 FuncT
= TypeVar('FuncT', bound
=Callable
)
21 def handle_type_error(method
: FuncT
) -> FuncT
:
23 def inner(cls
: Any
, *args
: Any
, **kwargs
: Any
) -> Any
:
25 return method(cls
, *args
, **kwargs
)
26 except (TypeError, AttributeError) as e
:
27 error_msg
= '{}: {}'.format(cls
.__name
__, e
)
28 raise SpecValidationError(error_msg
)
29 return cast(FuncT
, inner
)
32 class HostPlacementSpec(NamedTuple
):
37 def __str__(self
) -> str:
41 res
+= ':' + self
.network
43 res
+= '=' + self
.name
48 def from_json(cls
, data
: Union
[dict, str]) -> 'HostPlacementSpec':
49 if isinstance(data
, str):
50 return cls
.parse(data
)
53 def to_json(self
) -> str:
57 def parse(cls
, host
, require_network
=True):
58 # type: (str, bool) -> HostPlacementSpec
60 Split host into host, network, and (optional) daemon name parts. The network
61 part can be an IP, CIDR, or ceph addrvec like '[v2:1.2.3.4:3300,v1:1.2.3.4:6789]'.
68 "myhost:1.2.3.0/24=name"
69 "myhost:[v2:1.2.3.4:3000]=name"
70 "myhost:[v2:1.2.3.4:3000,v1:1.2.3.4:6789]=name"
72 # Matches from start to : or = or until end of string
73 host_re
= r
'^(.*?)(:|=|$)'
74 # Matches from : to = or until end of string
75 ip_re
= r
':(.*?)(=|$)'
76 # Matches from = to end of string
80 host_spec
= cls('', '', '')
82 match_host
= re
.search(host_re
, host
)
84 host_spec
= host_spec
._replace
(hostname
=match_host
.group(1))
86 name_match
= re
.search(name_re
, host
)
88 host_spec
= host_spec
._replace
(name
=name_match
.group(1))
90 ip_match
= re
.search(ip_re
, host
)
92 host_spec
= host_spec
._replace
(network
=ip_match
.group(1))
94 if not require_network
:
97 networks
= list() # type: List[str]
98 network
= host_spec
.network
99 # in case we have [v2:1.2.3.4:3000,v1:1.2.3.4:6478]
101 networks
= [x
for x
in network
.split(',')]
104 networks
.append(network
)
106 for network
in networks
:
107 # only if we have versioned network configs
108 if network
.startswith('v') or network
.startswith('[v'):
109 # if this is ipv6 we can't just simply split on ':' so do
110 # a split once and rsplit once to leave us with just ipv6 addr
111 network
= network
.split(':', 1)[1]
112 network
= network
.rsplit(':', 1)[0]
114 # if subnets are defined, also verify the validity
118 ip_address(unwrap_ipv6(network
))
119 except ValueError as e
:
125 def validate(self
) -> None:
126 assert_valid_host(self
.hostname
)
129 class PlacementSpec(object):
131 For APIs that need to specify a host subset
135 label
=None, # type: Optional[str]
136 hosts
=None, # type: Union[List[str],List[HostPlacementSpec], None]
137 count
=None, # type: Optional[int]
138 count_per_host
=None, # type: Optional[int]
139 host_pattern
=None, # type: Optional[str]
141 # type: (...) -> None
143 self
.hosts
= [] # type: List[HostPlacementSpec]
146 self
.set_hosts(hosts
)
148 self
.count
= count
# type: Optional[int]
149 self
.count_per_host
= count_per_host
# type: Optional[int]
151 #: fnmatch patterns to select hosts. Can also be a single host.
152 self
.host_pattern
= host_pattern
# type: Optional[str]
156 def is_empty(self
) -> bool:
160 and not self
.host_pattern
161 and self
.count
is None
162 and self
.count_per_host
is None
165 def __eq__(self
, other
: Any
) -> bool:
166 if isinstance(other
, PlacementSpec
):
167 return self
.label
== other
.label \
168 and self
.hosts
== other
.hosts \
169 and self
.count
== other
.count \
170 and self
.host_pattern
== other
.host_pattern \
171 and self
.count_per_host
== other
.count_per_host
172 return NotImplemented
174 def set_hosts(self
, hosts
: Union
[List
[str], List
[HostPlacementSpec
]]) -> None:
175 # To backpopulate the .hosts attribute when using labels or count
176 # in the orchestrator backend.
177 if all([isinstance(host
, HostPlacementSpec
) for host
in hosts
]):
178 self
.hosts
= hosts
# type: ignore
180 self
.hosts
= [HostPlacementSpec
.parse(x
, require_network
=False) # type: ignore
184 def filter_matching_hosts(self
, _get_hosts_func
: Callable
) -> List
[str]:
185 return self
.filter_matching_hostspecs(_get_hosts_func(as_hostspec
=True))
187 def filter_matching_hostspecs(self
, hostspecs
: Iterable
[HostSpec
]) -> List
[str]:
189 all_hosts
= [hs
.hostname
for hs
in hostspecs
]
190 return [h
.hostname
for h
in self
.hosts
if h
.hostname
in all_hosts
]
192 return [hs
.hostname
for hs
in hostspecs
if self
.label
in hs
.labels
]
193 all_hosts
= [hs
.hostname
for hs
in hostspecs
]
194 if self
.host_pattern
:
195 return fnmatch
.filter(all_hosts
, self
.host_pattern
)
198 def get_target_count(self
, hostspecs
: Iterable
[HostSpec
]) -> int:
201 return len(self
.filter_matching_hostspecs(hostspecs
)) * (self
.count_per_host
or 1)
203 def pretty_str(self
) -> str:
206 ... ps = PlacementSpec(...) # For all placement specs:
207 ... PlacementSpec.from_string(ps.pretty_str()) == ps
211 kv
.append(';'.join([str(h
) for h
in self
.hosts
]))
213 kv
.append('count:%d' % self
.count
)
214 if self
.count_per_host
:
215 kv
.append('count-per-host:%d' % self
.count_per_host
)
217 kv
.append('label:%s' % self
.label
)
218 if self
.host_pattern
:
219 kv
.append(self
.host_pattern
)
222 def __repr__(self
) -> str:
225 kv
.append('count=%d' % self
.count
)
226 if self
.count_per_host
:
227 kv
.append('count_per_host=%d' % self
.count_per_host
)
229 kv
.append('label=%s' % repr(self
.label
))
231 kv
.append('hosts={!r}'.format(self
.hosts
))
232 if self
.host_pattern
:
233 kv
.append('host_pattern={!r}'.format(self
.host_pattern
))
234 return "PlacementSpec(%s)" % ', '.join(kv
)
238 def from_json(cls
, data
: dict) -> 'PlacementSpec':
240 hosts
= c
.get('hosts', [])
244 c
['hosts'].append(HostPlacementSpec
.from_json(host
))
249 def to_json(self
) -> dict:
250 r
: Dict
[str, Any
] = {}
252 r
['label'] = self
.label
254 r
['hosts'] = [host
.to_json() for host
in self
.hosts
]
256 r
['count'] = self
.count
257 if self
.count_per_host
:
258 r
['count_per_host'] = self
.count_per_host
259 if self
.host_pattern
:
260 r
['host_pattern'] = self
.host_pattern
263 def validate(self
) -> None:
264 if self
.hosts
and self
.label
:
265 # TODO: a less generic Exception
266 raise SpecValidationError('Host and label are mutually exclusive')
267 if self
.count
is not None:
269 intval
= int(self
.count
)
270 except (ValueError, TypeError):
271 raise SpecValidationError("num/count must be a numeric value")
272 if self
.count
!= intval
:
273 raise SpecValidationError("num/count must be an integer value")
275 raise SpecValidationError("num/count must be >= 1")
276 if self
.count_per_host
is not None:
278 intval
= int(self
.count_per_host
)
279 except (ValueError, TypeError):
280 raise SpecValidationError("count-per-host must be a numeric value")
281 if self
.count_per_host
!= intval
:
282 raise SpecValidationError("count-per-host must be an integer value")
283 if self
.count_per_host
< 1:
284 raise SpecValidationError("count-per-host must be >= 1")
285 if self
.count_per_host
is not None and not (
290 raise SpecValidationError(
291 "count-per-host must be combined with label or hosts or host_pattern"
293 if self
.count
is not None and self
.count_per_host
is not None:
294 raise SpecValidationError("cannot combine count and count-per-host")
296 self
.count_per_host
is not None
298 and any([hs
.network
or hs
.name
for hs
in self
.hosts
])
300 raise SpecValidationError(
301 "count-per-host cannot be combined explicit placement with names or networks"
303 if self
.host_pattern
:
304 if not isinstance(self
.host_pattern
, str):
305 raise SpecValidationError('host_pattern must be of type string')
307 raise SpecValidationError('cannot combine host patterns and hosts')
313 def from_string(cls
, arg
):
314 # type: (Optional[str]) -> PlacementSpec
316 A single integer is parsed as a count:
317 >>> PlacementSpec.from_string('3')
318 PlacementSpec(count=3)
320 A list of names is parsed as host specifications:
321 >>> PlacementSpec.from_string('host1 host2')
322 PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacemen\
323 tSpec(hostname='host2', network='', name='')])
325 You can also prefix the hosts with a count as follows:
326 >>> PlacementSpec.from_string('2 host1 host2')
327 PlacementSpec(count=2, hosts=[HostPlacementSpec(hostname='host1', network='', name=''), Hos\
328 tPlacementSpec(hostname='host2', network='', name='')])
330 You can spefify labels using `label:<label>`
331 >>> PlacementSpec.from_string('label:mon')
332 PlacementSpec(label='mon')
334 Labels als support a count:
335 >>> PlacementSpec.from_string('3 label:mon')
336 PlacementSpec(count=3, label='mon')
338 fnmatch is also supported:
339 >>> PlacementSpec.from_string('data[1-3]')
340 PlacementSpec(host_pattern='data[1-3]')
342 >>> PlacementSpec.from_string(None)
345 if arg
is None or not arg
:
347 elif isinstance(arg
, str):
349 strings
= arg
.split(' ')
351 strings
= arg
.split(';')
352 elif ',' in arg
and '[' not in arg
:
353 # FIXME: this isn't quite right. we want to avoid breaking
354 # a list of mons with addrvecs... so we're basically allowing
355 # , most of the time, except when addrvecs are used. maybe
357 strings
= arg
.split(',')
361 raise SpecValidationError('invalid placement %s' % arg
)
364 count_per_host
= None
367 count
= int(strings
[0])
368 strings
= strings
[1:]
372 if s
.startswith('count:'):
374 count
= int(s
[len('count:'):])
380 if s
.startswith('count-per-host:'):
382 count_per_host
= int(s
[len('count-per-host:'):])
388 advanced_hostspecs
= [h
for h
in strings
if
389 (':' in h
or '=' in h
or not any(c
in '[]?*:=' for c
in h
)) and
391 for a_h
in advanced_hostspecs
:
394 labels
= [x
for x
in strings
if 'label:' in x
]
396 raise SpecValidationError('more than one label provided: {}'.format(labels
))
399 label
= labels
[0][6:] if labels
else None
401 host_patterns
= strings
402 if len(host_patterns
) > 1:
403 raise SpecValidationError(
404 'more than one host pattern provided: {}'.format(host_patterns
))
406 ps
= PlacementSpec(count
=count
,
407 count_per_host
=count_per_host
,
408 hosts
=advanced_hostspecs
,
410 host_pattern
=host_patterns
[0] if host_patterns
else None)
414 _service_spec_from_json_validate
= True
418 def service_spec_allow_invalid_from_json() -> Iterator
[None]:
420 I know this is evil, but unfortunately `ceph orch ls`
421 may return invalid OSD specs for OSDs not associated to
422 and specs. If you have a better idea, please!
424 global _service_spec_from_json_validate
425 _service_spec_from_json_validate
= False
427 _service_spec_from_json_validate
= True
430 class ServiceSpec(object):
432 Details of service creation.
434 Request to the orchestrator for a cluster of daemons
435 such as MDS, RGW, iscsi gateway, MONs, MGRs, Prometheus
437 This structure is supposed to be enough information to
440 KNOWN_SERVICE_TYPES
= 'alertmanager crash grafana iscsi mds mgr mon nfs ' \
441 'node-exporter osd prometheus rbd-mirror rgw agent ' \
442 'container ingress cephfs-mirror snmp-gateway'.split()
443 REQUIRES_SERVICE_ID
= 'iscsi mds nfs rgw container ingress '.split()
444 MANAGED_CONFIG_OPTIONS
= [
449 def _cls(cls
: Type
[ServiceSpecT
], service_type
: str) -> Type
[ServiceSpecT
]:
450 from ceph
.deployment
.drive_group
import DriveGroupSpec
454 'nfs': NFSServiceSpec
,
455 'osd': DriveGroupSpec
,
456 'iscsi': IscsiServiceSpec
,
457 'alertmanager': AlertManagerSpec
,
458 'ingress': IngressSpec
,
459 'container': CustomContainerSpec
,
460 'grafana': GrafanaSpec
,
461 'node-exporter': MonitoringSpec
,
462 'prometheus': MonitoringSpec
,
463 'snmp-gateway': SNMPGatewaySpec
,
464 }.get(service_type
, cls
)
465 if ret
== ServiceSpec
and not service_type
:
466 raise SpecValidationError('Spec needs a "service_type" key.')
469 def __new__(cls
: Type
[ServiceSpecT
], *args
: Any
, **kwargs
: Any
) -> ServiceSpecT
:
471 Some Python foo to make sure, we don't have an object
472 like `ServiceSpec('rgw')` of type `ServiceSpec`. Now we have:
474 >>> type(ServiceSpec('rgw')) == type(RGWSpec('rgw'))
478 if cls
!= ServiceSpec
:
479 return object.__new
__(cls
)
480 service_type
= kwargs
.get('service_type', args
[0] if args
else None)
481 sub_cls
: Any
= cls
._cls
(service_type
)
482 return object.__new
__(sub_cls
)
486 service_id
: Optional
[str] = None,
487 placement
: Optional
[PlacementSpec
] = None,
488 count
: Optional
[int] = None,
489 config
: Optional
[Dict
[str, str]] = None,
490 unmanaged
: bool = False,
491 preview_only
: bool = False,
492 networks
: Optional
[List
[str]] = None,
493 extra_container_args
: Optional
[List
[str]] = None,
496 #: See :ref:`orchestrator-cli-placement-spec`.
497 self
.placement
= PlacementSpec() if placement
is None else placement
# type: PlacementSpec
499 assert service_type
in ServiceSpec
.KNOWN_SERVICE_TYPES
, service_type
500 #: The type of the service. Needs to be either a Ceph
501 #: service (``mon``, ``crash``, ``mds``, ``mgr``, ``osd`` or
502 #: ``rbd-mirror``), a gateway (``nfs`` or ``rgw``), part of the
503 #: monitoring stack (``alertmanager``, ``grafana``, ``node-exporter`` or
504 #: ``prometheus``) or (``container``) for custom containers.
505 self
.service_type
= service_type
507 #: The name of the service. Required for ``iscsi``, ``mds``, ``nfs``, ``osd``, ``rgw``,
508 #: ``container``, ``ingress``
509 self
.service_id
= None
511 if self
.service_type
in self
.REQUIRES_SERVICE_ID
or self
.service_type
== 'osd':
512 self
.service_id
= service_id
514 #: If set to ``true``, the orchestrator will not deploy nor remove
515 #: any daemon associated with this service. Placement and all other properties
516 #: will be ignored. This is useful, if you do not want this service to be
517 #: managed temporarily. For cephadm, See :ref:`cephadm-spec-unmanaged`
518 self
.unmanaged
= unmanaged
519 self
.preview_only
= preview_only
521 #: A list of network identities instructing the daemons to only bind
522 #: on the particular networks in that list. In case the cluster is distributed
523 #: across multiple networks, you can add multiple networks. See
524 #: :ref:`cephadm-monitoring-networks-ports`,
525 #: :ref:`cephadm-rgw-networks` and :ref:`cephadm-mgr-networks`.
526 self
.networks
: List
[str] = networks
or []
528 self
.config
: Optional
[Dict
[str, str]] = None
530 self
.config
= {k
.replace(' ', '_'): v
for k
, v
in config
.items()}
532 self
.extra_container_args
: Optional
[List
[str]] = extra_container_args
536 def from_json(cls
: Type
[ServiceSpecT
], json_spec
: Dict
) -> ServiceSpecT
:
538 Initialize 'ServiceSpec' object data from a json structure
540 There are two valid styles for service specs:
558 some_option: the_value
559 networks: [10.10.0.0/16]
564 In https://tracker.ceph.com/issues/45321 we decided that we'd like to
565 prefer the new style as it is more readable and provides a better
566 understanding of what fields are special for a give service type.
568 Note, we'll need to stay compatible with both versions for the
569 the next two major releases (octoups, pacific).
571 :param json_spec: A valid dict with ServiceSpec
575 if not isinstance(json_spec
, dict):
576 raise SpecValidationError(
577 f
'Service Spec is not an (JSON or YAML) object. got "{str(json_spec)}"')
579 json_spec
= cls
.normalize_json(json_spec
)
583 # kludge to make `from_json` compatible to `Orchestrator.describe_service`
584 # Open question: Remove `service_id` form to_json?
585 if c
.get('service_name', ''):
586 service_type_id
= c
['service_name'].split('.', 1)
588 if not c
.get('service_type', ''):
589 c
['service_type'] = service_type_id
[0]
590 if not c
.get('service_id', '') and len(service_type_id
) > 1:
591 c
['service_id'] = service_type_id
[1]
592 del c
['service_name']
594 service_type
= c
.get('service_type', '')
595 _cls
= cls
._cls
(service_type
)
598 del c
['status'] # kludge to make us compatible to `ServiceDescription.to_json()`
600 return _cls
._from
_json
_impl
(c
) # type: ignore
603 def normalize_json(json_spec
: dict) -> dict:
604 networks
= json_spec
.get('networks')
607 if isinstance(networks
, list):
609 if not isinstance(networks
, str):
610 raise SpecValidationError(f
'Networks ({networks}) must be a string or list of strings')
611 json_spec
['networks'] = [networks
]
615 def _from_json_impl(cls
: Type
[ServiceSpecT
], json_spec
: dict) -> ServiceSpecT
:
616 args
= {} # type: Dict[str, Any]
617 for k
, v
in json_spec
.items():
619 v
= PlacementSpec
.from_json(v
)
625 if _service_spec_from_json_validate
:
629 def service_name(self
) -> str:
630 n
= self
.service_type
632 n
+= '.' + self
.service_id
635 def get_port_start(self
) -> List
[int]:
636 # If defined, we will allocate and number ports starting at this
640 def get_virtual_ip(self
) -> Optional
[str]:
644 # type: () -> OrderedDict[str, Any]
645 ret
: OrderedDict
[str, Any
] = OrderedDict()
646 ret
['service_type'] = self
.service_type
648 ret
['service_id'] = self
.service_id
649 ret
['service_name'] = self
.service_name()
650 if self
.placement
.to_json():
651 ret
['placement'] = self
.placement
.to_json()
653 ret
['unmanaged'] = self
.unmanaged
655 ret
['networks'] = self
.networks
656 if self
.extra_container_args
:
657 ret
['extra_container_args'] = self
.extra_container_args
660 for key
, val
in sorted(self
.__dict
__.items(), key
=lambda tpl
: tpl
[0]):
663 if hasattr(val
, 'to_json'):
671 def validate(self
) -> None:
672 if not self
.service_type
:
673 raise SpecValidationError('Cannot add Service: type required')
675 if self
.service_type
!= 'osd':
676 if self
.service_type
in self
.REQUIRES_SERVICE_ID
and not self
.service_id
:
677 raise SpecValidationError('Cannot add Service: id required')
678 if self
.service_type
not in self
.REQUIRES_SERVICE_ID
and self
.service_id
:
679 raise SpecValidationError(
680 f
'Service of type \'{self.service_type}\' should not contain a service id')
683 if not re
.match('^[a-zA-Z0-9_.-]+$', self
.service_id
):
684 raise SpecValidationError('Service id contains invalid characters, '
685 'only [a-zA-Z0-9_.-] allowed')
687 if self
.placement
is not None:
688 self
.placement
.validate()
690 for k
, v
in self
.config
.items():
691 if k
in self
.MANAGED_CONFIG_OPTIONS
:
692 raise SpecValidationError(
693 f
'Cannot set config option {k} in spec: it is managed by cephadm'
695 for network
in self
.networks
or []:
698 except ValueError as e
:
699 raise SpecValidationError(
700 f
'Cannot parse network {network}: {e}'
703 def __repr__(self
) -> str:
704 y
= yaml
.dump(cast(dict, self
), default_flow_style
=False)
705 return f
"{self.__class__.__name__}.from_json(yaml.safe_load('''{y}'''))"
707 def __eq__(self
, other
: Any
) -> bool:
708 return (self
.__class
__ == other
.__class
__
710 self
.__dict
__ == other
.__dict
__)
712 def one_line_str(self
) -> str:
713 return '<{} for service_name={}>'.format(self
.__class
__.__name
__, self
.service_name())
716 def yaml_representer(dumper
: 'yaml.SafeDumper', data
: 'ServiceSpec') -> Any
:
717 return dumper
.represent_dict(cast(Mapping
, data
.to_json().items()))
720 yaml
.add_representer(ServiceSpec
, ServiceSpec
.yaml_representer
)
723 class NFSServiceSpec(ServiceSpec
):
725 service_type
: str = 'nfs',
726 service_id
: Optional
[str] = None,
727 placement
: Optional
[PlacementSpec
] = None,
728 unmanaged
: bool = False,
729 preview_only
: bool = False,
730 config
: Optional
[Dict
[str, str]] = None,
731 networks
: Optional
[List
[str]] = None,
732 port
: Optional
[int] = None,
734 assert service_type
== 'nfs'
735 super(NFSServiceSpec
, self
).__init
__(
736 'nfs', service_id
=service_id
,
737 placement
=placement
, unmanaged
=unmanaged
, preview_only
=preview_only
,
738 config
=config
, networks
=networks
)
742 def get_port_start(self
) -> List
[int]:
747 def rados_config_name(self
):
749 return 'conf-' + self
.service_name()
752 yaml
.add_representer(NFSServiceSpec
, ServiceSpec
.yaml_representer
)
755 class RGWSpec(ServiceSpec
):
757 Settings to configure a (multisite) Ceph RGW
762 service_id: myrealm.myzone
767 rgw_frontend_port: 1234
768 rgw_frontend_type: beast
769 rgw_frontend_ssl_certificate: ...
771 See also: :ref:`orchestrator-cli-service-spec`
774 MANAGED_CONFIG_OPTIONS
= ServiceSpec
.MANAGED_CONFIG_OPTIONS
+ [
781 service_type
: str = 'rgw',
782 service_id
: Optional
[str] = None,
783 placement
: Optional
[PlacementSpec
] = None,
784 rgw_realm
: Optional
[str] = None,
785 rgw_zone
: Optional
[str] = None,
786 rgw_frontend_port
: Optional
[int] = None,
787 rgw_frontend_ssl_certificate
: Optional
[List
[str]] = None,
788 rgw_frontend_type
: Optional
[str] = None,
789 unmanaged
: bool = False,
791 preview_only
: bool = False,
792 config
: Optional
[Dict
[str, str]] = None,
793 networks
: Optional
[List
[str]] = None,
794 subcluster
: Optional
[str] = None, # legacy, only for from_json on upgrade
796 assert service_type
== 'rgw', service_type
798 # for backward compatibility with octopus spec files,
799 if not service_id
and (rgw_realm
and rgw_zone
):
800 service_id
= rgw_realm
+ '.' + rgw_zone
802 super(RGWSpec
, self
).__init
__(
803 'rgw', service_id
=service_id
,
804 placement
=placement
, unmanaged
=unmanaged
,
805 preview_only
=preview_only
, config
=config
, networks
=networks
)
807 #: The RGW realm associated with this service. Needs to be manually created
808 self
.rgw_realm
: Optional
[str] = rgw_realm
809 #: The RGW zone associated with this service. Needs to be manually created
810 self
.rgw_zone
: Optional
[str] = rgw_zone
811 #: Port of the RGW daemons
812 self
.rgw_frontend_port
: Optional
[int] = rgw_frontend_port
813 #: List of SSL certificates
814 self
.rgw_frontend_ssl_certificate
: Optional
[List
[str]] = rgw_frontend_ssl_certificate
815 #: civetweb or beast (default: beast). See :ref:`rgw_frontends`
816 self
.rgw_frontend_type
: Optional
[str] = rgw_frontend_type
820 def get_port_start(self
) -> List
[int]:
821 return [self
.get_port()]
823 def get_port(self
) -> int:
824 if self
.rgw_frontend_port
:
825 return self
.rgw_frontend_port
831 def validate(self
) -> None:
832 super(RGWSpec
, self
).validate()
834 if self
.rgw_realm
and not self
.rgw_zone
:
835 raise SpecValidationError(
836 'Cannot add RGW: Realm specified but no zone specified')
837 if self
.rgw_zone
and not self
.rgw_realm
:
838 raise SpecValidationError(
839 'Cannot add RGW: Zone specified but no realm specified')
842 yaml
.add_representer(RGWSpec
, ServiceSpec
.yaml_representer
)
845 class IscsiServiceSpec(ServiceSpec
):
847 service_type
: str = 'iscsi',
848 service_id
: Optional
[str] = None,
849 pool
: Optional
[str] = None,
850 trusted_ip_list
: Optional
[str] = None,
851 api_port
: Optional
[int] = None,
852 api_user
: Optional
[str] = None,
853 api_password
: Optional
[str] = None,
854 api_secure
: Optional
[bool] = None,
855 ssl_cert
: Optional
[str] = None,
856 ssl_key
: Optional
[str] = None,
857 placement
: Optional
[PlacementSpec
] = None,
858 unmanaged
: bool = False,
859 preview_only
: bool = False,
860 config
: Optional
[Dict
[str, str]] = None,
861 networks
: Optional
[List
[str]] = None,
863 assert service_type
== 'iscsi'
864 super(IscsiServiceSpec
, self
).__init
__('iscsi', service_id
=service_id
,
865 placement
=placement
, unmanaged
=unmanaged
,
866 preview_only
=preview_only
,
867 config
=config
, networks
=networks
)
869 #: RADOS pool where ceph-iscsi config data is stored.
871 #: list of trusted IP addresses
872 self
.trusted_ip_list
= trusted_ip_list
873 #: ``api_port`` as defined in the ``iscsi-gateway.cfg``
874 self
.api_port
= api_port
875 #: ``api_user`` as defined in the ``iscsi-gateway.cfg``
876 self
.api_user
= api_user
877 #: ``api_password`` as defined in the ``iscsi-gateway.cfg``
878 self
.api_password
= api_password
879 #: ``api_secure`` as defined in the ``iscsi-gateway.cfg``
880 self
.api_secure
= api_secure
882 self
.ssl_cert
= ssl_cert
884 self
.ssl_key
= ssl_key
886 if not self
.api_secure
and self
.ssl_cert
and self
.ssl_key
:
887 self
.api_secure
= True
889 def validate(self
) -> None:
890 super(IscsiServiceSpec
, self
).validate()
893 raise SpecValidationError(
894 'Cannot add ISCSI: No Pool specified')
896 # Do not need to check for api_user and api_password as they
897 # now default to 'admin' when setting up the gateway url. Older
898 # iSCSI specs from before this change should be fine as they will
899 # have been required to have an api_user and api_password set and
900 # will be unaffected by the new default value.
903 yaml
.add_representer(IscsiServiceSpec
, ServiceSpec
.yaml_representer
)
906 class IngressSpec(ServiceSpec
):
908 service_type
: str = 'ingress',
909 service_id
: Optional
[str] = None,
910 config
: Optional
[Dict
[str, str]] = None,
911 networks
: Optional
[List
[str]] = None,
912 placement
: Optional
[PlacementSpec
] = None,
913 backend_service
: Optional
[str] = None,
914 frontend_port
: Optional
[int] = None,
915 ssl_cert
: Optional
[str] = None,
916 ssl_key
: Optional
[str] = None,
917 ssl_dh_param
: Optional
[str] = None,
918 ssl_ciphers
: Optional
[List
[str]] = None,
919 ssl_options
: Optional
[List
[str]] = None,
920 monitor_port
: Optional
[int] = None,
921 monitor_user
: Optional
[str] = None,
922 monitor_password
: Optional
[str] = None,
923 enable_stats
: Optional
[bool] = None,
924 keepalived_password
: Optional
[str] = None,
925 virtual_ip
: Optional
[str] = None,
926 virtual_interface_networks
: Optional
[List
[str]] = [],
927 unmanaged
: bool = False,
930 assert service_type
== 'ingress'
931 super(IngressSpec
, self
).__init
__(
932 'ingress', service_id
=service_id
,
933 placement
=placement
, config
=config
,
936 self
.backend_service
= backend_service
937 self
.frontend_port
= frontend_port
938 self
.ssl_cert
= ssl_cert
939 self
.ssl_key
= ssl_key
940 self
.ssl_dh_param
= ssl_dh_param
941 self
.ssl_ciphers
= ssl_ciphers
942 self
.ssl_options
= ssl_options
943 self
.monitor_port
= monitor_port
944 self
.monitor_user
= monitor_user
945 self
.monitor_password
= monitor_password
946 self
.keepalived_password
= keepalived_password
947 self
.virtual_ip
= virtual_ip
948 self
.virtual_interface_networks
= virtual_interface_networks
or []
949 self
.unmanaged
= unmanaged
952 def get_port_start(self
) -> List
[int]:
953 return [cast(int, self
.frontend_port
),
954 cast(int, self
.monitor_port
)]
956 def get_virtual_ip(self
) -> Optional
[str]:
957 return self
.virtual_ip
959 def validate(self
) -> None:
960 super(IngressSpec
, self
).validate()
962 if not self
.backend_service
:
963 raise SpecValidationError(
964 'Cannot add ingress: No backend_service specified')
965 if not self
.frontend_port
:
966 raise SpecValidationError(
967 'Cannot add ingress: No frontend_port specified')
968 if not self
.monitor_port
:
969 raise SpecValidationError(
970 'Cannot add ingress: No monitor_port specified')
971 if not self
.virtual_ip
:
972 raise SpecValidationError(
973 'Cannot add ingress: No virtual_ip provided')
976 yaml
.add_representer(IngressSpec
, ServiceSpec
.yaml_representer
)
979 class CustomContainerSpec(ServiceSpec
):
981 service_type
: str = 'container',
982 service_id
: Optional
[str] = None,
983 config
: Optional
[Dict
[str, str]] = None,
984 networks
: Optional
[List
[str]] = None,
985 placement
: Optional
[PlacementSpec
] = None,
986 unmanaged
: bool = False,
987 preview_only
: bool = False,
988 image
: Optional
[str] = None,
989 entrypoint
: Optional
[str] = None,
990 uid
: Optional
[int] = None,
991 gid
: Optional
[int] = None,
992 volume_mounts
: Optional
[Dict
[str, str]] = {},
993 args
: Optional
[List
[str]] = [],
994 envs
: Optional
[List
[str]] = [],
995 privileged
: Optional
[bool] = False,
996 bind_mounts
: Optional
[List
[List
[str]]] = None,
997 ports
: Optional
[List
[int]] = [],
998 dirs
: Optional
[List
[str]] = [],
999 files
: Optional
[Dict
[str, Any
]] = {},
1001 assert service_type
== 'container'
1002 assert service_id
is not None
1003 assert image
is not None
1005 super(CustomContainerSpec
, self
).__init
__(
1006 service_type
, service_id
,
1007 placement
=placement
, unmanaged
=unmanaged
,
1008 preview_only
=preview_only
, config
=config
,
1012 self
.entrypoint
= entrypoint
1015 self
.volume_mounts
= volume_mounts
1018 self
.privileged
= privileged
1019 self
.bind_mounts
= bind_mounts
1024 def config_json(self
) -> Dict
[str, Any
]:
1026 Helper function to get the value of the `--config-json` cephadm
1027 command line option. It will contain all specification properties
1028 that haven't a `None` value. Such properties will get default
1030 :return: Returns a dictionary containing all specification
1034 for prop
in ['image', 'entrypoint', 'uid', 'gid', 'args',
1035 'envs', 'volume_mounts', 'privileged',
1036 'bind_mounts', 'ports', 'dirs', 'files']:
1037 value
= getattr(self
, prop
)
1038 if value
is not None:
1039 config_json
[prop
] = value
1043 yaml
.add_representer(CustomContainerSpec
, ServiceSpec
.yaml_representer
)
1046 class MonitoringSpec(ServiceSpec
):
1049 service_id
: Optional
[str] = None,
1050 config
: Optional
[Dict
[str, str]] = None,
1051 networks
: Optional
[List
[str]] = None,
1052 placement
: Optional
[PlacementSpec
] = None,
1053 unmanaged
: bool = False,
1054 preview_only
: bool = False,
1055 port
: Optional
[int] = None,
1057 assert service_type
in ['grafana', 'node-exporter', 'prometheus', 'alertmanager']
1059 super(MonitoringSpec
, self
).__init
__(
1060 service_type
, service_id
,
1061 placement
=placement
, unmanaged
=unmanaged
,
1062 preview_only
=preview_only
, config
=config
,
1065 self
.service_type
= service_type
1068 def get_port_start(self
) -> List
[int]:
1069 return [self
.get_port()]
1071 def get_port(self
) -> int:
1075 return {'prometheus': 9095,
1076 'node-exporter': 9100,
1077 'alertmanager': 9093,
1078 'grafana': 3000}[self
.service_type
]
1081 yaml
.add_representer(MonitoringSpec
, ServiceSpec
.yaml_representer
)
1084 class AlertManagerSpec(MonitoringSpec
):
1086 service_type
: str = 'alertmanager',
1087 service_id
: Optional
[str] = None,
1088 placement
: Optional
[PlacementSpec
] = None,
1089 unmanaged
: bool = False,
1090 preview_only
: bool = False,
1091 user_data
: Optional
[Dict
[str, Any
]] = None,
1092 config
: Optional
[Dict
[str, str]] = None,
1093 networks
: Optional
[List
[str]] = None,
1094 port
: Optional
[int] = None,
1096 assert service_type
== 'alertmanager'
1097 super(AlertManagerSpec
, self
).__init
__(
1098 'alertmanager', service_id
=service_id
,
1099 placement
=placement
, unmanaged
=unmanaged
,
1100 preview_only
=preview_only
, config
=config
, networks
=networks
, port
=port
)
1102 # Custom configuration.
1105 # service_type: alertmanager
1108 # default_webhook_urls:
1113 # default_webhook_urls - A list of additional URL's that are
1114 # added to the default receivers'
1115 # <webhook_configs> configuration.
1116 self
.user_data
= user_data
or {}
1118 def get_port_start(self
) -> List
[int]:
1119 return [self
.get_port(), 9094]
1121 def validate(self
) -> None:
1122 super(AlertManagerSpec
, self
).validate()
1124 if self
.port
== 9094:
1125 raise SpecValidationError(
1126 'Port 9094 is reserved for AlertManager cluster listen address')
1129 yaml
.add_representer(AlertManagerSpec
, ServiceSpec
.yaml_representer
)
1132 class GrafanaSpec(MonitoringSpec
):
1134 service_type
: str = 'grafana',
1135 service_id
: Optional
[str] = None,
1136 placement
: Optional
[PlacementSpec
] = None,
1137 unmanaged
: bool = False,
1138 preview_only
: bool = False,
1139 config
: Optional
[Dict
[str, str]] = None,
1140 networks
: Optional
[List
[str]] = None,
1141 port
: Optional
[int] = None,
1142 initial_admin_password
: Optional
[str] = None
1144 assert service_type
== 'grafana'
1145 super(GrafanaSpec
, self
).__init
__(
1146 'grafana', service_id
=service_id
,
1147 placement
=placement
, unmanaged
=unmanaged
,
1148 preview_only
=preview_only
, config
=config
, networks
=networks
, port
=port
)
1150 self
.initial_admin_password
= initial_admin_password
1153 yaml
.add_representer(GrafanaSpec
, ServiceSpec
.yaml_representer
)
1156 class SNMPGatewaySpec(ServiceSpec
):
1157 class SNMPVersion(str, enum
.Enum
):
1161 def to_json(self
) -> str:
1164 class SNMPAuthType(str, enum
.Enum
):
1168 def to_json(self
) -> str:
1171 class SNMPPrivacyType(str, enum
.Enum
):
1175 def to_json(self
) -> str:
1178 valid_destination_types
= [
1184 service_type
: str = 'snmp-gateway',
1185 snmp_version
: Optional
[SNMPVersion
] = None,
1186 snmp_destination
: str = '',
1187 credentials
: Dict
[str, str] = {},
1188 engine_id
: Optional
[str] = None,
1189 auth_protocol
: Optional
[SNMPAuthType
] = None,
1190 privacy_protocol
: Optional
[SNMPPrivacyType
] = None,
1191 placement
: Optional
[PlacementSpec
] = None,
1192 unmanaged
: bool = False,
1193 preview_only
: bool = False,
1194 port
: Optional
[int] = None,
1196 assert service_type
== 'snmp-gateway'
1198 super(SNMPGatewaySpec
, self
).__init
__(
1200 placement
=placement
,
1201 unmanaged
=unmanaged
,
1202 preview_only
=preview_only
)
1204 self
.service_type
= service_type
1205 self
.snmp_version
= snmp_version
1206 self
.snmp_destination
= snmp_destination
1208 self
.credentials
= credentials
1209 self
.engine_id
= engine_id
1210 self
.auth_protocol
= auth_protocol
1211 self
.privacy_protocol
= privacy_protocol
1214 def _from_json_impl(cls
, json_spec
: dict) -> 'SNMPGatewaySpec':
1216 cpy
= json_spec
.copy()
1218 ('snmp_version', SNMPGatewaySpec
.SNMPVersion
),
1219 ('auth_protocol', SNMPGatewaySpec
.SNMPAuthType
),
1220 ('privacy_protocol', SNMPGatewaySpec
.SNMPPrivacyType
),
1222 for d
in cpy
, cpy
.get('spec', {}):
1223 for key
, enum_cls
in types
:
1226 d
[key
] = enum_cls(d
[key
])
1228 raise SpecValidationError(f
'{key} unsupported. Must be one of '
1229 f
'{", ".join(enum_cls)}')
1230 return super(SNMPGatewaySpec
, cls
)._from
_json
_impl
(cpy
)
1233 def ports(self
) -> List
[int]:
1234 return [self
.port
or 9464]
1236 def get_port_start(self
) -> List
[int]:
1239 def validate(self
) -> None:
1240 super(SNMPGatewaySpec
, self
).validate()
1242 if not self
.credentials
:
1243 raise SpecValidationError(
1244 'Missing authentication information (credentials). '
1245 'SNMP V2c and V3 require credential information'
1247 elif not self
.snmp_version
:
1248 raise SpecValidationError(
1249 'Missing SNMP version (snmp_version)'
1252 creds_requirement
= {
1253 'V2c': ['snmp_community'],
1254 'V3': ['snmp_v3_auth_username', 'snmp_v3_auth_password']
1256 if self
.privacy_protocol
:
1257 creds_requirement
['V3'].append('snmp_v3_priv_password')
1259 missing
= [parm
for parm
in creds_requirement
[self
.snmp_version
]
1260 if parm
not in self
.credentials
]
1261 # check that credentials are correct for the version
1263 raise SpecValidationError(
1264 f
'SNMP {self.snmp_version} credentials are incomplete. Missing {", ".join(missing)}'
1268 if 10 <= len(self
.engine_id
) <= 64 and \
1269 is_hex(self
.engine_id
) and \
1270 len(self
.engine_id
) % 2 == 0:
1273 raise SpecValidationError(
1274 'engine_id must be a string containing 10-64 hex characters. '
1275 'Its length must be divisible by 2'
1279 if self
.snmp_version
== 'V3':
1280 raise SpecValidationError(
1281 'Must provide an engine_id for SNMP V3 notifications'
1284 if not self
.snmp_destination
:
1285 raise SpecValidationError(
1286 'SNMP destination (snmp_destination) must be provided'
1289 valid
, description
= valid_addr(self
.snmp_destination
)
1291 raise SpecValidationError(
1292 f
'SNMP destination (snmp_destination) is invalid: {description}'
1294 if description
not in self
.valid_destination_types
:
1295 raise SpecValidationError(
1296 f
'SNMP destination (snmp_destination) type ({description}) is invalid. '
1297 f
'Must be either: {", ".join(sorted(self.valid_destination_types))}'
1301 yaml
.add_representer(SNMPGatewaySpec
, ServiceSpec
.yaml_representer
)