3 from collections
import OrderedDict
4 from functools
import wraps
5 from ipaddress
import ip_network
, ip_address
6 from typing
import Optional
, Dict
, Any
, List
, Union
, Callable
, Iterable
, Type
, TypeVar
, cast
, \
11 from ceph
.deployment
.hostspec
import HostSpec
, SpecValidationError
12 from ceph
.deployment
.utils
import unwrap_ipv6
14 ServiceSpecT
= TypeVar('ServiceSpecT', bound
='ServiceSpec')
15 FuncT
= TypeVar('FuncT', bound
=Callable
)
18 def assert_valid_host(name
: str) -> None:
19 p
= re
.compile('^[a-zA-Z0-9-]+$')
21 assert len(name
) <= 250, 'name is too long (max 250 chars)'
22 for part
in name
.split('.'):
23 assert len(part
) > 0, '.-delimited name component must not be empty'
24 assert len(part
) <= 63, '.-delimited name component must not be more than 63 chars'
25 assert p
.match(part
), 'name component must include only a-z, 0-9, and -'
26 except AssertionError as e
:
27 raise SpecValidationError(str(e
))
30 def handle_type_error(method
: FuncT
) -> FuncT
:
32 def inner(cls
: Any
, *args
: Any
, **kwargs
: Any
) -> Any
:
34 return method(cls
, *args
, **kwargs
)
35 except (TypeError, AttributeError) as e
:
36 error_msg
= '{}: {}'.format(cls
.__name
__, e
)
37 raise SpecValidationError(error_msg
)
38 return cast(FuncT
, inner
)
41 class HostPlacementSpec(NamedTuple
):
46 def __str__(self
) -> str:
50 res
+= ':' + self
.network
52 res
+= '=' + self
.name
57 def from_json(cls
, data
: Union
[dict, str]) -> 'HostPlacementSpec':
58 if isinstance(data
, str):
59 return cls
.parse(data
)
62 def to_json(self
) -> str:
66 def parse(cls
, host
, require_network
=True):
67 # type: (str, bool) -> HostPlacementSpec
69 Split host into host, network, and (optional) daemon name parts. The network
70 part can be an IP, CIDR, or ceph addrvec like '[v2:1.2.3.4:3300,v1:1.2.3.4:6789]'.
77 "myhost:1.2.3.0/24=name"
78 "myhost:[v2:1.2.3.4:3000]=name"
79 "myhost:[v2:1.2.3.4:3000,v1:1.2.3.4:6789]=name"
81 # Matches from start to : or = or until end of string
82 host_re
= r
'^(.*?)(:|=|$)'
83 # Matches from : to = or until end of string
84 ip_re
= r
':(.*?)(=|$)'
85 # Matches from = to end of string
89 host_spec
= cls('', '', '')
91 match_host
= re
.search(host_re
, host
)
93 host_spec
= host_spec
._replace
(hostname
=match_host
.group(1))
95 name_match
= re
.search(name_re
, host
)
97 host_spec
= host_spec
._replace
(name
=name_match
.group(1))
99 ip_match
= re
.search(ip_re
, host
)
101 host_spec
= host_spec
._replace
(network
=ip_match
.group(1))
103 if not require_network
:
106 networks
= list() # type: List[str]
107 network
= host_spec
.network
108 # in case we have [v2:1.2.3.4:3000,v1:1.2.3.4:6478]
110 networks
= [x
for x
in network
.split(',')]
113 networks
.append(network
)
115 for network
in networks
:
116 # only if we have versioned network configs
117 if network
.startswith('v') or network
.startswith('[v'):
118 # if this is ipv6 we can't just simply split on ':' so do
119 # a split once and rsplit once to leave us with just ipv6 addr
120 network
= network
.split(':', 1)[1]
121 network
= network
.rsplit(':', 1)[0]
123 # if subnets are defined, also verify the validity
127 ip_address(unwrap_ipv6(network
))
128 except ValueError as e
:
134 def validate(self
) -> None:
135 assert_valid_host(self
.hostname
)
138 class PlacementSpec(object):
140 For APIs that need to specify a host subset
144 label
=None, # type: Optional[str]
145 hosts
=None, # type: Union[List[str],List[HostPlacementSpec], None]
146 count
=None, # type: Optional[int]
147 count_per_host
=None, # type: Optional[int]
148 host_pattern
=None, # type: Optional[str]
150 # type: (...) -> None
152 self
.hosts
= [] # type: List[HostPlacementSpec]
155 self
.set_hosts(hosts
)
157 self
.count
= count
# type: Optional[int]
158 self
.count_per_host
= count_per_host
# type: Optional[int]
160 #: fnmatch patterns to select hosts. Can also be a single host.
161 self
.host_pattern
= host_pattern
# type: Optional[str]
165 def is_empty(self
) -> bool:
169 and not self
.host_pattern
170 and self
.count
is None
171 and self
.count_per_host
is None
174 def __eq__(self
, other
: Any
) -> bool:
175 if isinstance(other
, PlacementSpec
):
176 return self
.label
== other
.label \
177 and self
.hosts
== other
.hosts \
178 and self
.count
== other
.count \
179 and self
.host_pattern
== other
.host_pattern \
180 and self
.count_per_host
== other
.count_per_host
181 return NotImplemented
183 def set_hosts(self
, hosts
: Union
[List
[str], List
[HostPlacementSpec
]]) -> None:
184 # To backpopulate the .hosts attribute when using labels or count
185 # in the orchestrator backend.
186 if all([isinstance(host
, HostPlacementSpec
) for host
in hosts
]):
187 self
.hosts
= hosts
# type: ignore
189 self
.hosts
= [HostPlacementSpec
.parse(x
, require_network
=False) # type: ignore
193 def filter_matching_hosts(self
, _get_hosts_func
: Callable
) -> List
[str]:
194 return self
.filter_matching_hostspecs(_get_hosts_func(as_hostspec
=True))
196 def filter_matching_hostspecs(self
, hostspecs
: Iterable
[HostSpec
]) -> List
[str]:
198 all_hosts
= [hs
.hostname
for hs
in hostspecs
]
199 return [h
.hostname
for h
in self
.hosts
if h
.hostname
in all_hosts
]
201 return [hs
.hostname
for hs
in hostspecs
if self
.label
in hs
.labels
]
202 all_hosts
= [hs
.hostname
for hs
in hostspecs
]
203 if self
.host_pattern
:
204 return fnmatch
.filter(all_hosts
, self
.host_pattern
)
207 def get_target_count(self
, hostspecs
: Iterable
[HostSpec
]) -> int:
210 return len(self
.filter_matching_hostspecs(hostspecs
)) * (self
.count_per_host
or 1)
212 def pretty_str(self
) -> str:
215 ... ps = PlacementSpec(...) # For all placement specs:
216 ... PlacementSpec.from_string(ps.pretty_str()) == ps
220 kv
.append(';'.join([str(h
) for h
in self
.hosts
]))
222 kv
.append('count:%d' % self
.count
)
223 if self
.count_per_host
:
224 kv
.append('count-per-host:%d' % self
.count_per_host
)
226 kv
.append('label:%s' % self
.label
)
227 if self
.host_pattern
:
228 kv
.append(self
.host_pattern
)
231 def __repr__(self
) -> str:
234 kv
.append('count=%d' % self
.count
)
235 if self
.count_per_host
:
236 kv
.append('count_per_host=%d' % self
.count_per_host
)
238 kv
.append('label=%s' % repr(self
.label
))
240 kv
.append('hosts={!r}'.format(self
.hosts
))
241 if self
.host_pattern
:
242 kv
.append('host_pattern={!r}'.format(self
.host_pattern
))
243 return "PlacementSpec(%s)" % ', '.join(kv
)
247 def from_json(cls
, data
: dict) -> 'PlacementSpec':
249 hosts
= c
.get('hosts', [])
253 c
['hosts'].append(HostPlacementSpec
.from_json(host
))
258 def to_json(self
) -> dict:
259 r
: Dict
[str, Any
] = {}
261 r
['label'] = self
.label
263 r
['hosts'] = [host
.to_json() for host
in self
.hosts
]
265 r
['count'] = self
.count
266 if self
.count_per_host
:
267 r
['count_per_host'] = self
.count_per_host
268 if self
.host_pattern
:
269 r
['host_pattern'] = self
.host_pattern
272 def validate(self
) -> None:
273 if self
.hosts
and self
.label
:
274 # TODO: a less generic Exception
275 raise SpecValidationError('Host and label are mutually exclusive')
276 if self
.count
is not None and self
.count
<= 0:
277 raise SpecValidationError("num/count must be > 1")
278 if self
.count_per_host
is not None and self
.count_per_host
< 1:
279 raise SpecValidationError("count-per-host must be >= 1")
280 if self
.count_per_host
is not None and not (
285 raise SpecValidationError(
286 "count-per-host must be combined with label or hosts or host_pattern"
288 if self
.count
is not None and self
.count_per_host
is not None:
289 raise SpecValidationError("cannot combine count and count-per-host")
291 self
.count_per_host
is not None
293 and any([hs
.network
or hs
.name
for hs
in self
.hosts
])
295 raise SpecValidationError(
296 "count-per-host cannot be combined explicit placement with names or networks"
298 if self
.host_pattern
and self
.hosts
:
299 raise SpecValidationError('cannot combine host patterns and hosts')
304 def from_string(cls
, arg
):
305 # type: (Optional[str]) -> PlacementSpec
307 A single integer is parsed as a count:
308 >>> PlacementSpec.from_string('3')
309 PlacementSpec(count=3)
311 A list of names is parsed as host specifications:
312 >>> PlacementSpec.from_string('host1 host2')
313 PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacemen\
314 tSpec(hostname='host2', network='', name='')])
316 You can also prefix the hosts with a count as follows:
317 >>> PlacementSpec.from_string('2 host1 host2')
318 PlacementSpec(count=2, hosts=[HostPlacementSpec(hostname='host1', network='', name=''), Hos\
319 tPlacementSpec(hostname='host2', network='', name='')])
321 You can spefify labels using `label:<label>`
322 >>> PlacementSpec.from_string('label:mon')
323 PlacementSpec(label='mon')
325 Labels als support a count:
326 >>> PlacementSpec.from_string('3 label:mon')
327 PlacementSpec(count=3, label='mon')
329 fnmatch is also supported:
330 >>> PlacementSpec.from_string('data[1-3]')
331 PlacementSpec(host_pattern='data[1-3]')
333 >>> PlacementSpec.from_string(None)
336 if arg
is None or not arg
:
338 elif isinstance(arg
, str):
340 strings
= arg
.split(' ')
342 strings
= arg
.split(';')
343 elif ',' in arg
and '[' not in arg
:
344 # FIXME: this isn't quite right. we want to avoid breaking
345 # a list of mons with addrvecs... so we're basically allowing
346 # , most of the time, except when addrvecs are used. maybe
348 strings
= arg
.split(',')
352 raise SpecValidationError('invalid placement %s' % arg
)
355 count_per_host
= None
358 count
= int(strings
[0])
359 strings
= strings
[1:]
363 if s
.startswith('count:'):
365 count
= int(s
[len('count:'):])
371 if s
.startswith('count-per-host:'):
373 count_per_host
= int(s
[len('count-per-host:'):])
379 advanced_hostspecs
= [h
for h
in strings
if
380 (':' in h
or '=' in h
or not any(c
in '[]?*:=' for c
in h
)) and
382 for a_h
in advanced_hostspecs
:
385 labels
= [x
for x
in strings
if 'label:' in x
]
387 raise SpecValidationError('more than one label provided: {}'.format(labels
))
390 label
= labels
[0][6:] if labels
else None
392 host_patterns
= strings
393 if len(host_patterns
) > 1:
394 raise SpecValidationError(
395 'more than one host pattern provided: {}'.format(host_patterns
))
397 ps
= PlacementSpec(count
=count
,
398 count_per_host
=count_per_host
,
399 hosts
=advanced_hostspecs
,
401 host_pattern
=host_patterns
[0] if host_patterns
else None)
405 class ServiceSpec(object):
407 Details of service creation.
409 Request to the orchestrator for a cluster of daemons
410 such as MDS, RGW, iscsi gateway, MONs, MGRs, Prometheus
412 This structure is supposed to be enough information to
415 KNOWN_SERVICE_TYPES
= 'alertmanager crash grafana iscsi mds mgr mon nfs ' \
416 'node-exporter osd prometheus rbd-mirror rgw ' \
417 'container cephadm-exporter ingress cephfs-mirror'.split()
418 REQUIRES_SERVICE_ID
= 'iscsi mds nfs osd rgw container ingress '.split()
419 MANAGED_CONFIG_OPTIONS
= [
424 def _cls(cls
: Type
[ServiceSpecT
], service_type
: str) -> Type
[ServiceSpecT
]:
425 from ceph
.deployment
.drive_group
import DriveGroupSpec
429 'nfs': NFSServiceSpec
,
430 'osd': DriveGroupSpec
,
431 'iscsi': IscsiServiceSpec
,
432 'alertmanager': AlertManagerSpec
,
433 'ingress': IngressSpec
,
434 'container': CustomContainerSpec
,
435 'grafana': MonitoringSpec
,
436 'node-exporter': MonitoringSpec
,
437 'prometheus': MonitoringSpec
,
438 }.get(service_type
, cls
)
439 if ret
== ServiceSpec
and not service_type
:
440 raise SpecValidationError('Spec needs a "service_type" key.')
443 def __new__(cls
: Type
[ServiceSpecT
], *args
: Any
, **kwargs
: Any
) -> ServiceSpecT
:
445 Some Python foo to make sure, we don't have an object
446 like `ServiceSpec('rgw')` of type `ServiceSpec`. Now we have:
448 >>> type(ServiceSpec('rgw')) == type(RGWSpec('rgw'))
452 if cls
!= ServiceSpec
:
453 return object.__new
__(cls
)
454 service_type
= kwargs
.get('service_type', args
[0] if args
else None)
455 sub_cls
: Any
= cls
._cls
(service_type
)
456 return object.__new
__(sub_cls
)
460 service_id
: Optional
[str] = None,
461 placement
: Optional
[PlacementSpec
] = None,
462 count
: Optional
[int] = None,
463 config
: Optional
[Dict
[str, str]] = None,
464 unmanaged
: bool = False,
465 preview_only
: bool = False,
466 networks
: Optional
[List
[str]] = None,
469 #: See :ref:`orchestrator-cli-placement-spec`.
470 self
.placement
= PlacementSpec() if placement
is None else placement
# type: PlacementSpec
472 assert service_type
in ServiceSpec
.KNOWN_SERVICE_TYPES
, service_type
473 #: The type of the service. Needs to be either a Ceph
474 #: service (``mon``, ``crash``, ``mds``, ``mgr``, ``osd`` or
475 #: ``rbd-mirror``), a gateway (``nfs`` or ``rgw``), part of the
476 #: monitoring stack (``alertmanager``, ``grafana``, ``node-exporter`` or
477 #: ``prometheus``) or (``container``) for custom containers.
478 self
.service_type
= service_type
480 #: The name of the service. Required for ``iscsi``, ``mds``, ``nfs``, ``osd``, ``rgw``,
481 #: ``container``, ``ingress``
482 self
.service_id
= None
484 if self
.service_type
in self
.REQUIRES_SERVICE_ID
:
485 self
.service_id
= service_id
487 #: If set to ``true``, the orchestrator will not deploy nor remove
488 #: any daemon associated with this service. Placement and all other properties
489 #: will be ignored. This is useful, if you do not want this service to be
490 #: managed temporarily. For cephadm, See :ref:`cephadm-spec-unmanaged`
491 self
.unmanaged
= unmanaged
492 self
.preview_only
= preview_only
494 #: A list of network identities instructing the daemons to only bind
495 #: on the particular networks in that list. In case the cluster is distributed
496 #: across multiple networks, you can add multiple networks. See
497 #: :ref:`cephadm-monitoring-networks-ports`,
498 #: :ref:`cephadm-rgw-networks` and :ref:`cephadm-mgr-networks`.
499 self
.networks
: List
[str] = networks
or []
501 self
.config
: Optional
[Dict
[str, str]] = None
503 self
.config
= {k
.replace(' ', '_'): v
for k
, v
in config
.items()}
507 def from_json(cls
: Type
[ServiceSpecT
], json_spec
: Dict
) -> ServiceSpecT
:
509 Initialize 'ServiceSpec' object data from a json structure
511 There are two valid styles for service specs:
529 some_option: the_value
530 networks: [10.10.0.0/16]
535 In https://tracker.ceph.com/issues/45321 we decided that we'd like to
536 prefer the new style as it is more readable and provides a better
537 understanding of what fields are special for a give service type.
539 Note, we'll need to stay compatible with both versions for the
540 the next two major releases (octoups, pacific).
542 :param json_spec: A valid dict with ServiceSpec
547 if not isinstance(json_spec
, dict):
548 raise SpecValidationError(
549 f
'Service Spec is not an (JSON or YAML) object. got "{str(json_spec)}"')
551 json_spec
= cls
.normalize_json(json_spec
)
555 # kludge to make `from_json` compatible to `Orchestrator.describe_service`
556 # Open question: Remove `service_id` form to_json?
557 if c
.get('service_name', ''):
558 service_type_id
= c
['service_name'].split('.', 1)
560 if not c
.get('service_type', ''):
561 c
['service_type'] = service_type_id
[0]
562 if not c
.get('service_id', '') and len(service_type_id
) > 1:
563 c
['service_id'] = service_type_id
[1]
564 del c
['service_name']
566 service_type
= c
.get('service_type', '')
567 _cls
= cls
._cls
(service_type
)
570 del c
['status'] # kludge to make us compatible to `ServiceDescription.to_json()`
572 return _cls
._from
_json
_impl
(c
) # type: ignore
575 def normalize_json(json_spec
: dict) -> dict:
576 networks
= json_spec
.get('networks')
579 if isinstance(networks
, list):
581 if not isinstance(networks
, str):
582 raise SpecValidationError(f
'Networks ({networks}) must be a string or list of strings')
583 json_spec
['networks'] = [networks
]
587 def _from_json_impl(cls
: Type
[ServiceSpecT
], json_spec
: dict) -> ServiceSpecT
:
588 args
= {} # type: Dict[str, Any]
589 for k
, v
in json_spec
.items():
591 v
= PlacementSpec
.from_json(v
)
600 def service_name(self
) -> str:
601 n
= self
.service_type
603 n
+= '.' + self
.service_id
606 def get_port_start(self
) -> List
[int]:
607 # If defined, we will allocate and number ports starting at this
611 def get_virtual_ip(self
) -> Optional
[str]:
615 # type: () -> OrderedDict[str, Any]
616 ret
: OrderedDict
[str, Any
] = OrderedDict()
617 ret
['service_type'] = self
.service_type
619 ret
['service_id'] = self
.service_id
620 ret
['service_name'] = self
.service_name()
621 ret
['placement'] = self
.placement
.to_json()
623 ret
['unmanaged'] = self
.unmanaged
625 ret
['networks'] = self
.networks
628 for key
, val
in sorted(self
.__dict
__.items(), key
=lambda tpl
: tpl
[0]):
631 if hasattr(val
, 'to_json'):
639 def validate(self
) -> None:
640 if not self
.service_type
:
641 raise SpecValidationError('Cannot add Service: type required')
643 if self
.service_type
in self
.REQUIRES_SERVICE_ID
:
644 if not self
.service_id
:
645 raise SpecValidationError('Cannot add Service: id required')
646 if not re
.match('^[a-zA-Z0-9_.-]+$', self
.service_id
):
647 raise SpecValidationError('Service id contains invalid characters, '
648 'only [a-zA-Z0-9_.-] allowed')
649 elif self
.service_id
:
650 raise SpecValidationError(
651 f
'Service of type \'{self.service_type}\' should not contain a service id')
653 if self
.placement
is not None:
654 self
.placement
.validate()
656 for k
, v
in self
.config
.items():
657 if k
in self
.MANAGED_CONFIG_OPTIONS
:
658 raise SpecValidationError(
659 f
'Cannot set config option {k} in spec: it is managed by cephadm'
661 for network
in self
.networks
or []:
664 except ValueError as e
:
665 raise SpecValidationError(
666 f
'Cannot parse network {network}: {e}'
669 def __repr__(self
) -> str:
670 return "{}({!r})".format(self
.__class
__.__name
__, self
.__dict
__)
672 def __eq__(self
, other
: Any
) -> bool:
673 return (self
.__class
__ == other
.__class
__
675 self
.__dict
__ == other
.__dict
__)
677 def one_line_str(self
) -> str:
678 return '<{} for service_name={}>'.format(self
.__class
__.__name
__, self
.service_name())
681 def yaml_representer(dumper
: 'yaml.SafeDumper', data
: 'ServiceSpec') -> Any
:
682 return dumper
.represent_dict(cast(Mapping
, data
.to_json().items()))
685 yaml
.add_representer(ServiceSpec
, ServiceSpec
.yaml_representer
)
688 class NFSServiceSpec(ServiceSpec
):
690 service_type
: str = 'nfs',
691 service_id
: Optional
[str] = None,
692 placement
: Optional
[PlacementSpec
] = None,
693 unmanaged
: bool = False,
694 preview_only
: bool = False,
695 config
: Optional
[Dict
[str, str]] = None,
696 networks
: Optional
[List
[str]] = None,
697 port
: Optional
[int] = None,
699 assert service_type
== 'nfs'
700 super(NFSServiceSpec
, self
).__init
__(
701 'nfs', service_id
=service_id
,
702 placement
=placement
, unmanaged
=unmanaged
, preview_only
=preview_only
,
703 config
=config
, networks
=networks
)
707 def get_port_start(self
) -> List
[int]:
712 def rados_config_name(self
):
714 return 'conf-' + self
.service_name()
717 yaml
.add_representer(NFSServiceSpec
, ServiceSpec
.yaml_representer
)
720 class RGWSpec(ServiceSpec
):
722 Settings to configure a (multisite) Ceph RGW
727 service_id: myrealm.myzone
732 rgw_frontend_port: 1234
733 rgw_frontend_type: beast
734 rgw_frontend_ssl_certificate: ...
736 See also: :ref:`orchestrator-cli-service-spec`
739 MANAGED_CONFIG_OPTIONS
= ServiceSpec
.MANAGED_CONFIG_OPTIONS
+ [
746 service_type
: str = 'rgw',
747 service_id
: Optional
[str] = None,
748 placement
: Optional
[PlacementSpec
] = None,
749 rgw_realm
: Optional
[str] = None,
750 rgw_zone
: Optional
[str] = None,
751 rgw_frontend_port
: Optional
[int] = None,
752 rgw_frontend_ssl_certificate
: Optional
[List
[str]] = None,
753 rgw_frontend_type
: Optional
[str] = None,
754 unmanaged
: bool = False,
756 preview_only
: bool = False,
757 config
: Optional
[Dict
[str, str]] = None,
758 networks
: Optional
[List
[str]] = None,
759 subcluster
: Optional
[str] = None, # legacy, only for from_json on upgrade
761 assert service_type
== 'rgw', service_type
763 # for backward compatibility with octopus spec files,
764 if not service_id
and (rgw_realm
and rgw_zone
):
765 service_id
= rgw_realm
+ '.' + rgw_zone
767 super(RGWSpec
, self
).__init
__(
768 'rgw', service_id
=service_id
,
769 placement
=placement
, unmanaged
=unmanaged
,
770 preview_only
=preview_only
, config
=config
, networks
=networks
)
772 #: The RGW realm associated with this service. Needs to be manually created
773 self
.rgw_realm
: Optional
[str] = rgw_realm
774 #: The RGW zone associated with this service. Needs to be manually created
775 self
.rgw_zone
: Optional
[str] = rgw_zone
776 #: Port of the RGW daemons
777 self
.rgw_frontend_port
: Optional
[int] = rgw_frontend_port
778 #: List of SSL certificates
779 self
.rgw_frontend_ssl_certificate
: Optional
[List
[str]] = rgw_frontend_ssl_certificate
780 #: civetweb or beast (default: beast). See :ref:`rgw_frontends`
781 self
.rgw_frontend_type
: Optional
[str] = rgw_frontend_type
785 def get_port_start(self
) -> List
[int]:
786 return [self
.get_port()]
788 def get_port(self
) -> int:
789 if self
.rgw_frontend_port
:
790 return self
.rgw_frontend_port
796 def validate(self
) -> None:
797 super(RGWSpec
, self
).validate()
799 if self
.rgw_realm
and not self
.rgw_zone
:
800 raise SpecValidationError(
801 'Cannot add RGW: Realm specified but no zone specified')
802 if self
.rgw_zone
and not self
.rgw_realm
:
803 raise SpecValidationError(
804 'Cannot add RGW: Zone specified but no realm specified')
807 yaml
.add_representer(RGWSpec
, ServiceSpec
.yaml_representer
)
810 class IscsiServiceSpec(ServiceSpec
):
812 service_type
: str = 'iscsi',
813 service_id
: Optional
[str] = None,
814 pool
: Optional
[str] = None,
815 trusted_ip_list
: Optional
[str] = None,
816 api_port
: Optional
[int] = None,
817 api_user
: Optional
[str] = None,
818 api_password
: Optional
[str] = None,
819 api_secure
: Optional
[bool] = None,
820 ssl_cert
: Optional
[str] = None,
821 ssl_key
: Optional
[str] = None,
822 placement
: Optional
[PlacementSpec
] = None,
823 unmanaged
: bool = False,
824 preview_only
: bool = False,
825 config
: Optional
[Dict
[str, str]] = None,
826 networks
: Optional
[List
[str]] = None,
828 assert service_type
== 'iscsi'
829 super(IscsiServiceSpec
, self
).__init
__('iscsi', service_id
=service_id
,
830 placement
=placement
, unmanaged
=unmanaged
,
831 preview_only
=preview_only
,
832 config
=config
, networks
=networks
)
834 #: RADOS pool where ceph-iscsi config data is stored.
836 #: list of trusted IP addresses
837 self
.trusted_ip_list
= trusted_ip_list
838 #: ``api_port`` as defined in the ``iscsi-gateway.cfg``
839 self
.api_port
= api_port
840 #: ``api_user`` as defined in the ``iscsi-gateway.cfg``
841 self
.api_user
= api_user
842 #: ``api_password`` as defined in the ``iscsi-gateway.cfg``
843 self
.api_password
= api_password
844 #: ``api_secure`` as defined in the ``iscsi-gateway.cfg``
845 self
.api_secure
= api_secure
847 self
.ssl_cert
= ssl_cert
849 self
.ssl_key
= ssl_key
851 if not self
.api_secure
and self
.ssl_cert
and self
.ssl_key
:
852 self
.api_secure
= True
854 def validate(self
) -> None:
855 super(IscsiServiceSpec
, self
).validate()
858 raise SpecValidationError(
859 'Cannot add ISCSI: No Pool specified')
861 # Do not need to check for api_user and api_password as they
862 # now default to 'admin' when setting up the gateway url. Older
863 # iSCSI specs from before this change should be fine as they will
864 # have been required to have an api_user and api_password set and
865 # will be unaffected by the new default value.
868 yaml
.add_representer(IscsiServiceSpec
, ServiceSpec
.yaml_representer
)
871 class AlertManagerSpec(ServiceSpec
):
873 service_type
: str = 'alertmanager',
874 service_id
: Optional
[str] = None,
875 placement
: Optional
[PlacementSpec
] = None,
876 unmanaged
: bool = False,
877 preview_only
: bool = False,
878 user_data
: Optional
[Dict
[str, Any
]] = None,
879 config
: Optional
[Dict
[str, str]] = None,
880 networks
: Optional
[List
[str]] = None,
881 port
: Optional
[int] = None,
883 assert service_type
== 'alertmanager'
884 super(AlertManagerSpec
, self
).__init
__(
885 'alertmanager', service_id
=service_id
,
886 placement
=placement
, unmanaged
=unmanaged
,
887 preview_only
=preview_only
, config
=config
, networks
=networks
)
889 # Custom configuration.
892 # service_type: alertmanager
895 # default_webhook_urls:
900 # default_webhook_urls - A list of additional URL's that are
901 # added to the default receivers'
902 # <webhook_configs> configuration.
903 self
.user_data
= user_data
or {}
906 def get_port_start(self
) -> List
[int]:
907 return [self
.get_port(), 9094]
909 def get_port(self
) -> int:
915 def validate(self
) -> None:
916 super(AlertManagerSpec
, self
).validate()
918 if self
.port
== 9094:
919 raise SpecValidationError(
920 'Port 9094 is reserved for AlertManager cluster listen address')
923 yaml
.add_representer(AlertManagerSpec
, ServiceSpec
.yaml_representer
)
926 class IngressSpec(ServiceSpec
):
928 service_type
: str = 'ingress',
929 service_id
: Optional
[str] = None,
930 config
: Optional
[Dict
[str, str]] = None,
931 networks
: Optional
[List
[str]] = None,
932 placement
: Optional
[PlacementSpec
] = None,
933 backend_service
: Optional
[str] = None,
934 frontend_port
: Optional
[int] = None,
935 ssl_cert
: Optional
[str] = None,
936 ssl_key
: Optional
[str] = None,
937 ssl_dh_param
: Optional
[str] = None,
938 ssl_ciphers
: Optional
[List
[str]] = None,
939 ssl_options
: Optional
[List
[str]] = None,
940 monitor_port
: Optional
[int] = None,
941 monitor_user
: Optional
[str] = None,
942 monitor_password
: Optional
[str] = None,
943 enable_stats
: Optional
[bool] = None,
944 keepalived_password
: Optional
[str] = None,
945 virtual_ip
: Optional
[str] = None,
946 virtual_interface_networks
: Optional
[List
[str]] = [],
947 unmanaged
: bool = False,
950 assert service_type
== 'ingress'
951 super(IngressSpec
, self
).__init
__(
952 'ingress', service_id
=service_id
,
953 placement
=placement
, config
=config
,
956 self
.backend_service
= backend_service
957 self
.frontend_port
= frontend_port
958 self
.ssl_cert
= ssl_cert
959 self
.ssl_key
= ssl_key
960 self
.ssl_dh_param
= ssl_dh_param
961 self
.ssl_ciphers
= ssl_ciphers
962 self
.ssl_options
= ssl_options
963 self
.monitor_port
= monitor_port
964 self
.monitor_user
= monitor_user
965 self
.monitor_password
= monitor_password
966 self
.keepalived_password
= keepalived_password
967 self
.virtual_ip
= virtual_ip
968 self
.virtual_interface_networks
= virtual_interface_networks
or []
969 self
.unmanaged
= unmanaged
972 def get_port_start(self
) -> List
[int]:
973 return [cast(int, self
.frontend_port
),
974 cast(int, self
.monitor_port
)]
976 def get_virtual_ip(self
) -> Optional
[str]:
977 return self
.virtual_ip
979 def validate(self
) -> None:
980 super(IngressSpec
, self
).validate()
982 if not self
.backend_service
:
983 raise SpecValidationError(
984 'Cannot add ingress: No backend_service specified')
985 if not self
.frontend_port
:
986 raise SpecValidationError(
987 'Cannot add ingress: No frontend_port specified')
988 if not self
.monitor_port
:
989 raise SpecValidationError(
990 'Cannot add ingress: No monitor_port specified')
991 if not self
.virtual_ip
:
992 raise SpecValidationError(
993 'Cannot add ingress: No virtual_ip provided')
996 yaml
.add_representer(IngressSpec
, ServiceSpec
.yaml_representer
)
999 class CustomContainerSpec(ServiceSpec
):
1001 service_type
: str = 'container',
1002 service_id
: Optional
[str] = None,
1003 config
: Optional
[Dict
[str, str]] = None,
1004 networks
: Optional
[List
[str]] = None,
1005 placement
: Optional
[PlacementSpec
] = None,
1006 unmanaged
: bool = False,
1007 preview_only
: bool = False,
1008 image
: Optional
[str] = None,
1009 entrypoint
: Optional
[str] = None,
1010 uid
: Optional
[int] = None,
1011 gid
: Optional
[int] = None,
1012 volume_mounts
: Optional
[Dict
[str, str]] = {},
1013 args
: Optional
[List
[str]] = [],
1014 envs
: Optional
[List
[str]] = [],
1015 privileged
: Optional
[bool] = False,
1016 bind_mounts
: Optional
[List
[List
[str]]] = None,
1017 ports
: Optional
[List
[int]] = [],
1018 dirs
: Optional
[List
[str]] = [],
1019 files
: Optional
[Dict
[str, Any
]] = {},
1021 assert service_type
== 'container'
1022 assert service_id
is not None
1023 assert image
is not None
1025 super(CustomContainerSpec
, self
).__init
__(
1026 service_type
, service_id
,
1027 placement
=placement
, unmanaged
=unmanaged
,
1028 preview_only
=preview_only
, config
=config
,
1032 self
.entrypoint
= entrypoint
1035 self
.volume_mounts
= volume_mounts
1038 self
.privileged
= privileged
1039 self
.bind_mounts
= bind_mounts
1044 def config_json(self
) -> Dict
[str, Any
]:
1046 Helper function to get the value of the `--config-json` cephadm
1047 command line option. It will contain all specification properties
1048 that haven't a `None` value. Such properties will get default
1050 :return: Returns a dictionary containing all specification
1054 for prop
in ['image', 'entrypoint', 'uid', 'gid', 'args',
1055 'envs', 'volume_mounts', 'privileged',
1056 'bind_mounts', 'ports', 'dirs', 'files']:
1057 value
= getattr(self
, prop
)
1058 if value
is not None:
1059 config_json
[prop
] = value
1063 yaml
.add_representer(CustomContainerSpec
, ServiceSpec
.yaml_representer
)
1066 class MonitoringSpec(ServiceSpec
):
1069 service_id
: Optional
[str] = None,
1070 config
: Optional
[Dict
[str, str]] = None,
1071 networks
: Optional
[List
[str]] = None,
1072 placement
: Optional
[PlacementSpec
] = None,
1073 unmanaged
: bool = False,
1074 preview_only
: bool = False,
1075 port
: Optional
[int] = None,
1077 assert service_type
in ['grafana', 'node-exporter', 'prometheus']
1079 super(MonitoringSpec
, self
).__init
__(
1080 service_type
, service_id
,
1081 placement
=placement
, unmanaged
=unmanaged
,
1082 preview_only
=preview_only
, config
=config
,
1085 self
.service_type
= service_type
1088 def get_port_start(self
) -> List
[int]:
1089 return [self
.get_port()]
1091 def get_port(self
) -> int:
1095 return {'prometheus': 9095,
1096 'node-exporter': 9100,
1097 'grafana': 3000}[self
.service_type
]