3 from collections
import namedtuple
4 from typing
import Optional
, Dict
, Any
, List
, Union
9 class ServiceSpecValidationError(Exception):
11 Defining an exception here is a bit problematic, cause you cannot properly catch it,
12 if it was raised in a different mgr module.
15 def __init__(self
, msg
):
16 super(ServiceSpecValidationError
, self
).__init
__(msg
)
19 def assert_valid_host(name
):
20 p
= re
.compile('^[a-zA-Z0-9-]+$')
22 assert len(name
) <= 250, 'name is too long (max 250 chars)'
23 for part
in name
.split('.'):
24 assert len(part
) > 0, '.-delimited name component must not be empty'
25 assert len(part
) <= 63, '.-delimited name component must not be more than 63 chars'
26 assert p
.match(part
), 'name component must include only a-z, 0-9, and -'
27 except AssertionError as e
:
28 raise ServiceSpecValidationError(e
)
31 class HostPlacementSpec(namedtuple('HostPlacementSpec', ['hostname', 'network', 'name'])):
36 res
+= ':' + self
.network
38 res
+= '=' + self
.name
42 def from_json(cls
, data
):
47 'hostname': self
.hostname
,
48 'network': self
.network
,
53 def parse(cls
, host
, require_network
=True):
54 # type: (str, bool) -> HostPlacementSpec
56 Split host into host, network, and (optional) daemon name parts. The network
57 part can be an IP, CIDR, or ceph addrvec like '[v2:1.2.3.4:3300,v1:1.2.3.4:6789]'.
64 "myhost:1.2.3.0/24=name"
65 "myhost:[v2:1.2.3.4:3000]=name"
66 "myhost:[v2:1.2.3.4:3000,v1:1.2.3.4:6789]=name"
68 # Matches from start to : or = or until end of string
69 host_re
= r
'^(.*?)(:|=|$)'
70 # Matches from : to = or until end of string
71 ip_re
= r
':(.*?)(=|$)'
72 # Matches from = to end of string
76 host_spec
= cls('', '', '')
78 match_host
= re
.search(host_re
, host
)
80 host_spec
= host_spec
._replace
(hostname
=match_host
.group(1))
82 name_match
= re
.search(name_re
, host
)
84 host_spec
= host_spec
._replace
(name
=name_match
.group(1))
86 ip_match
= re
.search(ip_re
, host
)
88 host_spec
= host_spec
._replace
(network
=ip_match
.group(1))
90 if not require_network
:
93 from ipaddress
import ip_network
, ip_address
94 networks
= list() # type: List[str]
95 network
= host_spec
.network
96 # in case we have [v2:1.2.3.4:3000,v1:1.2.3.4:6478]
98 networks
= [x
for x
in network
.split(',')]
100 networks
.append(network
)
101 for network
in networks
:
102 # only if we have versioned network configs
103 if network
.startswith('v') or network
.startswith('[v'):
104 network
= network
.split(':')[1]
106 # if subnets are defined, also verify the validity
108 ip_network(six
.text_type(network
))
110 ip_address(six
.text_type(network
))
111 except ValueError as e
:
118 assert_valid_host(self
.hostname
)
121 class PlacementSpec(object):
123 For APIs that need to specify a host subset
127 label
=None, # type: Optional[str]
128 hosts
=None, # type: Union[List[str],List[HostPlacementSpec]]
129 count
=None, # type: Optional[int]
130 host_pattern
=None # type: Optional[str]
132 # type: (...) -> None
134 self
.hosts
= [] # type: List[HostPlacementSpec]
137 if all([isinstance(host
, HostPlacementSpec
) for host
in hosts
]):
138 self
.hosts
= hosts
# type: ignore
140 self
.hosts
= [HostPlacementSpec
.parse(x
, require_network
=False) # type: ignore
143 self
.count
= count
# type: Optional[int]
145 #: fnmatch patterns to select hosts. Can also be a single host.
146 self
.host_pattern
= host_pattern
# type: Optional[str]
151 return self
.label
is None and \
153 not self
.host_pattern
and \
156 def set_hosts(self
, hosts
):
157 # To backpopulate the .hosts attribute when using labels or count
158 # in the orchestrator backend.
161 def pattern_matches_hosts(self
, all_hosts
):
162 # type: (List[str]) -> List[str]
163 if not self
.host_pattern
:
165 return fnmatch
.filter(all_hosts
, self
.host_pattern
)
167 def pretty_str(self
):
170 kv
.append('count:%d' % self
.count
)
172 kv
.append('label:%s' % self
.label
)
174 kv
.append('%s' % ','.join([str(h
) for h
in self
.hosts
]))
175 if self
.host_pattern
:
176 kv
.append(self
.host_pattern
)
182 kv
.append('count=%d' % self
.count
)
184 kv
.append('label=%s' % repr(self
.label
))
186 kv
.append('hosts={!r}'.format(self
.hosts
))
187 if self
.host_pattern
:
188 kv
.append('host_pattern={!r}'.format(self
.host_pattern
))
189 return "PlacementSpec(%s)" % ', '.join(kv
)
192 def from_json(cls
, data
):
193 hosts
= data
.get('hosts', [])
195 data
['hosts'] = [HostPlacementSpec
.from_json(host
) for host
in hosts
]
203 r
['label'] = self
.label
205 r
['hosts'] = [host
.to_json() for host
in self
.hosts
]
207 r
['count'] = self
.count
208 if self
.host_pattern
:
209 r
['host_pattern'] = self
.host_pattern
213 if self
.hosts
and self
.label
:
214 # TODO: a less generic Exception
215 raise ServiceSpecValidationError('Host and label are mutually exclusive')
216 if self
.count
is not None and self
.count
<= 0:
217 raise ServiceSpecValidationError("num/count must be > 1")
218 if self
.host_pattern
and self
.hosts
:
219 raise ServiceSpecValidationError('cannot combine host patterns and hosts')
224 def from_string(cls
, arg
):
225 # type: (Optional[str]) -> PlacementSpec
227 A single integer is parsed as a count:
228 >>> PlacementSpec.from_string('3')
229 PlacementSpec(count=3)
231 A list of names is parsed as host specifications:
232 >>> PlacementSpec.from_string('host1 host2')
233 PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacemen\
234 tSpec(hostname='host2', network='', name='')])
236 You can also prefix the hosts with a count as follows:
237 >>> PlacementSpec.from_string('2 host1 host2')
238 PlacementSpec(count=2, hosts=[HostPlacementSpec(hostname='host1', network='', name=''), Hos\
239 tPlacementSpec(hostname='host2', network='', name='')])
241 You can spefify labels using `label:<label>`
242 >>> PlacementSpec.from_string('label:mon')
243 PlacementSpec(label='mon')
245 Labels als support a count:
246 >>> PlacementSpec.from_string('3 label:mon')
247 PlacementSpec(count=3, label='mon')
249 fnmatch is also supported:
250 >>> PlacementSpec.from_string('data[1-3]')
251 PlacementSpec(host_pattern='data[1-3]')
253 >>> PlacementSpec.from_string(None)
256 if arg
is None or not arg
:
258 elif isinstance(arg
, str):
260 strings
= arg
.split(' ')
262 strings
= arg
.split(';')
263 elif ',' in arg
and '[' not in arg
:
264 # FIXME: this isn't quite right. we want to avoid breaking
265 # a list of mons with addrvecs... so we're basically allowing
266 # , most of the time, except when addrvecs are used. maybe
268 strings
= arg
.split(',')
272 raise ServiceSpecValidationError('invalid placement %s' % arg
)
277 count
= int(strings
[0])
278 strings
= strings
[1:]
282 if s
.startswith('count:'):
290 advanced_hostspecs
= [h
for h
in strings
if
291 (':' in h
or '=' in h
or not any(c
in '[]?*:=' for c
in h
)) and
293 for a_h
in advanced_hostspecs
:
296 labels
= [x
for x
in strings
if 'label:' in x
]
298 raise ServiceSpecValidationError('more than one label provided: {}'.format(labels
))
301 label
= labels
[0][6:] if labels
else None
303 host_patterns
= strings
304 if len(host_patterns
) > 1:
305 raise ServiceSpecValidationError(
306 'more than one host pattern provided: {}'.format(host_patterns
))
308 ps
= PlacementSpec(count
=count
,
309 hosts
=advanced_hostspecs
,
311 host_pattern
=host_patterns
[0] if host_patterns
else None)
315 class ServiceSpec(object):
317 Details of service creation.
319 Request to the orchestrator for a cluster of daemons
320 such as MDS, RGW, iscsi gateway, MONs, MGRs, Prometheus
322 This structure is supposed to be enough information to
326 KNOWN_SERVICE_TYPES
= 'alertmanager crash grafana mds mgr mon nfs ' \
327 'node-exporter osd prometheus rbd-mirror rgw'.split()
330 service_type
, # type: str
331 service_id
=None, # type: Optional[str]
332 placement
=None, # type: Optional[PlacementSpec]
333 count
=None, # type: Optional[int]
334 unmanaged
=False, # type: bool
336 self
.placement
= PlacementSpec() if placement
is None else placement
# type: PlacementSpec
338 assert service_type
in ServiceSpec
.KNOWN_SERVICE_TYPES
, service_type
339 self
.service_type
= service_type
340 self
.service_id
= service_id
341 self
.unmanaged
= unmanaged
344 def from_json(cls
, json_spec
):
345 # type: (dict) -> Any
347 # >>> ServiceSpecs = TypeVar('Base', bound=ServiceSpec)
348 # then, the real type is: (dict) -> ServiceSpecs
350 Initialize 'ServiceSpec' object data from a json structure
351 :param json_spec: A valid dict with ServiceSpec
353 from ceph
.deployment
.drive_group
import DriveGroupSpec
355 service_type
= json_spec
.get('service_type', '')
358 'nfs': NFSServiceSpec
,
359 'osd': DriveGroupSpec
360 }.get(service_type
, cls
)
362 if _cls
== ServiceSpec
and not service_type
:
363 raise ServiceSpecValidationError('Spec needs a "service_type" key.')
365 return _cls
._from
_json
_impl
(json_spec
) # type: ignore
368 def _from_json_impl(cls
, json_spec
):
369 args
= {} # type: Dict[str, Dict[Any, Any]]
370 for k
, v
in json_spec
.items():
372 v
= PlacementSpec
.from_json(v
)
379 def service_name(self
):
380 n
= self
.service_type
382 n
+= '.' + self
.service_id
386 # type: () -> Dict[str, Any]
388 for key
, val
in self
.__dict
__.items():
389 if hasattr(val
, 'to_json'):
396 if not self
.service_type
:
397 raise ServiceSpecValidationError('Cannot add Service: type required')
399 if self
.placement
is not None:
400 self
.placement
.validate()
403 return "{}({!r})".format(self
.__class
__.__name
__, self
.__dict
__)
405 def one_line_str(self
):
406 return '<{} for service_name={}>'.format(self
.__class
__.__name
__, self
.service_name())
409 def servicespec_validate_add(self
: ServiceSpec
):
410 # This must not be a method of ServiceSpec, otherwise you'll hunt
411 # sub-interpreter affinity bugs.
412 ServiceSpec
.validate(self
)
413 if self
.service_type
in ['mds', 'rgw', 'nfs'] and not self
.service_id
:
414 raise ServiceSpecValidationError('Cannot add Service: id required')
417 class NFSServiceSpec(ServiceSpec
):
418 def __init__(self
, service_id
, pool
=None, namespace
=None, placement
=None,
419 service_type
='nfs', unmanaged
=False):
420 assert service_type
== 'nfs'
421 super(NFSServiceSpec
, self
).__init
__(
422 'nfs', service_id
=service_id
,
423 placement
=placement
, unmanaged
=unmanaged
)
425 #: RADOS pool where NFS client recovery data is stored.
428 #: RADOS namespace where NFS client recovery data is stored in the pool.
429 self
.namespace
= namespace
431 def validate_add(self
):
432 servicespec_validate_add(self
)
435 raise ServiceSpecValidationError('Cannot add NFS: No Pool specified')
438 class RGWSpec(ServiceSpec
):
440 Settings to configure a (multisite) Ceph RGW
444 rgw_realm
=None, # type: Optional[str]
445 rgw_zone
=None, # type: Optional[str]
446 subcluster
=None, # type: Optional[str]
447 service_id
=None, # type: Optional[str]
450 rgw_frontend_port
=None, # type: Optional[int]
451 unmanaged
=False, # type: bool
452 ssl
=False, # type: bool
454 assert service_type
== 'rgw'
456 a
= service_id
.split('.', 2)
463 service_id
= '%s.%s.%s' % (rgw_realm
, rgw_zone
, subcluster
)
465 service_id
= '%s.%s' % (rgw_realm
, rgw_zone
)
466 super(RGWSpec
, self
).__init
__(
467 'rgw', service_id
=service_id
,
468 placement
=placement
, unmanaged
=unmanaged
)
470 self
.rgw_realm
= rgw_realm
471 self
.rgw_zone
= rgw_zone
472 self
.subcluster
= subcluster
473 self
.rgw_frontend_port
= rgw_frontend_port
477 if self
.rgw_frontend_port
:
478 return self
.rgw_frontend_port