5 from collections
import OrderedDict
6 from contextlib
import contextmanager
7 from functools
import wraps
8 from ipaddress
import ip_network
, ip_address
9 from typing
import Optional
, Dict
, Any
, List
, Union
, Callable
, Iterable
, Type
, TypeVar
, cast
, \
10 NamedTuple
, Mapping
, Iterator
14 from ceph
.deployment
.hostspec
import HostSpec
, SpecValidationError
, assert_valid_host
15 from ceph
.deployment
.utils
import unwrap_ipv6
, valid_addr
16 from ceph
.utils
import is_hex
18 ServiceSpecT
= TypeVar('ServiceSpecT', bound
='ServiceSpec')
19 FuncT
= TypeVar('FuncT', bound
=Callable
)
22 def handle_type_error(method
: FuncT
) -> FuncT
:
24 def inner(cls
: Any
, *args
: Any
, **kwargs
: Any
) -> Any
:
26 return method(cls
, *args
, **kwargs
)
27 except (TypeError, AttributeError) as e
:
28 error_msg
= '{}: {}'.format(cls
.__name
__, e
)
29 raise SpecValidationError(error_msg
)
30 return cast(FuncT
, inner
)
33 class HostPlacementSpec(NamedTuple
):
38 def __str__(self
) -> str:
42 res
+= ':' + self
.network
44 res
+= '=' + self
.name
49 def from_json(cls
, data
: Union
[dict, str]) -> 'HostPlacementSpec':
50 if isinstance(data
, str):
51 return cls
.parse(data
)
54 def to_json(self
) -> str:
58 def parse(cls
, host
, require_network
=True):
59 # type: (str, bool) -> HostPlacementSpec
61 Split host into host, network, and (optional) daemon name parts. The network
62 part can be an IP, CIDR, or ceph addrvec like '[v2:1.2.3.4:3300,v1:1.2.3.4:6789]'.
69 "myhost:1.2.3.0/24=name"
70 "myhost:[v2:1.2.3.4:3000]=name"
71 "myhost:[v2:1.2.3.4:3000,v1:1.2.3.4:6789]=name"
73 # Matches from start to : or = or until end of string
74 host_re
= r
'^(.*?)(:|=|$)'
75 # Matches from : to = or until end of string
76 ip_re
= r
':(.*?)(=|$)'
77 # Matches from = to end of string
81 host_spec
= cls('', '', '')
83 match_host
= re
.search(host_re
, host
)
85 host_spec
= host_spec
._replace
(hostname
=match_host
.group(1))
87 name_match
= re
.search(name_re
, host
)
89 host_spec
= host_spec
._replace
(name
=name_match
.group(1))
91 ip_match
= re
.search(ip_re
, host
)
93 host_spec
= host_spec
._replace
(network
=ip_match
.group(1))
95 if not require_network
:
98 networks
= list() # type: List[str]
99 network
= host_spec
.network
100 # in case we have [v2:1.2.3.4:3000,v1:1.2.3.4:6478]
102 networks
= [x
for x
in network
.split(',')]
105 networks
.append(network
)
107 for network
in networks
:
108 # only if we have versioned network configs
109 if network
.startswith('v') or network
.startswith('[v'):
110 # if this is ipv6 we can't just simply split on ':' so do
111 # a split once and rsplit once to leave us with just ipv6 addr
112 network
= network
.split(':', 1)[1]
113 network
= network
.rsplit(':', 1)[0]
115 # if subnets are defined, also verify the validity
119 ip_address(unwrap_ipv6(network
))
120 except ValueError as e
:
126 def validate(self
) -> None:
127 assert_valid_host(self
.hostname
)
130 class PlacementSpec(object):
132 For APIs that need to specify a host subset
136 label
=None, # type: Optional[str]
137 hosts
=None, # type: Union[List[str],List[HostPlacementSpec], None]
138 count
=None, # type: Optional[int]
139 count_per_host
=None, # type: Optional[int]
140 host_pattern
=None, # type: Optional[str]
142 # type: (...) -> None
144 self
.hosts
= [] # type: List[HostPlacementSpec]
147 self
.set_hosts(hosts
)
149 self
.count
= count
# type: Optional[int]
150 self
.count_per_host
= count_per_host
# type: Optional[int]
152 #: fnmatch patterns to select hosts. Can also be a single host.
153 self
.host_pattern
= host_pattern
# type: Optional[str]
157 def is_empty(self
) -> bool:
161 and not self
.host_pattern
162 and self
.count
is None
163 and self
.count_per_host
is None
166 def __eq__(self
, other
: Any
) -> bool:
167 if isinstance(other
, PlacementSpec
):
168 return self
.label
== other
.label \
169 and self
.hosts
== other
.hosts \
170 and self
.count
== other
.count \
171 and self
.host_pattern
== other
.host_pattern \
172 and self
.count_per_host
== other
.count_per_host
173 return NotImplemented
175 def set_hosts(self
, hosts
: Union
[List
[str], List
[HostPlacementSpec
]]) -> None:
176 # To backpopulate the .hosts attribute when using labels or count
177 # in the orchestrator backend.
178 if all([isinstance(host
, HostPlacementSpec
) for host
in hosts
]):
179 self
.hosts
= hosts
# type: ignore
181 self
.hosts
= [HostPlacementSpec
.parse(x
, require_network
=False) # type: ignore
185 def filter_matching_hosts(self
, _get_hosts_func
: Callable
) -> List
[str]:
186 return self
.filter_matching_hostspecs(_get_hosts_func(as_hostspec
=True))
188 def filter_matching_hostspecs(self
, hostspecs
: Iterable
[HostSpec
]) -> List
[str]:
190 all_hosts
= [hs
.hostname
for hs
in hostspecs
]
191 return [h
.hostname
for h
in self
.hosts
if h
.hostname
in all_hosts
]
193 return [hs
.hostname
for hs
in hostspecs
if self
.label
in hs
.labels
]
194 all_hosts
= [hs
.hostname
for hs
in hostspecs
]
195 if self
.host_pattern
:
196 return fnmatch
.filter(all_hosts
, self
.host_pattern
)
199 def get_target_count(self
, hostspecs
: Iterable
[HostSpec
]) -> int:
202 return len(self
.filter_matching_hostspecs(hostspecs
)) * (self
.count_per_host
or 1)
204 def pretty_str(self
) -> str:
207 ... ps = PlacementSpec(...) # For all placement specs:
208 ... PlacementSpec.from_string(ps.pretty_str()) == ps
212 kv
.append(';'.join([str(h
) for h
in self
.hosts
]))
214 kv
.append('count:%d' % self
.count
)
215 if self
.count_per_host
:
216 kv
.append('count-per-host:%d' % self
.count_per_host
)
218 kv
.append('label:%s' % self
.label
)
219 if self
.host_pattern
:
220 kv
.append(self
.host_pattern
)
223 def __repr__(self
) -> str:
226 kv
.append('count=%d' % self
.count
)
227 if self
.count_per_host
:
228 kv
.append('count_per_host=%d' % self
.count_per_host
)
230 kv
.append('label=%s' % repr(self
.label
))
232 kv
.append('hosts={!r}'.format(self
.hosts
))
233 if self
.host_pattern
:
234 kv
.append('host_pattern={!r}'.format(self
.host_pattern
))
235 return "PlacementSpec(%s)" % ', '.join(kv
)
239 def from_json(cls
, data
: dict) -> 'PlacementSpec':
241 hosts
= c
.get('hosts', [])
245 c
['hosts'].append(HostPlacementSpec
.from_json(host
))
250 def to_json(self
) -> dict:
251 r
: Dict
[str, Any
] = {}
253 r
['label'] = self
.label
255 r
['hosts'] = [host
.to_json() for host
in self
.hosts
]
257 r
['count'] = self
.count
258 if self
.count_per_host
:
259 r
['count_per_host'] = self
.count_per_host
260 if self
.host_pattern
:
261 r
['host_pattern'] = self
.host_pattern
264 def validate(self
) -> None:
265 if self
.hosts
and self
.label
:
266 # TODO: a less generic Exception
267 raise SpecValidationError('Host and label are mutually exclusive')
268 if self
.count
is not None:
270 intval
= int(self
.count
)
271 except (ValueError, TypeError):
272 raise SpecValidationError("num/count must be a numeric value")
273 if self
.count
!= intval
:
274 raise SpecValidationError("num/count must be an integer value")
276 raise SpecValidationError("num/count must be >= 1")
277 if self
.count_per_host
is not None:
279 intval
= int(self
.count_per_host
)
280 except (ValueError, TypeError):
281 raise SpecValidationError("count-per-host must be a numeric value")
282 if self
.count_per_host
!= intval
:
283 raise SpecValidationError("count-per-host must be an integer value")
284 if self
.count_per_host
< 1:
285 raise SpecValidationError("count-per-host must be >= 1")
286 if self
.count_per_host
is not None and not (
291 raise SpecValidationError(
292 "count-per-host must be combined with label or hosts or host_pattern"
294 if self
.count
is not None and self
.count_per_host
is not None:
295 raise SpecValidationError("cannot combine count and count-per-host")
297 self
.count_per_host
is not None
299 and any([hs
.network
or hs
.name
for hs
in self
.hosts
])
301 raise SpecValidationError(
302 "count-per-host cannot be combined explicit placement with names or networks"
304 if self
.host_pattern
:
305 if not isinstance(self
.host_pattern
, str):
306 raise SpecValidationError('host_pattern must be of type string')
308 raise SpecValidationError('cannot combine host patterns and hosts')
314 def from_string(cls
, arg
):
315 # type: (Optional[str]) -> PlacementSpec
317 A single integer is parsed as a count:
319 >>> PlacementSpec.from_string('3')
320 PlacementSpec(count=3)
322 A list of names is parsed as host specifications:
324 >>> PlacementSpec.from_string('host1 host2')
325 PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacemen\
326 tSpec(hostname='host2', network='', name='')])
328 You can also prefix the hosts with a count as follows:
330 >>> PlacementSpec.from_string('2 host1 host2')
331 PlacementSpec(count=2, hosts=[HostPlacementSpec(hostname='host1', network='', name=''), Hos\
332 tPlacementSpec(hostname='host2', network='', name='')])
334 You can specify labels using `label:<label>`
336 >>> PlacementSpec.from_string('label:mon')
337 PlacementSpec(label='mon')
339 Labels also support a count:
341 >>> PlacementSpec.from_string('3 label:mon')
342 PlacementSpec(count=3, label='mon')
344 fnmatch is also supported:
346 >>> PlacementSpec.from_string('data[1-3]')
347 PlacementSpec(host_pattern='data[1-3]')
349 >>> PlacementSpec.from_string(None)
352 if arg
is None or not arg
:
354 elif isinstance(arg
, str):
356 strings
= arg
.split(' ')
358 strings
= arg
.split(';')
359 elif ',' in arg
and '[' not in arg
:
360 # FIXME: this isn't quite right. we want to avoid breaking
361 # a list of mons with addrvecs... so we're basically allowing
362 # , most of the time, except when addrvecs are used. maybe
364 strings
= arg
.split(',')
368 raise SpecValidationError('invalid placement %s' % arg
)
371 count_per_host
= None
374 count
= int(strings
[0])
375 strings
= strings
[1:]
379 if s
.startswith('count:'):
381 count
= int(s
[len('count:'):])
387 if s
.startswith('count-per-host:'):
389 count_per_host
= int(s
[len('count-per-host:'):])
395 advanced_hostspecs
= [h
for h
in strings
if
396 (':' in h
or '=' in h
or not any(c
in '[]?*:=' for c
in h
)) and
398 for a_h
in advanced_hostspecs
:
401 labels
= [x
for x
in strings
if 'label:' in x
]
403 raise SpecValidationError('more than one label provided: {}'.format(labels
))
406 label
= labels
[0][6:] if labels
else None
408 host_patterns
= strings
409 if len(host_patterns
) > 1:
410 raise SpecValidationError(
411 'more than one host pattern provided: {}'.format(host_patterns
))
413 ps
= PlacementSpec(count
=count
,
414 count_per_host
=count_per_host
,
415 hosts
=advanced_hostspecs
,
417 host_pattern
=host_patterns
[0] if host_patterns
else None)
421 _service_spec_from_json_validate
= True
426 Class to specify custom config files to be mounted in daemon's container
429 _fields
= ['content', 'mount_path']
431 def __init__(self
, content
: str, mount_path
: str) -> None:
432 self
.content
: str = content
433 self
.mount_path
: str = mount_path
436 def to_json(self
) -> Dict
[str, Any
]:
438 'content': self
.content
,
439 'mount_path': self
.mount_path
,
443 def from_json(cls
, data
: Dict
[str, Any
]) -> "CustomConfig":
444 for k
in cls
._fields
:
446 raise SpecValidationError(f
'CustomConfig must have "{k}" field')
447 for k
in data
.keys():
448 if k
not in cls
._fields
:
449 raise SpecValidationError(f
'CustomConfig got unknown field "{k}"')
453 def filename(self
) -> str:
454 return os
.path
.basename(self
.mount_path
)
456 def __eq__(self
, other
: Any
) -> bool:
457 if isinstance(other
, CustomConfig
):
459 self
.content
== other
.content
460 and self
.mount_path
== other
.mount_path
462 return NotImplemented
464 def __repr__(self
) -> str:
465 return f
'CustomConfig({self.mount_path})'
467 def validate(self
) -> None:
468 if not isinstance(self
.content
, str):
469 raise SpecValidationError(
470 f
'CustomConfig content must be a string. Got {type(self.content)}')
471 if not isinstance(self
.mount_path
, str):
472 raise SpecValidationError(
473 f
'CustomConfig content must be a string. Got {type(self.mount_path)}')
477 def service_spec_allow_invalid_from_json() -> Iterator
[None]:
479 I know this is evil, but unfortunately `ceph orch ls`
480 may return invalid OSD specs for OSDs not associated to
481 and specs. If you have a better idea, please!
483 global _service_spec_from_json_validate
484 _service_spec_from_json_validate
= False
486 _service_spec_from_json_validate
= True
489 class ServiceSpec(object):
491 Details of service creation.
493 Request to the orchestrator for a cluster of daemons
494 such as MDS, RGW, iscsi gateway, MONs, MGRs, Prometheus
496 This structure is supposed to be enough information to
499 KNOWN_SERVICE_TYPES
= 'alertmanager crash grafana iscsi loki promtail mds mgr mon nfs ' \
500 'node-exporter osd prometheus rbd-mirror rgw agent ceph-exporter ' \
501 'container ingress cephfs-mirror snmp-gateway jaeger-tracing ' \
502 'elasticsearch jaeger-agent jaeger-collector jaeger-query'.split()
503 REQUIRES_SERVICE_ID
= 'iscsi mds nfs rgw container ingress '.split()
504 MANAGED_CONFIG_OPTIONS
= [
509 def _cls(cls
: Type
[ServiceSpecT
], service_type
: str) -> Type
[ServiceSpecT
]:
510 from ceph
.deployment
.drive_group
import DriveGroupSpec
515 'nfs': NFSServiceSpec
,
516 'osd': DriveGroupSpec
,
518 'iscsi': IscsiServiceSpec
,
519 'alertmanager': AlertManagerSpec
,
520 'ingress': IngressSpec
,
521 'container': CustomContainerSpec
,
522 'grafana': GrafanaSpec
,
523 'node-exporter': MonitoringSpec
,
524 'ceph-exporter': CephExporterSpec
,
525 'prometheus': PrometheusSpec
,
526 'loki': MonitoringSpec
,
527 'promtail': MonitoringSpec
,
528 'snmp-gateway': SNMPGatewaySpec
,
529 'elasticsearch': TracingSpec
,
530 'jaeger-agent': TracingSpec
,
531 'jaeger-collector': TracingSpec
,
532 'jaeger-query': TracingSpec
,
533 'jaeger-tracing': TracingSpec
,
534 }.get(service_type
, cls
)
535 if ret
== ServiceSpec
and not service_type
:
536 raise SpecValidationError('Spec needs a "service_type" key.')
539 def __new__(cls
: Type
[ServiceSpecT
], *args
: Any
, **kwargs
: Any
) -> ServiceSpecT
:
541 Some Python foo to make sure, we don't have an object
542 like `ServiceSpec('rgw')` of type `ServiceSpec`. Now we have:
544 >>> type(ServiceSpec('rgw')) == type(RGWSpec('rgw'))
548 if cls
!= ServiceSpec
:
549 return object.__new
__(cls
)
550 service_type
= kwargs
.get('service_type', args
[0] if args
else None)
551 sub_cls
: Any
= cls
._cls
(service_type
)
552 return object.__new
__(sub_cls
)
556 service_id
: Optional
[str] = None,
557 placement
: Optional
[PlacementSpec
] = None,
558 count
: Optional
[int] = None,
559 config
: Optional
[Dict
[str, str]] = None,
560 unmanaged
: bool = False,
561 preview_only
: bool = False,
562 networks
: Optional
[List
[str]] = None,
563 extra_container_args
: Optional
[List
[str]] = None,
564 extra_entrypoint_args
: Optional
[List
[str]] = None,
565 custom_configs
: Optional
[List
[CustomConfig
]] = None,
568 #: See :ref:`orchestrator-cli-placement-spec`.
569 self
.placement
= PlacementSpec() if placement
is None else placement
# type: PlacementSpec
571 assert service_type
in ServiceSpec
.KNOWN_SERVICE_TYPES
, service_type
572 #: The type of the service. Needs to be either a Ceph
573 #: service (``mon``, ``crash``, ``mds``, ``mgr``, ``osd`` or
574 #: ``rbd-mirror``), a gateway (``nfs`` or ``rgw``), part of the
575 #: monitoring stack (``alertmanager``, ``grafana``, ``node-exporter`` or
576 #: ``prometheus``) or (``container``) for custom containers.
577 self
.service_type
= service_type
579 #: The name of the service. Required for ``iscsi``, ``mds``, ``nfs``, ``osd``, ``rgw``,
580 #: ``container``, ``ingress``
581 self
.service_id
= None
583 if self
.service_type
in self
.REQUIRES_SERVICE_ID
or self
.service_type
== 'osd':
584 self
.service_id
= service_id
586 #: If set to ``true``, the orchestrator will not deploy nor remove
587 #: any daemon associated with this service. Placement and all other properties
588 #: will be ignored. This is useful, if you do not want this service to be
589 #: managed temporarily. For cephadm, See :ref:`cephadm-spec-unmanaged`
590 self
.unmanaged
= unmanaged
591 self
.preview_only
= preview_only
593 #: A list of network identities instructing the daemons to only bind
594 #: on the particular networks in that list. In case the cluster is distributed
595 #: across multiple networks, you can add multiple networks. See
596 #: :ref:`cephadm-monitoring-networks-ports`,
597 #: :ref:`cephadm-rgw-networks` and :ref:`cephadm-mgr-networks`.
598 self
.networks
: List
[str] = networks
or []
600 self
.config
: Optional
[Dict
[str, str]] = None
602 self
.config
= {k
.replace(' ', '_'): v
for k
, v
in config
.items()}
604 self
.extra_container_args
: Optional
[List
[str]] = extra_container_args
605 self
.extra_entrypoint_args
: Optional
[List
[str]] = extra_entrypoint_args
606 self
.custom_configs
: Optional
[List
[CustomConfig
]] = custom_configs
610 def from_json(cls
: Type
[ServiceSpecT
], json_spec
: Dict
) -> ServiceSpecT
:
612 Initialize 'ServiceSpec' object data from a json structure
614 There are two valid styles for service specs:
632 some_option: the_value
633 networks: [10.10.0.0/16]
638 In https://tracker.ceph.com/issues/45321 we decided that we'd like to
639 prefer the new style as it is more readable and provides a better
640 understanding of what fields are special for a give service type.
642 Note, we'll need to stay compatible with both versions for the
643 the next two major releases (octopus, pacific).
645 :param json_spec: A valid dict with ServiceSpec
649 if not isinstance(json_spec
, dict):
650 raise SpecValidationError(
651 f
'Service Spec is not an (JSON or YAML) object. got "{str(json_spec)}"')
653 json_spec
= cls
.normalize_json(json_spec
)
657 # kludge to make `from_json` compatible to `Orchestrator.describe_service`
658 # Open question: Remove `service_id` form to_json?
659 if c
.get('service_name', ''):
660 service_type_id
= c
['service_name'].split('.', 1)
662 if not c
.get('service_type', ''):
663 c
['service_type'] = service_type_id
[0]
664 if not c
.get('service_id', '') and len(service_type_id
) > 1:
665 c
['service_id'] = service_type_id
[1]
666 del c
['service_name']
668 service_type
= c
.get('service_type', '')
669 _cls
= cls
._cls
(service_type
)
672 del c
['status'] # kludge to make us compatible to `ServiceDescription.to_json()`
674 return _cls
._from
_json
_impl
(c
) # type: ignore
677 def normalize_json(json_spec
: dict) -> dict:
678 networks
= json_spec
.get('networks')
681 if isinstance(networks
, list):
683 if not isinstance(networks
, str):
684 raise SpecValidationError(f
'Networks ({networks}) must be a string or list of strings')
685 json_spec
['networks'] = [networks
]
689 def _from_json_impl(cls
: Type
[ServiceSpecT
], json_spec
: dict) -> ServiceSpecT
:
690 args
= {} # type: Dict[str, Any]
691 for k
, v
in json_spec
.items():
693 v
= PlacementSpec
.from_json(v
)
694 if k
== 'custom_configs':
695 v
= [CustomConfig
.from_json(c
) for c
in v
]
701 if _service_spec_from_json_validate
:
705 def service_name(self
) -> str:
706 n
= self
.service_type
708 n
+= '.' + self
.service_id
711 def get_port_start(self
) -> List
[int]:
712 # If defined, we will allocate and number ports starting at this
716 def get_virtual_ip(self
) -> Optional
[str]:
720 # type: () -> OrderedDict[str, Any]
721 ret
: OrderedDict
[str, Any
] = OrderedDict()
722 ret
['service_type'] = self
.service_type
724 ret
['service_id'] = self
.service_id
725 ret
['service_name'] = self
.service_name()
726 if self
.placement
.to_json():
727 ret
['placement'] = self
.placement
.to_json()
729 ret
['unmanaged'] = self
.unmanaged
731 ret
['networks'] = self
.networks
732 if self
.extra_container_args
:
733 ret
['extra_container_args'] = self
.extra_container_args
734 if self
.extra_entrypoint_args
:
735 ret
['extra_entrypoint_args'] = self
.extra_entrypoint_args
736 if self
.custom_configs
:
737 ret
['custom_configs'] = [c
.to_json() for c
in self
.custom_configs
]
740 for key
, val
in sorted(self
.__dict
__.items(), key
=lambda tpl
: tpl
[0]):
743 if hasattr(val
, 'to_json'):
751 def validate(self
) -> None:
752 if not self
.service_type
:
753 raise SpecValidationError('Cannot add Service: type required')
755 if self
.service_type
!= 'osd':
756 if self
.service_type
in self
.REQUIRES_SERVICE_ID
and not self
.service_id
:
757 raise SpecValidationError('Cannot add Service: id required')
758 if self
.service_type
not in self
.REQUIRES_SERVICE_ID
and self
.service_id
:
759 raise SpecValidationError(
760 f
'Service of type \'{self.service_type}\' should not contain a service id')
763 if not re
.match('^[a-zA-Z0-9_.-]+$', str(self
.service_id
)):
764 raise SpecValidationError('Service id contains invalid characters, '
765 'only [a-zA-Z0-9_.-] allowed')
767 if self
.placement
is not None:
768 self
.placement
.validate()
770 for k
, v
in self
.config
.items():
771 if k
in self
.MANAGED_CONFIG_OPTIONS
:
772 raise SpecValidationError(
773 f
'Cannot set config option {k} in spec: it is managed by cephadm'
775 for network
in self
.networks
or []:
778 except ValueError as e
:
779 raise SpecValidationError(
780 f
'Cannot parse network {network}: {e}'
783 def __repr__(self
) -> str:
784 y
= yaml
.dump(cast(dict, self
), default_flow_style
=False)
785 return f
"{self.__class__.__name__}.from_json(yaml.safe_load('''{y}'''))"
787 def __eq__(self
, other
: Any
) -> bool:
788 return (self
.__class
__ == other
.__class
__
790 self
.__dict
__ == other
.__dict
__)
792 def one_line_str(self
) -> str:
793 return '<{} for service_name={}>'.format(self
.__class
__.__name
__, self
.service_name())
796 def yaml_representer(dumper
: 'yaml.SafeDumper', data
: 'ServiceSpec') -> Any
:
797 return dumper
.represent_dict(cast(Mapping
, data
.to_json().items()))
800 yaml
.add_representer(ServiceSpec
, ServiceSpec
.yaml_representer
)
803 class NFSServiceSpec(ServiceSpec
):
805 service_type
: str = 'nfs',
806 service_id
: Optional
[str] = None,
807 placement
: Optional
[PlacementSpec
] = None,
808 unmanaged
: bool = False,
809 preview_only
: bool = False,
810 config
: Optional
[Dict
[str, str]] = None,
811 networks
: Optional
[List
[str]] = None,
812 port
: Optional
[int] = None,
813 virtual_ip
: Optional
[str] = None,
814 extra_container_args
: Optional
[List
[str]] = None,
815 extra_entrypoint_args
: Optional
[List
[str]] = None,
816 custom_configs
: Optional
[List
[CustomConfig
]] = None,
818 assert service_type
== 'nfs'
819 super(NFSServiceSpec
, self
).__init
__(
820 'nfs', service_id
=service_id
,
821 placement
=placement
, unmanaged
=unmanaged
, preview_only
=preview_only
,
822 config
=config
, networks
=networks
, extra_container_args
=extra_container_args
,
823 extra_entrypoint_args
=extra_entrypoint_args
, custom_configs
=custom_configs
)
826 self
.virtual_ip
= virtual_ip
828 def get_port_start(self
) -> List
[int]:
833 def rados_config_name(self
):
835 return 'conf-' + self
.service_name()
838 yaml
.add_representer(NFSServiceSpec
, ServiceSpec
.yaml_representer
)
841 class RGWSpec(ServiceSpec
):
843 Settings to configure a (multisite) Ceph RGW
848 service_id: myrealm.myzone
851 rgw_zonegroup: myzonegroup
854 rgw_frontend_port: 1234
855 rgw_frontend_type: beast
856 rgw_frontend_ssl_certificate: ...
858 See also: :ref:`orchestrator-cli-service-spec`
861 MANAGED_CONFIG_OPTIONS
= ServiceSpec
.MANAGED_CONFIG_OPTIONS
+ [
869 service_type
: str = 'rgw',
870 service_id
: Optional
[str] = None,
871 placement
: Optional
[PlacementSpec
] = None,
872 rgw_realm
: Optional
[str] = None,
873 rgw_zonegroup
: Optional
[str] = None,
874 rgw_zone
: Optional
[str] = None,
875 rgw_frontend_port
: Optional
[int] = None,
876 rgw_frontend_ssl_certificate
: Optional
[List
[str]] = None,
877 rgw_frontend_type
: Optional
[str] = None,
878 rgw_frontend_extra_args
: Optional
[List
[str]] = None,
879 unmanaged
: bool = False,
881 preview_only
: bool = False,
882 config
: Optional
[Dict
[str, str]] = None,
883 networks
: Optional
[List
[str]] = None,
884 subcluster
: Optional
[str] = None, # legacy, only for from_json on upgrade
885 extra_container_args
: Optional
[List
[str]] = None,
886 extra_entrypoint_args
: Optional
[List
[str]] = None,
887 custom_configs
: Optional
[List
[CustomConfig
]] = None,
888 rgw_realm_token
: Optional
[str] = None,
889 update_endpoints
: Optional
[bool] = False,
890 zone_endpoints
: Optional
[str] = None # commad separated endpoints list
892 assert service_type
== 'rgw', service_type
894 # for backward compatibility with octopus spec files,
895 if not service_id
and (rgw_realm
and rgw_zone
):
896 service_id
= rgw_realm
+ '.' + rgw_zone
898 super(RGWSpec
, self
).__init
__(
899 'rgw', service_id
=service_id
,
900 placement
=placement
, unmanaged
=unmanaged
,
901 preview_only
=preview_only
, config
=config
, networks
=networks
,
902 extra_container_args
=extra_container_args
, extra_entrypoint_args
=extra_entrypoint_args
,
903 custom_configs
=custom_configs
)
905 #: The RGW realm associated with this service. Needs to be manually created
906 #: if the spec is being applied directly to cephdam. In case of rgw module
907 #: the realm is created automatically.
908 self
.rgw_realm
: Optional
[str] = rgw_realm
909 #: The RGW zonegroup associated with this service. Needs to be manually created
910 #: if the spec is being applied directly to cephdam. In case of rgw module
911 #: the zonegroup is created automatically.
912 self
.rgw_zonegroup
: Optional
[str] = rgw_zonegroup
913 #: The RGW zone associated with this service. Needs to be manually created
914 #: if the spec is being applied directly to cephdam. In case of rgw module
915 #: the zone is created automatically.
916 self
.rgw_zone
: Optional
[str] = rgw_zone
917 #: Port of the RGW daemons
918 self
.rgw_frontend_port
: Optional
[int] = rgw_frontend_port
919 #: List of SSL certificates
920 self
.rgw_frontend_ssl_certificate
: Optional
[List
[str]] = rgw_frontend_ssl_certificate
921 #: civetweb or beast (default: beast). See :ref:`rgw_frontends`
922 self
.rgw_frontend_type
: Optional
[str] = rgw_frontend_type
923 #: List of extra arguments for rgw_frontend in the form opt=value. See :ref:`rgw_frontends`
924 self
.rgw_frontend_extra_args
: Optional
[List
[str]] = rgw_frontend_extra_args
927 self
.rgw_realm_token
= rgw_realm_token
928 self
.update_endpoints
= update_endpoints
929 self
.zone_endpoints
= zone_endpoints
931 def get_port_start(self
) -> List
[int]:
932 return [self
.get_port()]
934 def get_port(self
) -> int:
935 if self
.rgw_frontend_port
:
936 return self
.rgw_frontend_port
942 def validate(self
) -> None:
943 super(RGWSpec
, self
).validate()
945 if self
.rgw_realm
and not self
.rgw_zone
:
946 raise SpecValidationError(
947 'Cannot add RGW: Realm specified but no zone specified')
948 if self
.rgw_zone
and not self
.rgw_realm
:
949 raise SpecValidationError('Cannot add RGW: Zone specified but no realm specified')
951 if self
.rgw_frontend_type
is not None:
952 if self
.rgw_frontend_type
not in ['beast', 'civetweb']:
953 raise SpecValidationError(
954 'Invalid rgw_frontend_type value. Valid values are: beast, civetweb.\n'
955 'Additional rgw type parameters can be passed using rgw_frontend_extra_args.'
959 yaml
.add_representer(RGWSpec
, ServiceSpec
.yaml_representer
)
962 class IscsiServiceSpec(ServiceSpec
):
964 service_type
: str = 'iscsi',
965 service_id
: Optional
[str] = None,
966 pool
: Optional
[str] = None,
967 trusted_ip_list
: Optional
[str] = None,
968 api_port
: Optional
[int] = 5000,
969 api_user
: Optional
[str] = 'admin',
970 api_password
: Optional
[str] = 'admin',
971 api_secure
: Optional
[bool] = None,
972 ssl_cert
: Optional
[str] = None,
973 ssl_key
: Optional
[str] = None,
974 placement
: Optional
[PlacementSpec
] = None,
975 unmanaged
: bool = False,
976 preview_only
: bool = False,
977 config
: Optional
[Dict
[str, str]] = None,
978 networks
: Optional
[List
[str]] = None,
979 extra_container_args
: Optional
[List
[str]] = None,
980 extra_entrypoint_args
: Optional
[List
[str]] = None,
981 custom_configs
: Optional
[List
[CustomConfig
]] = None,
983 assert service_type
== 'iscsi'
984 super(IscsiServiceSpec
, self
).__init
__('iscsi', service_id
=service_id
,
985 placement
=placement
, unmanaged
=unmanaged
,
986 preview_only
=preview_only
,
987 config
=config
, networks
=networks
,
988 extra_container_args
=extra_container_args
,
989 extra_entrypoint_args
=extra_entrypoint_args
,
990 custom_configs
=custom_configs
)
992 #: RADOS pool where ceph-iscsi config data is stored.
994 #: list of trusted IP addresses
995 self
.trusted_ip_list
= trusted_ip_list
996 #: ``api_port`` as defined in the ``iscsi-gateway.cfg``
997 self
.api_port
= api_port
998 #: ``api_user`` as defined in the ``iscsi-gateway.cfg``
999 self
.api_user
= api_user
1000 #: ``api_password`` as defined in the ``iscsi-gateway.cfg``
1001 self
.api_password
= api_password
1002 #: ``api_secure`` as defined in the ``iscsi-gateway.cfg``
1003 self
.api_secure
= api_secure
1005 self
.ssl_cert
= ssl_cert
1007 self
.ssl_key
= ssl_key
1009 if not self
.api_secure
and self
.ssl_cert
and self
.ssl_key
:
1010 self
.api_secure
= True
1012 def get_port_start(self
) -> List
[int]:
1013 return [self
.api_port
or 5000]
1015 def validate(self
) -> None:
1016 super(IscsiServiceSpec
, self
).validate()
1019 raise SpecValidationError(
1020 'Cannot add ISCSI: No Pool specified')
1022 # Do not need to check for api_user and api_password as they
1023 # now default to 'admin' when setting up the gateway url. Older
1024 # iSCSI specs from before this change should be fine as they will
1025 # have been required to have an api_user and api_password set and
1026 # will be unaffected by the new default value.
1029 yaml
.add_representer(IscsiServiceSpec
, ServiceSpec
.yaml_representer
)
1032 class IngressSpec(ServiceSpec
):
1034 service_type
: str = 'ingress',
1035 service_id
: Optional
[str] = None,
1036 config
: Optional
[Dict
[str, str]] = None,
1037 networks
: Optional
[List
[str]] = None,
1038 placement
: Optional
[PlacementSpec
] = None,
1039 backend_service
: Optional
[str] = None,
1040 frontend_port
: Optional
[int] = None,
1041 ssl_cert
: Optional
[str] = None,
1042 ssl_key
: Optional
[str] = None,
1043 ssl_dh_param
: Optional
[str] = None,
1044 ssl_ciphers
: Optional
[List
[str]] = None,
1045 ssl_options
: Optional
[List
[str]] = None,
1046 monitor_port
: Optional
[int] = None,
1047 monitor_user
: Optional
[str] = None,
1048 monitor_password
: Optional
[str] = None,
1049 enable_stats
: Optional
[bool] = None,
1050 keepalived_password
: Optional
[str] = None,
1051 virtual_ip
: Optional
[str] = None,
1052 virtual_ips_list
: Optional
[List
[str]] = None,
1053 virtual_interface_networks
: Optional
[List
[str]] = [],
1054 unmanaged
: bool = False,
1056 keepalive_only
: bool = False,
1057 extra_container_args
: Optional
[List
[str]] = None,
1058 extra_entrypoint_args
: Optional
[List
[str]] = None,
1059 custom_configs
: Optional
[List
[CustomConfig
]] = None,
1061 assert service_type
== 'ingress'
1063 super(IngressSpec
, self
).__init
__(
1064 'ingress', service_id
=service_id
,
1065 placement
=placement
, config
=config
,
1067 extra_container_args
=extra_container_args
,
1068 extra_entrypoint_args
=extra_entrypoint_args
,
1069 custom_configs
=custom_configs
1071 self
.backend_service
= backend_service
1072 self
.frontend_port
= frontend_port
1073 self
.ssl_cert
= ssl_cert
1074 self
.ssl_key
= ssl_key
1075 self
.ssl_dh_param
= ssl_dh_param
1076 self
.ssl_ciphers
= ssl_ciphers
1077 self
.ssl_options
= ssl_options
1078 self
.monitor_port
= monitor_port
1079 self
.monitor_user
= monitor_user
1080 self
.monitor_password
= monitor_password
1081 self
.keepalived_password
= keepalived_password
1082 self
.virtual_ip
= virtual_ip
1083 self
.virtual_ips_list
= virtual_ips_list
1084 self
.virtual_interface_networks
= virtual_interface_networks
or []
1085 self
.unmanaged
= unmanaged
1087 self
.keepalive_only
= keepalive_only
1089 def get_port_start(self
) -> List
[int]:
1091 if self
.frontend_port
is not None:
1092 ports
.append(cast(int, self
.frontend_port
))
1093 if self
.monitor_port
is not None:
1094 ports
.append(cast(int, self
.monitor_port
))
1097 def get_virtual_ip(self
) -> Optional
[str]:
1098 return self
.virtual_ip
1100 def validate(self
) -> None:
1101 super(IngressSpec
, self
).validate()
1103 if not self
.backend_service
:
1104 raise SpecValidationError(
1105 'Cannot add ingress: No backend_service specified')
1106 if not self
.keepalive_only
and not self
.frontend_port
:
1107 raise SpecValidationError(
1108 'Cannot add ingress: No frontend_port specified')
1109 if not self
.monitor_port
:
1110 raise SpecValidationError(
1111 'Cannot add ingress: No monitor_port specified')
1112 if not self
.virtual_ip
and not self
.virtual_ips_list
:
1113 raise SpecValidationError(
1114 'Cannot add ingress: No virtual_ip provided')
1115 if self
.virtual_ip
is not None and self
.virtual_ips_list
is not None:
1116 raise SpecValidationError(
1117 'Cannot add ingress: Single and multiple virtual IPs specified')
1120 yaml
.add_representer(IngressSpec
, ServiceSpec
.yaml_representer
)
1123 class CustomContainerSpec(ServiceSpec
):
1125 service_type
: str = 'container',
1126 service_id
: Optional
[str] = None,
1127 config
: Optional
[Dict
[str, str]] = None,
1128 networks
: Optional
[List
[str]] = None,
1129 placement
: Optional
[PlacementSpec
] = None,
1130 unmanaged
: bool = False,
1131 preview_only
: bool = False,
1132 image
: Optional
[str] = None,
1133 entrypoint
: Optional
[str] = None,
1134 extra_entrypoint_args
: Optional
[List
[str]] = None,
1135 uid
: Optional
[int] = None,
1136 gid
: Optional
[int] = None,
1137 volume_mounts
: Optional
[Dict
[str, str]] = {},
1138 args
: Optional
[List
[str]] = [], # args for the container runtime, not entrypoint
1139 envs
: Optional
[List
[str]] = [],
1140 privileged
: Optional
[bool] = False,
1141 bind_mounts
: Optional
[List
[List
[str]]] = None,
1142 ports
: Optional
[List
[int]] = [],
1143 dirs
: Optional
[List
[str]] = [],
1144 files
: Optional
[Dict
[str, Any
]] = {},
1146 assert service_type
== 'container'
1147 assert service_id
is not None
1148 assert image
is not None
1150 super(CustomContainerSpec
, self
).__init
__(
1151 service_type
, service_id
,
1152 placement
=placement
, unmanaged
=unmanaged
,
1153 preview_only
=preview_only
, config
=config
,
1154 networks
=networks
, extra_entrypoint_args
=extra_entrypoint_args
)
1157 self
.entrypoint
= entrypoint
1160 self
.volume_mounts
= volume_mounts
1163 self
.privileged
= privileged
1164 self
.bind_mounts
= bind_mounts
1169 def config_json(self
) -> Dict
[str, Any
]:
1171 Helper function to get the value of the `--config-json` cephadm
1172 command line option. It will contain all specification properties
1173 that haven't a `None` value. Such properties will get default
1175 :return: Returns a dictionary containing all specification
1179 for prop
in ['image', 'entrypoint', 'uid', 'gid', 'args',
1180 'envs', 'volume_mounts', 'privileged',
1181 'bind_mounts', 'ports', 'dirs', 'files']:
1182 value
= getattr(self
, prop
)
1183 if value
is not None:
1184 config_json
[prop
] = value
1188 yaml
.add_representer(CustomContainerSpec
, ServiceSpec
.yaml_representer
)
1191 class MonitoringSpec(ServiceSpec
):
1194 service_id
: Optional
[str] = None,
1195 config
: Optional
[Dict
[str, str]] = None,
1196 networks
: Optional
[List
[str]] = None,
1197 placement
: Optional
[PlacementSpec
] = None,
1198 unmanaged
: bool = False,
1199 preview_only
: bool = False,
1200 port
: Optional
[int] = None,
1201 extra_container_args
: Optional
[List
[str]] = None,
1202 extra_entrypoint_args
: Optional
[List
[str]] = None,
1203 custom_configs
: Optional
[List
[CustomConfig
]] = None,
1205 assert service_type
in ['grafana', 'node-exporter', 'prometheus', 'alertmanager',
1208 super(MonitoringSpec
, self
).__init
__(
1209 service_type
, service_id
,
1210 placement
=placement
, unmanaged
=unmanaged
,
1211 preview_only
=preview_only
, config
=config
,
1212 networks
=networks
, extra_container_args
=extra_container_args
,
1213 extra_entrypoint_args
=extra_entrypoint_args
,
1214 custom_configs
=custom_configs
)
1216 self
.service_type
= service_type
1219 def get_port_start(self
) -> List
[int]:
1220 return [self
.get_port()]
1222 def get_port(self
) -> int:
1226 return {'prometheus': 9095,
1227 'node-exporter': 9100,
1228 'alertmanager': 9093,
1231 'promtail': 9080}[self
.service_type
]
1234 yaml
.add_representer(MonitoringSpec
, ServiceSpec
.yaml_representer
)
1237 class AlertManagerSpec(MonitoringSpec
):
1239 service_type
: str = 'alertmanager',
1240 service_id
: Optional
[str] = None,
1241 placement
: Optional
[PlacementSpec
] = None,
1242 unmanaged
: bool = False,
1243 preview_only
: bool = False,
1244 user_data
: Optional
[Dict
[str, Any
]] = None,
1245 config
: Optional
[Dict
[str, str]] = None,
1246 networks
: Optional
[List
[str]] = None,
1247 port
: Optional
[int] = None,
1248 secure
: bool = False,
1249 extra_container_args
: Optional
[List
[str]] = None,
1250 extra_entrypoint_args
: Optional
[List
[str]] = None,
1251 custom_configs
: Optional
[List
[CustomConfig
]] = None,
1253 assert service_type
== 'alertmanager'
1254 super(AlertManagerSpec
, self
).__init
__(
1255 'alertmanager', service_id
=service_id
,
1256 placement
=placement
, unmanaged
=unmanaged
,
1257 preview_only
=preview_only
, config
=config
, networks
=networks
, port
=port
,
1258 extra_container_args
=extra_container_args
, extra_entrypoint_args
=extra_entrypoint_args
,
1259 custom_configs
=custom_configs
)
1261 # Custom configuration.
1264 # service_type: alertmanager
1267 # default_webhook_urls:
1272 # default_webhook_urls - A list of additional URL's that are
1273 # added to the default receivers'
1274 # <webhook_configs> configuration.
1275 self
.user_data
= user_data
or {}
1276 self
.secure
= secure
1278 def get_port_start(self
) -> List
[int]:
1279 return [self
.get_port(), 9094]
1281 def validate(self
) -> None:
1282 super(AlertManagerSpec
, self
).validate()
1284 if self
.port
== 9094:
1285 raise SpecValidationError(
1286 'Port 9094 is reserved for AlertManager cluster listen address')
1289 yaml
.add_representer(AlertManagerSpec
, ServiceSpec
.yaml_representer
)
1292 class GrafanaSpec(MonitoringSpec
):
1294 service_type
: str = 'grafana',
1295 service_id
: Optional
[str] = None,
1296 placement
: Optional
[PlacementSpec
] = None,
1297 unmanaged
: bool = False,
1298 preview_only
: bool = False,
1299 config
: Optional
[Dict
[str, str]] = None,
1300 networks
: Optional
[List
[str]] = None,
1301 port
: Optional
[int] = None,
1302 protocol
: Optional
[str] = 'https',
1303 initial_admin_password
: Optional
[str] = None,
1304 anonymous_access
: Optional
[bool] = True,
1305 extra_container_args
: Optional
[List
[str]] = None,
1306 extra_entrypoint_args
: Optional
[List
[str]] = None,
1307 custom_configs
: Optional
[List
[CustomConfig
]] = None,
1309 assert service_type
== 'grafana'
1310 super(GrafanaSpec
, self
).__init
__(
1311 'grafana', service_id
=service_id
,
1312 placement
=placement
, unmanaged
=unmanaged
,
1313 preview_only
=preview_only
, config
=config
, networks
=networks
, port
=port
,
1314 extra_container_args
=extra_container_args
, extra_entrypoint_args
=extra_entrypoint_args
,
1315 custom_configs
=custom_configs
)
1317 self
.initial_admin_password
= initial_admin_password
1318 self
.anonymous_access
= anonymous_access
1319 self
.protocol
= protocol
1321 def validate(self
) -> None:
1322 super(GrafanaSpec
, self
).validate()
1323 if self
.protocol
not in ['http', 'https']:
1324 err_msg
= f
"Invalid protocol '{self.protocol}'. Valid values are: 'http', 'https'."
1325 raise SpecValidationError(err_msg
)
1327 if not self
.anonymous_access
and not self
.initial_admin_password
:
1328 err_msg
= ('Either initial_admin_password must be set or anonymous_access '
1329 'must be set to true. Otherwise the grafana dashboard will '
1331 raise SpecValidationError(err_msg
)
1334 yaml
.add_representer(GrafanaSpec
, ServiceSpec
.yaml_representer
)
1337 class PrometheusSpec(MonitoringSpec
):
1339 service_type
: str = 'prometheus',
1340 service_id
: Optional
[str] = None,
1341 placement
: Optional
[PlacementSpec
] = None,
1342 unmanaged
: bool = False,
1343 preview_only
: bool = False,
1344 config
: Optional
[Dict
[str, str]] = None,
1345 networks
: Optional
[List
[str]] = None,
1346 port
: Optional
[int] = None,
1347 retention_time
: Optional
[str] = None,
1348 retention_size
: Optional
[str] = None,
1349 extra_container_args
: Optional
[List
[str]] = None,
1350 extra_entrypoint_args
: Optional
[List
[str]] = None,
1351 custom_configs
: Optional
[List
[CustomConfig
]] = None,
1353 assert service_type
== 'prometheus'
1354 super(PrometheusSpec
, self
).__init
__(
1355 'prometheus', service_id
=service_id
,
1356 placement
=placement
, unmanaged
=unmanaged
,
1357 preview_only
=preview_only
, config
=config
, networks
=networks
, port
=port
,
1358 extra_container_args
=extra_container_args
, extra_entrypoint_args
=extra_entrypoint_args
,
1359 custom_configs
=custom_configs
)
1361 self
.retention_time
= retention_time
.strip() if retention_time
else None
1362 self
.retention_size
= retention_size
.strip() if retention_size
else None
1364 def validate(self
) -> None:
1365 super(PrometheusSpec
, self
).validate()
1367 if self
.retention_time
:
1368 valid_units
= ['y', 'w', 'd', 'h', 'm', 's']
1369 m
= re
.search(rf
"^(\d+)({'|'.join(valid_units)})$", self
.retention_time
)
1371 units
= ', '.join(valid_units
)
1372 raise SpecValidationError(f
"Invalid retention time. Valid units are: {units}")
1373 if self
.retention_size
:
1374 valid_units
= ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']
1375 m
= re
.search(rf
"^(\d+)({'|'.join(valid_units)})$", self
.retention_size
)
1377 units
= ', '.join(valid_units
)
1378 raise SpecValidationError(f
"Invalid retention size. Valid units are: {units}")
1381 yaml
.add_representer(PrometheusSpec
, ServiceSpec
.yaml_representer
)
1384 class SNMPGatewaySpec(ServiceSpec
):
1385 class SNMPVersion(str, enum
.Enum
):
1389 def to_json(self
) -> str:
1392 class SNMPAuthType(str, enum
.Enum
):
1396 def to_json(self
) -> str:
1399 class SNMPPrivacyType(str, enum
.Enum
):
1403 def to_json(self
) -> str:
1406 valid_destination_types
= [
1412 service_type
: str = 'snmp-gateway',
1413 snmp_version
: Optional
[SNMPVersion
] = None,
1414 snmp_destination
: str = '',
1415 credentials
: Dict
[str, str] = {},
1416 engine_id
: Optional
[str] = None,
1417 auth_protocol
: Optional
[SNMPAuthType
] = None,
1418 privacy_protocol
: Optional
[SNMPPrivacyType
] = None,
1419 placement
: Optional
[PlacementSpec
] = None,
1420 unmanaged
: bool = False,
1421 preview_only
: bool = False,
1422 port
: Optional
[int] = None,
1423 extra_container_args
: Optional
[List
[str]] = None,
1424 extra_entrypoint_args
: Optional
[List
[str]] = None,
1425 custom_configs
: Optional
[List
[CustomConfig
]] = None,
1427 assert service_type
== 'snmp-gateway'
1429 super(SNMPGatewaySpec
, self
).__init
__(
1431 placement
=placement
,
1432 unmanaged
=unmanaged
,
1433 preview_only
=preview_only
,
1434 extra_container_args
=extra_container_args
,
1435 extra_entrypoint_args
=extra_entrypoint_args
,
1436 custom_configs
=custom_configs
)
1438 self
.service_type
= service_type
1439 self
.snmp_version
= snmp_version
1440 self
.snmp_destination
= snmp_destination
1442 self
.credentials
= credentials
1443 self
.engine_id
= engine_id
1444 self
.auth_protocol
= auth_protocol
1445 self
.privacy_protocol
= privacy_protocol
1448 def _from_json_impl(cls
, json_spec
: dict) -> 'SNMPGatewaySpec':
1450 cpy
= json_spec
.copy()
1452 ('snmp_version', SNMPGatewaySpec
.SNMPVersion
),
1453 ('auth_protocol', SNMPGatewaySpec
.SNMPAuthType
),
1454 ('privacy_protocol', SNMPGatewaySpec
.SNMPPrivacyType
),
1456 for d
in cpy
, cpy
.get('spec', {}):
1457 for key
, enum_cls
in types
:
1460 d
[key
] = enum_cls(d
[key
])
1462 raise SpecValidationError(f
'{key} unsupported. Must be one of '
1463 f
'{", ".join(enum_cls)}')
1464 return super(SNMPGatewaySpec
, cls
)._from
_json
_impl
(cpy
)
1467 def ports(self
) -> List
[int]:
1468 return [self
.port
or 9464]
1470 def get_port_start(self
) -> List
[int]:
1473 def validate(self
) -> None:
1474 super(SNMPGatewaySpec
, self
).validate()
1476 if not self
.credentials
:
1477 raise SpecValidationError(
1478 'Missing authentication information (credentials). '
1479 'SNMP V2c and V3 require credential information'
1481 elif not self
.snmp_version
:
1482 raise SpecValidationError(
1483 'Missing SNMP version (snmp_version)'
1486 creds_requirement
= {
1487 'V2c': ['snmp_community'],
1488 'V3': ['snmp_v3_auth_username', 'snmp_v3_auth_password']
1490 if self
.privacy_protocol
:
1491 creds_requirement
['V3'].append('snmp_v3_priv_password')
1493 missing
= [parm
for parm
in creds_requirement
[self
.snmp_version
]
1494 if parm
not in self
.credentials
]
1495 # check that credentials are correct for the version
1497 raise SpecValidationError(
1498 f
'SNMP {self.snmp_version} credentials are incomplete. Missing {", ".join(missing)}'
1502 if 10 <= len(self
.engine_id
) <= 64 and \
1503 is_hex(self
.engine_id
) and \
1504 len(self
.engine_id
) % 2 == 0:
1507 raise SpecValidationError(
1508 'engine_id must be a string containing 10-64 hex characters. '
1509 'Its length must be divisible by 2'
1513 if self
.snmp_version
== 'V3':
1514 raise SpecValidationError(
1515 'Must provide an engine_id for SNMP V3 notifications'
1518 if not self
.snmp_destination
:
1519 raise SpecValidationError(
1520 'SNMP destination (snmp_destination) must be provided'
1523 valid
, description
= valid_addr(self
.snmp_destination
)
1525 raise SpecValidationError(
1526 f
'SNMP destination (snmp_destination) is invalid: {description}'
1528 if description
not in self
.valid_destination_types
:
1529 raise SpecValidationError(
1530 f
'SNMP destination (snmp_destination) type ({description}) is invalid. '
1531 f
'Must be either: {", ".join(sorted(self.valid_destination_types))}'
1535 yaml
.add_representer(SNMPGatewaySpec
, ServiceSpec
.yaml_representer
)
1538 class MDSSpec(ServiceSpec
):
1540 service_type
: str = 'mds',
1541 service_id
: Optional
[str] = None,
1542 placement
: Optional
[PlacementSpec
] = None,
1543 config
: Optional
[Dict
[str, str]] = None,
1544 unmanaged
: bool = False,
1545 preview_only
: bool = False,
1546 extra_container_args
: Optional
[List
[str]] = None,
1547 extra_entrypoint_args
: Optional
[List
[str]] = None,
1548 custom_configs
: Optional
[List
[CustomConfig
]] = None,
1550 assert service_type
== 'mds'
1551 super(MDSSpec
, self
).__init
__('mds', service_id
=service_id
,
1552 placement
=placement
,
1554 unmanaged
=unmanaged
,
1555 preview_only
=preview_only
,
1556 extra_container_args
=extra_container_args
,
1557 extra_entrypoint_args
=extra_entrypoint_args
,
1558 custom_configs
=custom_configs
)
1560 def validate(self
) -> None:
1561 super(MDSSpec
, self
).validate()
1563 if str(self
.service_id
)[0].isdigit():
1564 raise SpecValidationError('MDS service id cannot start with a numeric digit')
1567 yaml
.add_representer(MDSSpec
, ServiceSpec
.yaml_representer
)
1570 class MONSpec(ServiceSpec
):
1573 service_id
: Optional
[str] = None,
1574 placement
: Optional
[PlacementSpec
] = None,
1575 count
: Optional
[int] = None,
1576 config
: Optional
[Dict
[str, str]] = None,
1577 unmanaged
: bool = False,
1578 preview_only
: bool = False,
1579 networks
: Optional
[List
[str]] = None,
1580 extra_container_args
: Optional
[List
[str]] = None,
1581 custom_configs
: Optional
[List
[CustomConfig
]] = None,
1582 crush_locations
: Optional
[Dict
[str, List
[str]]] = None,
1584 assert service_type
== 'mon'
1585 super(MONSpec
, self
).__init
__('mon', service_id
=service_id
,
1586 placement
=placement
,
1589 unmanaged
=unmanaged
,
1590 preview_only
=preview_only
,
1592 extra_container_args
=extra_container_args
,
1593 custom_configs
=custom_configs
)
1595 self
.crush_locations
= crush_locations
1598 def validate(self
) -> None:
1599 if self
.crush_locations
:
1600 for host
, crush_locs
in self
.crush_locations
.items():
1602 assert_valid_host(host
)
1603 except SpecValidationError
as e
:
1604 err_str
= f
'Invalid hostname found in spec crush locations: {e}'
1605 raise SpecValidationError(err_str
)
1606 for cloc
in crush_locs
:
1607 if '=' not in cloc
or len(cloc
.split('=')) != 2:
1608 err_str
= ('Crush locations must be of form <bucket>=<location>. '
1609 f
'Found crush location: {cloc}')
1610 raise SpecValidationError(err_str
)
1613 yaml
.add_representer(MONSpec
, ServiceSpec
.yaml_representer
)
1616 class TracingSpec(ServiceSpec
):
1617 SERVICE_TYPES
= ['elasticsearch', 'jaeger-collector', 'jaeger-query', 'jaeger-agent']
1621 es_nodes
: Optional
[str] = None,
1622 without_query
: bool = False,
1623 service_id
: Optional
[str] = None,
1624 config
: Optional
[Dict
[str, str]] = None,
1625 networks
: Optional
[List
[str]] = None,
1626 placement
: Optional
[PlacementSpec
] = None,
1627 unmanaged
: bool = False,
1628 preview_only
: bool = False
1630 assert service_type
in TracingSpec
.SERVICE_TYPES
+ ['jaeger-tracing']
1632 super(TracingSpec
, self
).__init
__(
1633 service_type
, service_id
,
1634 placement
=placement
, unmanaged
=unmanaged
,
1635 preview_only
=preview_only
, config
=config
,
1637 self
.without_query
= without_query
1638 self
.es_nodes
= es_nodes
1640 def get_port_start(self
) -> List
[int]:
1641 return [self
.get_port()]
1643 def get_port(self
) -> int:
1644 return {'elasticsearch': 9200,
1645 'jaeger-agent': 6799,
1646 'jaeger-collector': 14250,
1647 'jaeger-query': 16686}[self
.service_type
]
1649 def get_tracing_specs(self
) -> List
[ServiceSpec
]:
1650 assert self
.service_type
== 'jaeger-tracing'
1651 specs
: List
[ServiceSpec
] = []
1652 daemons
: Dict
[str, Optional
[PlacementSpec
]] = {
1653 daemon
: None for daemon
in TracingSpec
.SERVICE_TYPES
}
1656 del daemons
['elasticsearch']
1657 if self
.without_query
:
1658 del daemons
['jaeger-query']
1660 daemons
.update({'jaeger-collector': self
.placement
})
1662 for daemon
, daemon_placement
in daemons
.items():
1663 specs
.append(TracingSpec(service_type
=daemon
,
1664 es_nodes
=self
.es_nodes
,
1665 placement
=daemon_placement
,
1666 unmanaged
=self
.unmanaged
,
1668 networks
=self
.networks
,
1669 preview_only
=self
.preview_only
1674 yaml
.add_representer(TracingSpec
, ServiceSpec
.yaml_representer
)
1677 class TunedProfileSpec():
1680 placement
: Optional
[PlacementSpec
] = None,
1681 settings
: Optional
[Dict
[str, str]] = None,
1683 self
.profile_name
= profile_name
1684 self
.placement
= placement
or PlacementSpec(host_pattern
='*')
1685 self
.settings
= settings
or {}
1686 self
._last
_updated
: str = ''
1689 def from_json(cls
, spec
: Dict
[str, Any
]) -> 'TunedProfileSpec':
1691 if 'profile_name' not in spec
:
1692 raise SpecValidationError('Tuned profile spec must include "profile_name" field')
1693 data
['profile_name'] = spec
['profile_name']
1694 if not isinstance(data
['profile_name'], str):
1695 raise SpecValidationError('"profile_name" field must be a string')
1696 if 'placement' in spec
:
1697 data
['placement'] = PlacementSpec
.from_json(spec
['placement'])
1698 if 'settings' in spec
:
1699 data
['settings'] = spec
['settings']
1702 def to_json(self
) -> Dict
[str, Any
]:
1703 res
: Dict
[str, Any
] = {}
1704 res
['profile_name'] = self
.profile_name
1705 res
['placement'] = self
.placement
.to_json()
1706 res
['settings'] = self
.settings
1709 def __eq__(self
, other
: Any
) -> bool:
1710 if isinstance(other
, TunedProfileSpec
):
1712 self
.placement
== other
.placement
1713 and self
.profile_name
== other
.profile_name
1714 and self
.settings
== other
.settings
1718 return NotImplemented
1720 def __repr__(self
) -> str:
1721 return f
'TunedProfile({self.profile_name})'
1723 def copy(self
) -> 'TunedProfileSpec':
1724 # for making deep copies so you can edit the settings in one without affecting the other
1725 # mostly for testing purposes
1726 return TunedProfileSpec(self
.profile_name
, self
.placement
, self
.settings
.copy())
1729 class CephExporterSpec(ServiceSpec
):
1731 service_type
: str = 'ceph-exporter',
1732 sock_dir
: Optional
[str] = None,
1734 port
: Optional
[int] = None,
1735 prio_limit
: Optional
[int] = 5,
1736 stats_period
: Optional
[int] = 5,
1737 placement
: Optional
[PlacementSpec
] = None,
1738 unmanaged
: bool = False,
1739 preview_only
: bool = False,
1740 extra_container_args
: Optional
[List
[str]] = None,
1742 assert service_type
== 'ceph-exporter'
1744 super(CephExporterSpec
, self
).__init
__(
1746 placement
=placement
,
1747 unmanaged
=unmanaged
,
1748 preview_only
=preview_only
,
1749 extra_container_args
=extra_container_args
)
1751 self
.service_type
= service_type
1752 self
.sock_dir
= sock_dir
1755 self
.prio_limit
= prio_limit
1756 self
.stats_period
= stats_period
1758 def validate(self
) -> None:
1759 super(CephExporterSpec
, self
).validate()
1761 if not isinstance(self
.prio_limit
, int):
1762 raise SpecValidationError(
1763 f
'prio_limit must be an integer. Got {type(self.prio_limit)}')
1764 if not isinstance(self
.stats_period
, int):
1765 raise SpecValidationError(
1766 f
'stats_period must be an integer. Got {type(self.stats_period)}')
1769 yaml
.add_representer(CephExporterSpec
, ServiceSpec
.yaml_representer
)