]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/python-common/ceph/deployment/service_spec.py
update ceph source to reef 18.1.2
[ceph.git] / ceph / src / python-common / ceph / deployment / service_spec.py
index 70757304ad9ebb75c6882ff7317e00e1a372724f..df99e3bbee201e51b200bb35db309ce81649edd7 100644 (file)
@@ -497,8 +497,9 @@ class ServiceSpec(object):
     start the services.
     """
     KNOWN_SERVICE_TYPES = 'alertmanager crash grafana iscsi loki promtail mds mgr mon nfs ' \
-                          'node-exporter osd prometheus rbd-mirror rgw agent ' \
-                          'container ingress cephfs-mirror snmp-gateway ceph-exporter'.split()
+                          '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()
     REQUIRES_SERVICE_ID = 'iscsi mds nfs rgw container ingress '.split()
     MANAGED_CONFIG_OPTIONS = [
         'mds_join_fs',
@@ -509,6 +510,7 @@ class ServiceSpec(object):
         from ceph.deployment.drive_group import DriveGroupSpec
 
         ret = {
+            'mon': MONSpec,
             'rgw': RGWSpec,
             'nfs': NFSServiceSpec,
             'osd': DriveGroupSpec,
@@ -524,6 +526,11 @@ class ServiceSpec(object):
             'loki': MonitoringSpec,
             'promtail': MonitoringSpec,
             'snmp-gateway': SNMPGatewaySpec,
+            'elasticsearch': TracingSpec,
+            'jaeger-agent': TracingSpec,
+            'jaeger-collector': TracingSpec,
+            'jaeger-query': TracingSpec,
+            'jaeger-tracing': TracingSpec,
         }.get(service_type, cls)
         if ret == ServiceSpec and not service_type:
             raise SpecValidationError('Spec needs a "service_type" key.')
@@ -633,7 +640,7 @@ class ServiceSpec(object):
         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).
+        the next two major releases (octopus, pacific).
 
         :param json_spec: A valid dict with ServiceSpec
 
@@ -803,6 +810,7 @@ class NFSServiceSpec(ServiceSpec):
                  config: Optional[Dict[str, str]] = None,
                  networks: Optional[List[str]] = None,
                  port: Optional[int] = None,
+                 virtual_ip: Optional[str] = None,
                  extra_container_args: Optional[List[str]] = None,
                  extra_entrypoint_args: Optional[List[str]] = None,
                  custom_configs: Optional[List[CustomConfig]] = None,
@@ -815,6 +823,7 @@ class NFSServiceSpec(ServiceSpec):
             extra_entrypoint_args=extra_entrypoint_args, custom_configs=custom_configs)
 
         self.port = port
+        self.virtual_ip = virtual_ip
 
     def get_port_start(self) -> List[int]:
         if self.port:
@@ -839,6 +848,7 @@ class RGWSpec(ServiceSpec):
         service_id: myrealm.myzone
         spec:
             rgw_realm: myrealm
+            rgw_zonegroup: myzonegroup
             rgw_zone: myzone
             ssl: true
             rgw_frontend_port: 1234
@@ -851,6 +861,7 @@ class RGWSpec(ServiceSpec):
     MANAGED_CONFIG_OPTIONS = ServiceSpec.MANAGED_CONFIG_OPTIONS + [
         'rgw_zone',
         'rgw_realm',
+        'rgw_zonegroup',
         'rgw_frontends',
     ]
 
@@ -859,10 +870,12 @@ class RGWSpec(ServiceSpec):
                  service_id: Optional[str] = None,
                  placement: Optional[PlacementSpec] = None,
                  rgw_realm: Optional[str] = None,
+                 rgw_zonegroup: Optional[str] = None,
                  rgw_zone: Optional[str] = None,
                  rgw_frontend_port: Optional[int] = None,
                  rgw_frontend_ssl_certificate: Optional[List[str]] = None,
                  rgw_frontend_type: Optional[str] = None,
+                 rgw_frontend_extra_args: Optional[List[str]] = None,
                  unmanaged: bool = False,
                  ssl: bool = False,
                  preview_only: bool = False,
@@ -872,6 +885,9 @@ class RGWSpec(ServiceSpec):
                  extra_container_args: Optional[List[str]] = None,
                  extra_entrypoint_args: Optional[List[str]] = None,
                  custom_configs: Optional[List[CustomConfig]] = None,
+                 rgw_realm_token: Optional[str] = None,
+                 update_endpoints: Optional[bool] = False,
+                 zone_endpoints: Optional[str] = None  # commad separated endpoints list
                  ):
         assert service_type == 'rgw', service_type
 
@@ -887,8 +903,16 @@ class RGWSpec(ServiceSpec):
             custom_configs=custom_configs)
 
         #: The RGW realm associated with this service. Needs to be manually created
+        #: if the spec is being applied directly to cephdam. In case of rgw module
+        #: the realm is created automatically.
         self.rgw_realm: Optional[str] = rgw_realm
+        #: The RGW zonegroup associated with this service. Needs to be manually created
+        #: if the spec is being applied directly to cephdam. In case of rgw module
+        #: the zonegroup is created automatically.
+        self.rgw_zonegroup: Optional[str] = rgw_zonegroup
         #: The RGW zone associated with this service. Needs to be manually created
+        #: if the spec is being applied directly to cephdam. In case of rgw module
+        #: the zone is created automatically.
         self.rgw_zone: Optional[str] = rgw_zone
         #: Port of the RGW daemons
         self.rgw_frontend_port: Optional[int] = rgw_frontend_port
@@ -896,8 +920,13 @@ class RGWSpec(ServiceSpec):
         self.rgw_frontend_ssl_certificate: Optional[List[str]] = rgw_frontend_ssl_certificate
         #: civetweb or beast (default: beast). See :ref:`rgw_frontends`
         self.rgw_frontend_type: Optional[str] = rgw_frontend_type
+        #: List of extra arguments for rgw_frontend in the form opt=value. See :ref:`rgw_frontends`
+        self.rgw_frontend_extra_args: Optional[List[str]] = rgw_frontend_extra_args
         #: enable SSL
         self.ssl = ssl
+        self.rgw_realm_token = rgw_realm_token
+        self.update_endpoints = update_endpoints
+        self.zone_endpoints = zone_endpoints
 
     def get_port_start(self) -> List[int]:
         return [self.get_port()]
@@ -917,8 +946,14 @@ class RGWSpec(ServiceSpec):
             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')
+            raise SpecValidationError('Cannot add RGW: Zone specified but no realm specified')
+
+        if self.rgw_frontend_type is not None:
+            if self.rgw_frontend_type not in ['beast', 'civetweb']:
+                raise SpecValidationError(
+                    'Invalid rgw_frontend_type value. Valid values are: beast, civetweb.\n'
+                    'Additional rgw type parameters can be passed using rgw_frontend_extra_args.'
+                )
 
 
 yaml.add_representer(RGWSpec, ServiceSpec.yaml_representer)
@@ -1018,6 +1053,7 @@ class IngressSpec(ServiceSpec):
                  virtual_interface_networks: Optional[List[str]] = [],
                  unmanaged: bool = False,
                  ssl: bool = False,
+                 keepalive_only: bool = False,
                  extra_container_args: Optional[List[str]] = None,
                  extra_entrypoint_args: Optional[List[str]] = None,
                  custom_configs: Optional[List[CustomConfig]] = None,
@@ -1048,10 +1084,15 @@ class IngressSpec(ServiceSpec):
         self.virtual_interface_networks = virtual_interface_networks or []
         self.unmanaged = unmanaged
         self.ssl = ssl
+        self.keepalive_only = keepalive_only
 
     def get_port_start(self) -> List[int]:
-        return [cast(int, self.frontend_port),
-                cast(int, self.monitor_port)]
+        ports = []
+        if self.frontend_port is not None:
+            ports.append(cast(int, self.frontend_port))
+        if self.monitor_port is not None:
+            ports.append(cast(int, self.monitor_port))
+        return ports
 
     def get_virtual_ip(self) -> Optional[str]:
         return self.virtual_ip
@@ -1062,7 +1103,7 @@ class IngressSpec(ServiceSpec):
         if not self.backend_service:
             raise SpecValidationError(
                 'Cannot add ingress: No backend_service specified')
-        if not self.frontend_port:
+        if not self.keepalive_only and not self.frontend_port:
             raise SpecValidationError(
                 'Cannot add ingress: No frontend_port specified')
         if not self.monitor_port:
@@ -1258,7 +1299,9 @@ class GrafanaSpec(MonitoringSpec):
                  config: Optional[Dict[str, str]] = None,
                  networks: Optional[List[str]] = None,
                  port: Optional[int] = None,
+                 protocol: Optional[str] = 'https',
                  initial_admin_password: Optional[str] = None,
+                 anonymous_access: Optional[bool] = True,
                  extra_container_args: Optional[List[str]] = None,
                  extra_entrypoint_args: Optional[List[str]] = None,
                  custom_configs: Optional[List[CustomConfig]] = None,
@@ -1272,6 +1315,20 @@ class GrafanaSpec(MonitoringSpec):
             custom_configs=custom_configs)
 
         self.initial_admin_password = initial_admin_password
+        self.anonymous_access = anonymous_access
+        self.protocol = protocol
+
+    def validate(self) -> None:
+        super(GrafanaSpec, self).validate()
+        if self.protocol not in ['http', 'https']:
+            err_msg = f"Invalid protocol '{self.protocol}'. Valid values are: 'http', 'https'."
+            raise SpecValidationError(err_msg)
+
+        if not self.anonymous_access and not self.initial_admin_password:
+            err_msg = ('Either initial_admin_password must be set or anonymous_access '
+                       'must be set to true. Otherwise the grafana dashboard will '
+                       'be inaccessible.')
+            raise SpecValidationError(err_msg)
 
 
 yaml.add_representer(GrafanaSpec, ServiceSpec.yaml_representer)
@@ -1304,6 +1361,22 @@ 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
 
+    def validate(self) -> None:
+        super(PrometheusSpec, self).validate()
+
+        if self.retention_time:
+            valid_units = ['y', 'w', 'd', 'h', 'm', 's']
+            m = re.search(rf"^(\d+)({'|'.join(valid_units)})$", self.retention_time)
+            if not m:
+                units = ', '.join(valid_units)
+                raise SpecValidationError(f"Invalid retention time. Valid units are: {units}")
+        if self.retention_size:
+            valid_units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']
+            m = re.search(rf"^(\d+)({'|'.join(valid_units)})$", self.retention_size)
+            if not m:
+                units = ', '.join(valid_units)
+                raise SpecValidationError(f"Invalid retention size. Valid units are: {units}")
+
 
 yaml.add_representer(PrometheusSpec, ServiceSpec.yaml_representer)
 
@@ -1494,6 +1567,113 @@ class MDSSpec(ServiceSpec):
 yaml.add_representer(MDSSpec, ServiceSpec.yaml_representer)
 
 
+class MONSpec(ServiceSpec):
+    def __init__(self,
+                 service_type: str,
+                 service_id: Optional[str] = None,
+                 placement: Optional[PlacementSpec] = None,
+                 count: Optional[int] = None,
+                 config: Optional[Dict[str, str]] = None,
+                 unmanaged: bool = False,
+                 preview_only: bool = False,
+                 networks: Optional[List[str]] = None,
+                 extra_container_args: Optional[List[str]] = None,
+                 custom_configs: Optional[List[CustomConfig]] = None,
+                 crush_locations: Optional[Dict[str, List[str]]] = None,
+                 ):
+        assert service_type == 'mon'
+        super(MONSpec, self).__init__('mon', service_id=service_id,
+                                      placement=placement,
+                                      count=count,
+                                      config=config,
+                                      unmanaged=unmanaged,
+                                      preview_only=preview_only,
+                                      networks=networks,
+                                      extra_container_args=extra_container_args,
+                                      custom_configs=custom_configs)
+
+        self.crush_locations = crush_locations
+        self.validate()
+
+    def validate(self) -> None:
+        if self.crush_locations:
+            for host, crush_locs in self.crush_locations.items():
+                try:
+                    assert_valid_host(host)
+                except SpecValidationError as e:
+                    err_str = f'Invalid hostname found in spec crush locations: {e}'
+                    raise SpecValidationError(err_str)
+                for cloc in crush_locs:
+                    if '=' not in cloc or len(cloc.split('=')) != 2:
+                        err_str = ('Crush locations must be of form <bucket>=<location>. '
+                                   f'Found crush location: {cloc}')
+                        raise SpecValidationError(err_str)
+
+
+yaml.add_representer(MONSpec, ServiceSpec.yaml_representer)
+
+
+class TracingSpec(ServiceSpec):
+    SERVICE_TYPES = ['elasticsearch', 'jaeger-collector', 'jaeger-query', 'jaeger-agent']
+
+    def __init__(self,
+                 service_type: str,
+                 es_nodes: Optional[str] = None,
+                 without_query: bool = False,
+                 service_id: Optional[str] = None,
+                 config: Optional[Dict[str, str]] = None,
+                 networks: Optional[List[str]] = None,
+                 placement: Optional[PlacementSpec] = None,
+                 unmanaged: bool = False,
+                 preview_only: bool = False
+                 ):
+        assert service_type in TracingSpec.SERVICE_TYPES + ['jaeger-tracing']
+
+        super(TracingSpec, self).__init__(
+            service_type, service_id,
+            placement=placement, unmanaged=unmanaged,
+            preview_only=preview_only, config=config,
+            networks=networks)
+        self.without_query = without_query
+        self.es_nodes = es_nodes
+
+    def get_port_start(self) -> List[int]:
+        return [self.get_port()]
+
+    def get_port(self) -> int:
+        return {'elasticsearch': 9200,
+                'jaeger-agent': 6799,
+                'jaeger-collector': 14250,
+                'jaeger-query': 16686}[self.service_type]
+
+    def get_tracing_specs(self) -> List[ServiceSpec]:
+        assert self.service_type == 'jaeger-tracing'
+        specs: List[ServiceSpec] = []
+        daemons: Dict[str, Optional[PlacementSpec]] = {
+            daemon: None for daemon in TracingSpec.SERVICE_TYPES}
+
+        if self.es_nodes:
+            del daemons['elasticsearch']
+        if self.without_query:
+            del daemons['jaeger-query']
+        if self.placement:
+            daemons.update({'jaeger-collector': self.placement})
+
+        for daemon, daemon_placement in daemons.items():
+            specs.append(TracingSpec(service_type=daemon,
+                                     es_nodes=self.es_nodes,
+                                     placement=daemon_placement,
+                                     unmanaged=self.unmanaged,
+                                     config=self.config,
+                                     networks=self.networks,
+                                     preview_only=self.preview_only
+                                     ))
+        return specs
+
+
+yaml.add_representer(TracingSpec, ServiceSpec.yaml_representer)
+
+
 class TunedProfileSpec():
     def __init__(self,
                  profile_name: str,