]> git.proxmox.com Git - ceph.git/blob - ceph/src/python-common/ceph/tests/test_service_spec.py
d3fb4329668bb74d193a10669969f888eced5703
[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
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
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 ])
71 def 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 )
90 def 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 ])
100 def 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 ])
134 def test_placement_target_size(p, hosts, size):
135 assert p.get_target_count(
136 [HostPlacementSpec(n, '', '') for n in hosts]
137 ) == size
138
139
140 def _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 ])
177 def 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
188 def 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
202 service_name: crash
203 placement:
204 host_pattern: '*'
205 ---
206 service_type: crash
207 service_name: crash
208 placement:
209 host_pattern: '*'
210 unmanaged: true
211 ---
212 service_type: rgw
213 service_id: default-rgw-realm.eu-central-1.1
214 service_name: rgw.default-rgw-realm.eu-central-1.1
215 placement:
216 hosts:
217 - ceph-001
218 networks:
219 - 10.0.0.0/8
220 - 192.168.0.0/16
221 spec:
222 rgw_frontend_type: civetweb
223 rgw_realm: default-rgw-realm
224 rgw_zone: eu-central-1
225 ---
226 service_type: osd
227 service_id: osd_spec_default
228 service_name: osd.osd_spec_default
229 placement:
230 host_pattern: '*'
231 spec:
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 ---
241 service_type: alertmanager
242 service_name: alertmanager
243 spec:
244 port: 1234
245 user_data:
246 default_webhook_urls:
247 - foo
248 ---
249 service_type: grafana
250 service_name: grafana
251 spec:
252 port: 1234
253 ---
254 service_type: grafana
255 service_name: grafana
256 spec:
257 initial_admin_password: secure
258 port: 1234
259 ---
260 service_type: ingress
261 service_id: rgw.foo
262 service_name: ingress.rgw.foo
263 placement:
264 hosts:
265 - host1
266 - host2
267 - host3
268 spec:
269 backend_service: rgw.foo
270 frontend_port: 8080
271 monitor_port: 8081
272 virtual_ip: 192.168.20.1/24
273 ---
274 service_type: nfs
275 service_id: mynfs
276 service_name: nfs.mynfs
277 spec:
278 port: 1234
279 ---
280 service_type: iscsi
281 service_id: iscsi
282 service_name: iscsi.iscsi
283 networks:
284 - ::0/8
285 spec:
286 api_user: api_user
287 pool: pool
288 trusted_ip_list:
289 - ::1
290 - ::2
291 ---
292 service_type: container
293 service_id: hello-world
294 service_name: container.hello-world
295 spec:
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 ---
325 service_type: snmp-gateway
326 service_name: snmp-gateway
327 placement:
328 count: 1
329 spec:
330 credentials:
331 snmp_community: public
332 snmp_destination: 192.168.1.42:162
333 snmp_version: V2c
334 ---
335 service_type: snmp-gateway
336 service_name: snmp-gateway
337 placement:
338 count: 1
339 spec:
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 ---
349 service_type: snmp-gateway
350 service_name: snmp-gateway
351 placement:
352 count: 1
353 spec:
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'))
363 def 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
371 def 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
379 def 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()
383
384
385
386 def test_repr():
387 val = """ServiceSpec.from_json(yaml.safe_load('''service_type: crash
388 service_name: crash
389 placement:
390 count: 42
391 '''))"""
392 obj = eval(val)
393 assert obj.service_type == 'crash'
394 assert val == repr(obj)
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(
446 service_type='rgw',
447 service_id='foo',
448 ),
449 RGWSpec(service_id='foo'),
450 True
451 ),
452 ])
453 def 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 ])
470 def 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
474
475 @pytest.mark.parametrize(
476 's_type,s_id',
477 [
478 ('mds', 's:id'), # MDS service_id cannot contain an invalid char ':'
479 ('mds', '1abc'), # MDS service_id cannot start with a numeric digit
480 ('mds', ''), # MDS service_id cannot be empty
481 ('rgw', '*s_id'),
482 ('nfs', 's/id'),
483 ('iscsi', 's@id'),
484 ('osd', 's;id'),
485 ])
486
487 def test_service_id_raises_invalid_char(s_type, s_id):
488 with pytest.raises(SpecValidationError):
489 spec = ServiceSpec.from_json(_get_dict_spec(s_type, s_id))
490 spec.validate()
491
492 def test_custom_container_spec():
493 spec = CustomContainerSpec(service_id='hello-world',
494 image='docker.io/library/hello-world:latest',
495 entrypoint='/usr/bin/bash',
496 uid=1000,
497 gid=2000,
498 volume_mounts={'foo': '/foo'},
499 args=['--foo'],
500 envs=['FOO=0815'],
501 bind_mounts=[
502 [
503 'type=bind',
504 'source=lib/modules',
505 'destination=/lib/modules',
506 'ro=true'
507 ]
508 ],
509 ports=[8080, 8443],
510 dirs=['foo', 'bar'],
511 files={
512 'foo.conf': 'foo\nbar',
513 'bar.conf': ['foo', 'bar']
514 })
515 assert spec.service_type == 'container'
516 assert spec.entrypoint == '/usr/bin/bash'
517 assert spec.uid == 1000
518 assert spec.gid == 2000
519 assert spec.volume_mounts == {'foo': '/foo'}
520 assert spec.args == ['--foo']
521 assert spec.envs == ['FOO=0815']
522 assert spec.bind_mounts == [
523 [
524 'type=bind',
525 'source=lib/modules',
526 'destination=/lib/modules',
527 'ro=true'
528 ]
529 ]
530 assert spec.ports == [8080, 8443]
531 assert spec.dirs == ['foo', 'bar']
532 assert spec.files == {
533 'foo.conf': 'foo\nbar',
534 'bar.conf': ['foo', 'bar']
535 }
536
537
538 def test_custom_container_spec_config_json():
539 spec = CustomContainerSpec(service_id='foo', image='foo', dirs=None)
540 config_json = spec.config_json()
541 for key in ['entrypoint', 'uid', 'gid', 'bind_mounts', 'dirs']:
542 assert key not in config_json
543
544
545 def test_ingress_spec():
546 yaml_str = """service_type: ingress
547 service_id: rgw.foo
548 placement:
549 hosts:
550 - host1
551 - host2
552 - host3
553 spec:
554 virtual_ip: 192.168.20.1/24
555 backend_service: rgw.foo
556 frontend_port: 8080
557 monitor_port: 8081
558 """
559 yaml_file = yaml.safe_load(yaml_str)
560 spec = ServiceSpec.from_json(yaml_file)
561 assert spec.service_type == "ingress"
562 assert spec.service_id == "rgw.foo"
563 assert spec.virtual_ip == "192.168.20.1/24"
564 assert spec.frontend_port == 8080
565 assert spec.monitor_port == 8081
566
567
568 @pytest.mark.parametrize("y, error_match", [
569 ("""
570 service_type: rgw
571 service_id: foo
572 placement:
573 count_per_host: "twelve"
574 """, "count-per-host must be a numeric value",),
575 ("""
576 service_type: rgw
577 service_id: foo
578 placement:
579 count_per_host: "2"
580 """, "count-per-host must be an integer value",),
581 ("""
582 service_type: rgw
583 service_id: foo
584 placement:
585 count_per_host: 7.36
586 """, "count-per-host must be an integer value",),
587 ("""
588 service_type: rgw
589 service_id: foo
590 placement:
591 count: "fifteen"
592 """, "num/count must be a numeric value",),
593 ("""
594 service_type: rgw
595 service_id: foo
596 placement:
597 count: "4"
598 """, "num/count must be an integer value",),
599 ("""
600 service_type: rgw
601 service_id: foo
602 placement:
603 count: 7.36
604 """, "num/count must be an integer value",),
605 ("""
606 service_type: rgw
607 service_id: foo
608 placement:
609 count: 0
610 """, "num/count must be >= 1",),
611 ("""
612 service_type: rgw
613 service_id: foo
614 placement:
615 count_per_host: 0
616 """, "count-per-host must be >= 1",),
617 ("""
618 service_type: snmp-gateway
619 service_name: snmp-gateway
620 placement:
621 count: 1
622 spec:
623 credentials:
624 snmp_v3_auth_password: mypassword
625 snmp_v3_auth_username: myuser
626 snmp_v3_priv_password: mysecret
627 port: 9464
628 engine_id: 8000c53f0000000000
629 privacy_protocol: WEIRD
630 snmp_destination: 192.168.122.1:162
631 auth_protocol: BIZARRE
632 snmp_version: V3
633 """, "auth_protocol unsupported. Must be one of MD5, SHA"),
634 ("""
635 ---
636 service_type: snmp-gateway
637 service_name: snmp-gateway
638 placement:
639 count: 1
640 spec:
641 credentials:
642 snmp_community: public
643 snmp_destination: 192.168.1.42:162
644 snmp_version: V4
645 """, 'snmp_version unsupported. Must be one of V2c, V3'),
646 ("""
647 ---
648 service_type: snmp-gateway
649 service_name: snmp-gateway
650 placement:
651 count: 1
652 spec:
653 credentials:
654 snmp_community: public
655 port: 9464
656 snmp_destination: 192.168.1.42:162
657 """, re.escape('Missing SNMP version (snmp_version)')),
658 ("""
659 ---
660 service_type: snmp-gateway
661 service_name: snmp-gateway
662 placement:
663 count: 1
664 spec:
665 credentials:
666 snmp_v3_auth_username: myuser
667 snmp_v3_auth_password: mypassword
668 port: 9464
669 auth_protocol: wah
670 snmp_destination: 192.168.1.42:162
671 snmp_version: V3
672 """, 'auth_protocol unsupported. Must be one of MD5, SHA'),
673 ("""
674 ---
675 service_type: snmp-gateway
676 service_name: snmp-gateway
677 placement:
678 count: 1
679 spec:
680 credentials:
681 snmp_v3_auth_username: myuser
682 snmp_v3_auth_password: mypassword
683 snmp_v3_priv_password: mysecret
684 port: 9464
685 auth_protocol: SHA
686 privacy_protocol: weewah
687 snmp_destination: 192.168.1.42:162
688 snmp_version: V3
689 """, 'privacy_protocol unsupported. Must be one of DES, AES'),
690 ("""
691 ---
692 service_type: snmp-gateway
693 service_name: snmp-gateway
694 placement:
695 count: 1
696 spec:
697 credentials:
698 snmp_v3_auth_username: myuser
699 snmp_v3_auth_password: mypassword
700 snmp_v3_priv_password: mysecret
701 port: 9464
702 auth_protocol: SHA
703 privacy_protocol: AES
704 snmp_destination: 192.168.1.42:162
705 snmp_version: V3
706 """, 'Must provide an engine_id for SNMP V3 notifications'),
707 ("""
708 ---
709 service_type: snmp-gateway
710 service_name: snmp-gateway
711 placement:
712 count: 1
713 spec:
714 credentials:
715 snmp_community: public
716 port: 9464
717 snmp_destination: 192.168.1.42
718 snmp_version: V2c
719 """, re.escape('SNMP destination (snmp_destination) type (IPv4) is invalid. Must be either: IPv4:Port, Name:Port')),
720 ("""
721 ---
722 service_type: snmp-gateway
723 service_name: snmp-gateway
724 placement:
725 count: 1
726 spec:
727 credentials:
728 snmp_v3_auth_username: myuser
729 snmp_v3_auth_password: mypassword
730 snmp_v3_priv_password: mysecret
731 port: 9464
732 auth_protocol: SHA
733 privacy_protocol: AES
734 engine_id: bogus
735 snmp_destination: 192.168.1.42:162
736 snmp_version: V3
737 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
738 ("""
739 ---
740 service_type: snmp-gateway
741 service_name: snmp-gateway
742 placement:
743 count: 1
744 spec:
745 credentials:
746 snmp_v3_auth_username: myuser
747 snmp_v3_auth_password: mypassword
748 port: 9464
749 auth_protocol: SHA
750 engine_id: 8000C53F0000000000
751 snmp_version: V3
752 """, re.escape('SNMP destination (snmp_destination) must be provided')),
753 ("""
754 ---
755 service_type: snmp-gateway
756 service_name: snmp-gateway
757 placement:
758 count: 1
759 spec:
760 credentials:
761 snmp_v3_auth_username: myuser
762 snmp_v3_auth_password: mypassword
763 snmp_v3_priv_password: mysecret
764 port: 9464
765 auth_protocol: SHA
766 privacy_protocol: AES
767 engine_id: 8000C53F0000000000
768 snmp_destination: my.imaginary.snmp-host
769 snmp_version: V3
770 """, re.escape('SNMP destination (snmp_destination) is invalid: DNS lookup failed')),
771 ("""
772 ---
773 service_type: snmp-gateway
774 service_name: snmp-gateway
775 placement:
776 count: 1
777 spec:
778 credentials:
779 snmp_v3_auth_username: myuser
780 snmp_v3_auth_password: mypassword
781 snmp_v3_priv_password: mysecret
782 port: 9464
783 auth_protocol: SHA
784 privacy_protocol: AES
785 engine_id: 8000C53F0000000000
786 snmp_destination: 10.79.32.10:fred
787 snmp_version: V3
788 """, re.escape('SNMP destination (snmp_destination) is invalid: Port must be numeric')),
789 ("""
790 ---
791 service_type: snmp-gateway
792 service_name: snmp-gateway
793 placement:
794 count: 1
795 spec:
796 credentials:
797 snmp_v3_auth_username: myuser
798 snmp_v3_auth_password: mypassword
799 snmp_v3_priv_password: mysecret
800 port: 9464
801 auth_protocol: SHA
802 privacy_protocol: AES
803 engine_id: 8000C53
804 snmp_destination: 10.79.32.10:162
805 snmp_version: V3
806 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
807 ("""
808 ---
809 service_type: snmp-gateway
810 service_name: snmp-gateway
811 placement:
812 count: 1
813 spec:
814 credentials:
815 snmp_v3_auth_username: myuser
816 snmp_v3_auth_password: mypassword
817 snmp_v3_priv_password: mysecret
818 port: 9464
819 auth_protocol: SHA
820 privacy_protocol: AES
821 engine_id: 8000C53DOH!
822 snmp_destination: 10.79.32.10:162
823 snmp_version: V3
824 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
825 ("""
826 ---
827 service_type: snmp-gateway
828 service_name: snmp-gateway
829 placement:
830 count: 1
831 spec:
832 credentials:
833 snmp_v3_auth_username: myuser
834 snmp_v3_auth_password: mypassword
835 snmp_v3_priv_password: mysecret
836 port: 9464
837 auth_protocol: SHA
838 privacy_protocol: AES
839 engine_id: 8000C53FCA7344403DC611EC9B985254002537A6C53FCA7344403DC6112537A60
840 snmp_destination: 10.79.32.10:162
841 snmp_version: V3
842 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
843 ("""
844 ---
845 service_type: snmp-gateway
846 service_name: snmp-gateway
847 placement:
848 count: 1
849 spec:
850 credentials:
851 snmp_v3_auth_username: myuser
852 snmp_v3_auth_password: mypassword
853 snmp_v3_priv_password: mysecret
854 port: 9464
855 auth_protocol: SHA
856 privacy_protocol: AES
857 engine_id: 8000C53F00000
858 snmp_destination: 10.79.32.10:162
859 snmp_version: V3
860 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
861 ])
862 def test_service_spec_validation_error(y, error_match):
863 data = yaml.safe_load(y)
864 with pytest.raises(SpecValidationError) as err:
865 specObj = ServiceSpec.from_json(data)
866 assert err.match(error_match)