]> git.proxmox.com Git - ceph.git/blob - ceph/src/python-common/ceph/tests/test_service_spec.py
update ceph source to reef 18.1.2
[ceph.git] / ceph / src / python-common / ceph / tests / test_service_spec.py
1 # flake8: noqa
2 import json
3 import re
4
5 import yaml
6
7 import pytest
8
9 from ceph.deployment.service_spec import HostPlacementSpec, PlacementSpec, \
10 ServiceSpec, RGWSpec, NFSServiceSpec, IscsiServiceSpec, AlertManagerSpec, \
11 CustomContainerSpec, GrafanaSpec, PrometheusSpec
12 from ceph.deployment.drive_group import DriveGroupSpec
13 from ceph.deployment.hostspec import SpecValidationError
14
15
16 @pytest.mark.parametrize("test_input,expected, require_network",
17 [("myhost", ('myhost', '', ''), False),
18 ("myhost=sname", ('myhost', '', 'sname'), False),
19 ("myhost:10.1.1.10", ('myhost', '10.1.1.10', ''), True),
20 ("myhost:10.1.1.10=sname", ('myhost', '10.1.1.10', 'sname'), True),
21 ("myhost:10.1.1.0/32", ('myhost', '10.1.1.0/32', ''), True),
22 ("myhost:10.1.1.0/32=sname", ('myhost', '10.1.1.0/32', 'sname'), True),
23 ("myhost:[v1:10.1.1.10:6789]", ('myhost', '[v1:10.1.1.10:6789]', ''), True),
24 ("myhost:[v1:10.1.1.10:6789]=sname", ('myhost', '[v1:10.1.1.10:6789]', 'sname'), True),
25 ("myhost:[v1:10.1.1.10:6789,v2:10.1.1.11:3000]", ('myhost', '[v1:10.1.1.10:6789,v2:10.1.1.11:3000]', ''), True),
26 ("myhost:[v1:10.1.1.10:6789,v2:10.1.1.11:3000]=sname", ('myhost', '[v1:10.1.1.10:6789,v2:10.1.1.11:3000]', 'sname'), True),
27 ])
28 def test_parse_host_placement_specs(test_input, expected, require_network):
29 ret = HostPlacementSpec.parse(test_input, require_network=require_network)
30 assert ret == expected
31 assert str(ret) == test_input
32
33 ps = PlacementSpec.from_string(test_input)
34 assert ps.pretty_str() == test_input
35 assert ps == PlacementSpec.from_string(ps.pretty_str())
36
37 # Testing the old verbose way of generating json. Don't remove:
38 assert ret == HostPlacementSpec.from_json({
39 'hostname': ret.hostname,
40 'network': ret.network,
41 'name': ret.name
42 })
43
44 assert ret == HostPlacementSpec.from_json(ret.to_json())
45
46
47 @pytest.mark.parametrize(
48 "spec, raise_exception, msg",
49 [
50 (GrafanaSpec(protocol=''), True, '^Invalid protocol'),
51 (GrafanaSpec(protocol='invalid'), True, '^Invalid protocol'),
52 (GrafanaSpec(protocol='-http'), True, '^Invalid protocol'),
53 (GrafanaSpec(protocol='-https'), True, '^Invalid protocol'),
54 (GrafanaSpec(protocol='http'), False, ''),
55 (GrafanaSpec(protocol='https'), False, ''),
56 (GrafanaSpec(anonymous_access=False), True, '^Either initial'), # we require inital_admin_password if anonymous_access is False
57 (GrafanaSpec(anonymous_access=False, initial_admin_password='test'), False, ''),
58 ])
59 def test_apply_grafana(spec: GrafanaSpec, raise_exception: bool, msg: str):
60 if raise_exception:
61 with pytest.raises(SpecValidationError, match=msg):
62 spec.validate()
63 else:
64 spec.validate()
65
66 @pytest.mark.parametrize(
67 "spec, raise_exception, msg",
68 [
69 # Valid retention_time values (valid units: 'y', 'w', 'd', 'h', 'm', 's')
70 (PrometheusSpec(retention_time='1y'), False, ''),
71 (PrometheusSpec(retention_time=' 10w '), False, ''),
72 (PrometheusSpec(retention_time=' 1348d'), False, ''),
73 (PrometheusSpec(retention_time='2000h '), False, ''),
74 (PrometheusSpec(retention_time='173847m'), False, ''),
75 (PrometheusSpec(retention_time='200s'), False, ''),
76 (PrometheusSpec(retention_time=' '), False, ''), # default value will be used
77 # Invalid retention_time values
78 (PrometheusSpec(retention_time='100k'), True, '^Invalid retention time'), # invalid unit
79 (PrometheusSpec(retention_time='10'), True, '^Invalid retention time'), # no unit
80 (PrometheusSpec(retention_time='100.00y'), True, '^Invalid retention time'), # invalid value and valid unit
81 (PrometheusSpec(retention_time='100.00k'), True, '^Invalid retention time'), # invalid value and invalid unit
82 (PrometheusSpec(retention_time='---'), True, '^Invalid retention time'), # invalid value
83
84 # Valid retention_size values (valid units: 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')
85 (PrometheusSpec(retention_size='123456789B'), False, ''),
86 (PrometheusSpec(retention_size=' 200KB'), False, ''),
87 (PrometheusSpec(retention_size='99999MB '), False, ''),
88 (PrometheusSpec(retention_size=' 10GB '), False, ''),
89 (PrometheusSpec(retention_size='100TB'), False, ''),
90 (PrometheusSpec(retention_size='500PB'), False, ''),
91 (PrometheusSpec(retention_size='200EB'), False, ''),
92 (PrometheusSpec(retention_size=' '), False, ''), # default value will be used
93
94 # Invalid retention_size values
95 (PrometheusSpec(retention_size='100b'), True, '^Invalid retention size'), # invalid unit (case sensitive)
96 (PrometheusSpec(retention_size='333kb'), True, '^Invalid retention size'), # invalid unit (case sensitive)
97 (PrometheusSpec(retention_size='2000'), True, '^Invalid retention size'), # no unit
98 (PrometheusSpec(retention_size='200.00PB'), True, '^Invalid retention size'), # invalid value and valid unit
99 (PrometheusSpec(retention_size='400.B'), True, '^Invalid retention size'), # invalid value and invalid unit
100 (PrometheusSpec(retention_size='10.000s'), True, '^Invalid retention size'), # invalid value and invalid unit
101 (PrometheusSpec(retention_size='...'), True, '^Invalid retention size'), # invalid value
102
103 # valid retention_size and valid retention_time
104 (PrometheusSpec(retention_time='1y', retention_size='100GB'), False, ''),
105 # invalid retention_time and valid retention_size
106 (PrometheusSpec(retention_time='1j', retention_size='100GB'), True, '^Invalid retention time'),
107 # valid retention_time and invalid retention_size
108 (PrometheusSpec(retention_time='1y', retention_size='100gb'), True, '^Invalid retention size'),
109 # valid retention_time and invalid retention_size
110 (PrometheusSpec(retention_time='1y', retention_size='100gb'), True, '^Invalid retention size'),
111 # invalid retention_time and invalid retention_size
112 (PrometheusSpec(retention_time='1i', retention_size='100gb'), True, '^Invalid retention time'),
113 ])
114 def test_apply_prometheus(spec: PrometheusSpec, raise_exception: bool, msg: str):
115 if raise_exception:
116 with pytest.raises(SpecValidationError, match=msg):
117 spec.validate()
118 else:
119 spec.validate()
120
121 @pytest.mark.parametrize(
122 "test_input,expected",
123 [
124 ('', "PlacementSpec()"),
125 ("count:2", "PlacementSpec(count=2)"),
126 ("3", "PlacementSpec(count=3)"),
127 ("host1 host2", "PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacementSpec(hostname='host2', network='', name='')])"),
128 ("host1;host2", "PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacementSpec(hostname='host2', network='', name='')])"),
129 ("host1,host2", "PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacementSpec(hostname='host2', network='', name='')])"),
130 ("host1 host2=b", "PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacementSpec(hostname='host2', network='', name='b')])"),
131 ("host1=a host2=b", "PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name='a'), HostPlacementSpec(hostname='host2', network='', name='b')])"),
132 ("host1:1.2.3.4=a host2:1.2.3.5=b", "PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='1.2.3.4', name='a'), HostPlacementSpec(hostname='host2', network='1.2.3.5', name='b')])"),
133 ("myhost:[v1:10.1.1.10:6789]", "PlacementSpec(hosts=[HostPlacementSpec(hostname='myhost', network='[v1:10.1.1.10:6789]', name='')])"),
134 ('2 host1 host2', "PlacementSpec(count=2, hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacementSpec(hostname='host2', network='', name='')])"),
135 ('label:foo', "PlacementSpec(label='foo')"),
136 ('3 label:foo', "PlacementSpec(count=3, label='foo')"),
137 ('*', "PlacementSpec(host_pattern='*')"),
138 ('3 data[1-3]', "PlacementSpec(count=3, host_pattern='data[1-3]')"),
139 ('3 data?', "PlacementSpec(count=3, host_pattern='data?')"),
140 ('3 data*', "PlacementSpec(count=3, host_pattern='data*')"),
141 ("count-per-host:4 label:foo", "PlacementSpec(count_per_host=4, label='foo')"),
142 ])
143 def test_parse_placement_specs(test_input, expected):
144 ret = PlacementSpec.from_string(test_input)
145 assert str(ret) == expected
146 assert PlacementSpec.from_string(ret.pretty_str()) == ret, f'"{ret.pretty_str()}" != "{test_input}"'
147
148 @pytest.mark.parametrize(
149 "test_input",
150 [
151 ("host=a host*"),
152 ("host=a label:wrong"),
153 ("host? host*"),
154 ('host=a count-per-host:0'),
155 ('host=a count-per-host:-10'),
156 ('count:2 count-per-host:1'),
157 ('host1=a host2=b count-per-host:2'),
158 ('host1:10/8 count-per-host:2'),
159 ('count-per-host:2'),
160 ]
161 )
162 def test_parse_placement_specs_raises(test_input):
163 with pytest.raises(SpecValidationError):
164 PlacementSpec.from_string(test_input)
165
166 @pytest.mark.parametrize("test_input",
167 # wrong subnet
168 [("myhost:1.1.1.1/24"),
169 # wrong ip format
170 ("myhost:1"),
171 ])
172 def test_parse_host_placement_specs_raises_wrong_format(test_input):
173 with pytest.raises(ValueError):
174 HostPlacementSpec.parse(test_input)
175
176
177 @pytest.mark.parametrize(
178 "p,hosts,size",
179 [
180 (
181 PlacementSpec(count=3),
182 ['host1', 'host2', 'host3', 'host4', 'host5'],
183 3
184 ),
185 (
186 PlacementSpec(host_pattern='*'),
187 ['host1', 'host2', 'host3', 'host4', 'host5'],
188 5
189 ),
190 (
191 PlacementSpec(count_per_host=2, host_pattern='*'),
192 ['host1', 'host2', 'host3', 'host4', 'host5'],
193 10
194 ),
195 (
196 PlacementSpec(host_pattern='foo*'),
197 ['foo1', 'foo2', 'bar1', 'bar2'],
198 2
199 ),
200 (
201 PlacementSpec(count_per_host=2, host_pattern='foo*'),
202 ['foo1', 'foo2', 'bar1', 'bar2'],
203 4
204 ),
205 ])
206 def test_placement_target_size(p, hosts, size):
207 assert p.get_target_count(
208 [HostPlacementSpec(n, '', '') for n in hosts]
209 ) == size
210
211
212 def _get_dict_spec(s_type, s_id):
213 dict_spec = {
214 "service_id": s_id,
215 "service_type": s_type,
216 "placement":
217 dict(hosts=["host1:1.1.1.1"])
218 }
219 if s_type == 'nfs':
220 pass
221 elif s_type == 'iscsi':
222 dict_spec['pool'] = 'pool'
223 dict_spec['api_user'] = 'api_user'
224 dict_spec['api_password'] = 'api_password'
225 elif s_type == 'osd':
226 dict_spec['spec'] = {
227 'data_devices': {
228 'all': True
229 }
230 }
231 elif s_type == 'rgw':
232 dict_spec['rgw_realm'] = 'realm'
233 dict_spec['rgw_zone'] = 'zone'
234
235 return dict_spec
236
237
238 @pytest.mark.parametrize(
239 "s_type,o_spec,s_id",
240 [
241 ("mgr", ServiceSpec, 'test'),
242 ("mon", ServiceSpec, 'test'),
243 ("mds", ServiceSpec, 'test'),
244 ("rgw", RGWSpec, 'realm.zone'),
245 ("nfs", NFSServiceSpec, 'test'),
246 ("iscsi", IscsiServiceSpec, 'test'),
247 ("osd", DriveGroupSpec, 'test'),
248 ])
249 def test_servicespec_map_test(s_type, o_spec, s_id):
250 spec = ServiceSpec.from_json(_get_dict_spec(s_type, s_id))
251 assert isinstance(spec, o_spec)
252 assert isinstance(spec.placement, PlacementSpec)
253 assert isinstance(spec.placement.hosts[0], HostPlacementSpec)
254 assert spec.placement.hosts[0].hostname == 'host1'
255 assert spec.placement.hosts[0].network == '1.1.1.1'
256 assert spec.placement.hosts[0].name == ''
257 assert spec.validate() is None
258 ServiceSpec.from_json(spec.to_json())
259
260
261 @pytest.mark.parametrize(
262 "realm, zone, frontend_type, raise_exception, msg",
263 [
264 ('realm', 'zone1', 'beast', False, ''),
265 ('realm', 'zone2', 'civetweb', False, ''),
266 ('realm', None, 'beast', True, 'Cannot add RGW: Realm specified but no zone specified'),
267 (None, 'zone1', 'beast', True, 'Cannot add RGW: Zone specified but no realm specified'),
268 ('realm', 'zone', 'invalid-beast', True, '^Invalid rgw_frontend_type value'),
269 ('realm', 'zone', 'invalid-civetweb', True, '^Invalid rgw_frontend_type value'),
270 ])
271 def test_rgw_servicespec_parse(realm, zone, frontend_type, raise_exception, msg):
272 spec = RGWSpec(service_id='foo',
273 rgw_realm=realm,
274 rgw_zone=zone,
275 rgw_frontend_type=frontend_type)
276 if raise_exception:
277 with pytest.raises(SpecValidationError, match=msg):
278 spec.validate()
279 else:
280 spec.validate()
281
282 def test_osd_unmanaged():
283 osd_spec = {"placement": {"host_pattern": "*"},
284 "service_id": "all-available-devices",
285 "service_name": "osd.all-available-devices",
286 "service_type": "osd",
287 "spec": {"data_devices": {"all": True}, "filter_logic": "AND", "objectstore": "bluestore"},
288 "unmanaged": True}
289
290 dg_spec = ServiceSpec.from_json(osd_spec)
291 assert dg_spec.unmanaged == True
292
293
294 @pytest.mark.parametrize("y",
295 """service_type: crash
296 service_name: crash
297 placement:
298 host_pattern: '*'
299 ---
300 service_type: crash
301 service_name: crash
302 placement:
303 host_pattern: '*'
304 unmanaged: true
305 ---
306 service_type: rgw
307 service_id: default-rgw-realm.eu-central-1.1
308 service_name: rgw.default-rgw-realm.eu-central-1.1
309 placement:
310 hosts:
311 - ceph-001
312 networks:
313 - 10.0.0.0/8
314 - 192.168.0.0/16
315 spec:
316 rgw_frontend_type: civetweb
317 rgw_realm: default-rgw-realm
318 rgw_zone: eu-central-1
319 ---
320 service_type: osd
321 service_id: osd_spec_default
322 service_name: osd.osd_spec_default
323 placement:
324 host_pattern: '*'
325 spec:
326 data_devices:
327 model: MC-55-44-XZ
328 db_devices:
329 model: SSD-123-foo
330 filter_logic: AND
331 objectstore: bluestore
332 wal_devices:
333 model: NVME-QQQQ-987
334 ---
335 service_type: alertmanager
336 service_name: alertmanager
337 spec:
338 port: 1234
339 user_data:
340 default_webhook_urls:
341 - foo
342 ---
343 service_type: grafana
344 service_name: grafana
345 spec:
346 anonymous_access: true
347 port: 1234
348 protocol: https
349 ---
350 service_type: grafana
351 service_name: grafana
352 spec:
353 anonymous_access: true
354 initial_admin_password: secure
355 port: 1234
356 protocol: https
357 ---
358 service_type: ingress
359 service_id: rgw.foo
360 service_name: ingress.rgw.foo
361 placement:
362 hosts:
363 - host1
364 - host2
365 - host3
366 spec:
367 backend_service: rgw.foo
368 frontend_port: 8080
369 monitor_port: 8081
370 virtual_ip: 192.168.20.1/24
371 ---
372 service_type: nfs
373 service_id: mynfs
374 service_name: nfs.mynfs
375 spec:
376 port: 1234
377 ---
378 service_type: iscsi
379 service_id: iscsi
380 service_name: iscsi.iscsi
381 networks:
382 - ::0/8
383 spec:
384 api_password: admin
385 api_port: 5000
386 api_user: admin
387 pool: pool
388 trusted_ip_list:
389 - ::1
390 - ::2
391 ---
392 service_type: container
393 service_id: hello-world
394 service_name: container.hello-world
395 spec:
396 args:
397 - --foo
398 bind_mounts:
399 - - type=bind
400 - source=lib/modules
401 - destination=/lib/modules
402 - ro=true
403 dirs:
404 - foo
405 - bar
406 entrypoint: /usr/bin/bash
407 envs:
408 - FOO=0815
409 files:
410 bar.conf:
411 - foo
412 - bar
413 foo.conf: 'foo
414
415 bar'
416 gid: 2000
417 image: docker.io/library/hello-world:latest
418 ports:
419 - 8080
420 - 8443
421 uid: 1000
422 volume_mounts:
423 foo: /foo
424 ---
425 service_type: snmp-gateway
426 service_name: snmp-gateway
427 placement:
428 count: 1
429 spec:
430 credentials:
431 snmp_community: public
432 snmp_destination: 192.168.1.42:162
433 snmp_version: V2c
434 ---
435 service_type: snmp-gateway
436 service_name: snmp-gateway
437 placement:
438 count: 1
439 spec:
440 auth_protocol: MD5
441 credentials:
442 snmp_v3_auth_password: mypassword
443 snmp_v3_auth_username: myuser
444 engine_id: 8000C53F00000000
445 port: 9464
446 snmp_destination: 192.168.1.42:162
447 snmp_version: V3
448 ---
449 service_type: snmp-gateway
450 service_name: snmp-gateway
451 placement:
452 count: 1
453 spec:
454 credentials:
455 snmp_v3_auth_password: mypassword
456 snmp_v3_auth_username: myuser
457 snmp_v3_priv_password: mysecret
458 engine_id: 8000C53F00000000
459 privacy_protocol: AES
460 snmp_destination: 192.168.1.42:162
461 snmp_version: V3
462 """.split('---\n'))
463 def test_yaml(y):
464 data = yaml.safe_load(y)
465 object = ServiceSpec.from_json(data)
466
467 assert yaml.dump(object) == y
468 assert yaml.dump(ServiceSpec.from_json(object.to_json())) == y
469
470
471 def test_alertmanager_spec_1():
472 spec = AlertManagerSpec()
473 assert spec.service_type == 'alertmanager'
474 assert isinstance(spec.user_data, dict)
475 assert len(spec.user_data.keys()) == 0
476 assert spec.get_port_start() == [9093, 9094]
477
478
479 def test_alertmanager_spec_2():
480 spec = AlertManagerSpec(user_data={'default_webhook_urls': ['foo']})
481 assert isinstance(spec.user_data, dict)
482 assert 'default_webhook_urls' in spec.user_data.keys()
483
484
485
486 def test_repr():
487 val = """ServiceSpec.from_json(yaml.safe_load('''service_type: crash
488 service_name: crash
489 placement:
490 count: 42
491 '''))"""
492 obj = eval(val)
493 assert obj.service_type == 'crash'
494 assert val == repr(obj)
495
496 @pytest.mark.parametrize("spec1, spec2, eq",
497 [
498 (
499 ServiceSpec(
500 service_type='mon'
501 ),
502 ServiceSpec(
503 service_type='mon'
504 ),
505 True
506 ),
507 (
508 ServiceSpec(
509 service_type='mon'
510 ),
511 ServiceSpec(
512 service_type='mon',
513 service_id='foo'
514 ),
515 True
516 ),
517 # Add service_type='mgr'
518 (
519 ServiceSpec(
520 service_type='osd'
521 ),
522 ServiceSpec(
523 service_type='osd',
524 ),
525 True
526 ),
527 (
528 ServiceSpec(
529 service_type='osd'
530 ),
531 DriveGroupSpec(),
532 True
533 ),
534 (
535 ServiceSpec(
536 service_type='osd'
537 ),
538 ServiceSpec(
539 service_type='osd',
540 service_id='foo',
541 ),
542 False
543 ),
544 (
545 ServiceSpec(
546 service_type='rgw',
547 service_id='foo',
548 ),
549 RGWSpec(service_id='foo'),
550 True
551 ),
552 ])
553 def test_spec_hash_eq(spec1: ServiceSpec,
554 spec2: ServiceSpec,
555 eq: bool):
556
557 assert (spec1 == spec2) is eq
558
559 @pytest.mark.parametrize(
560 "s_type,s_id,s_name",
561 [
562 ('mgr', 's_id', 'mgr'),
563 ('mon', 's_id', 'mon'),
564 ('mds', 's_id', 'mds.s_id'),
565 ('rgw', 's_id', 'rgw.s_id'),
566 ('nfs', 's_id', 'nfs.s_id'),
567 ('iscsi', 's_id', 'iscsi.s_id'),
568 ('osd', 's_id', 'osd.s_id'),
569 ])
570 def test_service_name(s_type, s_id, s_name):
571 spec = ServiceSpec.from_json(_get_dict_spec(s_type, s_id))
572 spec.validate()
573 assert spec.service_name() == s_name
574
575 @pytest.mark.parametrize(
576 's_type,s_id',
577 [
578 ('mds', 's:id'), # MDS service_id cannot contain an invalid char ':'
579 ('mds', '1abc'), # MDS service_id cannot start with a numeric digit
580 ('mds', ''), # MDS service_id cannot be empty
581 ('rgw', '*s_id'),
582 ('nfs', 's/id'),
583 ('iscsi', 's@id'),
584 ('osd', 's;id'),
585 ])
586
587 def test_service_id_raises_invalid_char(s_type, s_id):
588 with pytest.raises(SpecValidationError):
589 spec = ServiceSpec.from_json(_get_dict_spec(s_type, s_id))
590 spec.validate()
591
592 def test_custom_container_spec():
593 spec = CustomContainerSpec(service_id='hello-world',
594 image='docker.io/library/hello-world:latest',
595 entrypoint='/usr/bin/bash',
596 uid=1000,
597 gid=2000,
598 volume_mounts={'foo': '/foo'},
599 args=['--foo'],
600 envs=['FOO=0815'],
601 bind_mounts=[
602 [
603 'type=bind',
604 'source=lib/modules',
605 'destination=/lib/modules',
606 'ro=true'
607 ]
608 ],
609 ports=[8080, 8443],
610 dirs=['foo', 'bar'],
611 files={
612 'foo.conf': 'foo\nbar',
613 'bar.conf': ['foo', 'bar']
614 })
615 assert spec.service_type == 'container'
616 assert spec.entrypoint == '/usr/bin/bash'
617 assert spec.uid == 1000
618 assert spec.gid == 2000
619 assert spec.volume_mounts == {'foo': '/foo'}
620 assert spec.args == ['--foo']
621 assert spec.envs == ['FOO=0815']
622 assert spec.bind_mounts == [
623 [
624 'type=bind',
625 'source=lib/modules',
626 'destination=/lib/modules',
627 'ro=true'
628 ]
629 ]
630 assert spec.ports == [8080, 8443]
631 assert spec.dirs == ['foo', 'bar']
632 assert spec.files == {
633 'foo.conf': 'foo\nbar',
634 'bar.conf': ['foo', 'bar']
635 }
636
637
638 def test_custom_container_spec_config_json():
639 spec = CustomContainerSpec(service_id='foo', image='foo', dirs=None)
640 config_json = spec.config_json()
641 for key in ['entrypoint', 'uid', 'gid', 'bind_mounts', 'dirs']:
642 assert key not in config_json
643
644
645 def test_ingress_spec():
646 yaml_str = """service_type: ingress
647 service_id: rgw.foo
648 placement:
649 hosts:
650 - host1
651 - host2
652 - host3
653 spec:
654 virtual_ip: 192.168.20.1/24
655 backend_service: rgw.foo
656 frontend_port: 8080
657 monitor_port: 8081
658 """
659 yaml_file = yaml.safe_load(yaml_str)
660 spec = ServiceSpec.from_json(yaml_file)
661 assert spec.service_type == "ingress"
662 assert spec.service_id == "rgw.foo"
663 assert spec.virtual_ip == "192.168.20.1/24"
664 assert spec.frontend_port == 8080
665 assert spec.monitor_port == 8081
666
667
668 @pytest.mark.parametrize("y, error_match", [
669 ("""
670 service_type: rgw
671 service_id: foo
672 placement:
673 count_per_host: "twelve"
674 """, "count-per-host must be a numeric value",),
675 ("""
676 service_type: rgw
677 service_id: foo
678 placement:
679 count_per_host: "2"
680 """, "count-per-host must be an integer value",),
681 ("""
682 service_type: rgw
683 service_id: foo
684 placement:
685 count_per_host: 7.36
686 """, "count-per-host must be an integer value",),
687 ("""
688 service_type: rgw
689 service_id: foo
690 placement:
691 count: "fifteen"
692 """, "num/count must be a numeric value",),
693 ("""
694 service_type: rgw
695 service_id: foo
696 placement:
697 count: "4"
698 """, "num/count must be an integer value",),
699 ("""
700 service_type: rgw
701 service_id: foo
702 placement:
703 count: 7.36
704 """, "num/count must be an integer value",),
705 ("""
706 service_type: rgw
707 service_id: foo
708 placement:
709 count: 0
710 """, "num/count must be >= 1",),
711 ("""
712 service_type: rgw
713 service_id: foo
714 placement:
715 count_per_host: 0
716 """, "count-per-host must be >= 1",),
717 ("""
718 service_type: snmp-gateway
719 service_name: snmp-gateway
720 placement:
721 count: 1
722 spec:
723 credentials:
724 snmp_v3_auth_password: mypassword
725 snmp_v3_auth_username: myuser
726 snmp_v3_priv_password: mysecret
727 port: 9464
728 engine_id: 8000c53f0000000000
729 privacy_protocol: WEIRD
730 snmp_destination: 192.168.122.1:162
731 auth_protocol: BIZARRE
732 snmp_version: V3
733 """, "auth_protocol unsupported. Must be one of MD5, SHA"),
734 ("""
735 ---
736 service_type: snmp-gateway
737 service_name: snmp-gateway
738 placement:
739 count: 1
740 spec:
741 credentials:
742 snmp_community: public
743 snmp_destination: 192.168.1.42:162
744 snmp_version: V4
745 """, 'snmp_version unsupported. Must be one of V2c, V3'),
746 ("""
747 ---
748 service_type: snmp-gateway
749 service_name: snmp-gateway
750 placement:
751 count: 1
752 spec:
753 credentials:
754 snmp_community: public
755 port: 9464
756 snmp_destination: 192.168.1.42:162
757 """, re.escape('Missing SNMP version (snmp_version)')),
758 ("""
759 ---
760 service_type: snmp-gateway
761 service_name: snmp-gateway
762 placement:
763 count: 1
764 spec:
765 credentials:
766 snmp_v3_auth_username: myuser
767 snmp_v3_auth_password: mypassword
768 port: 9464
769 auth_protocol: wah
770 snmp_destination: 192.168.1.42:162
771 snmp_version: V3
772 """, 'auth_protocol unsupported. Must be one of MD5, SHA'),
773 ("""
774 ---
775 service_type: snmp-gateway
776 service_name: snmp-gateway
777 placement:
778 count: 1
779 spec:
780 credentials:
781 snmp_v3_auth_username: myuser
782 snmp_v3_auth_password: mypassword
783 snmp_v3_priv_password: mysecret
784 port: 9464
785 auth_protocol: SHA
786 privacy_protocol: weewah
787 snmp_destination: 192.168.1.42:162
788 snmp_version: V3
789 """, 'privacy_protocol unsupported. Must be one of DES, AES'),
790 ("""
791 ---
792 service_type: snmp-gateway
793 service_name: snmp-gateway
794 placement:
795 count: 1
796 spec:
797 credentials:
798 snmp_v3_auth_username: myuser
799 snmp_v3_auth_password: mypassword
800 snmp_v3_priv_password: mysecret
801 port: 9464
802 auth_protocol: SHA
803 privacy_protocol: AES
804 snmp_destination: 192.168.1.42:162
805 snmp_version: V3
806 """, 'Must provide an engine_id for SNMP V3 notifications'),
807 ("""
808 ---
809 service_type: snmp-gateway
810 service_name: snmp-gateway
811 placement:
812 count: 1
813 spec:
814 credentials:
815 snmp_community: public
816 port: 9464
817 snmp_destination: 192.168.1.42
818 snmp_version: V2c
819 """, re.escape('SNMP destination (snmp_destination) type (IPv4) is invalid. Must be either: IPv4:Port, Name:Port')),
820 ("""
821 ---
822 service_type: snmp-gateway
823 service_name: snmp-gateway
824 placement:
825 count: 1
826 spec:
827 credentials:
828 snmp_v3_auth_username: myuser
829 snmp_v3_auth_password: mypassword
830 snmp_v3_priv_password: mysecret
831 port: 9464
832 auth_protocol: SHA
833 privacy_protocol: AES
834 engine_id: bogus
835 snmp_destination: 192.168.1.42:162
836 snmp_version: V3
837 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
838 ("""
839 ---
840 service_type: snmp-gateway
841 service_name: snmp-gateway
842 placement:
843 count: 1
844 spec:
845 credentials:
846 snmp_v3_auth_username: myuser
847 snmp_v3_auth_password: mypassword
848 port: 9464
849 auth_protocol: SHA
850 engine_id: 8000C53F0000000000
851 snmp_version: V3
852 """, re.escape('SNMP destination (snmp_destination) must be provided')),
853 ("""
854 ---
855 service_type: snmp-gateway
856 service_name: snmp-gateway
857 placement:
858 count: 1
859 spec:
860 credentials:
861 snmp_v3_auth_username: myuser
862 snmp_v3_auth_password: mypassword
863 snmp_v3_priv_password: mysecret
864 port: 9464
865 auth_protocol: SHA
866 privacy_protocol: AES
867 engine_id: 8000C53F0000000000
868 snmp_destination: my.imaginary.snmp-host
869 snmp_version: V3
870 """, re.escape('SNMP destination (snmp_destination) is invalid: DNS lookup failed')),
871 ("""
872 ---
873 service_type: snmp-gateway
874 service_name: snmp-gateway
875 placement:
876 count: 1
877 spec:
878 credentials:
879 snmp_v3_auth_username: myuser
880 snmp_v3_auth_password: mypassword
881 snmp_v3_priv_password: mysecret
882 port: 9464
883 auth_protocol: SHA
884 privacy_protocol: AES
885 engine_id: 8000C53F0000000000
886 snmp_destination: 10.79.32.10:fred
887 snmp_version: V3
888 """, re.escape('SNMP destination (snmp_destination) is invalid: Port must be numeric')),
889 ("""
890 ---
891 service_type: snmp-gateway
892 service_name: snmp-gateway
893 placement:
894 count: 1
895 spec:
896 credentials:
897 snmp_v3_auth_username: myuser
898 snmp_v3_auth_password: mypassword
899 snmp_v3_priv_password: mysecret
900 port: 9464
901 auth_protocol: SHA
902 privacy_protocol: AES
903 engine_id: 8000C53
904 snmp_destination: 10.79.32.10:162
905 snmp_version: V3
906 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
907 ("""
908 ---
909 service_type: snmp-gateway
910 service_name: snmp-gateway
911 placement:
912 count: 1
913 spec:
914 credentials:
915 snmp_v3_auth_username: myuser
916 snmp_v3_auth_password: mypassword
917 snmp_v3_priv_password: mysecret
918 port: 9464
919 auth_protocol: SHA
920 privacy_protocol: AES
921 engine_id: 8000C53DOH!
922 snmp_destination: 10.79.32.10:162
923 snmp_version: V3
924 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
925 ("""
926 ---
927 service_type: snmp-gateway
928 service_name: snmp-gateway
929 placement:
930 count: 1
931 spec:
932 credentials:
933 snmp_v3_auth_username: myuser
934 snmp_v3_auth_password: mypassword
935 snmp_v3_priv_password: mysecret
936 port: 9464
937 auth_protocol: SHA
938 privacy_protocol: AES
939 engine_id: 8000C53FCA7344403DC611EC9B985254002537A6C53FCA7344403DC6112537A60
940 snmp_destination: 10.79.32.10:162
941 snmp_version: V3
942 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
943 ("""
944 ---
945 service_type: snmp-gateway
946 service_name: snmp-gateway
947 placement:
948 count: 1
949 spec:
950 credentials:
951 snmp_v3_auth_username: myuser
952 snmp_v3_auth_password: mypassword
953 snmp_v3_priv_password: mysecret
954 port: 9464
955 auth_protocol: SHA
956 privacy_protocol: AES
957 engine_id: 8000C53F00000
958 snmp_destination: 10.79.32.10:162
959 snmp_version: V3
960 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
961 ])
962 def test_service_spec_validation_error(y, error_match):
963 data = yaml.safe_load(y)
964 with pytest.raises(SpecValidationError) as err:
965 specObj = ServiceSpec.from_json(data)
966 assert err.match(error_match)