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