]>
git.proxmox.com Git - ceph.git/blob - ceph/src/python-common/ceph/tests/test_service_spec.py
d3fb4329668bb74d193a10669969f888eced5703
9 from ceph
.deployment
.service_spec
import HostPlacementSpec
, PlacementSpec
, \
10 ServiceSpec
, RGWSpec
, NFSServiceSpec
, IscsiServiceSpec
, AlertManagerSpec
, \
12 from ceph
.deployment
.drive_group
import DriveGroupSpec
13 from ceph
.deployment
.hostspec
import SpecValidationError
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),
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
33 ps
= PlacementSpec
.from_string(test_input
)
34 assert ps
.pretty_str() == test_input
35 assert ps
== PlacementSpec
.from_string(ps
.pretty_str())
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
,
44 assert ret
== HostPlacementSpec
.from_json(ret
.to_json())
49 @pytest.mark
.parametrize(
50 "test_input,expected",
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')"),
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}"'
76 @pytest.mark
.parametrize(
80 ("host=a label:wrong"),
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'),
90 def test_parse_placement_specs_raises(test_input
):
91 with pytest
.raises(SpecValidationError
):
92 PlacementSpec
.from_string(test_input
)
94 @pytest.mark
.parametrize("test_input",
96 [("myhost:1.1.1.1/24"),
100 def test_parse_host_placement_specs_raises_wrong_format(test_input
):
101 with pytest
.raises(ValueError):
102 HostPlacementSpec
.parse(test_input
)
105 @pytest.mark
.parametrize(
109 PlacementSpec(count
=3),
110 ['host1', 'host2', 'host3', 'host4', 'host5'],
114 PlacementSpec(host_pattern
='*'),
115 ['host1', 'host2', 'host3', 'host4', 'host5'],
119 PlacementSpec(count_per_host
=2, host_pattern
='*'),
120 ['host1', 'host2', 'host3', 'host4', 'host5'],
124 PlacementSpec(host_pattern
='foo*'),
125 ['foo1', 'foo2', 'bar1', 'bar2'],
129 PlacementSpec(count_per_host
=2, host_pattern
='foo*'),
130 ['foo1', 'foo2', 'bar1', 'bar2'],
134 def test_placement_target_size(p
, hosts
, size
):
135 assert p
.get_target_count(
136 [HostPlacementSpec(n
, '', '') for n
in hosts
]
140 def _get_dict_spec(s_type
, s_id
):
143 "service_type": s_type
,
145 dict(hosts
=["host1:1.1.1.1"])
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'] = {
159 elif s_type
== 'rgw':
160 dict_spec
['rgw_realm'] = 'realm'
161 dict_spec
['rgw_zone'] = 'zone'
166 @pytest.mark
.parametrize(
167 "s_type,o_spec,s_id",
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'),
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())
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"},
196 dg_spec
= ServiceSpec
.from_json(osd_spec
)
197 assert dg_spec
.unmanaged
== True
200 @pytest.mark
.parametrize("y",
201 """service_type: crash
213 service_id: default-rgw-realm.eu-central-1.1
214 service_name: rgw.default-rgw-realm.eu-central-1.1
222 rgw_frontend_type: civetweb
223 rgw_realm: default-rgw-realm
224 rgw_zone: eu-central-1
227 service_id: osd_spec_default
228 service_name: osd.osd_spec_default
237 objectstore: bluestore
241 service_type: alertmanager
242 service_name: alertmanager
246 default_webhook_urls:
249 service_type: grafana
250 service_name: grafana
254 service_type: grafana
255 service_name: grafana
257 initial_admin_password: secure
260 service_type: ingress
262 service_name: ingress.rgw.foo
269 backend_service: rgw.foo
272 virtual_ip: 192.168.20.1/24
276 service_name: nfs.mynfs
282 service_name: iscsi.iscsi
292 service_type: container
293 service_id: hello-world
294 service_name: container.hello-world
301 - destination=/lib/modules
306 entrypoint: /usr/bin/bash
317 image: docker.io/library/hello-world:latest
325 service_type: snmp-gateway
326 service_name: snmp-gateway
331 snmp_community: public
332 snmp_destination: 192.168.1.42:162
335 service_type: snmp-gateway
336 service_name: snmp-gateway
342 snmp_v3_auth_password: mypassword
343 snmp_v3_auth_username: myuser
344 engine_id: 8000C53F00000000
346 snmp_destination: 192.168.1.42:162
349 service_type: snmp-gateway
350 service_name: snmp-gateway
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
364 data
= yaml
.safe_load(y
)
365 object = ServiceSpec
.from_json(data
)
367 assert yaml
.dump(object) == y
368 assert yaml
.dump(ServiceSpec
.from_json(object.to_json())) == y
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]
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()
387 val
= """ServiceSpec.from_json(yaml.safe_load('''service_type: crash
393 assert obj
.service_type
== 'crash'
394 assert val
== repr(obj
)
396 @pytest.mark
.parametrize("spec1, spec2, eq",
417 # Add service_type='mgr'
449 RGWSpec(service_id
='foo'),
453 def test_spec_hash_eq(spec1
: ServiceSpec
,
457 assert (spec1
== spec2
) is eq
459 @pytest.mark
.parametrize(
460 "s_type,s_id,s_name",
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'),
470 def test_service_name(s_type
, s_id
, s_name
):
471 spec
= ServiceSpec
.from_json(_get_dict_spec(s_type
, s_id
))
473 assert spec
.service_name() == s_name
475 @pytest.mark
.parametrize(
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
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
))
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',
498 volume_mounts
={'foo': '/foo'},
504 'source=lib/modules',
505 'destination=/lib/modules',
512 'foo.conf': 'foo\nbar',
513 'bar.conf': ['foo', 'bar']
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
== [
525 'source=lib/modules',
526 'destination=/lib/modules',
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']
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
545 def test_ingress_spec():
546 yaml_str
= """service_type: ingress
554 virtual_ip: 192.168.20.1/24
555 backend_service: rgw.foo
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
568 @pytest.mark
.parametrize("y, error_match", [
573 count_per_host: "twelve"
574 """, "count-per-host must be a numeric value",),
580 """, "count-per-host must be an integer value",),
586 """, "count-per-host must be an integer value",),
592 """, "num/count must be a numeric value",),
598 """, "num/count must be an integer value",),
604 """, "num/count must be an integer value",),
610 """, "num/count must be >= 1",),
616 """, "count-per-host must be >= 1",),
618 service_type: snmp-gateway
619 service_name: snmp-gateway
624 snmp_v3_auth_password: mypassword
625 snmp_v3_auth_username: myuser
626 snmp_v3_priv_password: mysecret
628 engine_id: 8000c53f0000000000
629 privacy_protocol: WEIRD
630 snmp_destination: 192.168.122.1:162
631 auth_protocol: BIZARRE
633 """, "auth_protocol unsupported. Must be one of MD5, SHA"),
636 service_type: snmp-gateway
637 service_name: snmp-gateway
642 snmp_community: public
643 snmp_destination: 192.168.1.42:162
645 """, 'snmp_version unsupported. Must be one of V2c, V3'),
648 service_type: snmp-gateway
649 service_name: snmp-gateway
654 snmp_community: public
656 snmp_destination: 192.168.1.42:162
657 """, re
.escape('Missing SNMP version (snmp_version)')),
660 service_type: snmp-gateway
661 service_name: snmp-gateway
666 snmp_v3_auth_username: myuser
667 snmp_v3_auth_password: mypassword
670 snmp_destination: 192.168.1.42:162
672 """, 'auth_protocol unsupported. Must be one of MD5, SHA'),
675 service_type: snmp-gateway
676 service_name: snmp-gateway
681 snmp_v3_auth_username: myuser
682 snmp_v3_auth_password: mypassword
683 snmp_v3_priv_password: mysecret
686 privacy_protocol: weewah
687 snmp_destination: 192.168.1.42:162
689 """, 'privacy_protocol unsupported. Must be one of DES, AES'),
692 service_type: snmp-gateway
693 service_name: snmp-gateway
698 snmp_v3_auth_username: myuser
699 snmp_v3_auth_password: mypassword
700 snmp_v3_priv_password: mysecret
703 privacy_protocol: AES
704 snmp_destination: 192.168.1.42:162
706 """, 'Must provide an engine_id for SNMP V3 notifications'),
709 service_type: snmp-gateway
710 service_name: snmp-gateway
715 snmp_community: public
717 snmp_destination: 192.168.1.42
719 """, re
.escape('SNMP destination (snmp_destination) type (IPv4) is invalid. Must be either: IPv4:Port, Name:Port')),
722 service_type: snmp-gateway
723 service_name: snmp-gateway
728 snmp_v3_auth_username: myuser
729 snmp_v3_auth_password: mypassword
730 snmp_v3_priv_password: mysecret
733 privacy_protocol: AES
735 snmp_destination: 192.168.1.42:162
737 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
740 service_type: snmp-gateway
741 service_name: snmp-gateway
746 snmp_v3_auth_username: myuser
747 snmp_v3_auth_password: mypassword
750 engine_id: 8000C53F0000000000
752 """, re
.escape('SNMP destination (snmp_destination) must be provided')),
755 service_type: snmp-gateway
756 service_name: snmp-gateway
761 snmp_v3_auth_username: myuser
762 snmp_v3_auth_password: mypassword
763 snmp_v3_priv_password: mysecret
766 privacy_protocol: AES
767 engine_id: 8000C53F0000000000
768 snmp_destination: my.imaginary.snmp-host
770 """, re
.escape('SNMP destination (snmp_destination) is invalid: DNS lookup failed')),
773 service_type: snmp-gateway
774 service_name: snmp-gateway
779 snmp_v3_auth_username: myuser
780 snmp_v3_auth_password: mypassword
781 snmp_v3_priv_password: mysecret
784 privacy_protocol: AES
785 engine_id: 8000C53F0000000000
786 snmp_destination: 10.79.32.10:fred
788 """, re
.escape('SNMP destination (snmp_destination) is invalid: Port must be numeric')),
791 service_type: snmp-gateway
792 service_name: snmp-gateway
797 snmp_v3_auth_username: myuser
798 snmp_v3_auth_password: mypassword
799 snmp_v3_priv_password: mysecret
802 privacy_protocol: AES
804 snmp_destination: 10.79.32.10:162
806 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
809 service_type: snmp-gateway
810 service_name: snmp-gateway
815 snmp_v3_auth_username: myuser
816 snmp_v3_auth_password: mypassword
817 snmp_v3_priv_password: mysecret
820 privacy_protocol: AES
821 engine_id: 8000C53DOH!
822 snmp_destination: 10.79.32.10:162
824 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
827 service_type: snmp-gateway
828 service_name: snmp-gateway
833 snmp_v3_auth_username: myuser
834 snmp_v3_auth_password: mypassword
835 snmp_v3_priv_password: mysecret
838 privacy_protocol: AES
839 engine_id: 8000C53FCA7344403DC611EC9B985254002537A6C53FCA7344403DC6112537A60
840 snmp_destination: 10.79.32.10:162
842 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
845 service_type: snmp-gateway
846 service_name: snmp-gateway
851 snmp_v3_auth_username: myuser
852 snmp_v3_auth_password: mypassword
853 snmp_v3_priv_password: mysecret
856 privacy_protocol: AES
857 engine_id: 8000C53F00000
858 snmp_destination: 10.79.32.10:162
860 """, 'engine_id must be a string containing 10-64 hex characters. Its length must be divisible by 2'),
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
)