]>
git.proxmox.com Git - ceph.git/blob - ceph/src/python-common/ceph/tests/test_service_spec.py
502057f5ca3b6c90b86bdd49fe5a3b4375a1f363
9 from ceph
.deployment
.service_spec
import (
22 from ceph
.deployment
.drive_group
import DriveGroupSpec
23 from ceph
.deployment
.hostspec
import SpecValidationError
26 @pytest.mark
.parametrize("test_input,expected, require_network",
27 [("myhost", ('myhost', '', ''), False),
28 ("myhost=sname", ('myhost', '', 'sname'), False),
29 ("myhost:10.1.1.10", ('myhost', '10.1.1.10', ''), True),
30 ("myhost:10.1.1.10=sname", ('myhost', '10.1.1.10', 'sname'), True),
31 ("myhost:10.1.1.0/32", ('myhost', '10.1.1.0/32', ''), True),
32 ("myhost:10.1.1.0/32=sname", ('myhost', '10.1.1.0/32', 'sname'), True),
33 ("myhost:[v1:10.1.1.10:6789]", ('myhost', '[v1:10.1.1.10:6789]', ''), True),
34 ("myhost:[v1:10.1.1.10:6789]=sname", ('myhost', '[v1:10.1.1.10:6789]', 'sname'), True),
35 ("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),
36 ("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),
38 def test_parse_host_placement_specs(test_input
, expected
, require_network
):
39 ret
= HostPlacementSpec
.parse(test_input
, require_network
=require_network
)
40 assert ret
== expected
41 assert str(ret
) == test_input
43 ps
= PlacementSpec
.from_string(test_input
)
44 assert ps
.pretty_str() == test_input
45 assert ps
== PlacementSpec
.from_string(ps
.pretty_str())
47 # Testing the old verbose way of generating json. Don't remove:
48 assert ret
== HostPlacementSpec
.from_json({
49 'hostname': ret
.hostname
,
50 'network': ret
.network
,
54 assert ret
== HostPlacementSpec
.from_json(ret
.to_json())
57 @pytest.mark
.parametrize(
58 "spec, raise_exception, msg",
60 (GrafanaSpec(protocol
=''), True, '^Invalid protocol'),
61 (GrafanaSpec(protocol
='invalid'), True, '^Invalid protocol'),
62 (GrafanaSpec(protocol
='-http'), True, '^Invalid protocol'),
63 (GrafanaSpec(protocol
='-https'), True, '^Invalid protocol'),
64 (GrafanaSpec(protocol
='http'), False, ''),
65 (GrafanaSpec(protocol
='https'), False, ''),
66 (GrafanaSpec(anonymous_access
=False), True, '^Either initial'), # we require inital_admin_password if anonymous_access is False
67 (GrafanaSpec(anonymous_access
=False, initial_admin_password
='test'), False, ''),
69 def test_apply_grafana(spec
: GrafanaSpec
, raise_exception
: bool, msg
: str):
71 with pytest
.raises(SpecValidationError
, match
=msg
):
76 @pytest.mark
.parametrize(
77 "spec, raise_exception, msg",
79 # Valid retention_time values (valid units: 'y', 'w', 'd', 'h', 'm', 's')
80 (PrometheusSpec(retention_time
='1y'), False, ''),
81 (PrometheusSpec(retention_time
=' 10w '), False, ''),
82 (PrometheusSpec(retention_time
=' 1348d'), False, ''),
83 (PrometheusSpec(retention_time
='2000h '), False, ''),
84 (PrometheusSpec(retention_time
='173847m'), False, ''),
85 (PrometheusSpec(retention_time
='200s'), False, ''),
86 (PrometheusSpec(retention_time
=' '), False, ''), # default value will be used
87 # Invalid retention_time values
88 (PrometheusSpec(retention_time
='100k'), True, '^Invalid retention time'), # invalid unit
89 (PrometheusSpec(retention_time
='10'), True, '^Invalid retention time'), # no unit
90 (PrometheusSpec(retention_time
='100.00y'), True, '^Invalid retention time'), # invalid value and valid unit
91 (PrometheusSpec(retention_time
='100.00k'), True, '^Invalid retention time'), # invalid value and invalid unit
92 (PrometheusSpec(retention_time
='---'), True, '^Invalid retention time'), # invalid value
94 # Valid retention_size values (valid units: 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')
95 (PrometheusSpec(retention_size
='123456789B'), False, ''),
96 (PrometheusSpec(retention_size
=' 200KB'), False, ''),
97 (PrometheusSpec(retention_size
='99999MB '), False, ''),
98 (PrometheusSpec(retention_size
=' 10GB '), False, ''),
99 (PrometheusSpec(retention_size
='100TB'), False, ''),
100 (PrometheusSpec(retention_size
='500PB'), False, ''),
101 (PrometheusSpec(retention_size
='200EB'), False, ''),
102 (PrometheusSpec(retention_size
=' '), False, ''), # default value will be used
104 # Invalid retention_size values
105 (PrometheusSpec(retention_size
='100b'), True, '^Invalid retention size'), # invalid unit (case sensitive)
106 (PrometheusSpec(retention_size
='333kb'), True, '^Invalid retention size'), # invalid unit (case sensitive)
107 (PrometheusSpec(retention_size
='2000'), True, '^Invalid retention size'), # no unit
108 (PrometheusSpec(retention_size
='200.00PB'), True, '^Invalid retention size'), # invalid value and valid unit
109 (PrometheusSpec(retention_size
='400.B'), True, '^Invalid retention size'), # invalid value and invalid unit
110 (PrometheusSpec(retention_size
='10.000s'), True, '^Invalid retention size'), # invalid value and invalid unit
111 (PrometheusSpec(retention_size
='...'), True, '^Invalid retention size'), # invalid value
113 # valid retention_size and valid retention_time
114 (PrometheusSpec(retention_time
='1y', retention_size
='100GB'), False, ''),
115 # invalid retention_time and valid retention_size
116 (PrometheusSpec(retention_time
='1j', retention_size
='100GB'), True, '^Invalid retention time'),
117 # valid retention_time and invalid retention_size
118 (PrometheusSpec(retention_time
='1y', retention_size
='100gb'), True, '^Invalid retention size'),
119 # valid retention_time and invalid retention_size
120 (PrometheusSpec(retention_time
='1y', retention_size
='100gb'), True, '^Invalid retention size'),
121 # invalid retention_time and invalid retention_size
122 (PrometheusSpec(retention_time
='1i', retention_size
='100gb'), True, '^Invalid retention time'),
124 def test_apply_prometheus(spec
: PrometheusSpec
, raise_exception
: bool, msg
: str):
126 with pytest
.raises(SpecValidationError
, match
=msg
):
131 @pytest.mark
.parametrize(
132 "test_input,expected",
134 ('', "PlacementSpec()"),
135 ("count:2", "PlacementSpec(count=2)"),
136 ("3", "PlacementSpec(count=3)"),
137 ("host1 host2", "PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacementSpec(hostname='host2', network='', name='')])"),
138 ("host1;host2", "PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacementSpec(hostname='host2', network='', name='')])"),
139 ("host1,host2", "PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacementSpec(hostname='host2', network='', name='')])"),
140 ("host1 host2=b", "PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacementSpec(hostname='host2', network='', name='b')])"),
141 ("host1=a host2=b", "PlacementSpec(hosts=[HostPlacementSpec(hostname='host1', network='', name='a'), HostPlacementSpec(hostname='host2', network='', name='b')])"),
142 ("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')])"),
143 ("myhost:[v1:10.1.1.10:6789]", "PlacementSpec(hosts=[HostPlacementSpec(hostname='myhost', network='[v1:10.1.1.10:6789]', name='')])"),
144 ('2 host1 host2', "PlacementSpec(count=2, hosts=[HostPlacementSpec(hostname='host1', network='', name=''), HostPlacementSpec(hostname='host2', network='', name='')])"),
145 ('label:foo', "PlacementSpec(label='foo')"),
146 ('3 label:foo', "PlacementSpec(count=3, label='foo')"),
147 ('*', "PlacementSpec(host_pattern='*')"),
148 ('3 data[1-3]', "PlacementSpec(count=3, host_pattern='data[1-3]')"),
149 ('3 data?', "PlacementSpec(count=3, host_pattern='data?')"),
150 ('3 data*', "PlacementSpec(count=3, host_pattern='data*')"),
151 ("count-per-host:4 label:foo", "PlacementSpec(count_per_host=4, label='foo')"),
153 def test_parse_placement_specs(test_input
, expected
):
154 ret
= PlacementSpec
.from_string(test_input
)
155 assert str(ret
) == expected
156 assert PlacementSpec
.from_string(ret
.pretty_str()) == ret
, f
'"{ret.pretty_str()}" != "{test_input}"'
158 @pytest.mark
.parametrize(
162 ("host=a label:wrong"),
164 ('host=a count-per-host:0'),
165 ('host=a count-per-host:-10'),
166 ('count:2 count-per-host:1'),
167 ('host1=a host2=b count-per-host:2'),
168 ('host1:10/8 count-per-host:2'),
169 ('count-per-host:2'),
172 def test_parse_placement_specs_raises(test_input
):
173 with pytest
.raises(SpecValidationError
):
174 PlacementSpec
.from_string(test_input
)
176 @pytest.mark
.parametrize("test_input",
178 [("myhost:1.1.1.1/24"),
182 def test_parse_host_placement_specs_raises_wrong_format(test_input
):
183 with pytest
.raises(ValueError):
184 HostPlacementSpec
.parse(test_input
)
187 @pytest.mark
.parametrize(
191 PlacementSpec(count
=3),
192 ['host1', 'host2', 'host3', 'host4', 'host5'],
196 PlacementSpec(host_pattern
='*'),
197 ['host1', 'host2', 'host3', 'host4', 'host5'],
201 PlacementSpec(count_per_host
=2, host_pattern
='*'),
202 ['host1', 'host2', 'host3', 'host4', 'host5'],
206 PlacementSpec(host_pattern
='foo*'),
207 ['foo1', 'foo2', 'bar1', 'bar2'],
211 PlacementSpec(count_per_host
=2, host_pattern
='foo*'),
212 ['foo1', 'foo2', 'bar1', 'bar2'],
216 def test_placement_target_size(p
, hosts
, size
):
217 assert p
.get_target_count(
218 [HostPlacementSpec(n
, '', '') for n
in hosts
]
222 def _get_dict_spec(s_type
, s_id
):
225 "service_type": s_type
,
227 dict(hosts
=["host1:1.1.1.1"])
231 elif s_type
== 'iscsi':
232 dict_spec
['pool'] = 'pool'
233 dict_spec
['api_user'] = 'api_user'
234 dict_spec
['api_password'] = 'api_password'
235 elif s_type
== 'osd':
236 dict_spec
['spec'] = {
241 elif s_type
== 'rgw':
242 dict_spec
['rgw_realm'] = 'realm'
243 dict_spec
['rgw_zone'] = 'zone'
248 @pytest.mark
.parametrize(
249 "s_type,o_spec,s_id",
251 ("mgr", ServiceSpec
, 'test'),
252 ("mon", ServiceSpec
, 'test'),
253 ("mds", ServiceSpec
, 'test'),
254 ("rgw", RGWSpec
, 'realm.zone'),
255 ("nfs", NFSServiceSpec
, 'test'),
256 ("iscsi", IscsiServiceSpec
, 'test'),
257 ("osd", DriveGroupSpec
, 'test'),
259 def test_servicespec_map_test(s_type
, o_spec
, s_id
):
260 spec
= ServiceSpec
.from_json(_get_dict_spec(s_type
, s_id
))
261 assert isinstance(spec
, o_spec
)
262 assert isinstance(spec
.placement
, PlacementSpec
)
263 assert isinstance(spec
.placement
.hosts
[0], HostPlacementSpec
)
264 assert spec
.placement
.hosts
[0].hostname
== 'host1'
265 assert spec
.placement
.hosts
[0].network
== '1.1.1.1'
266 assert spec
.placement
.hosts
[0].name
== ''
267 assert spec
.validate() is None
268 ServiceSpec
.from_json(spec
.to_json())
271 @pytest.mark
.parametrize(
272 "realm, zone, frontend_type, raise_exception, msg",
274 ('realm', 'zone1', 'beast', False, ''),
275 ('realm', 'zone2', 'civetweb', False, ''),
276 ('realm', None, 'beast', True, 'Cannot add RGW: Realm specified but no zone specified'),
277 (None, 'zone1', 'beast', True, 'Cannot add RGW: Zone specified but no realm specified'),
278 ('realm', 'zone', 'invalid-beast', True, '^Invalid rgw_frontend_type value'),
279 ('realm', 'zone', 'invalid-civetweb', True, '^Invalid rgw_frontend_type value'),
281 def test_rgw_servicespec_parse(realm
, zone
, frontend_type
, raise_exception
, msg
):
282 spec
= RGWSpec(service_id
='foo',
285 rgw_frontend_type
=frontend_type
)
287 with pytest
.raises(SpecValidationError
, match
=msg
):
292 def test_osd_unmanaged():
293 osd_spec
= {"placement": {"host_pattern": "*"},
294 "service_id": "all-available-devices",
295 "service_name": "osd.all-available-devices",
296 "service_type": "osd",
297 "spec": {"data_devices": {"all": True}, "filter_logic": "AND", "objectstore": "bluestore"},
300 dg_spec
= ServiceSpec
.from_json(osd_spec
)
301 assert dg_spec
.unmanaged
== True
304 @pytest.mark
.parametrize("y",
305 """service_type: crash
317 service_id: default-rgw-realm.eu-central-1.1
318 service_name: rgw.default-rgw-realm.eu-central-1.1
326 rgw_frontend_type: civetweb
327 rgw_realm: default-rgw-realm
328 rgw_zone: eu-central-1
331 service_id: osd_spec_default
332 service_name: osd.osd_spec_default
341 objectstore: bluestore
345 service_type: alertmanager
346 service_name: alertmanager
350 default_webhook_urls:
353 service_type: grafana
354 service_name: grafana
356 anonymous_access: true
360 service_type: grafana
361 service_name: grafana
363 anonymous_access: true
364 initial_admin_password: secure
368 service_type: ingress
370 service_name: ingress.rgw.foo
377 backend_service: rgw.foo
378 first_virtual_router_id: 50
381 virtual_ip: 192.168.20.1/24
385 service_name: nfs.mynfs
391 service_name: iscsi.iscsi
403 service_type: container
404 service_id: hello-world
405 service_name: container.hello-world
412 - destination=/lib/modules
417 entrypoint: /usr/bin/bash
428 image: docker.io/library/hello-world:latest
436 service_type: snmp-gateway
437 service_name: snmp-gateway
442 snmp_community: public
443 snmp_destination: 192.168.1.42:162
446 service_type: snmp-gateway
447 service_name: snmp-gateway
453 snmp_v3_auth_password: mypassword
454 snmp_v3_auth_username: myuser
455 engine_id: 8000C53F00000000
457 snmp_destination: 192.168.1.42:162
460 service_type: snmp-gateway
461 service_name: snmp-gateway
466 snmp_v3_auth_password: mypassword
467 snmp_v3_auth_username: myuser
468 snmp_v3_priv_password: mysecret
469 engine_id: 8000C53F00000000
470 privacy_protocol: AES
471 snmp_destination: 192.168.1.42:162
475 data
= yaml
.safe_load(y
)
476 object = ServiceSpec
.from_json(data
)
478 assert yaml
.dump(object) == y
479 assert yaml
.dump(ServiceSpec
.from_json(object.to_json())) == y
482 def test_alertmanager_spec_1():
483 spec
= AlertManagerSpec()
484 assert spec
.service_type
== 'alertmanager'
485 assert isinstance(spec
.user_data
, dict)
486 assert len(spec
.user_data
.keys()) == 0
487 assert spec
.get_port_start() == [9093, 9094]
490 def test_alertmanager_spec_2():
491 spec
= AlertManagerSpec(user_data
={'default_webhook_urls': ['foo']})
492 assert isinstance(spec
.user_data
, dict)
493 assert 'default_webhook_urls' in spec
.user_data
.keys()
498 val
= """ServiceSpec.from_json(yaml.safe_load('''service_type: crash
504 assert obj
.service_type
== 'crash'
505 assert val
== repr(obj
)
507 @pytest.mark
.parametrize("spec1, spec2, eq",
528 # Add service_type='mgr'
560 RGWSpec(service_id
='foo'),
564 def test_spec_hash_eq(spec1
: ServiceSpec
,
568 assert (spec1
== spec2
) is eq
570 @pytest.mark
.parametrize(
571 "s_type,s_id,s_name",
573 ('mgr', 's_id', 'mgr'),
574 ('mon', 's_id', 'mon'),
575 ('mds', 's_id', 'mds.s_id'),
576 ('rgw', 's_id', 'rgw.s_id'),
577 ('nfs', 's_id', 'nfs.s_id'),
578 ('iscsi', 's_id', 'iscsi.s_id'),
579 ('osd', 's_id', 'osd.s_id'),
581 def test_service_name(s_type
, s_id
, s_name
):
582 spec
= ServiceSpec
.from_json(_get_dict_spec(s_type
, s_id
))
584 assert spec
.service_name() == s_name
586 @pytest.mark
.parametrize(
589 ('mds', 's:id'), # MDS service_id cannot contain an invalid char ':'
590 ('mds', '1abc'), # MDS service_id cannot start with a numeric digit
591 ('mds', ''), # MDS service_id cannot be empty
598 def test_service_id_raises_invalid_char(s_type
, s_id
):
599 with pytest
.raises(SpecValidationError
):
600 spec
= ServiceSpec
.from_json(_get_dict_spec(s_type
, s_id
))
603 def test_custom_container_spec():
604 spec
= CustomContainerSpec(service_id
='hello-world',
605 image
='docker.io/library/hello-world:latest',
606 entrypoint
='/usr/bin/bash',
609 volume_mounts
={'foo': '/foo'},
615 'source=lib/modules',
616 'destination=/lib/modules',
623 'foo.conf': 'foo\nbar',
624 'bar.conf': ['foo', 'bar']
626 assert spec
.service_type
== 'container'
627 assert spec
.entrypoint
== '/usr/bin/bash'
628 assert spec
.uid
== 1000
629 assert spec
.gid
== 2000
630 assert spec
.volume_mounts
== {'foo': '/foo'}
631 assert spec
.args
== ['--foo']
632 assert spec
.envs
== ['FOO=0815']
633 assert spec
.bind_mounts
== [
636 'source=lib/modules',
637 'destination=/lib/modules',
641 assert spec
.ports
== [8080, 8443]
642 assert spec
.dirs
== ['foo', 'bar']
643 assert spec
.files
== {
644 'foo.conf': 'foo\nbar',
645 'bar.conf': ['foo', 'bar']
649 def test_custom_container_spec_config_json():
650 spec
= CustomContainerSpec(service_id
='foo', image
='foo', dirs
=None)
651 config_json
= spec
.config_json()
652 for key
in ['entrypoint', 'uid', 'gid', 'bind_mounts', 'dirs']:
653 assert key
not in config_json
656 def test_ingress_spec():
657 yaml_str
= """service_type: ingress
665 virtual_ip: 192.168.20.1/24
666 backend_service: rgw.foo
670 yaml_file
= yaml
.safe_load(yaml_str
)
671 spec
= ServiceSpec
.from_json(yaml_file
)
672 assert spec
.service_type
== "ingress"
673 assert spec
.service_id
== "rgw.foo"
674 assert spec
.virtual_ip
== "192.168.20.1/24"
675 assert spec
.frontend_port
== 8080
676 assert spec
.monitor_port
== 8081
679 @pytest.mark
.parametrize("y, error_match", [
684 count_per_host: "twelve"
685 """, "count-per-host must be a numeric value",),
691 """, "count-per-host must be an integer value",),
697 """, "count-per-host must be an integer value",),
703 """, "num/count must be a numeric value",),
709 """, "num/count must be an integer value",),
715 """, "num/count must be an integer value",),
721 """, "num/count must be >= 1",),
727 """, "count-per-host must be >= 1",),
729 service_type: snmp-gateway
730 service_name: snmp-gateway
735 snmp_v3_auth_password: mypassword
736 snmp_v3_auth_username: myuser
737 snmp_v3_priv_password: mysecret
739 engine_id: 8000c53f0000000000
740 privacy_protocol: WEIRD
741 snmp_destination: 192.168.122.1:162
742 auth_protocol: BIZARRE
744 """, "auth_protocol unsupported. Must be one of MD5, SHA"),
747 service_type: snmp-gateway
748 service_name: snmp-gateway
753 snmp_community: public
754 snmp_destination: 192.168.1.42:162
756 """, 'snmp_version unsupported. Must be one of V2c, V3'),
759 service_type: snmp-gateway
760 service_name: snmp-gateway
765 snmp_community: public
767 snmp_destination: 192.168.1.42:162
768 """, re
.escape('Missing SNMP version (snmp_version)')),
771 service_type: snmp-gateway
772 service_name: snmp-gateway
777 snmp_v3_auth_username: myuser
778 snmp_v3_auth_password: mypassword
781 snmp_destination: 192.168.1.42:162
783 """, 'auth_protocol unsupported. Must be one of MD5, SHA'),
786 service_type: snmp-gateway
787 service_name: snmp-gateway
792 snmp_v3_auth_username: myuser
793 snmp_v3_auth_password: mypassword
794 snmp_v3_priv_password: mysecret
797 privacy_protocol: weewah
798 snmp_destination: 192.168.1.42:162
800 """, 'privacy_protocol unsupported. Must be one of DES, AES'),
803 service_type: snmp-gateway
804 service_name: snmp-gateway
809 snmp_v3_auth_username: myuser
810 snmp_v3_auth_password: mypassword
811 snmp_v3_priv_password: mysecret
814 privacy_protocol: AES
815 snmp_destination: 192.168.1.42:162
817 """, 'Must provide an engine_id for SNMP V3 notifications'),
820 service_type: snmp-gateway
821 service_name: snmp-gateway
826 snmp_community: public
828 snmp_destination: 192.168.1.42
830 """, re
.escape('SNMP destination (snmp_destination) type (IPv4) is invalid. Must be either: IPv4:Port, Name:Port')),
833 service_type: snmp-gateway
834 service_name: snmp-gateway
839 snmp_v3_auth_username: myuser
840 snmp_v3_auth_password: mypassword
841 snmp_v3_priv_password: mysecret
844 privacy_protocol: AES
846 snmp_destination: 192.168.1.42:162
848 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
851 service_type: snmp-gateway
852 service_name: snmp-gateway
857 snmp_v3_auth_username: myuser
858 snmp_v3_auth_password: mypassword
861 engine_id: 8000C53F0000000000
863 """, re
.escape('SNMP destination (snmp_destination) must be provided')),
866 service_type: snmp-gateway
867 service_name: snmp-gateway
872 snmp_v3_auth_username: myuser
873 snmp_v3_auth_password: mypassword
874 snmp_v3_priv_password: mysecret
877 privacy_protocol: AES
878 engine_id: 8000C53F0000000000
879 snmp_destination: my.imaginary.snmp-host
881 """, re
.escape('SNMP destination (snmp_destination) is invalid: DNS lookup failed')),
884 service_type: snmp-gateway
885 service_name: snmp-gateway
890 snmp_v3_auth_username: myuser
891 snmp_v3_auth_password: mypassword
892 snmp_v3_priv_password: mysecret
895 privacy_protocol: AES
896 engine_id: 8000C53F0000000000
897 snmp_destination: 10.79.32.10:fred
899 """, re
.escape('SNMP destination (snmp_destination) is invalid: Port must be numeric')),
902 service_type: snmp-gateway
903 service_name: snmp-gateway
908 snmp_v3_auth_username: myuser
909 snmp_v3_auth_password: mypassword
910 snmp_v3_priv_password: mysecret
913 privacy_protocol: AES
915 snmp_destination: 10.79.32.10:162
917 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
920 service_type: snmp-gateway
921 service_name: snmp-gateway
926 snmp_v3_auth_username: myuser
927 snmp_v3_auth_password: mypassword
928 snmp_v3_priv_password: mysecret
931 privacy_protocol: AES
932 engine_id: 8000C53DOH!
933 snmp_destination: 10.79.32.10:162
935 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
938 service_type: snmp-gateway
939 service_name: snmp-gateway
944 snmp_v3_auth_username: myuser
945 snmp_v3_auth_password: mypassword
946 snmp_v3_priv_password: mysecret
949 privacy_protocol: AES
950 engine_id: 8000C53FCA7344403DC611EC9B985254002537A6C53FCA7344403DC6112537A60
951 snmp_destination: 10.79.32.10:162
953 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
956 service_type: snmp-gateway
957 service_name: snmp-gateway
962 snmp_v3_auth_username: myuser
963 snmp_v3_auth_password: mypassword
964 snmp_v3_priv_password: mysecret
967 privacy_protocol: AES
968 engine_id: 8000C53F00000
969 snmp_destination: 10.79.32.10:162
971 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
973 def test_service_spec_validation_error(y
, error_match
):
974 data
= yaml
.safe_load(y
)
975 with pytest
.raises(SpecValidationError
) as err
:
976 specObj
= ServiceSpec
.from_json(data
)
977 assert err
.match(error_match
)
980 @pytest.mark
.parametrize("y, ec_args, ee_args, ec_final_args, ee_final_args", [
982 service_type: container
983 service_id: hello-world
984 service_name: container.hello-world
991 - destination=/lib/modules
996 entrypoint: /usr/bin/bash
1007 image: docker.io/library/hello-world:latest
1019 id="no_extra_args"),
1021 service_type: container
1022 service_id: hello-world
1023 service_name: container.hello-world
1027 extra_entrypoint_args:
1029 - "--enable-confetti"
1032 - source=lib/modules
1033 - destination=/lib/modules
1038 entrypoint: /usr/bin/bash
1049 image: docker.io/library/hello-world:latest
1058 ["--lasers=blue", "--enable-confetti"],
1060 ["--lasers=blue", "--enable-confetti"],
1061 id="only_extra_entrypoint_args_spec"),
1063 service_type: container
1064 service_id: hello-world
1065 service_name: container.hello-world
1071 - source=lib/modules
1072 - destination=/lib/modules
1077 entrypoint: /usr/bin/bash
1088 image: docker.io/library/hello-world:latest
1095 extra_entrypoint_args:
1097 - "--enable-confetti"
1100 ["--lasers blue", "--enable-confetti"],
1102 ["--lasers", "blue", "--enable-confetti"],
1103 id="only_extra_entrypoint_args_toplevel"),
1107 service_name: nfs.mynfs
1110 extra_entrypoint_args:
1112 - "--title=Custom NFS Options"
1113 extra_container_args:
1114 - "--cap-add=CAP_NET_BIND_SERVICE"
1115 - "--oom-score-adj=12"
1117 ["--cap-add=CAP_NET_BIND_SERVICE", "--oom-score-adj=12"],
1118 ["--lasers=blue", "--title=Custom NFS Options"],
1119 ["--cap-add=CAP_NET_BIND_SERVICE", "--oom-score-adj=12"],
1120 ["--lasers=blue", "--title=Custom", "NFS", "Options"],
1121 id="both_kinds_nfs"),
1123 service_type: container
1124 service_id: hello-world
1125 service_name: container.hello-world
1131 - source=lib/modules
1132 - destination=/lib/modules
1137 entrypoint: /usr/bin/bash
1148 image: docker.io/library/hello-world:latest
1155 extra_entrypoint_args:
1156 - argument: "--lasers=blue"
1158 - argument: "--enable-confetti"
1162 {"argument": "--lasers=blue", "split": True},
1163 {"argument": "--enable-confetti", "split": False},
1168 "--enable-confetti",
1170 id="only_extra_entrypoint_args_obj_toplevel"),
1172 service_type: container
1173 service_id: hello-world
1174 service_name: container.hello-world
1180 - source=lib/modules
1181 - destination=/lib/modules
1186 entrypoint: /usr/bin/bash
1197 image: docker.io/library/hello-world:latest
1204 extra_entrypoint_args:
1205 - argument: "--lasers=blue"
1207 - argument: "--enable-confetti"
1211 {"argument": "--lasers=blue", "split": True},
1212 {"argument": "--enable-confetti", "split": False},
1217 "--enable-confetti",
1219 id="only_extra_entrypoint_args_obj_indented"),
1223 service_name: nfs.mynfs
1226 extra_entrypoint_args:
1227 - argument: "--lasers=blue"
1228 - argument: "--title=Custom NFS Options"
1229 extra_container_args:
1230 - argument: "--cap-add=CAP_NET_BIND_SERVICE"
1231 - argument: "--oom-score-adj=12"
1234 {"argument": "--cap-add=CAP_NET_BIND_SERVICE", "split": False},
1235 {"argument": "--oom-score-adj=12", "split": False},
1238 {"argument": "--lasers=blue", "split": False},
1239 {"argument": "--title=Custom NFS Options", "split": False},
1242 "--cap-add=CAP_NET_BIND_SERVICE",
1243 "--oom-score-adj=12",
1247 "--title=Custom NFS Options",
1249 id="both_kinds_obj_nfs"),
1251 def test_extra_args_handling(y
, ec_args
, ee_args
, ec_final_args
, ee_final_args
):
1252 data
= yaml
.safe_load(y
)
1253 spec_obj
= ServiceSpec
.from_json(data
)
1255 assert ArgumentSpec
.map_json(spec_obj
.extra_container_args
) == ec_args
1256 assert ArgumentSpec
.map_json(spec_obj
.extra_entrypoint_args
) == ee_args
1257 if ec_final_args
is None:
1258 assert spec_obj
.extra_container_args
is None
1261 for args
in spec_obj
.extra_container_args
:
1262 ec_res
.extend(args
.to_args())
1263 assert ec_res
== ec_final_args
1264 if ee_final_args
is None:
1265 assert spec_obj
.extra_entrypoint_args
is None
1268 for args
in spec_obj
.extra_entrypoint_args
:
1269 ee_res
.extend(args
.to_args())
1270 assert ee_res
== ee_final_args