]> git.proxmox.com Git - ceph.git/blob - ceph/src/cephadm/tests/test_cephadm.py
import ceph 15.2.13
[ceph.git] / ceph / src / cephadm / tests / test_cephadm.py
1 # type: ignore
2 import mock
3 from mock import patch
4 import os
5 import sys
6 import unittest
7
8 import pytest
9
10 with patch('builtins.open', create=True):
11 from importlib.machinery import SourceFileLoader
12 cd = SourceFileLoader('cephadm', 'cephadm').load_module()
13
14 class TestCephAdm(object):
15
16 def test_docker_unit_file(self):
17 cd.args = mock.Mock()
18 cd.container_path = '/usr/bin/docker'
19 r = cd.get_unit_file('9b9d7609-f4d5-4aba-94c8-effa764d96c9')
20 assert 'Requires=docker.service' in r
21 cd.container_path = '/usr/sbin/podman'
22 r = cd.get_unit_file('9b9d7609-f4d5-4aba-94c8-effa764d96c9')
23 assert 'Requires=docker.service' not in r
24
25 def test_is_not_fsid(self):
26 assert not cd.is_fsid('no-uuid')
27
28 def test_is_fsid(self):
29 assert cd.is_fsid('e863154d-33c7-4350-bca5-921e0467e55b')
30
31 def test__get_parser_image(self):
32 args = cd._parse_args(['--image', 'foo', 'version'])
33 assert args.image == 'foo'
34
35 def test_CustomValidation(self):
36 assert cd._parse_args(['deploy', '--name', 'mon.a', '--fsid', 'fsid'])
37
38 with pytest.raises(SystemExit):
39 cd._parse_args(['deploy', '--name', 'wrong', '--fsid', 'fsid'])
40
41 @pytest.mark.parametrize("test_input, expected", [
42 ("podman version 1.6.2", (1,6,2)),
43 ("podman version 1.6.2-stable2", (1,6,2)),
44 ])
45 def test_parse_podman_version(self, test_input, expected):
46 assert cd._parse_podman_version(test_input) == expected
47
48 def test_parse_podman_version_invalid(self):
49 with pytest.raises(ValueError) as res:
50 cd._parse_podman_version('podman version inval.id')
51 assert 'inval' in str(res.value)
52
53 @pytest.mark.parametrize("test_input, expected", [
54 (
55 """
56 default via 192.168.178.1 dev enxd89ef3f34260 proto dhcp metric 100
57 10.0.0.0/8 via 10.4.0.1 dev tun0 proto static metric 50
58 10.3.0.0/21 via 10.4.0.1 dev tun0 proto static metric 50
59 10.4.0.1 dev tun0 proto kernel scope link src 10.4.0.2 metric 50
60 137.1.0.0/16 via 10.4.0.1 dev tun0 proto static metric 50
61 138.1.0.0/16 via 10.4.0.1 dev tun0 proto static metric 50
62 139.1.0.0/16 via 10.4.0.1 dev tun0 proto static metric 50
63 140.1.0.0/17 via 10.4.0.1 dev tun0 proto static metric 50
64 141.1.0.0/16 via 10.4.0.1 dev tun0 proto static metric 50
65 169.254.0.0/16 dev docker0 scope link metric 1000
66 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
67 192.168.39.0/24 dev virbr1 proto kernel scope link src 192.168.39.1 linkdown
68 192.168.122.0/24 dev virbr0 proto kernel scope link src 192.168.122.1 linkdown
69 192.168.178.0/24 dev enxd89ef3f34260 proto kernel scope link src 192.168.178.28 metric 100
70 192.168.178.1 dev enxd89ef3f34260 proto static scope link metric 100
71 195.135.221.12 via 192.168.178.1 dev enxd89ef3f34260 proto static metric 100
72 """,
73 {
74 '10.4.0.1': ['10.4.0.2'],
75 '172.17.0.0/16': ['172.17.0.1'],
76 '192.168.39.0/24': ['192.168.39.1'],
77 '192.168.122.0/24': ['192.168.122.1'],
78 '192.168.178.0/24': ['192.168.178.28']
79 }
80 ), (
81 """
82 default via 10.3.64.1 dev eno1 proto static metric 100
83 10.3.64.0/24 dev eno1 proto kernel scope link src 10.3.64.23 metric 100
84 10.3.64.0/24 dev eno1 proto kernel scope link src 10.3.64.27 metric 100
85 10.88.0.0/16 dev cni-podman0 proto kernel scope link src 10.88.0.1 linkdown
86 172.21.0.0/20 via 172.21.3.189 dev tun0
87 172.21.1.0/20 via 172.21.3.189 dev tun0
88 172.21.2.1 via 172.21.3.189 dev tun0
89 172.21.3.1 dev tun0 proto kernel scope link src 172.21.3.2
90 172.21.4.0/24 via 172.21.3.1 dev tun0
91 172.21.5.0/24 via 172.21.3.1 dev tun0
92 172.21.6.0/24 via 172.21.3.1 dev tun0
93 172.21.7.0/24 via 172.21.3.1 dev tun0
94 192.168.122.0/24 dev virbr0 proto kernel scope link src 192.168.122.1 linkdown
95 """,
96 {
97 '10.3.64.0/24': ['10.3.64.23', '10.3.64.27'],
98 '10.88.0.0/16': ['10.88.0.1'],
99 '172.21.3.1': ['172.21.3.2'],
100 '192.168.122.0/24': ['192.168.122.1']}
101 ),
102 ])
103 def test_parse_ipv4_route(self, test_input, expected):
104 assert cd._parse_ipv4_route(test_input) == expected
105
106 @pytest.mark.parametrize("test_routes, test_ips, expected", [
107 (
108 """
109 ::1 dev lo proto kernel metric 256 pref medium
110 fdbc:7574:21fe:9200::/64 dev wlp2s0 proto ra metric 600 pref medium
111 fdd8:591e:4969:6363::/64 dev wlp2s0 proto ra metric 600 pref medium
112 fde4:8dba:82e1::/64 dev eth1 proto kernel metric 256 expires 1844sec pref medium
113 fe80::/64 dev tun0 proto kernel metric 256 pref medium
114 fe80::/64 dev wlp2s0 proto kernel metric 600 pref medium
115 default dev tun0 proto static metric 50 pref medium
116 default via fe80::2480:28ec:5097:3fe2 dev wlp2s0 proto ra metric 20600 pref medium
117 """,
118 """
119 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
120 inet6 ::1/128 scope host
121 valid_lft forever preferred_lft forever
122 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
123 inet6 fdd8:591e:4969:6363:4c52:cafe:8dd4:dc4/64 scope global temporary dynamic
124 valid_lft 86394sec preferred_lft 14394sec
125 inet6 fdbc:7574:21fe:9200:4c52:cafe:8dd4:dc4/64 scope global temporary dynamic
126 valid_lft 6745sec preferred_lft 3145sec
127 inet6 fdd8:591e:4969:6363:103a:abcd:af1f:57f3/64 scope global temporary deprecated dynamic
128 valid_lft 86394sec preferred_lft 0sec
129 inet6 fdbc:7574:21fe:9200:103a:abcd:af1f:57f3/64 scope global temporary deprecated dynamic
130 valid_lft 6745sec preferred_lft 0sec
131 inet6 fdd8:591e:4969:6363:a128:1234:2bdd:1b6f/64 scope global temporary deprecated dynamic
132 valid_lft 86394sec preferred_lft 0sec
133 inet6 fdbc:7574:21fe:9200:a128:1234:2bdd:1b6f/64 scope global temporary deprecated dynamic
134 valid_lft 6745sec preferred_lft 0sec
135 inet6 fdd8:591e:4969:6363:d581:4321:380b:3905/64 scope global temporary deprecated dynamic
136 valid_lft 86394sec preferred_lft 0sec
137 inet6 fdbc:7574:21fe:9200:d581:4321:380b:3905/64 scope global temporary deprecated dynamic
138 valid_lft 6745sec preferred_lft 0sec
139 inet6 fe80::1111:2222:3333:4444/64 scope link noprefixroute
140 valid_lft forever preferred_lft forever
141 inet6 fde4:8dba:82e1:0:ec4a:e402:e9df:b357/64 scope global temporary dynamic
142 valid_lft 1074sec preferred_lft 1074sec
143 inet6 fde4:8dba:82e1:0:5054:ff:fe72:61af/64 scope global dynamic mngtmpaddr
144 valid_lft 1074sec preferred_lft 1074sec
145 12: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 state UNKNOWN qlen 100
146 inet6 fe80::cafe:cafe:cafe:cafe/64 scope link stable-privacy
147 valid_lft forever preferred_lft forever
148 """,
149 {
150 "::1": ["::1"],
151 "fdbc:7574:21fe:9200::/64": ["fdbc:7574:21fe:9200:4c52:cafe:8dd4:dc4",
152 "fdbc:7574:21fe:9200:103a:abcd:af1f:57f3",
153 "fdbc:7574:21fe:9200:a128:1234:2bdd:1b6f",
154 "fdbc:7574:21fe:9200:d581:4321:380b:3905"],
155 "fdd8:591e:4969:6363::/64": ["fdd8:591e:4969:6363:4c52:cafe:8dd4:dc4",
156 "fdd8:591e:4969:6363:103a:abcd:af1f:57f3",
157 "fdd8:591e:4969:6363:a128:1234:2bdd:1b6f",
158 "fdd8:591e:4969:6363:d581:4321:380b:3905"],
159 "fde4:8dba:82e1::/64": ["fde4:8dba:82e1:0:ec4a:e402:e9df:b357",
160 "fde4:8dba:82e1:0:5054:ff:fe72:61af"],
161 "fe80::/64": ["fe80::1111:2222:3333:4444",
162 "fe80::cafe:cafe:cafe:cafe"]
163 }
164 )])
165 def test_parse_ipv6_route(self, test_routes, test_ips, expected):
166 assert cd._parse_ipv6_route(test_routes, test_ips) == expected
167
168 def test_is_ipv6(self):
169 cd.logger = mock.Mock()
170 for good in ("[::1]", "::1",
171 "fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"):
172 assert cd.is_ipv6(good)
173 for bad in ("127.0.0.1",
174 "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffg",
175 "1:2:3:4:5:6:7:8:9", "fd00::1::1", "[fg::1]"):
176 assert not cd.is_ipv6(bad)
177
178 def test_unwrap_ipv6(self):
179 def unwrap_test(address, expected):
180 assert cd.unwrap_ipv6(address) == expected
181
182 tests = [
183 ('::1', '::1'), ('[::1]', '::1'),
184 ('[fde4:8dba:82e1:0:5054:ff:fe6a:357]', 'fde4:8dba:82e1:0:5054:ff:fe6a:357'),
185 ('can actually be any string', 'can actually be any string'),
186 ('[but needs to be stripped] ', '[but needs to be stripped] ')]
187 for address, expected in tests:
188 unwrap_test(address, expected)
189
190 def test_wrap_ipv6(self):
191 def wrap_test(address, expected):
192 assert cd.wrap_ipv6(address) == expected
193
194 tests = [
195 ('::1', '[::1]'), ('[::1]', '[::1]'),
196 ('fde4:8dba:82e1:0:5054:ff:fe6a:357',
197 '[fde4:8dba:82e1:0:5054:ff:fe6a:357]'),
198 ('myhost.example.com', 'myhost.example.com'),
199 ('192.168.0.1', '192.168.0.1'),
200 ('', ''), ('fd00::1::1', 'fd00::1::1')]
201 for address, expected in tests:
202 wrap_test(address, expected)
203
204 @mock.patch('cephadm.call_throws')
205 @mock.patch('cephadm.get_parm')
206 def test_registry_login(self, get_parm, call_throws):
207
208 # test normal valid login with url, username and password specified
209 call_throws.return_value = '', '', 0
210 args = cd._parse_args(['registry-login', '--registry-url', 'sample-url', '--registry-username', 'sample-user', '--registry-password', 'sample-pass'])
211 cd.args = args
212 retval = cd.command_registry_login()
213 assert retval == 0
214
215 # test bad login attempt with invalid arguments given
216 args = cd._parse_args(['registry-login', '--registry-url', 'bad-args-url'])
217 cd.args = args
218 with pytest.raises(Exception) as e:
219 assert cd.command_registry_login()
220 assert str(e.value) == ('Invalid custom registry arguments received. To login to a custom registry include '
221 '--registry-url, --registry-username and --registry-password options or --registry-json option')
222
223 # test normal valid login with json file
224 get_parm.return_value = {"url": "sample-url", "username": "sample-username", "password": "sample-password"}
225 args = cd._parse_args(['registry-login', '--registry-json', 'sample-json'])
226 cd.args = args
227 retval = cd.command_registry_login()
228 assert retval == 0
229
230 # test bad login attempt with bad json file
231 get_parm.return_value = {"bad-json": "bad-json"}
232 args = cd._parse_args(['registry-login', '--registry-json', 'sample-json'])
233 cd.args = args
234 with pytest.raises(Exception) as e:
235 assert cd.command_registry_login()
236 assert str(e.value) == ("json provided for custom registry login did not include all necessary fields. "
237 "Please setup json file as\n"
238 "{\n"
239 " \"url\": \"REGISTRY_URL\",\n"
240 " \"username\": \"REGISTRY_USERNAME\",\n"
241 " \"password\": \"REGISTRY_PASSWORD\"\n"
242 "}\n")
243
244 # test login attempt with valid arguments where login command fails
245 call_throws.side_effect = Exception
246 args = cd._parse_args(['registry-login', '--registry-url', 'sample-url', '--registry-username', 'sample-user', '--registry-password', 'sample-pass'])
247 cd.args = args
248 with pytest.raises(Exception) as e:
249 cd.command_registry_login()
250 assert str(e.value) == "Failed to login to custom registry @ sample-url as sample-user with given password"
251
252 def test_get_image_info_from_inspect(self):
253 # podman
254 out = """204a01f9b0b6710dd0c0af7f37ce7139c47ff0f0105d778d7104c69282dfbbf1,[docker.io/ceph/ceph@sha256:1cc9b824e1b076cdff52a9aa3f0cc8557d879fb2fbbba0cafed970aca59a3992]"""
255 r = cd.get_image_info_from_inspect(out, 'registry/ceph/ceph:latest')
256 assert r == {
257 'image_id': '204a01f9b0b6710dd0c0af7f37ce7139c47ff0f0105d778d7104c69282dfbbf1',
258 'repo_digest': 'docker.io/ceph/ceph@sha256:1cc9b824e1b076cdff52a9aa3f0cc8557d879fb2fbbba0cafed970aca59a3992'
259 }
260
261 # docker
262 out = """sha256:16f4549cf7a8f112bbebf7946749e961fbbd1b0838627fe619aab16bc17ce552,[quay.ceph.io/ceph-ci/ceph@sha256:4e13da36c1bd6780b312a985410ae678984c37e6a9493a74c87e4a50b9bda41f]"""
263 r = cd.get_image_info_from_inspect(out, 'registry/ceph/ceph:latest')
264 assert r == {
265 'image_id': '16f4549cf7a8f112bbebf7946749e961fbbd1b0838627fe619aab16bc17ce552',
266 'repo_digest': 'quay.ceph.io/ceph-ci/ceph@sha256:4e13da36c1bd6780b312a985410ae678984c37e6a9493a74c87e4a50b9bda41f'
267 }
268
269 def test_dict_get(self):
270 result = cd.dict_get({'a': 1}, 'a', require=True)
271 assert result == 1
272 result = cd.dict_get({'a': 1}, 'b')
273 assert result is None
274 result = cd.dict_get({'a': 1}, 'b', default=2)
275 assert result == 2
276
277 def test_dict_get_error(self):
278 with pytest.raises(cd.Error):
279 cd.dict_get({'a': 1}, 'b', require=True)
280
281 def test_dict_get_join(self):
282 result = cd.dict_get_join({'foo': ['a', 'b']}, 'foo')
283 assert result == 'a\nb'
284 result = cd.dict_get_join({'foo': [1, 2]}, 'foo')
285 assert result == '1\n2'
286 result = cd.dict_get_join({'bar': 'a'}, 'bar')
287 assert result == 'a'
288 result = cd.dict_get_join({'a': 1}, 'a')
289 assert result == 1
290
291 def test_last_local_images(self):
292 out = '''
293 docker.io/ceph/daemon-base@
294 docker.io/ceph/ceph:v15.2.5
295 docker.io/ceph/daemon-base:octopus
296 '''
297 image = cd._filter_last_local_ceph_image(out)
298 assert image == 'docker.io/ceph/ceph:v15.2.5'
299
300
301 class TestCustomContainer(unittest.TestCase):
302 cc: cd.CustomContainer
303
304 def setUp(self):
305 self.cc = cd.CustomContainer(
306 'e863154d-33c7-4350-bca5-921e0467e55b',
307 'container',
308 config_json={
309 'entrypoint': 'bash',
310 'gid': 1000,
311 'args': [
312 '--no-healthcheck',
313 '-p 6800:6800'
314 ],
315 'envs': ['SECRET=password'],
316 'ports': [8080, 8443],
317 'volume_mounts': {
318 '/CONFIG_DIR': '/foo/conf',
319 'bar/config': '/bar:ro'
320 },
321 'bind_mounts': [
322 [
323 'type=bind',
324 'source=/CONFIG_DIR',
325 'destination=/foo/conf',
326 ''
327 ],
328 [
329 'type=bind',
330 'source=bar/config',
331 'destination=/bar:ro',
332 'ro=true'
333 ]
334 ]
335 },
336 image='docker.io/library/hello-world:latest'
337 )
338
339 def test_entrypoint(self):
340 self.assertEqual(self.cc.entrypoint, 'bash')
341
342 def test_uid_gid(self):
343 self.assertEqual(self.cc.uid, 65534)
344 self.assertEqual(self.cc.gid, 1000)
345
346 def test_ports(self):
347 self.assertEqual(self.cc.ports, [8080, 8443])
348
349 def test_get_container_args(self):
350 result = self.cc.get_container_args()
351 self.assertEqual(result, [
352 '--no-healthcheck',
353 '-p 6800:6800'
354 ])
355
356 def test_get_container_envs(self):
357 result = self.cc.get_container_envs()
358 self.assertEqual(result, ['SECRET=password'])
359
360 def test_get_container_mounts(self):
361 result = self.cc.get_container_mounts('/xyz')
362 self.assertDictEqual(result, {
363 '/CONFIG_DIR': '/foo/conf',
364 '/xyz/bar/config': '/bar:ro'
365 })
366
367 def test_get_container_binds(self):
368 result = self.cc.get_container_binds('/xyz')
369 self.assertEqual(result, [
370 [
371 'type=bind',
372 'source=/CONFIG_DIR',
373 'destination=/foo/conf',
374 ''
375 ],
376 [
377 'type=bind',
378 'source=/xyz/bar/config',
379 'destination=/bar:ro',
380 'ro=true'
381 ]
382 ])
383
384
385 class TestMonitoring(object):
386 @mock.patch('cephadm.call')
387 def test_get_version_alertmanager(self, _call):
388 ctx = mock.Mock()
389 daemon_type = 'alertmanager'
390
391 # binary `prometheus`
392 _call.return_value = '', '{}, version 0.16.1'.format(daemon_type), 0
393 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
394 assert version == '0.16.1'
395
396 # binary `prometheus-alertmanager`
397 _call.side_effect = (
398 ('', '', 1),
399 ('', '{}, version 0.16.1'.format(daemon_type), 0),
400 )
401 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
402 assert version == '0.16.1'
403
404 @mock.patch('cephadm.call')
405 def test_get_version_prometheus(self, _call):
406 ctx = mock.Mock()
407 daemon_type = 'prometheus'
408 _call.return_value = '', '{}, version 0.16.1'.format(daemon_type), 0
409 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
410 assert version == '0.16.1'
411
412 @mock.patch('cephadm.call')
413 def test_get_version_node_exporter(self, _call):
414 ctx = mock.Mock()
415 daemon_type = 'node-exporter'
416 _call.return_value = '', '{}, version 0.16.1'.format(daemon_type.replace('-', '_')), 0
417 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
418 assert version == '0.16.1'