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:
318 >>> PlacementSpec.from_string('3')
319 PlacementSpec(count=3)
321 A list of names is parsed as host specifications:
323 >>> PlacementSpec.from_string('host1 host2')
324 PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacemen\
325 tSpec(hostname='host2', network='', name='')])
327 You can also prefix the hosts with a count as follows:
329 >>> PlacementSpec.from_string('2 host1 host2')
330 PlacementSpec(count=2, hosts=[HostPlacementSpec(hostname='host1', network='', name=''), Hos\
331 tPlacementSpec(hostname='host2', network='', name='')])
333 You can specify labels using `label:<label>`
335 >>> PlacementSpec.from_string('label:mon')
336 PlacementSpec(label='mon')
338 Labels also support a count:
340 >>> PlacementSpec.from_string('3 label:mon')
341 PlacementSpec(count=3, label='mon')
343 fnmatch is also supported:
345 >>> PlacementSpec.from_string('data[1-3]')
346 PlacementSpec(host_pattern='data[1-3]')
348 >>> PlacementSpec.from_string(None)
351 if arg
is None or not arg
:
353 elif isinstance(arg
, str):
355 strings
= arg
.split(' ')
357 strings
= arg
.split(';')
358 elif ',' in arg
and '[' not in arg
:
359 # FIXME: this isn't quite right. we want to avoid breaking
360 # a list of mons with addrvecs... so we're basically allowing
361 # , most of the time, except when addrvecs are used. maybe
363 strings
= arg
.split(',')
367 raise SpecValidationError('invalid placement %s' % arg
)
370 count_per_host
= None
373 count
= int(strings
[0])
374 strings
= strings
[1:]
378 if s
.startswith('count:'):
380 count
= int(s
[len('count:'):])
386 if s
.startswith('count-per-host:'):
388 count_per_host
= int(s
[len('count-per-host:'):])
394 advanced_hostspecs
= [h
for h
in strings
if
395 (':' in h
or '=' in h
or not any(c
in '[]?*:=' for c
in h
)) and
397 for a_h
in advanced_hostspecs
:
400 labels
= [x
for x
in strings
if 'label:' in x
]
402 raise SpecValidationError('more than one label provided: {}'.format(labels
))
405 label
= labels
[0][6:] if labels
else None
407 host_patterns
= strings
408 if len(host_patterns
) > 1:
409 raise SpecValidationError(
410 'more than one host pattern provided: {}'.format(host_patterns
))
412 ps
= PlacementSpec(count
=count
,
413 count_per_host
=count_per_host
,
414 hosts
=advanced_hostspecs
,
416 host_pattern
=host_patterns
[0] if host_patterns
else None)
420 _service_spec_from_json_validate
= True
424 def service_spec_allow_invalid_from_json() -> Iterator
[None]:
426 I know this is evil, but unfortunately `ceph orch ls`
427 may return invalid OSD specs for OSDs not associated to
428 and specs. If you have a better idea, please!
430 global _service_spec_from_json_validate
431 _service_spec_from_json_validate
= False
433 _service_spec_from_json_validate
= True
436 class ServiceSpec(object):
438 Details of service creation.
440 Request to the orchestrator for a cluster of daemons
441 such as MDS, RGW, iscsi gateway, MONs, MGRs, Prometheus
443 This structure is supposed to be enough information to
446 KNOWN_SERVICE_TYPES
= 'alertmanager crash grafana iscsi mds mgr mon nfs ' \
447 'node-exporter osd prometheus rbd-mirror rgw agent ' \
448 'container ingress cephfs-mirror snmp-gateway'.split()
449 REQUIRES_SERVICE_ID
= 'iscsi mds nfs rgw container ingress '.split()
450 MANAGED_CONFIG_OPTIONS
= [
455 def _cls(cls
: Type
[ServiceSpecT
], service_type
: str) -> Type
[ServiceSpecT
]:
456 from ceph
.deployment
.drive_group
import DriveGroupSpec
460 'nfs': NFSServiceSpec
,
461 'osd': DriveGroupSpec
,
462 'iscsi': IscsiServiceSpec
,
463 'alertmanager': AlertManagerSpec
,
464 'ingress': IngressSpec
,
465 'container': CustomContainerSpec
,
466 'grafana': GrafanaSpec
,
467 'node-exporter': MonitoringSpec
,
468 'prometheus': MonitoringSpec
,
469 'snmp-gateway': SNMPGatewaySpec
,
470 }.get(service_type
, cls
)
471 if ret
== ServiceSpec
and not service_type
:
472 raise SpecValidationError('Spec needs a "service_type" key.')
475 def __new__(cls
: Type
[ServiceSpecT
], *args
: Any
, **kwargs
: Any
) -> ServiceSpecT
:
477 Some Python foo to make sure, we don't have an object
478 like `ServiceSpec('rgw')` of type `ServiceSpec`. Now we have:
480 >>> type(ServiceSpec('rgw')) == type(RGWSpec('rgw'))
484 if cls
!= ServiceSpec
:
485 return object.__new
__(cls
)
486 service_type
= kwargs
.get('service_type', args
[0] if args
else None)
487 sub_cls
: Any
= cls
._cls
(service_type
)
488 return object.__new
__(sub_cls
)
492 service_id
: Optional
[str] = None,
493 placement
: Optional
[PlacementSpec
] = None,
494 count
: Optional
[int] = None,
495 config
: Optional
[Dict
[str, str]] = None,
496 unmanaged
: bool = False,
497 preview_only
: bool = False,
498 networks
: Optional
[List
[str]] = None,
499 extra_container_args
: Optional
[List
[str]] = None,
502 #: See :ref:`orchestrator-cli-placement-spec`.
503 self
.placement
= PlacementSpec() if placement
is None else placement
# type: PlacementSpec
505 assert service_type
in ServiceSpec
.KNOWN_SERVICE_TYPES
, service_type
506 #: The type of the service. Needs to be either a Ceph
507 #: service (``mon``, ``crash``, ``mds``, ``mgr``, ``osd`` or
508 #: ``rbd-mirror``), a gateway (``nfs`` or ``rgw``), part of the
509 #: monitoring stack (``alertmanager``, ``grafana``, ``node-exporter`` or
510 #: ``prometheus``) or (``container``) for custom containers.
511 self
.service_type
= service_type
513 #: The name of the service. Required for ``iscsi``, ``mds``, ``nfs``, ``osd``, ``rgw``,
514 #: ``container``, ``ingress``
515 self
.service_id
= None
517 if self
.service_type
in self
.REQUIRES_SERVICE_ID
or self
.service_type
== 'osd':
518 self
.service_id
= service_id
520 #: If set to ``true``, the orchestrator will not deploy nor remove
521 #: any daemon associated with this service. Placement and all other properties
522 #: will be ignored. This is useful, if you do not want this service to be
523 #: managed temporarily. For cephadm, See :ref:`cephadm-spec-unmanaged`
524 self
.unmanaged
= unmanaged
525 self
.preview_only
= preview_only
527 #: A list of network identities instructing the daemons to only bind
528 #: on the particular networks in that list. In case the cluster is distributed
529 #: across multiple networks, you can add multiple networks. See
530 #: :ref:`cephadm-monitoring-networks-ports`,
531 #: :ref:`cephadm-rgw-networks` and :ref:`cephadm-mgr-networks`.
532 self
.networks
: List
[str] = networks
or []
534 self
.config
: Optional
[Dict
[str, str]] = None
536 self
.config
= {k
.replace(' ', '_'): v
for k
, v
in config
.items()}
538 self
.extra_container_args
: Optional
[List
[str]] = extra_container_args
542 def from_json(cls
: Type
[ServiceSpecT
], json_spec
: Dict
) -> ServiceSpecT
:
544 Initialize 'ServiceSpec' object data from a json structure
546 There are two valid styles for service specs:
564 some_option: the_value
565 networks: [10.10.0.0/16]
570 In https://tracker.ceph.com/issues/45321 we decided that we'd like to
571 prefer the new style as it is more readable and provides a better
572 understanding of what fields are special for a give service type.
574 Note, we'll need to stay compatible with both versions for the
575 the next two major releases (octoups, pacific).
577 :param json_spec: A valid dict with ServiceSpec
581 if not isinstance(json_spec
, dict):
582 raise SpecValidationError(
583 f
'Service Spec is not an (JSON or YAML) object. got "{str(json_spec)}"')
585 json_spec
= cls
.normalize_json(json_spec
)
589 # kludge to make `from_json` compatible to `Orchestrator.describe_service`
590 # Open question: Remove `service_id` form to_json?
591 if c
.get('service_name', ''):
592 service_type_id
= c
['service_name'].split('.', 1)
594 if not c
.get('service_type', ''):
595 c
['service_type'] = service_type_id
[0]
596 if not c
.get('service_id', '') and len(service_type_id
) > 1:
597 c
['service_id'] = service_type_id
[1]
598 del c
['service_name']
600 service_type
= c
.get('service_type', '')
601 _cls
= cls
._cls
(service_type
)
604 del c
['status'] # kludge to make us compatible to `ServiceDescription.to_json()`
606 return _cls
._from
_json
_impl
(c
) # type: ignore
609 def normalize_json(json_spec
: dict) -> dict:
610 networks
= json_spec
.get('networks')
613 if isinstance(networks
, list):
615 if not isinstance(networks
, str):
616 raise SpecValidationError(f
'Networks ({networks}) must be a string or list of strings')
617 json_spec
['networks'] = [networks
]
621 def _from_json_impl(cls
: Type
[ServiceSpecT
], json_spec
: dict) -> ServiceSpecT
:
622 args
= {} # type: Dict[str, Any]
623 for k
, v
in json_spec
.items():
625 v
= PlacementSpec
.from_json(v
)
631 if _service_spec_from_json_validate
:
635 def service_name(self
) -> str:
636 n
= self
.service_type
638 n
+= '.' + self
.service_id
641 def get_port_start(self
) -> List
[int]:
642 # If defined, we will allocate and number ports starting at this
646 def get_virtual_ip(self
) -> Optional
[str]:
650 # type: () -> OrderedDict[str, Any]
651 ret
: OrderedDict
[str, Any
] = OrderedDict()
652 ret
['service_type'] = self
.service_type
654 ret
['service_id'] = self
.service_id
655 ret
['service_name'] = self
.service_name()
656 if self
.placement
.to_json():
657 ret
['placement'] = self
.placement
.to_json()
659 ret
['unmanaged'] = self
.unmanaged
661 ret
['networks'] = self
.networks
662 if self
.extra_container_args
:
663 ret
['extra_container_args'] = self
.extra_container_args
666 for key
, val
in sorted(self
.__dict
__.items(), key
=lambda tpl
: tpl
[0]):
669 if hasattr(val
, 'to_json'):
677 def validate(self
) -> None:
678 if not self
.service_type
:
679 raise SpecValidationError('Cannot add Service: type required')
681 if self
.service_type
!= 'osd':
682 if self
.service_type
in self
.REQUIRES_SERVICE_ID
and not self
.service_id
:
683 raise SpecValidationError('Cannot add Service: id required')
684 if self
.service_type
not in self
.REQUIRES_SERVICE_ID
and self
.service_id
:
685 raise SpecValidationError(
686 f
'Service of type \'{self.service_type}\' should not contain a service id')
689 if not re
.match('^[a-zA-Z0-9_.-]+$', self
.service_id
):
690 raise SpecValidationError('Service id contains invalid characters, '
691 'only [a-zA-Z0-9_.-] allowed')
693 if self
.placement
is not None:
694 self
.placement
.validate()
696 for k
, v
in self
.config
.items():
697 if k
in self
.MANAGED_CONFIG_OPTIONS
:
698 raise SpecValidationError(
699 f
'Cannot set config option {k} in spec: it is managed by cephadm'
701 for network
in self
.networks
or []:
704 except ValueError as e
:
705 raise SpecValidationError(
706 f
'Cannot parse network {network}: {e}'
709 def __repr__(self
) -> str:
710 y
= yaml
.dump(cast(dict, self
), default_flow_style
=False)
711 return f
"{self.__class__.__name__}.from_json(yaml.safe_load('''{y}'''))"
713 def __eq__(self
, other
: Any
) -> bool:
714 return (self
.__class
__ == other
.__class
__
716 self
.__dict
__ == other
.__dict
__)
718 def one_line_str(self
) -> str:
719 return '<{} for service_name={}>'.format(self
.__class
__.__name
__, self
.service_name())
722 def yaml_representer(dumper
: 'yaml.SafeDumper', data
: 'ServiceSpec') -> Any
:
723 return dumper
.represent_dict(cast(Mapping
, data
.to_json().items()))
726 yaml
.add_representer(ServiceSpec
, ServiceSpec
.yaml_representer
)
729 class NFSServiceSpec(ServiceSpec
):
731 service_type
: str = 'nfs',
732 service_id
: Optional
[str] = None,
733 placement
: Optional
[PlacementSpec
] = None,
734 unmanaged
: bool = False,
735 preview_only
: bool = False,
736 config
: Optional
[Dict
[str, str]] = None,
737 networks
: Optional
[List
[str]] = None,
738 port
: Optional
[int] = None,
740 assert service_type
== 'nfs'
741 super(NFSServiceSpec
, self
).__init
__(
742 'nfs', service_id
=service_id
,
743 placement
=placement
, unmanaged
=unmanaged
, preview_only
=preview_only
,
744 config
=config
, networks
=networks
)
748 def get_port_start(self
) -> List
[int]:
753 def rados_config_name(self
):
755 return 'conf-' + self
.service_name()
758 yaml
.add_representer(NFSServiceSpec
, ServiceSpec
.yaml_representer
)
761 class RGWSpec(ServiceSpec
):
763 Settings to configure a (multisite) Ceph RGW
768 service_id: myrealm.myzone
773 rgw_frontend_port: 1234
774 rgw_frontend_type: beast
775 rgw_frontend_ssl_certificate: ...
777 See also: :ref:`orchestrator-cli-service-spec`
780 MANAGED_CONFIG_OPTIONS
= ServiceSpec
.MANAGED_CONFIG_OPTIONS
+ [
787 service_type
: str = 'rgw',
788 service_id
: Optional
[str] = None,
789 placement
: Optional
[PlacementSpec
] = None,
790 rgw_realm
: Optional
[str] = None,
791 rgw_zone
: Optional
[str] = None,
792 rgw_frontend_port
: Optional
[int] = None,
793 rgw_frontend_ssl_certificate
: Optional
[List
[str]] = None,
794 rgw_frontend_type
: Optional
[str] = None,
795 unmanaged
: bool = False,
797 preview_only
: bool = False,
798 config
: Optional
[Dict
[str, str]] = None,
799 networks
: Optional
[List
[str]] = None,
800 subcluster
: Optional
[str] = None, # legacy, only for from_json on upgrade
802 assert service_type
== 'rgw', service_type
804 # for backward compatibility with octopus spec files,
805 if not service_id
and (rgw_realm
and rgw_zone
):
806 service_id
= rgw_realm
+ '.' + rgw_zone
808 super(RGWSpec
, self
).__init
__(
809 'rgw', service_id
=service_id
,
810 placement
=placement
, unmanaged
=unmanaged
,
811 preview_only
=preview_only
, config
=config
, networks
=networks
)
813 #: The RGW realm associated with this service. Needs to be manually created
814 self
.rgw_realm
: Optional
[str] = rgw_realm
815 #: The RGW zone associated with this service. Needs to be manually created
816 self
.rgw_zone
: Optional
[str] = rgw_zone
817 #: Port of the RGW daemons
818 self
.rgw_frontend_port
: Optional
[int] = rgw_frontend_port
819 #: List of SSL certificates
820 self
.rgw_frontend_ssl_certificate
: Optional
[List
[str]] = rgw_frontend_ssl_certificate
821 #: civetweb or beast (default: beast). See :ref:`rgw_frontends`
822 self
.rgw_frontend_type
: Optional
[str] = rgw_frontend_type
826 def get_port_start(self
) -> List
[int]:
827 return [self
.get_port()]
829 def get_port(self
) -> int:
830 if self
.rgw_frontend_port
:
831 return self
.rgw_frontend_port
837 def validate(self
) -> None:
838 super(RGWSpec
, self
).validate()
840 if self
.rgw_realm
and not self
.rgw_zone
:
841 raise SpecValidationError(
842 'Cannot add RGW: Realm specified but no zone specified')
843 if self
.rgw_zone
and not self
.rgw_realm
:
844 raise SpecValidationError(
845 'Cannot add RGW: Zone specified but no realm specified')
848 yaml
.add_representer(RGWSpec
, ServiceSpec
.yaml_representer
)
851 class IscsiServiceSpec(ServiceSpec
):
853 service_type
: str = 'iscsi',
854 service_id
: Optional
[str] = None,
855 pool
: Optional
[str] = None,
856 trusted_ip_list
: Optional
[str] = None,
857 api_port
: Optional
[int] = None,
858 api_user
: Optional
[str] = None,
859 api_password
: Optional
[str] = None,
860 api_secure
: Optional
[bool] = None,
861 ssl_cert
: Optional
[str] = None,
862 ssl_key
: Optional
[str] = None,
863 placement
: Optional
[PlacementSpec
] = None,
864 unmanaged
: bool = False,
865 preview_only
: bool = False,
866 config
: Optional
[Dict
[str, str]] = None,
867 networks
: Optional
[List
[str]] = None,
869 assert service_type
== 'iscsi'
870 super(IscsiServiceSpec
, self
).__init
__('iscsi', service_id
=service_id
,
871 placement
=placement
, unmanaged
=unmanaged
,
872 preview_only
=preview_only
,
873 config
=config
, networks
=networks
)
875 #: RADOS pool where ceph-iscsi config data is stored.
877 #: list of trusted IP addresses
878 self
.trusted_ip_list
= trusted_ip_list
879 #: ``api_port`` as defined in the ``iscsi-gateway.cfg``
880 self
.api_port
= api_port
881 #: ``api_user`` as defined in the ``iscsi-gateway.cfg``
882 self
.api_user
= api_user
883 #: ``api_password`` as defined in the ``iscsi-gateway.cfg``
884 self
.api_password
= api_password
885 #: ``api_secure`` as defined in the ``iscsi-gateway.cfg``
886 self
.api_secure
= api_secure
888 self
.ssl_cert
= ssl_cert
890 self
.ssl_key
= ssl_key
892 if not self
.api_secure
and self
.ssl_cert
and self
.ssl_key
:
893 self
.api_secure
= True
895 def validate(self
) -> None:
896 super(IscsiServiceSpec
, self
).validate()
899 raise SpecValidationError(
900 'Cannot add ISCSI: No Pool specified')
902 # Do not need to check for api_user and api_password as they
903 # now default to 'admin' when setting up the gateway url. Older
904 # iSCSI specs from before this change should be fine as they will
905 # have been required to have an api_user and api_password set and
906 # will be unaffected by the new default value.
909 yaml
.add_representer(IscsiServiceSpec
, ServiceSpec
.yaml_representer
)
912 class IngressSpec(ServiceSpec
):
914 service_type
: str = 'ingress',
915 service_id
: Optional
[str] = None,
916 config
: Optional
[Dict
[str, str]] = None,
917 networks
: Optional
[List
[str]] = None,
918 placement
: Optional
[PlacementSpec
] = None,
919 backend_service
: Optional
[str] = None,
920 frontend_port
: Optional
[int] = None,
921 ssl_cert
: Optional
[str] = None,
922 ssl_key
: Optional
[str] = None,
923 ssl_dh_param
: Optional
[str] = None,
924 ssl_ciphers
: Optional
[List
[str]] = None,
925 ssl_options
: Optional
[List
[str]] = None,
926 monitor_port
: Optional
[int] = None,
927 monitor_user
: Optional
[str] = None,
928 monitor_password
: Optional
[str] = None,
929 enable_stats
: Optional
[bool] = None,
930 keepalived_password
: Optional
[str] = None,
931 virtual_ip
: Optional
[str] = None,
932 virtual_interface_networks
: Optional
[List
[str]] = [],
933 unmanaged
: bool = False,
936 assert service_type
== 'ingress'
937 super(IngressSpec
, self
).__init
__(
938 'ingress', service_id
=service_id
,
939 placement
=placement
, config
=config
,
942 self
.backend_service
= backend_service
943 self
.frontend_port
= frontend_port
944 self
.ssl_cert
= ssl_cert
945 self
.ssl_key
= ssl_key
946 self
.ssl_dh_param
= ssl_dh_param
947 self
.ssl_ciphers
= ssl_ciphers
948 self
.ssl_options
= ssl_options
949 self
.monitor_port
= monitor_port
950 self
.monitor_user
= monitor_user
951 self
.monitor_password
= monitor_password
952 self
.keepalived_password
= keepalived_password
953 self
.virtual_ip
= virtual_ip
954 self
.virtual_interface_networks
= virtual_interface_networks
or []
955 self
.unmanaged
= unmanaged
958 def get_port_start(self
) -> List
[int]:
959 return [cast(int, self
.frontend_port
),
960 cast(int, self
.monitor_port
)]
962 def get_virtual_ip(self
) -> Optional
[str]:
963 return self
.virtual_ip
965 def validate(self
) -> None:
966 super(IngressSpec
, self
).validate()
968 if not self
.backend_service
:
969 raise SpecValidationError(
970 'Cannot add ingress: No backend_service specified')
971 if not self
.frontend_port
:
972 raise SpecValidationError(
973 'Cannot add ingress: No frontend_port specified')
974 if not self
.monitor_port
:
975 raise SpecValidationError(
976 'Cannot add ingress: No monitor_port specified')
977 if not self
.virtual_ip
:
978 raise SpecValidationError(
979 'Cannot add ingress: No virtual_ip provided')
982 yaml
.add_representer(IngressSpec
, ServiceSpec
.yaml_representer
)
985 class CustomContainerSpec(ServiceSpec
):
987 service_type
: str = 'container',
988 service_id
: Optional
[str] = None,
989 config
: Optional
[Dict
[str, str]] = None,
990 networks
: Optional
[List
[str]] = None,
991 placement
: Optional
[PlacementSpec
] = None,
992 unmanaged
: bool = False,
993 preview_only
: bool = False,
994 image
: Optional
[str] = None,
995 entrypoint
: Optional
[str] = None,
996 uid
: Optional
[int] = None,
997 gid
: Optional
[int] = None,
998 volume_mounts
: Optional
[Dict
[str, str]] = {},
999 args
: Optional
[List
[str]] = [],
1000 envs
: Optional
[List
[str]] = [],
1001 privileged
: Optional
[bool] = False,
1002 bind_mounts
: Optional
[List
[List
[str]]] = None,
1003 ports
: Optional
[List
[int]] = [],
1004 dirs
: Optional
[List
[str]] = [],
1005 files
: Optional
[Dict
[str, Any
]] = {},
1007 assert service_type
== 'container'
1008 assert service_id
is not None
1009 assert image
is not None
1011 super(CustomContainerSpec
, self
).__init
__(
1012 service_type
, service_id
,
1013 placement
=placement
, unmanaged
=unmanaged
,
1014 preview_only
=preview_only
, config
=config
,
1018 self
.entrypoint
= entrypoint
1021 self
.volume_mounts
= volume_mounts
1024 self
.privileged
= privileged
1025 self
.bind_mounts
= bind_mounts
1030 def config_json(self
) -> Dict
[str, Any
]:
1032 Helper function to get the value of the `--config-json` cephadm
1033 command line option. It will contain all specification properties
1034 that haven't a `None` value. Such properties will get default
1036 :return: Returns a dictionary containing all specification
1040 for prop
in ['image', 'entrypoint', 'uid', 'gid', 'args',
1041 'envs', 'volume_mounts', 'privileged',
1042 'bind_mounts', 'ports', 'dirs', 'files']:
1043 value
= getattr(self
, prop
)
1044 if value
is not None:
1045 config_json
[prop
] = value
1049 yaml
.add_representer(CustomContainerSpec
, ServiceSpec
.yaml_representer
)
1052 class MonitoringSpec(ServiceSpec
):
1055 service_id
: Optional
[str] = None,
1056 config
: Optional
[Dict
[str, str]] = None,
1057 networks
: Optional
[List
[str]] = None,
1058 placement
: Optional
[PlacementSpec
] = None,
1059 unmanaged
: bool = False,
1060 preview_only
: bool = False,
1061 port
: Optional
[int] = None,
1063 assert service_type
in ['grafana', 'node-exporter', 'prometheus', 'alertmanager']
1065 super(MonitoringSpec
, self
).__init
__(
1066 service_type
, service_id
,
1067 placement
=placement
, unmanaged
=unmanaged
,
1068 preview_only
=preview_only
, config
=config
,
1071 self
.service_type
= service_type
1074 def get_port_start(self
) -> List
[int]:
1075 return [self
.get_port()]
1077 def get_port(self
) -> int:
1081 return {'prometheus': 9095,
1082 'node-exporter': 9100,
1083 'alertmanager': 9093,
1084 'grafana': 3000}[self
.service_type
]
1087 yaml
.add_representer(MonitoringSpec
, ServiceSpec
.yaml_representer
)
1090 class AlertManagerSpec(MonitoringSpec
):
1092 service_type
: str = 'alertmanager',
1093 service_id
: Optional
[str] = None,
1094 placement
: Optional
[PlacementSpec
] = None,
1095 unmanaged
: bool = False,
1096 preview_only
: bool = False,
1097 user_data
: Optional
[Dict
[str, Any
]] = None,
1098 config
: Optional
[Dict
[str, str]] = None,
1099 networks
: Optional
[List
[str]] = None,
1100 port
: Optional
[int] = None,
1102 assert service_type
== 'alertmanager'
1103 super(AlertManagerSpec
, self
).__init
__(
1104 'alertmanager', service_id
=service_id
,
1105 placement
=placement
, unmanaged
=unmanaged
,
1106 preview_only
=preview_only
, config
=config
, networks
=networks
, port
=port
)
1108 # Custom configuration.
1111 # service_type: alertmanager
1114 # default_webhook_urls:
1119 # default_webhook_urls - A list of additional URL's that are
1120 # added to the default receivers'
1121 # <webhook_configs> configuration.
1122 self
.user_data
= user_data
or {}
1124 def get_port_start(self
) -> List
[int]:
1125 return [self
.get_port(), 9094]
1127 def validate(self
) -> None:
1128 super(AlertManagerSpec
, self
).validate()
1130 if self
.port
== 9094:
1131 raise SpecValidationError(
1132 'Port 9094 is reserved for AlertManager cluster listen address')
1135 yaml
.add_representer(AlertManagerSpec
, ServiceSpec
.yaml_representer
)
1138 class GrafanaSpec(MonitoringSpec
):
1140 service_type
: str = 'grafana',
1141 service_id
: Optional
[str] = None,
1142 placement
: Optional
[PlacementSpec
] = None,
1143 unmanaged
: bool = False,
1144 preview_only
: bool = False,
1145 config
: Optional
[Dict
[str, str]] = None,
1146 networks
: Optional
[List
[str]] = None,
1147 port
: Optional
[int] = None,
1148 initial_admin_password
: Optional
[str] = None
1150 assert service_type
== 'grafana'
1151 super(GrafanaSpec
, self
).__init
__(
1152 'grafana', service_id
=service_id
,
1153 placement
=placement
, unmanaged
=unmanaged
,
1154 preview_only
=preview_only
, config
=config
, networks
=networks
, port
=port
)
1156 self
.initial_admin_password
= initial_admin_password
1159 yaml
.add_representer(GrafanaSpec
, ServiceSpec
.yaml_representer
)
1162 class SNMPGatewaySpec(ServiceSpec
):
1163 class SNMPVersion(str, enum
.Enum
):
1167 def to_json(self
) -> str:
1170 class SNMPAuthType(str, enum
.Enum
):
1174 def to_json(self
) -> str:
1177 class SNMPPrivacyType(str, enum
.Enum
):
1181 def to_json(self
) -> str:
1184 valid_destination_types
= [
1190 service_type
: str = 'snmp-gateway',
1191 snmp_version
: Optional
[SNMPVersion
] = None,
1192 snmp_destination
: str = '',
1193 credentials
: Dict
[str, str] = {},
1194 engine_id
: Optional
[str] = None,
1195 auth_protocol
: Optional
[SNMPAuthType
] = None,
1196 privacy_protocol
: Optional
[SNMPPrivacyType
] = None,
1197 placement
: Optional
[PlacementSpec
] = None,
1198 unmanaged
: bool = False,
1199 preview_only
: bool = False,
1200 port
: Optional
[int] = None,
1202 assert service_type
== 'snmp-gateway'
1204 super(SNMPGatewaySpec
, self
).__init
__(
1206 placement
=placement
,
1207 unmanaged
=unmanaged
,
1208 preview_only
=preview_only
)
1210 self
.service_type
= service_type
1211 self
.snmp_version
= snmp_version
1212 self
.snmp_destination
= snmp_destination
1214 self
.credentials
= credentials
1215 self
.engine_id
= engine_id
1216 self
.auth_protocol
= auth_protocol
1217 self
.privacy_protocol
= privacy_protocol
1220 def _from_json_impl(cls
, json_spec
: dict) -> 'SNMPGatewaySpec':
1222 cpy
= json_spec
.copy()
1224 ('snmp_version', SNMPGatewaySpec
.SNMPVersion
),
1225 ('auth_protocol', SNMPGatewaySpec
.SNMPAuthType
),
1226 ('privacy_protocol', SNMPGatewaySpec
.SNMPPrivacyType
),
1228 for d
in cpy
, cpy
.get('spec', {}):
1229 for key
, enum_cls
in types
:
1232 d
[key
] = enum_cls(d
[key
])
1234 raise SpecValidationError(f
'{key} unsupported. Must be one of '
1235 f
'{", ".join(enum_cls)}')
1236 return super(SNMPGatewaySpec
, cls
)._from
_json
_impl
(cpy
)
1239 def ports(self
) -> List
[int]:
1240 return [self
.port
or 9464]
1242 def get_port_start(self
) -> List
[int]:
1245 def validate(self
) -> None:
1246 super(SNMPGatewaySpec
, self
).validate()
1248 if not self
.credentials
:
1249 raise SpecValidationError(
1250 'Missing authentication information (credentials). '
1251 'SNMP V2c and V3 require credential information'
1253 elif not self
.snmp_version
:
1254 raise SpecValidationError(
1255 'Missing SNMP version (snmp_version)'
1258 creds_requirement
= {
1259 'V2c': ['snmp_community'],
1260 'V3': ['snmp_v3_auth_username', 'snmp_v3_auth_password']
1262 if self
.privacy_protocol
:
1263 creds_requirement
['V3'].append('snmp_v3_priv_password')
1265 missing
= [parm
for parm
in creds_requirement
[self
.snmp_version
]
1266 if parm
not in self
.credentials
]
1267 # check that credentials are correct for the version
1269 raise SpecValidationError(
1270 f
'SNMP {self.snmp_version} credentials are incomplete. Missing {", ".join(missing)}'
1274 if 10 <= len(self
.engine_id
) <= 64 and \
1275 is_hex(self
.engine_id
) and \
1276 len(self
.engine_id
) % 2 == 0:
1279 raise SpecValidationError(
1280 'engine_id must be a string containing 10-64 hex characters. '
1281 'Its length must be divisible by 2'
1285 if self
.snmp_version
== 'V3':
1286 raise SpecValidationError(
1287 'Must provide an engine_id for SNMP V3 notifications'
1290 if not self
.snmp_destination
:
1291 raise SpecValidationError(
1292 'SNMP destination (snmp_destination) must be provided'
1295 valid
, description
= valid_addr(self
.snmp_destination
)
1297 raise SpecValidationError(
1298 f
'SNMP destination (snmp_destination) is invalid: {description}'
1300 if description
not in self
.valid_destination_types
:
1301 raise SpecValidationError(
1302 f
'SNMP destination (snmp_destination) type ({description}) is invalid. '
1303 f
'Must be either: {", ".join(sorted(self.valid_destination_types))}'
1307 yaml
.add_representer(SNMPGatewaySpec
, ServiceSpec
.yaml_representer
)