]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/python-common/ceph/deployment/service_spec.py
import 15.2.2 octopus source
[ceph.git] / ceph / src / python-common / ceph / deployment / service_spec.py
index 45474c07aa87872404f01a8f0939d6ee52b9a617..5bcf7d91a9ae1a2f9aed2e7840e45177aaaf0372 100644 (file)
@@ -1,6 +1,7 @@
 import fnmatch
 import re
 from collections import namedtuple
+from functools import wraps
 from typing import Optional, Dict, Any, List, Union
 
 import six
@@ -28,6 +29,17 @@ def assert_valid_host(name):
         raise ServiceSpecValidationError(e)
 
 
+def handle_type_error(method):
+    @wraps(method)
+    def inner(cls, *args, **kwargs):
+        try:
+            return method(cls, *args, **kwargs)
+        except (TypeError, AttributeError) as e:
+            error_msg = '{}: {}'.format(cls.__name__, e)
+        raise ServiceSpecValidationError(error_msg)
+    return inner
+
+
 class HostPlacementSpec(namedtuple('HostPlacementSpec', ['hostname', 'network', 'name'])):
     def __str__(self):
         res = ''
@@ -39,6 +51,7 @@ class HostPlacementSpec(namedtuple('HostPlacementSpec', ['hostname', 'network',
         return res
 
     @classmethod
+    @handle_type_error
     def from_json(cls, data):
         return cls(**data)
 
@@ -97,7 +110,9 @@ class HostPlacementSpec(namedtuple('HostPlacementSpec', ['hostname', 'network',
         if ',' in network:
             networks = [x for x in network.split(',')]
         else:
-            networks.append(network)
+            if network != '':
+                networks.append(network)
+
         for network in networks:
             # only if we have versioned network configs
             if network.startswith('v') or network.startswith('[v'):
@@ -189,11 +204,17 @@ class PlacementSpec(object):
         return "PlacementSpec(%s)" % ', '.join(kv)
 
     @classmethod
+    @handle_type_error
     def from_json(cls, data):
-        hosts = data.get('hosts', [])
+        c = data.copy()
+        hosts = c.get('hosts', [])
         if hosts:
-            data['hosts'] = [HostPlacementSpec.from_json(host) for host in hosts]
-        _cls = cls(**data)
+            c['hosts'] = []
+            for host in hosts:
+                c['hosts'].append(HostPlacementSpec.parse(host) if
+                                  isinstance(host, str) else
+                                  HostPlacementSpec.from_json(host))
+        _cls = cls(**c)
         _cls.validate()
         return _cls
 
@@ -323,9 +344,38 @@ class ServiceSpec(object):
     start the services.
 
     """
-    KNOWN_SERVICE_TYPES = 'alertmanager crash grafana mds mgr mon nfs ' \
+    KNOWN_SERVICE_TYPES = 'alertmanager crash grafana iscsi mds mgr mon nfs ' \
                           'node-exporter osd prometheus rbd-mirror rgw'.split()
 
+    @classmethod
+    def _cls(cls, service_type):
+        from ceph.deployment.drive_group import DriveGroupSpec
+
+        ret = {
+            'rgw': RGWSpec,
+            'nfs': NFSServiceSpec,
+            'osd': DriveGroupSpec,
+            'iscsi': IscsiServiceSpec,
+        }.get(service_type, cls)
+        if ret == ServiceSpec and not service_type:
+            raise ServiceSpecValidationError('Spec needs a "service_type" key.')
+        return ret
+
+    def __new__(cls, *args, **kwargs):
+        """
+        Some Python foo to make sure, we don't have an object
+        like `ServiceSpec('rgw')` of type `ServiceSpec`. Now we have:
+
+        >>> type(ServiceSpec('rgw')) == type(RGWSpec('rgw'))
+        True
+
+        """
+        if cls != ServiceSpec:
+            return object.__new__(cls)
+        service_type = kwargs.get('service_type', args[0] if args else None)
+        sub_cls = cls._cls(service_type)
+        return object.__new__(sub_cls)
+
     def __init__(self,
                  service_type,     # type: str
                  service_id=None,  # type: Optional[str]
@@ -341,6 +391,7 @@ class ServiceSpec(object):
         self.unmanaged = unmanaged
 
     @classmethod
+    @handle_type_error
     def from_json(cls, json_spec):
         # type: (dict) -> Any
         # Python 3:
@@ -350,19 +401,27 @@ class ServiceSpec(object):
         Initialize 'ServiceSpec' object data from a json structure
         :param json_spec: A valid dict with ServiceSpec
         """
-        from ceph.deployment.drive_group import DriveGroupSpec
 
-        service_type = json_spec.get('service_type', '')
-        _cls = {
-            'rgw': RGWSpec,
-            'nfs': NFSServiceSpec,
-            'osd': DriveGroupSpec
-        }.get(service_type, cls)
+        c = json_spec.copy()
 
-        if _cls == ServiceSpec and not service_type:
-            raise ServiceSpecValidationError('Spec needs a "service_type" key.')
+        # 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(json_spec)  # type: ignore
+        return _cls._from_json_impl(c)  # type: ignore
 
     @classmethod
     def _from_json_impl(cls, json_spec):
@@ -374,7 +433,9 @@ class ServiceSpec(object):
                 args.update(v)
                 continue
             args.update({k: v})
-        return cls(**args)
+        _cls = cls(**args)
+        _cls.validate()
+        return _cls
 
     def service_name(self):
         n = self.service_type
@@ -390,6 +451,8 @@ class ServiceSpec(object):
                 val = val.to_json()
             if val:
                 c[key] = val
+
+        c['service_name'] = self.service_name()
         return c
 
     def validate(self):
@@ -410,12 +473,12 @@ def servicespec_validate_add(self: ServiceSpec):
     # This must not be a method of ServiceSpec, otherwise you'll hunt
     # sub-interpreter affinity bugs.
     ServiceSpec.validate(self)
-    if self.service_type in ['mds', 'rgw', 'nfs'] and not self.service_id:
+    if self.service_type in ['mds', 'rgw', 'nfs', 'iscsi'] and not self.service_id:
         raise ServiceSpecValidationError('Cannot add Service: id required')
 
 
 class NFSServiceSpec(ServiceSpec):
-    def __init__(self, service_id, pool=None, namespace=None, placement=None,
+    def __init__(self, service_id=None, pool=None, namespace=None, placement=None,
                  service_type='nfs', unmanaged=False):
         assert service_type == 'nfs'
         super(NFSServiceSpec, self).__init__(
@@ -434,6 +497,18 @@ class NFSServiceSpec(ServiceSpec):
         if not self.pool:
             raise ServiceSpecValidationError('Cannot add NFS: No Pool specified')
 
+    def rados_config_name(self):
+        # type: () -> str
+        return 'conf-' + self.service_name()
+
+    def rados_config_location(self):
+        # type: () -> str
+        url = 'rados://' + self.pool + '/'
+        if self.namespace:
+            url += self.namespace + '/'
+        url += self.rados_config_name()
+        return url
+
 
 class RGWSpec(ServiceSpec):
     """
@@ -441,17 +516,19 @@ class RGWSpec(ServiceSpec):
 
     """
     def __init__(self,
+                 service_type='rgw',
+                 service_id=None,  # type: Optional[str]
+                 placement=None,
                  rgw_realm=None,  # type: Optional[str]
                  rgw_zone=None,  # type: Optional[str]
                  subcluster=None,  # type: Optional[str]
-                 service_id=None,  # type: Optional[str]
-                 placement=None,
-                 service_type='rgw',
                  rgw_frontend_port=None,  # type: Optional[int]
+                 rgw_frontend_ssl_certificate=None,  # type Optional[List[str]]
+                 rgw_frontend_ssl_key=None,  # type: Optional[List[str]]
                  unmanaged=False,  # type: bool
                  ssl=False,   # type: bool
                  ):
-        assert service_type == 'rgw'
+        assert service_type == 'rgw', service_type
         if service_id:
             a = service_id.split('.', 2)
             rgw_realm = a[0]
@@ -471,6 +548,8 @@ class RGWSpec(ServiceSpec):
         self.rgw_zone = rgw_zone
         self.subcluster = subcluster
         self.rgw_frontend_port = rgw_frontend_port
+        self.rgw_frontend_ssl_certificate = rgw_frontend_ssl_certificate
+        self.rgw_frontend_ssl_key = rgw_frontend_ssl_key
         self.ssl = ssl
 
     def get_port(self):
@@ -480,3 +559,49 @@ class RGWSpec(ServiceSpec):
             return 443
         else:
             return 80
+
+    def rgw_frontends_config_value(self):
+        ports = []
+        if self.ssl:
+            ports.append(f"ssl_port={self.get_port()}")
+            ports.append(f"ssl_certificate=config://rgw/cert/{self.rgw_realm}/{self.rgw_zone}.crt")
+            ports.append(f"ssl_key=config://rgw/cert/{self.rgw_realm}/{self.rgw_zone}.key")
+        else:
+            ports.append(f"port={self.get_port()}")
+        return f'beast {" ".join(ports)}'
+
+
+class IscsiServiceSpec(ServiceSpec):
+    def __init__(self, service_id, pool=None,
+                 placement=None,
+                 trusted_ip_list=None,
+                 fqdn_enabled=None,
+                 api_port=None,
+                 api_user=None,
+                 api_password=None,
+                 api_secure=None,
+                 ssl_cert=None,
+                 ssl_key=None,
+                 service_type='iscsi',
+                 unmanaged=False):
+        assert service_type == 'iscsi'
+        super(IscsiServiceSpec, self).__init__('iscsi', service_id=service_id,
+                                               placement=placement, unmanaged=unmanaged)
+
+        #: RADOS pool where ceph-iscsi config data is stored.
+        self.pool = pool
+        self.trusted_ip_list = trusted_ip_list
+        self.fqdn_enabled = fqdn_enabled
+        self.api_port = api_port
+        self.api_user = api_user
+        self.api_password = api_password
+        self.api_secure = api_secure
+        self.ssl_cert = ssl_cert
+        self.ssl_key = ssl_key
+
+    def validate_add(self):
+        servicespec_validate_add(self)
+
+        if not self.pool:
+            raise ServiceSpecValidationError(
+                'Cannot add ISCSI: No Pool specified')