]> git.proxmox.com Git - ceph.git/blame - ceph/src/python-common/ceph/tests/test_service_spec.py
import quincy beta 17.1.0
[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:
286 api_user: api_user
287 pool: pool
288 trusted_ip_list:
289 - ::1
290 - ::2
291---
292service_type: container
293service_id: hello-world
294service_name: container.hello-world
295spec:
296 args:
297 - --foo
298 bind_mounts:
299 - - type=bind
300 - source=lib/modules
301 - destination=/lib/modules
302 - ro=true
303 dirs:
304 - foo
305 - bar
306 entrypoint: /usr/bin/bash
307 envs:
308 - FOO=0815
309 files:
310 bar.conf:
311 - foo
312 - bar
313 foo.conf: 'foo
314
315 bar'
316 gid: 2000
317 image: docker.io/library/hello-world:latest
318 ports:
319 - 8080
320 - 8443
321 uid: 1000
322 volume_mounts:
323 foo: /foo
324---
325service_type: snmp-gateway
326service_name: snmp-gateway
327placement:
328 count: 1
329spec:
330 credentials:
331 snmp_community: public
332 snmp_destination: 192.168.1.42:162
333 snmp_version: V2c
334---
335service_type: snmp-gateway
336service_name: snmp-gateway
337placement:
338 count: 1
339spec:
340 auth_protocol: MD5
341 credentials:
342 snmp_v3_auth_password: mypassword
343 snmp_v3_auth_username: myuser
344 engine_id: 8000C53F00000000
345 port: 9464
346 snmp_destination: 192.168.1.42:162
347 snmp_version: V3
348---
349service_type: snmp-gateway
350service_name: snmp-gateway
351placement:
352 count: 1
353spec:
354 credentials:
355 snmp_v3_auth_password: mypassword
356 snmp_v3_auth_username: myuser
357 snmp_v3_priv_password: mysecret
358 engine_id: 8000C53F00000000
359 privacy_protocol: AES
360 snmp_destination: 192.168.1.42:162
361 snmp_version: V3
362""".split('---\n'))
363def test_yaml(y):
364 data = yaml.safe_load(y)
365 object = ServiceSpec.from_json(data)
366
367 assert yaml.dump(object) == y
368 assert yaml.dump(ServiceSpec.from_json(object.to_json())) == y
369
370
371def test_alertmanager_spec_1():
372 spec = AlertManagerSpec()
373 assert spec.service_type == 'alertmanager'
374 assert isinstance(spec.user_data, dict)
375 assert len(spec.user_data.keys()) == 0
376 assert spec.get_port_start() == [9093, 9094]
377
378
379def test_alertmanager_spec_2():
380 spec = AlertManagerSpec(user_data={'default_webhook_urls': ['foo']})
381 assert isinstance(spec.user_data, dict)
382 assert 'default_webhook_urls' in spec.user_data.keys()
f6b5b4d7 383
f6b5b4d7 384
20effc67
TL
385
386def test_repr():
387 val = """ServiceSpec.from_json(yaml.safe_load('''service_type: crash
388service_name: crash
389placement:
390 count: 42
391'''))"""
392 obj = eval(val)
393 assert obj.service_type == 'crash'
394 assert val == repr(obj)
f6b5b4d7
TL
395
396@pytest.mark.parametrize("spec1, spec2, eq",
397 [
398 (
399 ServiceSpec(
400 service_type='mon'
401 ),
402 ServiceSpec(
403 service_type='mon'
404 ),
405 True
406 ),
407 (
408 ServiceSpec(
409 service_type='mon'
410 ),
411 ServiceSpec(
412 service_type='mon',
413 service_id='foo'
414 ),
415 True
416 ),
417 # Add service_type='mgr'
418 (
419 ServiceSpec(
420 service_type='osd'
421 ),
422 ServiceSpec(
423 service_type='osd',
424 ),
425 True
426 ),
427 (
428 ServiceSpec(
429 service_type='osd'
430 ),
431 DriveGroupSpec(),
432 True
433 ),
434 (
435 ServiceSpec(
436 service_type='osd'
437 ),
438 ServiceSpec(
439 service_type='osd',
440 service_id='foo',
441 ),
442 False
443 ),
444 (
445 ServiceSpec(
f67539c2
TL
446 service_type='rgw',
447 service_id='foo',
f6b5b4d7 448 ),
f67539c2 449 RGWSpec(service_id='foo'),
f6b5b4d7
TL
450 True
451 ),
452 ])
453def test_spec_hash_eq(spec1: ServiceSpec,
454 spec2: ServiceSpec,
455 eq: bool):
456
457 assert (spec1 == spec2) is eq
458
459@pytest.mark.parametrize(
460 "s_type,s_id,s_name",
461 [
462 ('mgr', 's_id', 'mgr'),
463 ('mon', 's_id', 'mon'),
464 ('mds', 's_id', 'mds.s_id'),
465 ('rgw', 's_id', 'rgw.s_id'),
466 ('nfs', 's_id', 'nfs.s_id'),
467 ('iscsi', 's_id', 'iscsi.s_id'),
468 ('osd', 's_id', 'osd.s_id'),
469 ])
470def test_service_name(s_type, s_id, s_name):
471 spec = ServiceSpec.from_json(_get_dict_spec(s_type, s_id))
472 spec.validate()
473 assert spec.service_name() == s_name
f67539c2
TL
474
475@pytest.mark.parametrize(
476 's_type,s_id',
477 [
478 ('mds', 's:id'),
479 ('rgw', '*s_id'),
480 ('nfs', 's/id'),
481 ('iscsi', 's@id'),
482 ('osd', 's;id'),
483 ])
484
485def test_service_id_raises_invalid_char(s_type, s_id):
486 with pytest.raises(SpecValidationError):
487 spec = ServiceSpec.from_json(_get_dict_spec(s_type, s_id))
488 spec.validate()
20effc67
TL
489
490def test_custom_container_spec():
491 spec = CustomContainerSpec(service_id='hello-world',
492 image='docker.io/library/hello-world:latest',
493 entrypoint='/usr/bin/bash',
494 uid=1000,
495 gid=2000,
496 volume_mounts={'foo': '/foo'},
497 args=['--foo'],
498 envs=['FOO=0815'],
499 bind_mounts=[
500 [
501 'type=bind',
502 'source=lib/modules',
503 'destination=/lib/modules',
504 'ro=true'
505 ]
506 ],
507 ports=[8080, 8443],
508 dirs=['foo', 'bar'],
509 files={
510 'foo.conf': 'foo\nbar',
511 'bar.conf': ['foo', 'bar']
512 })
513 assert spec.service_type == 'container'
514 assert spec.entrypoint == '/usr/bin/bash'
515 assert spec.uid == 1000
516 assert spec.gid == 2000
517 assert spec.volume_mounts == {'foo': '/foo'}
518 assert spec.args == ['--foo']
519 assert spec.envs == ['FOO=0815']
520 assert spec.bind_mounts == [
521 [
522 'type=bind',
523 'source=lib/modules',
524 'destination=/lib/modules',
525 'ro=true'
526 ]
527 ]
528 assert spec.ports == [8080, 8443]
529 assert spec.dirs == ['foo', 'bar']
530 assert spec.files == {
531 'foo.conf': 'foo\nbar',
532 'bar.conf': ['foo', 'bar']
533 }
534
535
536def test_custom_container_spec_config_json():
537 spec = CustomContainerSpec(service_id='foo', image='foo', dirs=None)
538 config_json = spec.config_json()
539 for key in ['entrypoint', 'uid', 'gid', 'bind_mounts', 'dirs']:
540 assert key not in config_json
541
542
543def test_ingress_spec():
544 yaml_str = """service_type: ingress
545service_id: rgw.foo
546placement:
547 hosts:
548 - host1
549 - host2
550 - host3
551spec:
552 virtual_ip: 192.168.20.1/24
553 backend_service: rgw.foo
554 frontend_port: 8080
555 monitor_port: 8081
556"""
557 yaml_file = yaml.safe_load(yaml_str)
558 spec = ServiceSpec.from_json(yaml_file)
559 assert spec.service_type == "ingress"
560 assert spec.service_id == "rgw.foo"
561 assert spec.virtual_ip == "192.168.20.1/24"
562 assert spec.frontend_port == 8080
563 assert spec.monitor_port == 8081
564
565
566@pytest.mark.parametrize("y, error_match", [
567 ("""
568service_type: rgw
569service_id: foo
570placement:
571 count_per_host: "twelve"
572""", "count-per-host must be a numeric value",),
573 ("""
574service_type: rgw
575service_id: foo
576placement:
577 count_per_host: "2"
578""", "count-per-host must be an integer value",),
579 ("""
580service_type: rgw
581service_id: foo
582placement:
583 count_per_host: 7.36
584""", "count-per-host must be an integer value",),
585 ("""
586service_type: rgw
587service_id: foo
588placement:
589 count: "fifteen"
590""", "num/count must be a numeric value",),
591 ("""
592service_type: rgw
593service_id: foo
594placement:
595 count: "4"
596""", "num/count must be an integer value",),
597 ("""
598service_type: rgw
599service_id: foo
600placement:
601 count: 7.36
602""", "num/count must be an integer value",),
603 ("""
604service_type: rgw
605service_id: foo
606placement:
607 count: 0
608""", "num/count must be >= 1",),
609 ("""
610service_type: rgw
611service_id: foo
612placement:
613 count_per_host: 0
614""", "count-per-host must be >= 1",),
615 ("""
616service_type: snmp-gateway
617service_name: snmp-gateway
618placement:
619 count: 1
620spec:
621 credentials:
622 snmp_v3_auth_password: mypassword
623 snmp_v3_auth_username: myuser
624 snmp_v3_priv_password: mysecret
625 port: 9464
626 engine_id: 8000c53f0000000000
627 privacy_protocol: WEIRD
628 snmp_destination: 192.168.122.1:162
629 auth_protocol: BIZARRE
630 snmp_version: V3
631""", "auth_protocol unsupported. Must be one of MD5, SHA"),
632 ("""
633---
634service_type: snmp-gateway
635service_name: snmp-gateway
636placement:
637 count: 1
638spec:
639 credentials:
640 snmp_community: public
641 snmp_destination: 192.168.1.42:162
642 snmp_version: V4
643""", 'snmp_version unsupported. Must be one of V2c, V3'),
644 ("""
645---
646service_type: snmp-gateway
647service_name: snmp-gateway
648placement:
649 count: 1
650spec:
651 credentials:
652 snmp_community: public
653 port: 9464
654 snmp_destination: 192.168.1.42:162
655""", re.escape('Missing SNMP version (snmp_version)')),
656 ("""
657---
658service_type: snmp-gateway
659service_name: snmp-gateway
660placement:
661 count: 1
662spec:
663 credentials:
664 snmp_v3_auth_username: myuser
665 snmp_v3_auth_password: mypassword
666 port: 9464
667 auth_protocol: wah
668 snmp_destination: 192.168.1.42:162
669 snmp_version: V3
670""", 'auth_protocol unsupported. Must be one of MD5, SHA'),
671 ("""
672---
673service_type: snmp-gateway
674service_name: snmp-gateway
675placement:
676 count: 1
677spec:
678 credentials:
679 snmp_v3_auth_username: myuser
680 snmp_v3_auth_password: mypassword
681 snmp_v3_priv_password: mysecret
682 port: 9464
683 auth_protocol: SHA
684 privacy_protocol: weewah
685 snmp_destination: 192.168.1.42:162
686 snmp_version: V3
687""", 'privacy_protocol unsupported. Must be one of DES, AES'),
688 ("""
689---
690service_type: snmp-gateway
691service_name: snmp-gateway
692placement:
693 count: 1
694spec:
695 credentials:
696 snmp_v3_auth_username: myuser
697 snmp_v3_auth_password: mypassword
698 snmp_v3_priv_password: mysecret
699 port: 9464
700 auth_protocol: SHA
701 privacy_protocol: AES
702 snmp_destination: 192.168.1.42:162
703 snmp_version: V3
704""", 'Must provide an engine_id for SNMP V3 notifications'),
705 ("""
706---
707service_type: snmp-gateway
708service_name: snmp-gateway
709placement:
710 count: 1
711spec:
712 credentials:
713 snmp_community: public
714 port: 9464
715 snmp_destination: 192.168.1.42
716 snmp_version: V2c
717""", re.escape('SNMP destination (snmp_destination) type (IPv4) is invalid. Must be either: IPv4:Port, Name:Port')),
718 ("""
719---
720service_type: snmp-gateway
721service_name: snmp-gateway
722placement:
723 count: 1
724spec:
725 credentials:
726 snmp_v3_auth_username: myuser
727 snmp_v3_auth_password: mypassword
728 snmp_v3_priv_password: mysecret
729 port: 9464
730 auth_protocol: SHA
731 privacy_protocol: AES
732 engine_id: bogus
733 snmp_destination: 192.168.1.42:162
734 snmp_version: V3
735""", 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
736 ("""
737---
738service_type: snmp-gateway
739service_name: snmp-gateway
740placement:
741 count: 1
742spec:
743 credentials:
744 snmp_v3_auth_username: myuser
745 snmp_v3_auth_password: mypassword
746 port: 9464
747 auth_protocol: SHA
748 engine_id: 8000C53F0000000000
749 snmp_version: V3
750""", re.escape('SNMP destination (snmp_destination) must be provided')),
751 ("""
752---
753service_type: snmp-gateway
754service_name: snmp-gateway
755placement:
756 count: 1
757spec:
758 credentials:
759 snmp_v3_auth_username: myuser
760 snmp_v3_auth_password: mypassword
761 snmp_v3_priv_password: mysecret
762 port: 9464
763 auth_protocol: SHA
764 privacy_protocol: AES
765 engine_id: 8000C53F0000000000
766 snmp_destination: my.imaginary.snmp-host
767 snmp_version: V3
768""", re.escape('SNMP destination (snmp_destination) is invalid: DNS lookup failed')),
769 ("""
770---
771service_type: snmp-gateway
772service_name: snmp-gateway
773placement:
774 count: 1
775spec:
776 credentials:
777 snmp_v3_auth_username: myuser
778 snmp_v3_auth_password: mypassword
779 snmp_v3_priv_password: mysecret
780 port: 9464
781 auth_protocol: SHA
782 privacy_protocol: AES
783 engine_id: 8000C53F0000000000
784 snmp_destination: 10.79.32.10:fred
785 snmp_version: V3
786""", re.escape('SNMP destination (snmp_destination) is invalid: Port must be numeric')),
787 ("""
788---
789service_type: snmp-gateway
790service_name: snmp-gateway
791placement:
792 count: 1
793spec:
794 credentials:
795 snmp_v3_auth_username: myuser
796 snmp_v3_auth_password: mypassword
797 snmp_v3_priv_password: mysecret
798 port: 9464
799 auth_protocol: SHA
800 privacy_protocol: AES
801 engine_id: 8000C53
802 snmp_destination: 10.79.32.10:162
803 snmp_version: V3
804""", 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
805 ("""
806---
807service_type: snmp-gateway
808service_name: snmp-gateway
809placement:
810 count: 1
811spec:
812 credentials:
813 snmp_v3_auth_username: myuser
814 snmp_v3_auth_password: mypassword
815 snmp_v3_priv_password: mysecret
816 port: 9464
817 auth_protocol: SHA
818 privacy_protocol: AES
819 engine_id: 8000C53DOH!
820 snmp_destination: 10.79.32.10:162
821 snmp_version: V3
822""", 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
823 ("""
824---
825service_type: snmp-gateway
826service_name: snmp-gateway
827placement:
828 count: 1
829spec:
830 credentials:
831 snmp_v3_auth_username: myuser
832 snmp_v3_auth_password: mypassword
833 snmp_v3_priv_password: mysecret
834 port: 9464
835 auth_protocol: SHA
836 privacy_protocol: AES
837 engine_id: 8000C53FCA7344403DC611EC9B985254002537A6C53FCA7344403DC6112537A60
838 snmp_destination: 10.79.32.10:162
839 snmp_version: V3
840""", 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
841 ("""
842---
843service_type: snmp-gateway
844service_name: snmp-gateway
845placement:
846 count: 1
847spec:
848 credentials:
849 snmp_v3_auth_username: myuser
850 snmp_v3_auth_password: mypassword
851 snmp_v3_priv_password: mysecret
852 port: 9464
853 auth_protocol: SHA
854 privacy_protocol: AES
855 engine_id: 8000C53F00000
856 snmp_destination: 10.79.32.10:162
857 snmp_version: V3
858""", 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
859 ])
860def test_service_spec_validation_error(y, error_match):
861 data = yaml.safe_load(y)
862 with pytest.raises(SpecValidationError) as err:
863 specObj = ServiceSpec.from_json(data)
864 assert err.match(error_match)