]> git.proxmox.com Git - ceph.git/blame_incremental - 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
1# flake8: noqa
2import json
3import re
4
5import yaml
6
7import pytest
8
9from ceph.deployment.service_spec import HostPlacementSpec, PlacementSpec, \
10 ServiceSpec, RGWSpec, NFSServiceSpec, IscsiServiceSpec, AlertManagerSpec, \
11 CustomContainerSpec
12from ceph.deployment.drive_group import DriveGroupSpec
13from 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 ])
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
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
48
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*')"),
69 ("count-per-host:4 label:foo", "PlacementSpec(count_per_host=4, label='foo')"),
70 ])
71def test_parse_placement_specs(test_input, expected):
72 ret = PlacementSpec.from_string(test_input)
73 assert str(ret) == expected
74 assert PlacementSpec.from_string(ret.pretty_str()) == ret, f'"{ret.pretty_str()}" != "{test_input}"'
75
76@pytest.mark.parametrize(
77 "test_input",
78 [
79 ("host=a host*"),
80 ("host=a label:wrong"),
81 ("host? host*"),
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'),
88 ]
89)
90def test_parse_placement_specs_raises(test_input):
91 with pytest.raises(SpecValidationError):
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"),
99 ])
100def test_parse_host_placement_specs_raises_wrong_format(test_input):
101 with pytest.raises(ValueError):
102 HostPlacementSpec.parse(test_input)
103
104
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
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':
148 pass
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
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'),
174 ("iscsi", IscsiServiceSpec, 'test'),
175 ("osd", DriveGroupSpec, 'test'),
176 ])
177def test_servicespec_map_test(s_type, o_spec, s_id):
178 spec = ServiceSpec.from_json(_get_dict_spec(s_type, s_id))
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 == ''
185 assert spec.validate() is None
186 ServiceSpec.from_json(spec.to_json())
187
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
198
199
200@pytest.mark.parametrize("y",
201"""service_type: crash
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:
217 - ceph-001
218networks:
219- 10.0.0.0/8
220- 192.168.0.0/16
221spec:
222 rgw_frontend_type: civetweb
223 rgw_realm: default-rgw-realm
224 rgw_zone: eu-central-1
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
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:
286 api_password: admin
287 api_port: 5000
288 api_user: admin
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()
385
386
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)
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(
448 service_type='rgw',
449 service_id='foo',
450 ),
451 RGWSpec(service_id='foo'),
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
476
477@pytest.mark.parametrize(
478 's_type,s_id',
479 [
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
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()
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)