import fnmatch
import re
from collections import namedtuple
+from functools import wraps
from typing import Optional, Dict, Any, List, Union
import six
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 = ''
return res
@classmethod
+ @handle_type_error
def from_json(cls, data):
return cls(**data)
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'):
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
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]
self.unmanaged = unmanaged
@classmethod
+ @handle_type_error
def from_json(cls, json_spec):
# type: (dict) -> Any
# Python 3:
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):
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
val = val.to_json()
if val:
c[key] = val
+
+ c['service_name'] = self.service_name()
return c
def validate(self):
# 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__(
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):
"""
"""
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]
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):
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')