def validate(self) -> None: super(RGWSpec, self).validate() if self.rgw_realm and not self.rgw_zone: raise SpecValidationError( 'Cannot add RGW: Realm specified but no zone specified') if self.rgw_zone and not self.rgw_realm: raise SpecValidationError( 'Cannot add RGW: Zone specified but no realm specified')
def validate(self) -> None: super(SNMPGatewaySpec, self).validate() if not self.credentials: raise SpecValidationError( 'Missing authentication information (credentials). ' 'SNMP V2c and V3 require credential information' ) elif not self.snmp_version: raise SpecValidationError( 'Missing SNMP version (snmp_version)' ) creds_requirement = { 'V2c': ['snmp_community'], 'V3': ['snmp_v3_auth_username', 'snmp_v3_auth_password'] } if self.privacy_protocol: creds_requirement['V3'].append('snmp_v3_priv_password') missing = [parm for parm in creds_requirement[self.snmp_version] if parm not in self.credentials] # check that credentials are correct for the version if missing: raise SpecValidationError( f'SNMP {self.snmp_version} credentials are incomplete. Missing {", ".join(missing)}' ) if self.engine_id: if 10 <= len(self.engine_id) <= 64 and \ is_hex(self.engine_id) and \ len(self.engine_id) % 2 == 0: pass else: raise SpecValidationError( 'engine_id must be a string containing 10-64 hex characters. ' 'Its length must be divisible by 2' ) else: if self.snmp_version == 'V3': raise SpecValidationError( 'Must provide an engine_id for SNMP V3 notifications' ) if not self.snmp_destination: raise SpecValidationError( 'SNMP destination (snmp_destination) must be provided' ) else: valid, description = valid_addr(self.snmp_destination) if not valid: raise SpecValidationError( f'SNMP destination (snmp_destination) is invalid: {description}' ) if description not in self.valid_destination_types: raise SpecValidationError( f'SNMP destination (snmp_destination) type ({description}) is invalid. ' f'Must be either: {", ".join(sorted(self.valid_destination_types))}' )
def validate(self) -> None: super(IngressSpec, self).validate() if not self.backend_service: raise SpecValidationError( 'Cannot add ingress: No backend_service specified') if not self.frontend_port: raise SpecValidationError( 'Cannot add ingress: No frontend_port specified') if not self.monitor_port: raise SpecValidationError( 'Cannot add ingress: No monitor_port specified') if not self.virtual_ip: raise SpecValidationError( 'Cannot add ingress: No virtual_ip provided')
def validate(self) -> None: super(AlertManagerSpec, self).validate() if self.port == 9094: raise SpecValidationError( 'Port 9094 is reserved for AlertManager cluster listen address' )
def _check_type(name: str, value: Optional[str], options: List[str]) -> None: if not value: return if value not in options: raise SpecValidationError( f'{name} unsupported. Must be one of {", ".join(sorted(options))}' )
def validate(self) -> None: if self.hosts and self.label: # TODO: a less generic Exception raise SpecValidationError('Host and label are mutually exclusive') if self.count is not None and self.count <= 0: raise SpecValidationError("num/count must be > 1") if self.count_per_host is not None and self.count_per_host < 1: raise SpecValidationError("count-per-host must be >= 1") if self.count_per_host is not None and not (self.label or self.hosts or self.host_pattern): raise SpecValidationError( "count-per-host must be combined with label or hosts or host_pattern" ) if self.count is not None and self.count_per_host is not None: raise SpecValidationError( "cannot combine count and count-per-host") if (self.count_per_host is not None and self.hosts and any([hs.network or hs.name for hs in self.hosts])): raise SpecValidationError( "count-per-host cannot be combined explicit placement with names or networks" ) if self.host_pattern and self.hosts: raise SpecValidationError('cannot combine host patterns and hosts') for h in self.hosts: h.validate()
def assert_valid_host(name: str) -> None: p = re.compile('^[a-zA-Z0-9-]+$') try: assert len(name) <= 250, 'name is too long (max 250 chars)' for part in name.split('.'): assert len(part) > 0, '.-delimited name component must not be empty' assert len(part) <= 63, '.-delimited name component must not be more than 63 chars' assert p.match(part), 'name component must include only a-z, 0-9, and -' except AssertionError as e: raise SpecValidationError(str(e))
def normalize_json(json_spec: dict) -> dict: networks = json_spec.get('networks') if networks is None: return json_spec if isinstance(networks, list): return json_spec if not isinstance(networks, str): raise SpecValidationError(f'Networks ({networks}) must be a string or list of strings') json_spec['networks'] = [networks] return json_spec
def validate(self) -> None: if not self.service_type: raise SpecValidationError('Cannot add Service: type required') if self.service_type in self.REQUIRES_SERVICE_ID: if not self.service_id: raise SpecValidationError('Cannot add Service: id required') if not re.match('^[a-zA-Z0-9_.-]+$', self.service_id): raise SpecValidationError( 'Service id contains invalid characters, ' 'only [a-zA-Z0-9_.-] allowed') elif self.service_id: raise SpecValidationError( f'Service of type \'{self.service_type}\' should not contain a service id' ) if self.placement is not None: self.placement.validate() if self.config: for k, v in self.config.items(): if k in self.MANAGED_CONFIG_OPTIONS: raise SpecValidationError( f'Cannot set config option {k} in spec: it is managed by cephadm' ) for network in self.networks or []: try: ip_network(network) except ValueError as e: raise SpecValidationError( f'Cannot parse network {network}: {e}')
def _cls(cls: Type[ServiceSpecT], service_type: str) -> Type[ServiceSpecT]: from ceph.deployment.drive_group import DriveGroupSpec ret = { 'rgw': RGWSpec, 'nfs': NFSServiceSpec, 'osd': DriveGroupSpec, 'iscsi': IscsiServiceSpec, 'alertmanager': AlertManagerSpec, 'ingress': IngressSpec, 'container': CustomContainerSpec, }.get(service_type, cls) if ret == ServiceSpec and not service_type: raise SpecValidationError('Spec needs a "service_type" key.') return ret
def _from_json_impl(cls, json_spec: dict) -> 'SNMPGatewaySpec': cpy = json_spec.copy() types = [ ('snmp_version', SNMPGatewaySpec.SNMPVersion), ('auth_protocol', SNMPGatewaySpec.SNMPAuthType), ('privacy_protocol', SNMPGatewaySpec.SNMPPrivacyType), ] for d in cpy, cpy.get('spec', {}): for key, enum_cls in types: try: if key in d: d[key] = enum_cls(d[key]) except ValueError: raise SpecValidationError(f'{key} unsupported. Must be one of ' f'{", ".join(enum_cls)}') return super(SNMPGatewaySpec, cls)._from_json_impl(cpy)
def _cls(cls: Type[ServiceSpecT], service_type: str) -> Type[ServiceSpecT]: from ceph.deployment.drive_group import DriveGroupSpec ret = { 'rgw': RGWSpec, 'nfs': NFSServiceSpec, 'osd': DriveGroupSpec, 'iscsi': IscsiServiceSpec, 'alertmanager': AlertManagerSpec, 'ingress': IngressSpec, 'container': CustomContainerSpec, 'grafana': GrafanaSpec, 'node-exporter': MonitoringSpec, 'prometheus': MonitoringSpec, 'snmp-gateway': SNMPGatewaySpec, }.get(service_type, cls) if ret == ServiceSpec and not service_type: raise SpecValidationError('Spec needs a "service_type" key.') return ret
def validate(self) -> None: super(IscsiServiceSpec, self).validate() if not self.pool: raise SpecValidationError('Cannot add ISCSI: No Pool specified')
def validate(self) -> None: super(MDSSpec, self).validate() if str(self.service_id)[0].isdigit(): raise SpecValidationError( 'MDS service id cannot start with a numeric digit')
def from_json(cls: Type[ServiceSpecT], json_spec: Dict) -> ServiceSpecT: """ Initialize 'ServiceSpec' object data from a json structure There are two valid styles for service specs: the "old" style: .. code:: yaml service_type: nfs service_id: foo pool: mypool namespace: myns and the "new" style: .. code:: yaml service_type: nfs service_id: foo config: some_option: the_value networks: [10.10.0.0/16] spec: pool: mypool namespace: myns In https://tracker.ceph.com/issues/45321 we decided that we'd like to prefer the new style as it is more readable and provides a better understanding of what fields are special for a give service type. Note, we'll need to stay compatible with both versions for the the next two major releases (octoups, pacific). :param json_spec: A valid dict with ServiceSpec :meta private: """ if not isinstance(json_spec, dict): raise SpecValidationError( f'Service Spec is not an (JSON or YAML) object. got "{str(json_spec)}"' ) json_spec = cls.normalize_json(json_spec) c = json_spec.copy() # kludge to make `from_json` compatible to `Orchestrator.describe_service` # Open question: Remove `service_id` form to_json? if c.get('service_name', ''): service_type_id = c['service_name'].split('.', 1) if not c.get('service_type', ''): c['service_type'] = service_type_id[0] if not c.get('service_id', '') and len(service_type_id) > 1: c['service_id'] = service_type_id[1] del c['service_name'] service_type = c.get('service_type', '') _cls = cls._cls(service_type) if 'status' in c: del c[ 'status'] # kludge to make us compatible to `ServiceDescription.to_json()` return _cls._from_json_impl(c) # type: ignore
def inner(cls: Any, *args: Any, **kwargs: Any) -> Any: try: return method(cls, *args, **kwargs) except (TypeError, AttributeError) as e: error_msg = '{}: {}'.format(cls.__name__, e) raise SpecValidationError(error_msg)
def from_string(cls, arg): # type: (Optional[str]) -> PlacementSpec """ A single integer is parsed as a count: >>> PlacementSpec.from_string('3') PlacementSpec(count=3) A list of names is parsed as host specifications: >>> PlacementSpec.from_string('host1 host2') PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacemen\ tSpec(hostname='host2', network='', name='')]) You can also prefix the hosts with a count as follows: >>> PlacementSpec.from_string('2 host1 host2') PlacementSpec(count=2, hosts=[HostPlacementSpec(hostname='host1', network='', name=''), Hos\ tPlacementSpec(hostname='host2', network='', name='')]) You can spefify labels using `label:<label>` >>> PlacementSpec.from_string('label:mon') PlacementSpec(label='mon') Labels als support a count: >>> PlacementSpec.from_string('3 label:mon') PlacementSpec(count=3, label='mon') fnmatch is also supported: >>> PlacementSpec.from_string('data[1-3]') PlacementSpec(host_pattern='data[1-3]') >>> PlacementSpec.from_string(None) PlacementSpec() """ if arg is None or not arg: strings = [] elif isinstance(arg, str): if ' ' in arg: strings = arg.split(' ') elif ';' in arg: strings = arg.split(';') elif ',' in arg and '[' not in arg: # FIXME: this isn't quite right. we want to avoid breaking # a list of mons with addrvecs... so we're basically allowing # , most of the time, except when addrvecs are used. maybe # ok? strings = arg.split(',') else: strings = [arg] else: raise SpecValidationError('invalid placement %s' % arg) count = None count_per_host = None if strings: try: count = int(strings[0]) strings = strings[1:] except ValueError: pass for s in strings: if s.startswith('count:'): try: count = int(s[len('count:'):]) strings.remove(s) break except ValueError: pass for s in strings: if s.startswith('count-per-host:'): try: count_per_host = int(s[len('count-per-host:'):]) strings.remove(s) break except ValueError: pass 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 ] for a_h in advanced_hostspecs: strings.remove(a_h) labels = [x for x in strings if 'label:' in x] if len(labels) > 1: raise SpecValidationError( 'more than one label provided: {}'.format(labels)) for l in labels: strings.remove(l) label = labels[0][6:] if labels else None host_patterns = strings if len(host_patterns) > 1: raise SpecValidationError( 'more than one host pattern provided: {}'.format( host_patterns)) 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) return ps
def validate(self) -> None: if self.hosts and self.label: # TODO: a less generic Exception raise SpecValidationError('Host and label are mutually exclusive') if self.count is not None: try: intval = int(self.count) except (ValueError, TypeError): raise SpecValidationError("num/count must be a numeric value") if self.count != intval: raise SpecValidationError("num/count must be an integer value") if self.count < 1: raise SpecValidationError("num/count must be >= 1") if self.count_per_host is not None: try: intval = int(self.count_per_host) except (ValueError, TypeError): raise SpecValidationError("count-per-host must be a numeric value") if self.count_per_host != intval: raise SpecValidationError("count-per-host must be an integer value") if self.count_per_host < 1: raise SpecValidationError("count-per-host must be >= 1") if self.count_per_host is not None and not ( self.label or self.hosts or self.host_pattern ): raise SpecValidationError( "count-per-host must be combined with label or hosts or host_pattern" ) if self.count is not None and self.count_per_host is not None: raise SpecValidationError("cannot combine count and count-per-host") if ( self.count_per_host is not None and self.hosts and any([hs.network or hs.name for hs in self.hosts]) ): raise SpecValidationError( "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 self.hosts: raise SpecValidationError('cannot combine host patterns and hosts') for h in self.hosts: h.validate()