diff options
Diffstat (limited to '')
-rw-r--r-- | src/python-common/ceph/deployment/service_spec.py | 203 |
1 files changed, 182 insertions, 21 deletions
diff --git a/src/python-common/ceph/deployment/service_spec.py b/src/python-common/ceph/deployment/service_spec.py index be9f3e8ea..704dfe6f0 100644 --- a/src/python-common/ceph/deployment/service_spec.py +++ b/src/python-common/ceph/deployment/service_spec.py @@ -127,17 +127,120 @@ class HostPlacementSpec(NamedTuple): assert_valid_host(self.hostname) +HostPatternType = Union[str, None, Dict[str, Union[str, bool, None]], "HostPattern"] + + +class PatternType(enum.Enum): + fnmatch = 'fnmatch' + regex = 'regex' + + +class HostPattern(): + def __init__(self, + pattern: Optional[str] = None, + pattern_type: PatternType = PatternType.fnmatch) -> None: + self.pattern: Optional[str] = pattern + self.pattern_type: PatternType = pattern_type + self.compiled_regex = None + if self.pattern_type == PatternType.regex and self.pattern: + self.compiled_regex = re.compile(self.pattern) + + def filter_hosts(self, hosts: List[str]) -> List[str]: + if not self.pattern: + return [] + if not self.pattern_type or self.pattern_type == PatternType.fnmatch: + return fnmatch.filter(hosts, self.pattern) + elif self.pattern_type == PatternType.regex: + if not self.compiled_regex: + self.compiled_regex = re.compile(self.pattern) + return [h for h in hosts if re.match(self.compiled_regex, h)] + raise SpecValidationError(f'Got unexpected pattern_type: {self.pattern_type}') + + @classmethod + def to_host_pattern(cls, arg: HostPatternType) -> "HostPattern": + if arg is None: + return cls() + elif isinstance(arg, str): + return cls(arg) + elif isinstance(arg, cls): + return arg + elif isinstance(arg, dict): + if 'pattern' not in arg: + raise SpecValidationError("Got dict for host pattern " + f"with no pattern field: {arg}") + pattern = arg['pattern'] + if not pattern: + raise SpecValidationError("Got dict for host pattern" + f"with empty pattern: {arg}") + assert isinstance(pattern, str) + if 'pattern_type' in arg: + pattern_type = arg['pattern_type'] + if not pattern_type or pattern_type == 'fnmatch': + return cls(pattern, pattern_type=PatternType.fnmatch) + elif pattern_type == 'regex': + return cls(pattern, pattern_type=PatternType.regex) + else: + raise SpecValidationError("Got dict for host pattern " + f"with unknown pattern type: {arg}") + return cls(pattern) + raise SpecValidationError(f"Cannot convert {type(arg)} object to HostPattern") + + def __eq__(self, other: Any) -> bool: + try: + other_hp = self.to_host_pattern(other) + except SpecValidationError: + return False + return self.pattern == other_hp.pattern and self.pattern_type == other_hp.pattern_type + + def pretty_str(self) -> str: + # Placement specs must be able to be converted between the Python object + # representation and a pretty str both ways. So we need a corresponding + # function for HostPattern to convert it to a pretty str that we can + # convert back later. + res = self.pattern if self.pattern else '' + if self.pattern_type == PatternType.regex: + res = 'regex:' + res + return res + + @classmethod + def from_pretty_str(cls, val: str) -> "HostPattern": + if 'regex:' in val: + return cls(val[6:], pattern_type=PatternType.regex) + else: + return cls(val) + + def __repr__(self) -> str: + return f'HostPattern(pattern=\'{self.pattern}\', pattern_type={str(self.pattern_type)})' + + def to_json(self) -> Union[str, Dict[str, Any], None]: + if self.pattern_type and self.pattern_type != PatternType.fnmatch: + return { + 'pattern': self.pattern, + 'pattern_type': self.pattern_type.name + } + return self.pattern + + @classmethod + def from_json(self, val: Dict[str, Any]) -> "HostPattern": + return self.to_host_pattern(val) + + def __bool__(self) -> bool: + if self.pattern: + return True + return False + + class PlacementSpec(object): """ For APIs that need to specify a host subset """ def __init__(self, - label=None, # type: Optional[str] - hosts=None, # type: Union[List[str],List[HostPlacementSpec], None] - count=None, # type: Optional[int] - count_per_host=None, # type: Optional[int] - host_pattern=None, # type: Optional[str] + label: Optional[str] = None, + hosts: Union[List[str], List[HostPlacementSpec], None] = None, + count: Optional[int] = None, + count_per_host: Optional[int] = None, + host_pattern: HostPatternType = None, ): # type: (...) -> None self.label = label @@ -150,7 +253,7 @@ class PlacementSpec(object): self.count_per_host = count_per_host # type: Optional[int] #: fnmatch patterns to select hosts. Can also be a single host. - self.host_pattern = host_pattern # type: Optional[str] + self.host_pattern: HostPattern = HostPattern.to_host_pattern(host_pattern) self.validate() @@ -190,10 +293,11 @@ class PlacementSpec(object): all_hosts = [hs.hostname for hs in hostspecs] return [h.hostname for h in self.hosts if h.hostname in all_hosts] if self.label: - return [hs.hostname for hs in hostspecs if self.label in hs.labels] - all_hosts = [hs.hostname for hs in hostspecs] + all_hosts = [hs.hostname for hs in hostspecs if self.label in hs.labels] + else: + all_hosts = [hs.hostname for hs in hostspecs] if self.host_pattern: - return fnmatch.filter(all_hosts, self.host_pattern) + return self.host_pattern.filter_hosts(all_hosts) return all_hosts def get_target_count(self, hostspecs: Iterable[HostSpec]) -> int: @@ -217,7 +321,7 @@ class PlacementSpec(object): if self.label: kv.append('label:%s' % self.label) if self.host_pattern: - kv.append(self.host_pattern) + kv.append(self.host_pattern.pretty_str()) return ';'.join(kv) def __repr__(self) -> str: @@ -258,7 +362,7 @@ class PlacementSpec(object): if self.count_per_host: r['count_per_host'] = self.count_per_host if self.host_pattern: - r['host_pattern'] = self.host_pattern + r['host_pattern'] = self.host_pattern.to_json() return r def validate(self) -> None: @@ -302,8 +406,9 @@ class PlacementSpec(object): "count-per-host cannot be combined explicit placement with names or networks" ) if self.host_pattern: - if not isinstance(self.host_pattern, str): - raise SpecValidationError('host_pattern must be of type string') + # if we got an invalid type for the host_pattern, it would have + # triggered a SpecValidationError when attemptying to convert it + # to a HostPattern type, so no type checking is needed here. if self.hosts: raise SpecValidationError('cannot combine host patterns and hosts') @@ -341,10 +446,17 @@ tPlacementSpec(hostname='host2', network='', name='')]) >>> PlacementSpec.from_string('3 label:mon') PlacementSpec(count=3, label='mon') - fnmatch is also supported: + You can specify a regex to match with `regex:<regex>` + + >>> PlacementSpec.from_string('regex:Foo[0-9]|Bar[0-9]') + PlacementSpec(host_pattern=HostPattern(pattern='Foo[0-9]|Bar[0-9]', \ +pattern_type=PatternType.regex)) + + fnmatch is the default for a single string if "regex:" is not provided: >>> PlacementSpec.from_string('data[1-3]') - PlacementSpec(host_pattern='data[1-3]') + PlacementSpec(host_pattern=HostPattern(pattern='data[1-3]', \ +pattern_type=PatternType.fnmatch)) >>> PlacementSpec.from_string(None) PlacementSpec() @@ -394,7 +506,8 @@ tPlacementSpec(hostname='host2', network='', name='')]) advanced_hostspecs = [h for h in strings if (':' in h or '=' in h or not any(c in '[]?*:=' for c in h)) and - 'label:' not in h] + 'label:' not in h and + 'regex:' not in h] for a_h in advanced_hostspecs: strings.remove(a_h) @@ -406,15 +519,20 @@ tPlacementSpec(hostname='host2', network='', name='')]) label = labels[0][6:] if labels else None host_patterns = strings + host_pattern: Optional[HostPattern] = None if len(host_patterns) > 1: raise SpecValidationError( 'more than one host pattern provided: {}'.format(host_patterns)) + if host_patterns: + # host_patterns is a list not > 1, and not empty, so we should + # be guaranteed just a single string here + host_pattern = HostPattern.from_pretty_str(host_patterns[0]) ps = PlacementSpec(count=count, count_per_host=count_per_host, hosts=advanced_hostspecs, label=label, - host_pattern=host_patterns[0] if host_patterns else None) + host_pattern=host_pattern) return ps @@ -625,7 +743,8 @@ class ServiceSpec(object): KNOWN_SERVICE_TYPES = 'alertmanager crash grafana iscsi nvmeof loki promtail mds mgr mon nfs ' \ 'node-exporter osd prometheus rbd-mirror rgw agent ceph-exporter ' \ 'container ingress cephfs-mirror snmp-gateway jaeger-tracing ' \ - 'elasticsearch jaeger-agent jaeger-collector jaeger-query'.split() + 'elasticsearch jaeger-agent jaeger-collector jaeger-query ' \ + 'node-proxy'.split() REQUIRES_SERVICE_ID = 'iscsi nvmeof mds nfs rgw container ingress '.split() MANAGED_CONFIG_OPTIONS = [ 'mds_join_fs', @@ -951,6 +1070,7 @@ class NFSServiceSpec(ServiceSpec): extra_container_args: Optional[GeneralArgList] = None, extra_entrypoint_args: Optional[GeneralArgList] = None, enable_haproxy_protocol: bool = False, + idmap_conf: Optional[Dict[str, Dict[str, str]]] = None, custom_configs: Optional[List[CustomConfig]] = None, ): assert service_type == 'nfs' @@ -963,6 +1083,7 @@ class NFSServiceSpec(ServiceSpec): self.port = port self.virtual_ip = virtual_ip self.enable_haproxy_protocol = enable_haproxy_protocol + self.idmap_conf = idmap_conf def get_port_start(self) -> List[int]: if self.port: @@ -1294,6 +1415,7 @@ class IngressSpec(ServiceSpec): extra_entrypoint_args: Optional[GeneralArgList] = None, enable_haproxy_protocol: bool = False, custom_configs: Optional[List[CustomConfig]] = None, + health_check_interval: Optional[str] = None, ): assert service_type == 'ingress' @@ -1326,6 +1448,8 @@ class IngressSpec(ServiceSpec): self.ssl = ssl self.keepalive_only = keepalive_only self.enable_haproxy_protocol = enable_haproxy_protocol + self.health_check_interval = health_check_interval.strip( + ) if health_check_interval else None def get_port_start(self) -> List[int]: ports = [] @@ -1356,6 +1480,13 @@ class IngressSpec(ServiceSpec): if self.virtual_ip is not None and self.virtual_ips_list is not None: raise SpecValidationError( 'Cannot add ingress: Single and multiple virtual IPs specified') + if self.health_check_interval: + valid_units = ['s', 'm', 'h'] + m = re.search(rf"^(\d+)({'|'.join(valid_units)})$", self.health_check_interval) + if not m: + raise SpecValidationError( + f'Cannot add ingress: Invalid health_check_interval specified. ' + f'Valid units are: {valid_units}') yaml.add_representer(IngressSpec, ServiceSpec.yaml_representer) @@ -1372,7 +1503,6 @@ class CustomContainerSpec(ServiceSpec): preview_only: bool = False, image: Optional[str] = None, entrypoint: Optional[str] = None, - extra_entrypoint_args: Optional[GeneralArgList] = None, uid: Optional[int] = None, gid: Optional[int] = None, volume_mounts: Optional[Dict[str, str]] = {}, @@ -1384,6 +1514,9 @@ class CustomContainerSpec(ServiceSpec): ports: Optional[List[int]] = [], dirs: Optional[List[str]] = [], files: Optional[Dict[str, Any]] = {}, + extra_container_args: Optional[GeneralArgList] = None, + extra_entrypoint_args: Optional[GeneralArgList] = None, + custom_configs: Optional[List[CustomConfig]] = None, ): assert service_type == 'container' assert service_id is not None @@ -1393,7 +1526,9 @@ class CustomContainerSpec(ServiceSpec): service_type, service_id, placement=placement, unmanaged=unmanaged, preview_only=preview_only, config=config, - networks=networks, extra_entrypoint_args=extra_entrypoint_args) + networks=networks, extra_container_args=extra_container_args, + extra_entrypoint_args=extra_entrypoint_args, + custom_configs=custom_configs) self.image = image self.entrypoint = entrypoint @@ -1426,6 +1561,19 @@ class CustomContainerSpec(ServiceSpec): config_json[prop] = value return config_json + def validate(self) -> None: + super(CustomContainerSpec, self).validate() + + if self.args and self.extra_container_args: + raise SpecValidationError( + '"args" and "extra_container_args" are mutually exclusive ' + '(and both serve the same purpose)') + + if self.files and self.custom_configs: + raise SpecValidationError( + '"files" and "custom_configs" are mutually exclusive ' + '(and both serve the same purpose)') + yaml.add_representer(CustomContainerSpec, ServiceSpec.yaml_representer) @@ -1540,6 +1688,7 @@ class GrafanaSpec(MonitoringSpec): preview_only: bool = False, config: Optional[Dict[str, str]] = None, networks: Optional[List[str]] = None, + only_bind_port_on_networks: bool = False, port: Optional[int] = None, protocol: Optional[str] = 'https', initial_admin_password: Optional[str] = None, @@ -1560,6 +1709,12 @@ class GrafanaSpec(MonitoringSpec): self.anonymous_access = anonymous_access self.protocol = protocol + # whether ports daemons for this service bind to should + # bind to only hte networks listed in networks param, or + # to all networks. Defaults to false which is saying to bind + # on all networks. + self.only_bind_port_on_networks = only_bind_port_on_networks + def validate(self) -> None: super(GrafanaSpec, self).validate() if self.protocol not in ['http', 'https']: @@ -1585,6 +1740,7 @@ class PrometheusSpec(MonitoringSpec): preview_only: bool = False, config: Optional[Dict[str, str]] = None, networks: Optional[List[str]] = None, + only_bind_port_on_networks: bool = False, port: Optional[int] = None, retention_time: Optional[str] = None, retention_size: Optional[str] = None, @@ -1602,6 +1758,7 @@ class PrometheusSpec(MonitoringSpec): self.retention_time = retention_time.strip() if retention_time else None self.retention_size = retention_size.strip() if retention_size else None + self.only_bind_port_on_networks = only_bind_port_on_networks def validate(self) -> None: super(PrometheusSpec, self).validate() @@ -1820,6 +1977,7 @@ class MONSpec(ServiceSpec): preview_only: bool = False, networks: Optional[List[str]] = None, extra_container_args: Optional[GeneralArgList] = None, + extra_entrypoint_args: Optional[GeneralArgList] = None, custom_configs: Optional[List[CustomConfig]] = None, crush_locations: Optional[Dict[str, List[str]]] = None, ): @@ -1832,6 +1990,7 @@ class MONSpec(ServiceSpec): preview_only=preview_only, networks=networks, extra_container_args=extra_container_args, + extra_entrypoint_args=extra_entrypoint_args, custom_configs=custom_configs) self.crush_locations = crush_locations @@ -1980,6 +2139,7 @@ class CephExporterSpec(ServiceSpec): unmanaged: bool = False, preview_only: bool = False, extra_container_args: Optional[GeneralArgList] = None, + extra_entrypoint_args: Optional[GeneralArgList] = None, ): assert service_type == 'ceph-exporter' @@ -1988,7 +2148,8 @@ class CephExporterSpec(ServiceSpec): placement=placement, unmanaged=unmanaged, preview_only=preview_only, - extra_container_args=extra_container_args) + extra_container_args=extra_container_args, + extra_entrypoint_args=extra_entrypoint_args) self.service_type = service_type self.sock_dir = sock_dir |