]> git.proxmox.com Git - ceph.git/blob - ceph/src/cephadm/tests/test_cephadm.py
update source to Ceph Pacific 16.2.2
[ceph.git] / ceph / src / cephadm / tests / test_cephadm.py
1 # type: ignore
2 from typing import List, Optional
3 import mock
4 from mock import patch, call
5 import os
6 import sys
7 import unittest
8 import threading
9 import time
10 import errno
11 import socket
12 from http.server import HTTPServer
13 from urllib.request import Request, urlopen
14 from urllib.error import HTTPError
15
16 import pytest
17
18 from .fixtures import exporter
19
20 with patch('builtins.open', create=True):
21 from importlib.machinery import SourceFileLoader
22 cd = SourceFileLoader('cephadm', 'cephadm').load_module()
23
24 class TestCephAdm(object):
25
26 @staticmethod
27 def mock_docker():
28 docker = mock.Mock(cd.Docker)
29 docker.path = '/usr/bin/docker'
30 return docker
31
32 @staticmethod
33 def mock_podman():
34 podman = mock.Mock(cd.Podman)
35 podman.path = '/usr/bin/podman'
36 podman.version = (2, 1, 0)
37 return podman
38
39 def test_docker_unit_file(self):
40 ctx = mock.Mock()
41 ctx.container_engine = self.mock_docker()
42 r = cd.get_unit_file(ctx, '9b9d7609-f4d5-4aba-94c8-effa764d96c9')
43 assert 'Requires=docker.service' in r
44 ctx.container_engine = self.mock_podman()
45 r = cd.get_unit_file(ctx, '9b9d7609-f4d5-4aba-94c8-effa764d96c9')
46 assert 'Requires=docker.service' not in r
47
48 @mock.patch('cephadm.logger')
49 def test_attempt_bind(self, logger):
50 ctx = None
51 address = None
52 port = 0
53
54 def os_error(errno):
55 _os_error = OSError()
56 _os_error.errno = errno
57 return _os_error
58
59 for side_effect, expected_exception in (
60 (os_error(errno.EADDRINUSE), cd.PortOccupiedError),
61 (os_error(errno.EAFNOSUPPORT), OSError),
62 (os_error(errno.EADDRNOTAVAIL), OSError),
63 (None, None),
64 ):
65 _socket = mock.Mock()
66 _socket.bind.side_effect = side_effect
67 try:
68 cd.attempt_bind(ctx, _socket, address, port)
69 except Exception as e:
70 assert isinstance(e, expected_exception)
71 else:
72 if expected_exception is not None:
73 assert False
74
75 @mock.patch('cephadm.attempt_bind')
76 @mock.patch('cephadm.logger')
77 def test_port_in_use(self, logger, attempt_bind):
78 empty_ctx = None
79
80 assert cd.port_in_use(empty_ctx, 9100) == False
81
82 attempt_bind.side_effect = cd.PortOccupiedError('msg')
83 assert cd.port_in_use(empty_ctx, 9100) == True
84
85 os_error = OSError()
86 os_error.errno = errno.EADDRNOTAVAIL
87 attempt_bind.side_effect = os_error
88 assert cd.port_in_use(empty_ctx, 9100) == False
89
90 os_error = OSError()
91 os_error.errno = errno.EAFNOSUPPORT
92 attempt_bind.side_effect = os_error
93 assert cd.port_in_use(empty_ctx, 9100) == False
94
95 @mock.patch('socket.socket')
96 @mock.patch('cephadm.logger')
97 def test_check_ip_port_success(self, logger, _socket):
98 ctx = mock.Mock()
99 ctx.skip_ping_check = False # enables executing port check with `check_ip_port`
100
101 for address, address_family in (
102 ('0.0.0.0', socket.AF_INET),
103 ('::', socket.AF_INET6),
104 ):
105 try:
106 cd.check_ip_port(ctx, address, 9100)
107 except:
108 assert False
109 else:
110 assert _socket.call_args == call(address_family, socket.SOCK_STREAM)
111
112 @mock.patch('socket.socket')
113 @mock.patch('cephadm.logger')
114 def test_check_ip_port_failure(self, logger, _socket):
115 ctx = mock.Mock()
116 ctx.skip_ping_check = False # enables executing port check with `check_ip_port`
117
118 def os_error(errno):
119 _os_error = OSError()
120 _os_error.errno = errno
121 return _os_error
122
123 for address, address_family in (
124 ('0.0.0.0', socket.AF_INET),
125 ('::', socket.AF_INET6),
126 ):
127 for side_effect, expected_exception in (
128 (os_error(errno.EADDRINUSE), cd.PortOccupiedError),
129 (os_error(errno.EADDRNOTAVAIL), OSError),
130 (os_error(errno.EAFNOSUPPORT), OSError),
131 (None, None),
132 ):
133 mock_socket_obj = mock.Mock()
134 mock_socket_obj.bind.side_effect = side_effect
135 _socket.return_value = mock_socket_obj
136 try:
137 cd.check_ip_port(ctx, address, 9100)
138 except Exception as e:
139 assert isinstance(e, expected_exception)
140 else:
141 if side_effect is not None:
142 assert False
143
144
145 def test_is_not_fsid(self):
146 assert not cd.is_fsid('no-uuid')
147
148 def test_is_fsid(self):
149 assert cd.is_fsid('e863154d-33c7-4350-bca5-921e0467e55b')
150
151 def test__get_parser_image(self):
152 args = cd._parse_args(['--image', 'foo', 'version'])
153 assert args.image == 'foo'
154
155 def test_CustomValidation(self):
156 assert cd._parse_args(['deploy', '--name', 'mon.a', '--fsid', 'fsid'])
157
158 with pytest.raises(SystemExit):
159 cd._parse_args(['deploy', '--name', 'wrong', '--fsid', 'fsid'])
160
161 @pytest.mark.parametrize("test_input, expected", [
162 ("1.6.2", (1,6,2)),
163 ("1.6.2-stable2", (1,6,2)),
164 ])
165 def test_parse_podman_version(self, test_input, expected):
166 assert cd._parse_podman_version(test_input) == expected
167
168 def test_parse_podman_version_invalid(self):
169 with pytest.raises(ValueError) as res:
170 cd._parse_podman_version('inval.id')
171 assert 'inval' in str(res.value)
172
173 @pytest.mark.parametrize("test_input, expected", [
174 (
175 """
176 default via 192.168.178.1 dev enxd89ef3f34260 proto dhcp metric 100
177 10.0.0.0/8 via 10.4.0.1 dev tun0 proto static metric 50
178 10.3.0.0/21 via 10.4.0.1 dev tun0 proto static metric 50
179 10.4.0.1 dev tun0 proto kernel scope link src 10.4.0.2 metric 50
180 137.1.0.0/16 via 10.4.0.1 dev tun0 proto static metric 50
181 138.1.0.0/16 via 10.4.0.1 dev tun0 proto static metric 50
182 139.1.0.0/16 via 10.4.0.1 dev tun0 proto static metric 50
183 140.1.0.0/17 via 10.4.0.1 dev tun0 proto static metric 50
184 141.1.0.0/16 via 10.4.0.1 dev tun0 proto static metric 50
185 169.254.0.0/16 dev docker0 scope link metric 1000
186 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
187 192.168.39.0/24 dev virbr1 proto kernel scope link src 192.168.39.1 linkdown
188 192.168.122.0/24 dev virbr0 proto kernel scope link src 192.168.122.1 linkdown
189 192.168.178.0/24 dev enxd89ef3f34260 proto kernel scope link src 192.168.178.28 metric 100
190 192.168.178.1 dev enxd89ef3f34260 proto static scope link metric 100
191 195.135.221.12 via 192.168.178.1 dev enxd89ef3f34260 proto static metric 100
192 """,
193 {
194 '10.4.0.1': {'tun0': ['10.4.0.2']},
195 '172.17.0.0/16': {'docker0': ['172.17.0.1']},
196 '192.168.39.0/24': {'virbr1': ['192.168.39.1']},
197 '192.168.122.0/24': {'virbr0': ['192.168.122.1']},
198 '192.168.178.0/24': {'enxd89ef3f34260': ['192.168.178.28']}
199 }
200 ), (
201 """
202 default via 10.3.64.1 dev eno1 proto static metric 100
203 10.3.64.0/24 dev eno1 proto kernel scope link src 10.3.64.23 metric 100
204 10.3.64.0/24 dev eno1 proto kernel scope link src 10.3.64.27 metric 100
205 10.88.0.0/16 dev cni-podman0 proto kernel scope link src 10.88.0.1 linkdown
206 172.21.0.0/20 via 172.21.3.189 dev tun0
207 172.21.1.0/20 via 172.21.3.189 dev tun0
208 172.21.2.1 via 172.21.3.189 dev tun0
209 172.21.3.1 dev tun0 proto kernel scope link src 172.21.3.2
210 172.21.4.0/24 via 172.21.3.1 dev tun0
211 172.21.5.0/24 via 172.21.3.1 dev tun0
212 172.21.6.0/24 via 172.21.3.1 dev tun0
213 172.21.7.0/24 via 172.21.3.1 dev tun0
214 192.168.122.0/24 dev virbr0 proto kernel scope link src 192.168.122.1 linkdown
215 """,
216 {
217 '10.3.64.0/24': {'eno1': ['10.3.64.23', '10.3.64.27']},
218 '10.88.0.0/16': {'cni-podman0': ['10.88.0.1']},
219 '172.21.3.1': {'tun0': ['172.21.3.2']},
220 '192.168.122.0/24': {'virbr0': ['192.168.122.1']}}
221 ),
222 ])
223 def test_parse_ipv4_route(self, test_input, expected):
224 assert cd._parse_ipv4_route(test_input) == expected
225
226 @pytest.mark.parametrize("test_routes, test_ips, expected", [
227 (
228 """
229 ::1 dev lo proto kernel metric 256 pref medium
230 fe80::/64 dev eno1 proto kernel metric 100 pref medium
231 fe80::/64 dev br-3d443496454c proto kernel metric 256 linkdown pref medium
232 fe80::/64 dev tun0 proto kernel metric 256 pref medium
233 fe80::/64 dev br-4355f5dbb528 proto kernel metric 256 pref medium
234 fe80::/64 dev docker0 proto kernel metric 256 linkdown pref medium
235 fe80::/64 dev cni-podman0 proto kernel metric 256 linkdown pref medium
236 fe80::/64 dev veth88ba1e8 proto kernel metric 256 pref medium
237 fe80::/64 dev vethb6e5fc7 proto kernel metric 256 pref medium
238 fe80::/64 dev vethaddb245 proto kernel metric 256 pref medium
239 fe80::/64 dev vethbd14d6b proto kernel metric 256 pref medium
240 fe80::/64 dev veth13e8fd2 proto kernel metric 256 pref medium
241 fe80::/64 dev veth1d3aa9e proto kernel metric 256 pref medium
242 fe80::/64 dev vethe485ca9 proto kernel metric 256 pref medium
243 """,
244 """
245 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
246 inet6 ::1/128 scope host
247 valid_lft forever preferred_lft forever
248 2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
249 inet6 fe80::225:90ff:fee5:26e8/64 scope link noprefixroute
250 valid_lft forever preferred_lft forever
251 6: br-3d443496454c: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 state DOWN
252 inet6 fe80::42:23ff:fe9d:ee4/64 scope link
253 valid_lft forever preferred_lft forever
254 7: br-4355f5dbb528: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
255 inet6 fe80::42:6eff:fe35:41fe/64 scope link
256 valid_lft forever preferred_lft forever
257 8: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 state DOWN
258 inet6 fe80::42:faff:fee6:40a0/64 scope link
259 valid_lft forever preferred_lft forever
260 11: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 state UNKNOWN qlen 100
261 inet6 fe80::98a6:733e:dafd:350/64 scope link stable-privacy
262 valid_lft forever preferred_lft forever
263 28: cni-podman0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 state DOWN qlen 1000
264 inet6 fe80::3449:cbff:fe89:b87e/64 scope link
265 valid_lft forever preferred_lft forever
266 31: vethaddb245@if30: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
267 inet6 fe80::90f7:3eff:feed:a6bb/64 scope link
268 valid_lft forever preferred_lft forever
269 33: veth88ba1e8@if32: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
270 inet6 fe80::d:f5ff:fe73:8c82/64 scope link
271 valid_lft forever preferred_lft forever
272 35: vethbd14d6b@if34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
273 inet6 fe80::b44f:8ff:fe6f:813d/64 scope link
274 valid_lft forever preferred_lft forever
275 37: vethb6e5fc7@if36: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
276 inet6 fe80::4869:c6ff:feaa:8afe/64 scope link
277 valid_lft forever preferred_lft forever
278 39: veth13e8fd2@if38: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
279 inet6 fe80::78f4:71ff:fefe:eb40/64 scope link
280 valid_lft forever preferred_lft forever
281 41: veth1d3aa9e@if40: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
282 inet6 fe80::24bd:88ff:fe28:5b18/64 scope link
283 valid_lft forever preferred_lft forever
284 43: vethe485ca9@if42: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
285 inet6 fe80::6425:87ff:fe42:b9f0/64 scope link
286 valid_lft forever preferred_lft forever
287 """,
288 {
289 "fe80::/64": {
290 "eno1": [
291 "fe80::225:90ff:fee5:26e8"
292 ],
293 "br-3d443496454c": [
294 "fe80::42:23ff:fe9d:ee4"
295 ],
296 "tun0": [
297 "fe80::98a6:733e:dafd:350"
298 ],
299 "br-4355f5dbb528": [
300 "fe80::42:6eff:fe35:41fe"
301 ],
302 "docker0": [
303 "fe80::42:faff:fee6:40a0"
304 ],
305 "cni-podman0": [
306 "fe80::3449:cbff:fe89:b87e"
307 ],
308 "veth88ba1e8": [
309 "fe80::d:f5ff:fe73:8c82"
310 ],
311 "vethb6e5fc7": [
312 "fe80::4869:c6ff:feaa:8afe"
313 ],
314 "vethaddb245": [
315 "fe80::90f7:3eff:feed:a6bb"
316 ],
317 "vethbd14d6b": [
318 "fe80::b44f:8ff:fe6f:813d"
319 ],
320 "veth13e8fd2": [
321 "fe80::78f4:71ff:fefe:eb40"
322 ],
323 "veth1d3aa9e": [
324 "fe80::24bd:88ff:fe28:5b18"
325 ],
326 "vethe485ca9": [
327 "fe80::6425:87ff:fe42:b9f0"
328 ]
329 }
330 }
331 ),
332 (
333 """
334 ::1 dev lo proto kernel metric 256 pref medium
335 2001:1458:301:eb::100:1a dev ens20f0 proto kernel metric 100 pref medium
336 2001:1458:301:eb::/64 dev ens20f0 proto ra metric 100 pref medium
337 fd01:1458:304:5e::/64 dev ens20f0 proto ra metric 100 pref medium
338 fe80::/64 dev ens20f0 proto kernel metric 100 pref medium
339 default proto ra metric 100
340 nexthop via fe80::46ec:ce00:b8a0:d3c8 dev ens20f0 weight 1
341 nexthop via fe80::46ec:ce00:b8a2:33c8 dev ens20f0 weight 1 pref medium
342 """,
343 """
344 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
345 inet6 ::1/128 scope host
346 valid_lft forever preferred_lft forever
347 2: ens20f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
348 inet6 2001:1458:301:eb::100:1a/128 scope global dynamic noprefixroute
349 valid_lft 590879sec preferred_lft 590879sec
350 inet6 fe80::2e60:cff:fef8:da41/64 scope link noprefixroute
351 valid_lft forever preferred_lft forever
352 """,
353 {
354 '2001:1458:301:eb::/64': {
355 'ens20f0': [
356 '2001:1458:301:eb::100:1a'
357 ],
358 },
359 'fe80::/64': {
360 'ens20f0': ['fe80::2e60:cff:fef8:da41'],
361 },
362 'fd01:1458:304:5e::/64': {
363 'ens20f0': []
364 },
365 }
366 ),
367 ])
368 def test_parse_ipv6_route(self, test_routes, test_ips, expected):
369 assert cd._parse_ipv6_route(test_routes, test_ips) == expected
370
371 def test_is_ipv6(self):
372 cd.logger = mock.Mock()
373 for good in ("[::1]", "::1",
374 "fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"):
375 assert cd.is_ipv6(good)
376 for bad in ("127.0.0.1",
377 "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffg",
378 "1:2:3:4:5:6:7:8:9", "fd00::1::1", "[fg::1]"):
379 assert not cd.is_ipv6(bad)
380
381 def test_unwrap_ipv6(self):
382 def unwrap_test(address, expected):
383 assert cd.unwrap_ipv6(address) == expected
384
385 tests = [
386 ('::1', '::1'), ('[::1]', '::1'),
387 ('[fde4:8dba:82e1:0:5054:ff:fe6a:357]', 'fde4:8dba:82e1:0:5054:ff:fe6a:357'),
388 ('can actually be any string', 'can actually be any string'),
389 ('[but needs to be stripped] ', '[but needs to be stripped] ')]
390 for address, expected in tests:
391 unwrap_test(address, expected)
392
393 def test_wrap_ipv6(self):
394 def wrap_test(address, expected):
395 assert cd.wrap_ipv6(address) == expected
396
397 tests = [
398 ('::1', '[::1]'), ('[::1]', '[::1]'),
399 ('fde4:8dba:82e1:0:5054:ff:fe6a:357',
400 '[fde4:8dba:82e1:0:5054:ff:fe6a:357]'),
401 ('myhost.example.com', 'myhost.example.com'),
402 ('192.168.0.1', '192.168.0.1'),
403 ('', ''), ('fd00::1::1', 'fd00::1::1')]
404 for address, expected in tests:
405 wrap_test(address, expected)
406
407 @mock.patch('cephadm.call_throws')
408 @mock.patch('cephadm.get_parm')
409 def test_registry_login(self, get_parm, call_throws):
410
411 # test normal valid login with url, username and password specified
412 call_throws.return_value = '', '', 0
413 ctx: Optional[cd.CephadmContext] = cd.cephadm_init_ctx(
414 ['registry-login', '--registry-url', 'sample-url',
415 '--registry-username', 'sample-user', '--registry-password',
416 'sample-pass'])
417 ctx.container_engine = self.mock_docker()
418 assert ctx
419 retval = cd.command_registry_login(ctx)
420 assert retval == 0
421
422 # test bad login attempt with invalid arguments given
423 ctx: Optional[cd.CephadmContext] = cd.cephadm_init_ctx(
424 ['registry-login', '--registry-url', 'bad-args-url'])
425 assert ctx
426 with pytest.raises(Exception) as e:
427 assert cd.command_registry_login(ctx)
428 assert str(e.value) == ('Invalid custom registry arguments received. To login to a custom registry include '
429 '--registry-url, --registry-username and --registry-password options or --registry-json option')
430
431 # test normal valid login with json file
432 get_parm.return_value = {"url": "sample-url", "username": "sample-username", "password": "sample-password"}
433 ctx: Optional[cd.CephadmContext] = cd.cephadm_init_ctx(
434 ['registry-login', '--registry-json', 'sample-json'])
435 ctx.container_engine = self.mock_docker()
436 assert ctx
437 retval = cd.command_registry_login(ctx)
438 assert retval == 0
439
440 # test bad login attempt with bad json file
441 get_parm.return_value = {"bad-json": "bad-json"}
442 ctx: Optional[cd.CephadmContext] = cd.cephadm_init_ctx(
443 ['registry-login', '--registry-json', 'sample-json'])
444 assert ctx
445 with pytest.raises(Exception) as e:
446 assert cd.command_registry_login(ctx)
447 assert str(e.value) == ("json provided for custom registry login did not include all necessary fields. "
448 "Please setup json file as\n"
449 "{\n"
450 " \"url\": \"REGISTRY_URL\",\n"
451 " \"username\": \"REGISTRY_USERNAME\",\n"
452 " \"password\": \"REGISTRY_PASSWORD\"\n"
453 "}\n")
454
455 # test login attempt with valid arguments where login command fails
456 call_throws.side_effect = Exception
457 ctx: Optional[cd.CephadmContext] = cd.cephadm_init_ctx(
458 ['registry-login', '--registry-url', 'sample-url',
459 '--registry-username', 'sample-user', '--registry-password',
460 'sample-pass'])
461 assert ctx
462 with pytest.raises(Exception) as e:
463 cd.command_registry_login(ctx)
464 assert str(e.value) == "Failed to login to custom registry @ sample-url as sample-user with given password"
465
466 def test_get_image_info_from_inspect(self):
467 # podman
468 out = """204a01f9b0b6710dd0c0af7f37ce7139c47ff0f0105d778d7104c69282dfbbf1,[docker.io/ceph/ceph@sha256:1cc9b824e1b076cdff52a9aa3f0cc8557d879fb2fbbba0cafed970aca59a3992]"""
469 r = cd.get_image_info_from_inspect(out, 'registry/ceph/ceph:latest')
470 print(r)
471 assert r == {
472 'image_id': '204a01f9b0b6710dd0c0af7f37ce7139c47ff0f0105d778d7104c69282dfbbf1',
473 'repo_digests': ['docker.io/ceph/ceph@sha256:1cc9b824e1b076cdff52a9aa3f0cc8557d879fb2fbbba0cafed970aca59a3992']
474 }
475
476 # docker
477 out = """sha256:16f4549cf7a8f112bbebf7946749e961fbbd1b0838627fe619aab16bc17ce552,[quay.ceph.io/ceph-ci/ceph@sha256:4e13da36c1bd6780b312a985410ae678984c37e6a9493a74c87e4a50b9bda41f]"""
478 r = cd.get_image_info_from_inspect(out, 'registry/ceph/ceph:latest')
479 assert r == {
480 'image_id': '16f4549cf7a8f112bbebf7946749e961fbbd1b0838627fe619aab16bc17ce552',
481 'repo_digests': ['quay.ceph.io/ceph-ci/ceph@sha256:4e13da36c1bd6780b312a985410ae678984c37e6a9493a74c87e4a50b9bda41f']
482 }
483
484 # multiple digests (podman)
485 out = """e935122ab143a64d92ed1fbb27d030cf6e2f0258207be1baf1b509c466aeeb42,[docker.io/prom/prometheus@sha256:e4ca62c0d62f3e886e684806dfe9d4e0cda60d54986898173c1083856cfda0f4 docker.io/prom/prometheus@sha256:efd99a6be65885c07c559679a0df4ec709604bcdd8cd83f0d00a1a683b28fb6a]"""
486 r = cd.get_image_info_from_inspect(out, 'registry/prom/prometheus:latest')
487 assert r == {
488 'image_id': 'e935122ab143a64d92ed1fbb27d030cf6e2f0258207be1baf1b509c466aeeb42',
489 'repo_digests': [
490 'docker.io/prom/prometheus@sha256:e4ca62c0d62f3e886e684806dfe9d4e0cda60d54986898173c1083856cfda0f4',
491 'docker.io/prom/prometheus@sha256:efd99a6be65885c07c559679a0df4ec709604bcdd8cd83f0d00a1a683b28fb6a',
492 ]
493 }
494
495
496 def test_dict_get(self):
497 result = cd.dict_get({'a': 1}, 'a', require=True)
498 assert result == 1
499 result = cd.dict_get({'a': 1}, 'b')
500 assert result is None
501 result = cd.dict_get({'a': 1}, 'b', default=2)
502 assert result == 2
503
504 def test_dict_get_error(self):
505 with pytest.raises(cd.Error):
506 cd.dict_get({'a': 1}, 'b', require=True)
507
508 def test_dict_get_join(self):
509 result = cd.dict_get_join({'foo': ['a', 'b']}, 'foo')
510 assert result == 'a\nb'
511 result = cd.dict_get_join({'foo': [1, 2]}, 'foo')
512 assert result == '1\n2'
513 result = cd.dict_get_join({'bar': 'a'}, 'bar')
514 assert result == 'a'
515 result = cd.dict_get_join({'a': 1}, 'a')
516 assert result == 1
517
518 def test_last_local_images(self):
519 out = '''
520 docker.io/ceph/daemon-base@
521 docker.io/ceph/ceph:v15.2.5
522 docker.io/ceph/daemon-base:octopus
523 '''
524 image = cd._filter_last_local_ceph_image(out)
525 assert image == 'docker.io/ceph/ceph:v15.2.5'
526
527
528 class TestCustomContainer(unittest.TestCase):
529 cc: cd.CustomContainer
530
531 def setUp(self):
532 self.cc = cd.CustomContainer(
533 'e863154d-33c7-4350-bca5-921e0467e55b',
534 'container',
535 config_json={
536 'entrypoint': 'bash',
537 'gid': 1000,
538 'args': [
539 '--no-healthcheck',
540 '-p 6800:6800'
541 ],
542 'envs': ['SECRET=password'],
543 'ports': [8080, 8443],
544 'volume_mounts': {
545 '/CONFIG_DIR': '/foo/conf',
546 'bar/config': '/bar:ro'
547 },
548 'bind_mounts': [
549 [
550 'type=bind',
551 'source=/CONFIG_DIR',
552 'destination=/foo/conf',
553 ''
554 ],
555 [
556 'type=bind',
557 'source=bar/config',
558 'destination=/bar:ro',
559 'ro=true'
560 ]
561 ]
562 },
563 image='docker.io/library/hello-world:latest'
564 )
565
566 def test_entrypoint(self):
567 self.assertEqual(self.cc.entrypoint, 'bash')
568
569 def test_uid_gid(self):
570 self.assertEqual(self.cc.uid, 65534)
571 self.assertEqual(self.cc.gid, 1000)
572
573 def test_ports(self):
574 self.assertEqual(self.cc.ports, [8080, 8443])
575
576 def test_get_container_args(self):
577 result = self.cc.get_container_args()
578 self.assertEqual(result, [
579 '--no-healthcheck',
580 '-p 6800:6800'
581 ])
582
583 def test_get_container_envs(self):
584 result = self.cc.get_container_envs()
585 self.assertEqual(result, ['SECRET=password'])
586
587 def test_get_container_mounts(self):
588 result = self.cc.get_container_mounts('/xyz')
589 self.assertDictEqual(result, {
590 '/CONFIG_DIR': '/foo/conf',
591 '/xyz/bar/config': '/bar:ro'
592 })
593
594 def test_get_container_binds(self):
595 result = self.cc.get_container_binds('/xyz')
596 self.assertEqual(result, [
597 [
598 'type=bind',
599 'source=/CONFIG_DIR',
600 'destination=/foo/conf',
601 ''
602 ],
603 [
604 'type=bind',
605 'source=/xyz/bar/config',
606 'destination=/bar:ro',
607 'ro=true'
608 ]
609 ])
610
611
612 class TestCephadmExporter(object):
613 exporter: cd.CephadmDaemon
614 files_created: List[str] = []
615 crt = """-----BEGIN CERTIFICATE-----
616 MIIC1zCCAb8CEFHoZE2MfUVzo53fzzBKAT0wDQYJKoZIhvcNAQENBQAwKjENMAsG
617 A1UECgwEQ2VwaDEZMBcGA1UECwwQY2VwaGFkbS1leHBvcnRlcjAeFw0yMDExMjUy
618 MzEwNTVaFw0zMDExMjMyMzEwNTVaMCoxDTALBgNVBAoMBENlcGgxGTAXBgNVBAsM
619 EGNlcGhhZG0tZXhwb3J0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
620 AQCsTfcJcXbREqfx1zTUuEmK+lJn9WWjk0URRF1Z+QgPkascNdkX16PnvhbGwXmF
621 BTdAcNl7V0U+z4EsGJ7hJsB7qTq6Rb6wNl7r0OxjeWOmB9xbF4Q/KR5yrbM1DA9A
622 B5fNswrUXViku5Y2jlOAz+ZMBhYxMx0edqhxSn297j04Z6RF4Mvkc43v0FH7Ju7k
623 O5+0VbdzcOdu37DFpoE4Ll2MZ/GuAHcJ8SD06sEdzFEjRCraav976743XcUlhZGX
624 ZTTG/Zf/a+wuCjtMG3od7vRFfuRrM5oTE133DuQ5deR7ybcZNDyopDjHF8xB1bAk
625 IOz4SbP6Q25K99Czm1K+3kMLAgMBAAEwDQYJKoZIhvcNAQENBQADggEBACmtvZb8
626 dJGHx/WC0/JHxnEJCJM2qnn87ELzbbIQL1w1Yb/I6JQYPgq+WiQPaHaLL9eYsm0l
627 dFwvrh+WC0JpXDfADnUnkTSB/WpZ2nC+2JxBptrQEuIcqNXpcJd0bKDiHunv04JI
628 uEVpTAK05dBV38qNmIlu4HyB4OEnuQpyOr9xpIhdxuJ95O9K0j5BIw98ZaEwYNUP
629 Rm3YlQwfS6R5xaBvL9kyfxyAD2joNj44q6w/5zj4egXVIA5VpkQm8DmMtu0Pd2NG
630 dzfYRmqrDolh+rty8HiyIxzeDJQ5bj6LKbUkmABvX50nDySVyMfHmt461/n7W65R
631 CHFLoOmfJJik+Uc=\n-----END CERTIFICATE-----
632 """
633 key = """-----BEGIN PRIVATE KEY-----
634 MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCsTfcJcXbREqfx
635 1zTUuEmK+lJn9WWjk0URRF1Z+QgPkascNdkX16PnvhbGwXmFBTdAcNl7V0U+z4Es
636 GJ7hJsB7qTq6Rb6wNl7r0OxjeWOmB9xbF4Q/KR5yrbM1DA9AB5fNswrUXViku5Y2
637 jlOAz+ZMBhYxMx0edqhxSn297j04Z6RF4Mvkc43v0FH7Ju7kO5+0VbdzcOdu37DF
638 poE4Ll2MZ/GuAHcJ8SD06sEdzFEjRCraav976743XcUlhZGXZTTG/Zf/a+wuCjtM
639 G3od7vRFfuRrM5oTE133DuQ5deR7ybcZNDyopDjHF8xB1bAkIOz4SbP6Q25K99Cz
640 m1K+3kMLAgMBAAECggEASnAwToMXWsGdjqxzpYasNv9oBIOO0nk4OHp5ffpJUjiT
641 XM+ip1tA80g7HMjPD/mt4gge3NtaDgWlf4Bve0O7mnEE7x5cgFIs9eG/jkYOF9eD
642 ilMBjivcfJywNDWujPH60iIMhqyBNEHaZl1ck+S9UJC8m6rCZLvMj40n/5riFfBy
643 1sjf2uOwcfWrjSj9Ju4wlMI6khSSz2aYC7glQQ/fo2+YArbEUcy60iloPQ6wEgZK
644 okoVWZA9AehwLcnRjkwd9EVmMMtRGPE/AcP4s/kKA0tRDRicPLN727Ke/yxv+Ppo
645 hbIZIcOn7soOFAENcodJ4YRSCd++QfCNaVAi7vwWWQKBgQDeBY4vvr+H0brbSjQg
646 O7Fpqub/fxZY3UoHWDqWs2X4o3qhDqaTQODpuYtCm8YQE//55JoLWKAD0evq5dLS
647 YLrtC1Vyxf+TA7opCUjWBe+liyndbJdB5q0zF7qdWUtQKGVSWyUWhK8gHa6M64fP
648 oi83DD7F0OGusTWGtfbceErk/wKBgQDGrJLRo/5xnAH5VmPfNu+S6h0M2qM6CYwe
649 Y5wHFG2uQQct73adf53SkhvZVmOzJsWQbVnlDOKMhqazcs+7VWRgO5X3naWVcctE
650 Hggw9MgpbXAWFOI5sNYsCYE58E+fTHjE6O4A3MhMCsze+CIC3sKuPQBBiL9bWSOX
651 8POswqfl9QKBgDe/nVxPwTgRaaH2l/AgDQRDbY1qE+psZlJBzTRaB5jPM9ONIjaH
652 a/JELLuk8a7H1tagmC2RK1zKMTriSnWY5FbxKZuQLAR2QyBavHdBNlOTBggbZD+f
653 9I2Hv8wSx95wxkBPsphc6Lxft5ya55czWjewU3LIaGK9DHuu5TWm3udxAoGBAJGP
654 PsJ59KIoOwoDUYjpJv3sqPwR9CVBeXeKY3aMcQ+KdUgiejVKmsb8ZYsG0GUhsv3u
655 ID7BAfsTbG9tXuVR2wjmnymcRwUHKnXtyvKTZVN06vpCsryx4zjAff2FI9ECpjke
656 r8HSAK41+4QhKEoSC3C9IMLi/dBfrsRTtTSOKZVBAoGBAI2dl5HEIFpufaI4toWM
657 LO5HFrlXgRDGoc/+Byr5/8ZZpYpU115Ol/q6M+l0koV2ygJ9jeJJEllFWykIDS6F
658 XxazFI74swAqobHb2ZS/SLhoVxE82DdSeXrjkTvUjNtrW5zs1gIMKBR4nD6H8AqL
659 iMN28C2bKGao5UHvdER1rGy7
660 -----END PRIVATE KEY-----
661 """
662 token = "MyAccessToken"
663
664 @classmethod
665 def setup_class(cls):
666 # create the ssl files
667 fname = os.path.join(os.getcwd(), 'crt')
668 with open(fname, 'w') as crt:
669 crt.write(cls.crt)
670 cls.files_created.append(fname)
671 fname = os.path.join(os.getcwd(), 'key')
672 with open(fname, 'w') as crt:
673 crt.write(cls.key)
674 cls.files_created.append(fname)
675 fname = os.path.join(os.getcwd(), 'token')
676 with open(fname, 'w') as crt:
677 crt.write(cls.token)
678 cls.files_created.append(fname)
679 # start a simple http instance to test the requesthandler
680 cls.server = HTTPServer(('0.0.0.0', 9443), cd.CephadmDaemonHandler)
681 cls.server.cephadm_cache = cd.CephadmCache()
682 cls.server.token = cls.token
683 t = threading.Thread(target=cls.server.serve_forever)
684 t.daemon = True
685 t.start()
686
687 @classmethod
688 def teardown_class(cls):
689 cls.server.shutdown()
690 assert len(cls.files_created) > 0
691 for f in cls.files_created:
692 os.remove(f)
693
694 def setup_method(self):
695 # re-init the cache for every test
696 TestCephadmExporter.server.cephadm_cache = cd.CephadmCache()
697
698 def teardown_method(self):
699 pass
700
701 def test_files_ready(self):
702 assert os.path.exists(os.path.join(os.getcwd(), 'crt'))
703 assert os.path.exists(os.path.join(os.getcwd(), 'key'))
704 assert os.path.exists(os.path.join(os.getcwd(), 'token'))
705
706 def test_can_run(self, exporter):
707 assert exporter.can_run
708
709 def test_token_valid(self, exporter):
710 assert exporter.token == self.token
711
712 def test_unit_name(self,exporter):
713 assert exporter.unit_name
714 assert exporter.unit_name == "ceph-foobar-cephadm-exporter.test.service"
715
716 def test_unit_run(self,exporter):
717 assert exporter.unit_run
718 lines = exporter.unit_run.split('\n')
719 assert len(lines) == 2
720 assert "cephadm exporter --fsid foobar --id test --port 9443 &" in lines[1]
721
722 def test_binary_path(self, exporter):
723 assert os.path.isfile(exporter.binary_path)
724
725 def test_systemd_unit(self, exporter):
726 assert exporter.unit_file
727
728 def test_validate_passes(self, exporter):
729 config = {
730 "crt": self.crt,
731 "key": self.key,
732 "token": self.token,
733 }
734 cd.CephadmDaemon.validate_config(config)
735
736 def test_validate_fails(self, exporter):
737 config = {
738 "key": self.key,
739 "token": self.token,
740 }
741 with pytest.raises(cd.Error):
742 cd.CephadmDaemon.validate_config(config)
743
744 def test_port_active(self, exporter):
745 assert exporter.port_active == True
746
747 def test_rqst_health_200(self):
748 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
749 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs)
750 r = urlopen(req)
751 assert r.status == 200
752
753 def test_rqst_all_inactive_500(self):
754 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
755 req=Request("http://localhost:9443/v1/metadata",headers=hdrs)
756 try:
757 r = urlopen(req)
758 except HTTPError as e:
759 assert e.code == 500
760
761 def test_rqst_no_auth_401(self):
762 req=Request("http://localhost:9443/v1/metadata")
763 try:
764 urlopen(req)
765 except HTTPError as e:
766 assert e.code == 401
767
768 def test_rqst_bad_auth_401(self):
769 hdrs={"Authorization":f"Bearer BogusAuthToken"}
770 req=Request("http://localhost:9443/v1/metadata",headers=hdrs)
771 try:
772 urlopen(req)
773 except HTTPError as e:
774 assert e.code == 401
775
776 def test_rqst_badURL_404(self):
777 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
778 req=Request("http://localhost:9443/v1/metazoic",headers=hdrs)
779 try:
780 urlopen(req)
781 except HTTPError as e:
782 assert e.code == 404
783
784 def test_rqst_inactive_task_204(self):
785 # all tasks initialise as inactive, and then 'go' active as their thread starts
786 # so we can pick any task to check for an inactive response (no content)
787 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
788 req=Request("http://localhost:9443/v1/metadata/disks",headers=hdrs)
789 r = urlopen(req)
790 assert r.status == 204
791
792 def test_rqst_active_task_200(self):
793 TestCephadmExporter.server.cephadm_cache.tasks['host'] = 'active'
794 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
795 req=Request("http://localhost:9443/v1/metadata/host",headers=hdrs)
796 r = urlopen(req)
797 assert r.status == 200
798
799 def test_rqst_all_206(self):
800 TestCephadmExporter.server.cephadm_cache.tasks['disks'] = 'active'
801 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
802 req=Request("http://localhost:9443/v1/metadata",headers=hdrs)
803 r = urlopen(req)
804 assert r.status == 206
805
806 def test_rqst_disks_200(self):
807 TestCephadmExporter.server.cephadm_cache.tasks['disks'] = 'active'
808 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
809 req=Request("http://localhost:9443/v1/metadata/disks",headers=hdrs)
810 r = urlopen(req)
811 assert r.status == 200
812
813 def test_thread_exception(self, exporter):
814 # run is patched to invoke a mocked scrape_host thread that will raise so
815 # we check here that the exception handler updates the cache object as we'd
816 # expect with the error
817 exporter.run()
818 assert exporter.cephadm_cache.host['scrape_errors']
819 assert exporter.cephadm_cache.host['scrape_errors'] == ['ValueError exception: wah']
820 assert exporter.cephadm_cache.errors == ['host thread stopped']
821
822 # Test the requesthandler does the right thing with invalid methods...
823 # ie. return a "501" - Not Implemented / Unsupported Method
824 def test_invalid_method_HEAD(self):
825 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
826 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="HEAD")
827 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
828 urlopen(req)
829
830 def test_invalid_method_DELETE(self):
831 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
832 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="DELETE")
833 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
834 urlopen(req)
835
836 def test_invalid_method_POST(self):
837 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
838 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="POST")
839 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
840 urlopen(req)
841
842 def test_invalid_method_PUT(self):
843 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
844 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="PUT")
845 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
846 urlopen(req)
847
848 def test_invalid_method_CONNECT(self):
849 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
850 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="CONNECT")
851 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
852 urlopen(req)
853
854 def test_invalid_method_TRACE(self):
855 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
856 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="TRACE")
857 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
858 urlopen(req)
859
860 def test_invalid_method_OPTIONS(self):
861 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
862 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="OPTIONS")
863 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
864 urlopen(req)
865
866 def test_invalid_method_PATCH(self):
867 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
868 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="PATCH")
869 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
870 urlopen(req)
871
872 def test_ipv4_subnet(self):
873 rc, v, msg = cd.check_subnet('192.168.1.0/24')
874 assert rc == 0 and v[0] == 4
875
876 def test_ipv4_subnet_list(self):
877 rc, v, msg = cd.check_subnet('192.168.1.0/24,10.90.90.0/24')
878 assert rc == 0 and not msg
879
880 def test_ipv4_subnet_badlist(self):
881 rc, v, msg = cd.check_subnet('192.168.1.0/24,192.168.1.1')
882 assert rc == 1 and msg
883
884 def test_ipv4_subnet_mixed(self):
885 rc, v, msg = cd.check_subnet('192.168.100.0/24,fe80::/64')
886 assert rc == 0 and v == [4,6]
887
888 def test_ipv6_subnet(self):
889 rc, v, msg = cd.check_subnet('fe80::/64')
890 assert rc == 0 and v[0] == 6
891
892 def test_subnet_mask_missing(self):
893 rc, v, msg = cd.check_subnet('192.168.1.58')
894 assert rc == 1 and msg
895
896 def test_subnet_mask_junk(self):
897 rc, v, msg = cd.check_subnet('wah')
898 assert rc == 1 and msg
899
900
901 class TestMaintenance:
902 systemd_target = "ceph.00000000-0000-0000-0000-000000c0ffee.target"
903
904 def test_systemd_target_OK(self, tmp_path):
905 base = tmp_path
906 wants = base / "ceph.target.wants"
907 wants.mkdir()
908 target = wants / TestMaintenance.systemd_target
909 target.touch()
910 cd.UNIT_DIR = str(base)
911
912 assert cd.systemd_target_state(target.name)
913
914 def test_systemd_target_NOTOK(self, tmp_path):
915 base = tmp_path
916 cd.UNIT_DIR = str(base)
917 assert not cd.systemd_target_state(TestMaintenance.systemd_target)
918
919 def test_parser_OK(self):
920 args = cd._parse_args(['host-maintenance', 'enter'])
921 assert args.maintenance_action == 'enter'
922
923 def test_parser_BAD(self):
924 with pytest.raises(SystemExit):
925 cd._parse_args(['host-maintenance', 'wah'])
926
927
928 class TestMonitoring(object):
929 @mock.patch('cephadm.call')
930 def test_get_version_alertmanager(self, _call):
931 ctx = mock.Mock()
932 daemon_type = 'alertmanager'
933
934 # binary `prometheus`
935 _call.return_value = '', '{}, version 0.16.1'.format(daemon_type), 0
936 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
937 assert version == '0.16.1'
938
939 # binary `prometheus-alertmanager`
940 _call.side_effect = (
941 ('', '', 1),
942 ('', '{}, version 0.16.1'.format(daemon_type), 0),
943 )
944 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
945 assert version == '0.16.1'
946
947 @mock.patch('cephadm.call')
948 def test_get_version_prometheus(self, _call):
949 ctx = mock.Mock()
950 daemon_type = 'prometheus'
951 _call.return_value = '', '{}, version 0.16.1'.format(daemon_type), 0
952 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
953 assert version == '0.16.1'
954
955 @mock.patch('cephadm.call')
956 def test_get_version_node_exporter(self, _call):
957 ctx = mock.Mock()
958 daemon_type = 'node-exporter'
959 _call.return_value = '', '{}, version 0.16.1'.format(daemon_type.replace('-', '_')), 0
960 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
961 assert version == '0.16.1'