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