]> git.proxmox.com Git - ceph.git/blame - ceph/src/cephadm/tests/test_cephadm.py
import ceph 16.2.6
[ceph.git] / ceph / src / cephadm / tests / test_cephadm.py
CommitLineData
e306af50 1# type: ignore
b3b6e05e
TL
2
3import errno
522d829b 4import json
9f95a23c
TL
5import mock
6import os
b3b6e05e
TL
7import pytest
8import socket
9f95a23c 9import sys
f67539c2 10import time
b3b6e05e
TL
11import threading
12import unittest
13
f67539c2
TL
14from http.server import HTTPServer
15from urllib.request import Request, urlopen
16from urllib.error import HTTPError
9f95a23c 17
b3b6e05e 18from typing import List, Optional
9f95a23c 19
b3b6e05e
TL
20from .fixtures import (
21 cephadm_fs,
22 exporter,
23 mock_docker,
24 mock_podman,
25 with_cephadm_ctx,
26)
f67539c2 27
b3b6e05e
TL
28
29with mock.patch('builtins.open', create=True):
9f95a23c
TL
30 from importlib.machinery import SourceFileLoader
31 cd = SourceFileLoader('cephadm', 'cephadm').load_module()
9f95a23c 32
f67539c2 33
522d829b
TL
34def get_ceph_conf(
35 fsid='00000000-0000-0000-0000-0000deadbeef',
36 mon_host='[v2:192.168.1.1:3300/0,v1:192.168.1.1:6789/0]'):
37 return f'''
38# minimal ceph.conf for {fsid}
39[global]
40 fsid = {fsid}
41 mon_host = {mon_host}
42'''
43
b3b6e05e 44class TestCephAdm(object):
f67539c2
TL
45
46 def test_docker_unit_file(self):
522d829b 47 ctx = cd.CephadmContext()
b3b6e05e 48 ctx.container_engine = mock_docker()
f67539c2
TL
49 r = cd.get_unit_file(ctx, '9b9d7609-f4d5-4aba-94c8-effa764d96c9')
50 assert 'Requires=docker.service' in r
b3b6e05e 51 ctx.container_engine = mock_podman()
f67539c2
TL
52 r = cd.get_unit_file(ctx, '9b9d7609-f4d5-4aba-94c8-effa764d96c9')
53 assert 'Requires=docker.service' not in r
54
55 @mock.patch('cephadm.logger')
56 def test_attempt_bind(self, logger):
57 ctx = None
58 address = None
59 port = 0
60
61 def os_error(errno):
62 _os_error = OSError()
63 _os_error.errno = errno
64 return _os_error
65
66 for side_effect, expected_exception in (
67 (os_error(errno.EADDRINUSE), cd.PortOccupiedError),
b3b6e05e
TL
68 (os_error(errno.EAFNOSUPPORT), cd.Error),
69 (os_error(errno.EADDRNOTAVAIL), cd.Error),
f67539c2
TL
70 (None, None),
71 ):
72 _socket = mock.Mock()
73 _socket.bind.side_effect = side_effect
74 try:
75 cd.attempt_bind(ctx, _socket, address, port)
76 except Exception as e:
77 assert isinstance(e, expected_exception)
78 else:
79 if expected_exception is not None:
80 assert False
81
82 @mock.patch('cephadm.attempt_bind')
83 @mock.patch('cephadm.logger')
84 def test_port_in_use(self, logger, attempt_bind):
85 empty_ctx = None
86
87 assert cd.port_in_use(empty_ctx, 9100) == False
88
89 attempt_bind.side_effect = cd.PortOccupiedError('msg')
90 assert cd.port_in_use(empty_ctx, 9100) == True
91
92 os_error = OSError()
93 os_error.errno = errno.EADDRNOTAVAIL
94 attempt_bind.side_effect = os_error
95 assert cd.port_in_use(empty_ctx, 9100) == False
96
97 os_error = OSError()
98 os_error.errno = errno.EAFNOSUPPORT
99 attempt_bind.side_effect = os_error
100 assert cd.port_in_use(empty_ctx, 9100) == False
101
102 @mock.patch('socket.socket')
103 @mock.patch('cephadm.logger')
104 def test_check_ip_port_success(self, logger, _socket):
522d829b 105 ctx = cd.CephadmContext()
f67539c2
TL
106 ctx.skip_ping_check = False # enables executing port check with `check_ip_port`
107
108 for address, address_family in (
109 ('0.0.0.0', socket.AF_INET),
110 ('::', socket.AF_INET6),
111 ):
112 try:
113 cd.check_ip_port(ctx, address, 9100)
114 except:
115 assert False
116 else:
b3b6e05e 117 assert _socket.call_args == mock.call(address_family, socket.SOCK_STREAM)
f67539c2
TL
118
119 @mock.patch('socket.socket')
120 @mock.patch('cephadm.logger')
121 def test_check_ip_port_failure(self, logger, _socket):
522d829b 122 ctx = cd.CephadmContext()
f67539c2
TL
123 ctx.skip_ping_check = False # enables executing port check with `check_ip_port`
124
125 def os_error(errno):
126 _os_error = OSError()
127 _os_error.errno = errno
128 return _os_error
129
130 for address, address_family in (
131 ('0.0.0.0', socket.AF_INET),
132 ('::', socket.AF_INET6),
133 ):
134 for side_effect, expected_exception in (
135 (os_error(errno.EADDRINUSE), cd.PortOccupiedError),
b3b6e05e
TL
136 (os_error(errno.EADDRNOTAVAIL), cd.Error),
137 (os_error(errno.EAFNOSUPPORT), cd.Error),
f67539c2
TL
138 (None, None),
139 ):
140 mock_socket_obj = mock.Mock()
141 mock_socket_obj.bind.side_effect = side_effect
142 _socket.return_value = mock_socket_obj
143 try:
144 cd.check_ip_port(ctx, address, 9100)
145 except Exception as e:
146 assert isinstance(e, expected_exception)
147 else:
148 if side_effect is not None:
149 assert False
150
151
f91f0fd5 152 def test_is_not_fsid(self):
9f95a23c
TL
153 assert not cd.is_fsid('no-uuid')
154
f91f0fd5
TL
155 def test_is_fsid(self):
156 assert cd.is_fsid('e863154d-33c7-4350-bca5-921e0467e55b')
157
9f95a23c
TL
158 def test__get_parser_image(self):
159 args = cd._parse_args(['--image', 'foo', 'version'])
160 assert args.image == 'foo'
161
522d829b
TL
162 def test_parse_mem_usage(self):
163 cd.logger = mock.Mock()
164 len, summary = cd._parse_mem_usage(0, 'c6290e3f1489,-- / --')
165 assert summary == {}
166
9f95a23c
TL
167 def test_CustomValidation(self):
168 assert cd._parse_args(['deploy', '--name', 'mon.a', '--fsid', 'fsid'])
169
170 with pytest.raises(SystemExit):
171 cd._parse_args(['deploy', '--name', 'wrong', '--fsid', 'fsid'])
172
173 @pytest.mark.parametrize("test_input, expected", [
f67539c2
TL
174 ("1.6.2", (1,6,2)),
175 ("1.6.2-stable2", (1,6,2)),
9f95a23c
TL
176 ])
177 def test_parse_podman_version(self, test_input, expected):
178 assert cd._parse_podman_version(test_input) == expected
179
180 def test_parse_podman_version_invalid(self):
181 with pytest.raises(ValueError) as res:
f67539c2 182 cd._parse_podman_version('inval.id')
9f95a23c
TL
183 assert 'inval' in str(res.value)
184
f6b5b4d7
TL
185 def test_is_ipv6(self):
186 cd.logger = mock.Mock()
187 for good in ("[::1]", "::1",
188 "fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"):
189 assert cd.is_ipv6(good)
190 for bad in ("127.0.0.1",
191 "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffg",
192 "1:2:3:4:5:6:7:8:9", "fd00::1::1", "[fg::1]"):
193 assert not cd.is_ipv6(bad)
194
195 def test_unwrap_ipv6(self):
196 def unwrap_test(address, expected):
197 assert cd.unwrap_ipv6(address) == expected
198
199 tests = [
200 ('::1', '::1'), ('[::1]', '::1'),
201 ('[fde4:8dba:82e1:0:5054:ff:fe6a:357]', 'fde4:8dba:82e1:0:5054:ff:fe6a:357'),
202 ('can actually be any string', 'can actually be any string'),
203 ('[but needs to be stripped] ', '[but needs to be stripped] ')]
204 for address, expected in tests:
205 unwrap_test(address, expected)
206
f91f0fd5
TL
207 def test_wrap_ipv6(self):
208 def wrap_test(address, expected):
209 assert cd.wrap_ipv6(address) == expected
210
211 tests = [
212 ('::1', '[::1]'), ('[::1]', '[::1]'),
213 ('fde4:8dba:82e1:0:5054:ff:fe6a:357',
214 '[fde4:8dba:82e1:0:5054:ff:fe6a:357]'),
215 ('myhost.example.com', 'myhost.example.com'),
216 ('192.168.0.1', '192.168.0.1'),
217 ('', ''), ('fd00::1::1', 'fd00::1::1')]
218 for address, expected in tests:
219 wrap_test(address, expected)
220
f6b5b4d7
TL
221 @mock.patch('cephadm.call_throws')
222 @mock.patch('cephadm.get_parm')
223 def test_registry_login(self, get_parm, call_throws):
224
225 # test normal valid login with url, username and password specified
226 call_throws.return_value = '', '', 0
b3b6e05e 227 ctx: cd.CephadmContext = cd.cephadm_init_ctx(
f67539c2
TL
228 ['registry-login', '--registry-url', 'sample-url',
229 '--registry-username', 'sample-user', '--registry-password',
230 'sample-pass'])
b3b6e05e 231 ctx.container_engine = mock_docker()
f67539c2 232 retval = cd.command_registry_login(ctx)
f6b5b4d7
TL
233 assert retval == 0
234
235 # test bad login attempt with invalid arguments given
b3b6e05e 236 ctx: cd.CephadmContext = cd.cephadm_init_ctx(
f67539c2 237 ['registry-login', '--registry-url', 'bad-args-url'])
f6b5b4d7 238 with pytest.raises(Exception) as e:
f67539c2 239 assert cd.command_registry_login(ctx)
f6b5b4d7
TL
240 assert str(e.value) == ('Invalid custom registry arguments received. To login to a custom registry include '
241 '--registry-url, --registry-username and --registry-password options or --registry-json option')
242
243 # test normal valid login with json file
244 get_parm.return_value = {"url": "sample-url", "username": "sample-username", "password": "sample-password"}
b3b6e05e 245 ctx: cd.CephadmContext = cd.cephadm_init_ctx(
f67539c2 246 ['registry-login', '--registry-json', 'sample-json'])
b3b6e05e 247 ctx.container_engine = mock_docker()
f67539c2 248 retval = cd.command_registry_login(ctx)
f6b5b4d7
TL
249 assert retval == 0
250
251 # test bad login attempt with bad json file
252 get_parm.return_value = {"bad-json": "bad-json"}
b3b6e05e 253 ctx: cd.CephadmContext = cd.cephadm_init_ctx(
f67539c2 254 ['registry-login', '--registry-json', 'sample-json'])
f6b5b4d7 255 with pytest.raises(Exception) as e:
f67539c2 256 assert cd.command_registry_login(ctx)
f6b5b4d7
TL
257 assert str(e.value) == ("json provided for custom registry login did not include all necessary fields. "
258 "Please setup json file as\n"
259 "{\n"
260 " \"url\": \"REGISTRY_URL\",\n"
261 " \"username\": \"REGISTRY_USERNAME\",\n"
262 " \"password\": \"REGISTRY_PASSWORD\"\n"
263 "}\n")
264
265 # test login attempt with valid arguments where login command fails
266 call_throws.side_effect = Exception
b3b6e05e 267 ctx: cd.CephadmContext = cd.cephadm_init_ctx(
f67539c2
TL
268 ['registry-login', '--registry-url', 'sample-url',
269 '--registry-username', 'sample-user', '--registry-password',
270 'sample-pass'])
f6b5b4d7 271 with pytest.raises(Exception) as e:
f67539c2 272 cd.command_registry_login(ctx)
f6b5b4d7 273 assert str(e.value) == "Failed to login to custom registry @ sample-url as sample-user with given password"
f91f0fd5
TL
274
275 def test_get_image_info_from_inspect(self):
276 # podman
cd265ab1 277 out = """204a01f9b0b6710dd0c0af7f37ce7139c47ff0f0105d778d7104c69282dfbbf1,[docker.io/ceph/ceph@sha256:1cc9b824e1b076cdff52a9aa3f0cc8557d879fb2fbbba0cafed970aca59a3992]"""
f91f0fd5 278 r = cd.get_image_info_from_inspect(out, 'registry/ceph/ceph:latest')
f67539c2 279 print(r)
f91f0fd5
TL
280 assert r == {
281 'image_id': '204a01f9b0b6710dd0c0af7f37ce7139c47ff0f0105d778d7104c69282dfbbf1',
f67539c2 282 'repo_digests': ['docker.io/ceph/ceph@sha256:1cc9b824e1b076cdff52a9aa3f0cc8557d879fb2fbbba0cafed970aca59a3992']
f91f0fd5
TL
283 }
284
285 # docker
cd265ab1 286 out = """sha256:16f4549cf7a8f112bbebf7946749e961fbbd1b0838627fe619aab16bc17ce552,[quay.ceph.io/ceph-ci/ceph@sha256:4e13da36c1bd6780b312a985410ae678984c37e6a9493a74c87e4a50b9bda41f]"""
f91f0fd5
TL
287 r = cd.get_image_info_from_inspect(out, 'registry/ceph/ceph:latest')
288 assert r == {
289 'image_id': '16f4549cf7a8f112bbebf7946749e961fbbd1b0838627fe619aab16bc17ce552',
f67539c2 290 'repo_digests': ['quay.ceph.io/ceph-ci/ceph@sha256:4e13da36c1bd6780b312a985410ae678984c37e6a9493a74c87e4a50b9bda41f']
f91f0fd5
TL
291 }
292
f67539c2
TL
293 # multiple digests (podman)
294 out = """e935122ab143a64d92ed1fbb27d030cf6e2f0258207be1baf1b509c466aeeb42,[docker.io/prom/prometheus@sha256:e4ca62c0d62f3e886e684806dfe9d4e0cda60d54986898173c1083856cfda0f4 docker.io/prom/prometheus@sha256:efd99a6be65885c07c559679a0df4ec709604bcdd8cd83f0d00a1a683b28fb6a]"""
295 r = cd.get_image_info_from_inspect(out, 'registry/prom/prometheus:latest')
296 assert r == {
297 'image_id': 'e935122ab143a64d92ed1fbb27d030cf6e2f0258207be1baf1b509c466aeeb42',
298 'repo_digests': [
299 'docker.io/prom/prometheus@sha256:e4ca62c0d62f3e886e684806dfe9d4e0cda60d54986898173c1083856cfda0f4',
300 'docker.io/prom/prometheus@sha256:efd99a6be65885c07c559679a0df4ec709604bcdd8cd83f0d00a1a683b28fb6a',
301 ]
302 }
303
304
f91f0fd5
TL
305 def test_dict_get(self):
306 result = cd.dict_get({'a': 1}, 'a', require=True)
307 assert result == 1
308 result = cd.dict_get({'a': 1}, 'b')
309 assert result is None
310 result = cd.dict_get({'a': 1}, 'b', default=2)
311 assert result == 2
312
313 def test_dict_get_error(self):
314 with pytest.raises(cd.Error):
315 cd.dict_get({'a': 1}, 'b', require=True)
316
317 def test_dict_get_join(self):
318 result = cd.dict_get_join({'foo': ['a', 'b']}, 'foo')
319 assert result == 'a\nb'
320 result = cd.dict_get_join({'foo': [1, 2]}, 'foo')
321 assert result == '1\n2'
322 result = cd.dict_get_join({'bar': 'a'}, 'bar')
323 assert result == 'a'
324 result = cd.dict_get_join({'a': 1}, 'a')
325 assert result == 1
326
adb31ebb
TL
327 def test_last_local_images(self):
328 out = '''
329docker.io/ceph/daemon-base@
330docker.io/ceph/ceph:v15.2.5
331docker.io/ceph/daemon-base:octopus
332 '''
333 image = cd._filter_last_local_ceph_image(out)
334 assert image == 'docker.io/ceph/ceph:v15.2.5'
335
b3b6e05e
TL
336 def test_normalize_image_digest(self):
337 s = 'myhostname:5000/ceph/ceph@sha256:753886ad9049004395ae990fbb9b096923b5a518b819283141ee8716ddf55ad1'
338 assert cd.normalize_image_digest(s) == s
339
340 s = 'ceph/ceph:latest'
341 assert cd.normalize_image_digest(s) == f'{cd.DEFAULT_REGISTRY}/{s}'
f91f0fd5 342
522d829b
TL
343 @pytest.mark.parametrize('fsid, ceph_conf, list_daemons, result, err, ',
344 [
345 (
346 None,
347 None,
348 [],
349 None,
350 None,
351 ),
352 (
353 '00000000-0000-0000-0000-0000deadbeef',
354 None,
355 [],
356 '00000000-0000-0000-0000-0000deadbeef',
357 None,
358 ),
359 (
360 '00000000-0000-0000-0000-0000deadbeef',
361 None,
362 [
363 {'fsid': '10000000-0000-0000-0000-0000deadbeef'},
364 {'fsid': '20000000-0000-0000-0000-0000deadbeef'},
365 ],
366 '00000000-0000-0000-0000-0000deadbeef',
367 None,
368 ),
369 (
370 None,
371 None,
372 [
373 {'fsid': '00000000-0000-0000-0000-0000deadbeef'},
374 ],
375 '00000000-0000-0000-0000-0000deadbeef',
376 None,
377 ),
378 (
379 None,
380 None,
381 [
382 {'fsid': '10000000-0000-0000-0000-0000deadbeef'},
383 {'fsid': '20000000-0000-0000-0000-0000deadbeef'},
384 ],
385 None,
386 r'Cannot infer an fsid',
387 ),
388 (
389 None,
390 get_ceph_conf(fsid='00000000-0000-0000-0000-0000deadbeef'),
391 [],
392 '00000000-0000-0000-0000-0000deadbeef',
393 None,
394 ),
395 (
396 None,
397 get_ceph_conf(fsid='00000000-0000-0000-0000-0000deadbeef'),
398 [
399 {'fsid': '00000000-0000-0000-0000-0000deadbeef'},
400 ],
401 '00000000-0000-0000-0000-0000deadbeef',
402 None,
403 ),
404 (
405 None,
406 get_ceph_conf(fsid='00000000-0000-0000-0000-0000deadbeef'),
407 [
408 {'fsid': '10000000-0000-0000-0000-0000deadbeef'},
409 {'fsid': '20000000-0000-0000-0000-0000deadbeef'},
410 ],
411 None,
412 r'Cannot infer an fsid',
413 ),
414 ])
415 @mock.patch('cephadm.call')
416 def test_infer_fsid(self, _call, fsid, ceph_conf, list_daemons, result, err, cephadm_fs):
417 # build the context
418 ctx = cd.CephadmContext()
419 ctx.fsid = fsid
420
421 # mock the decorator
422 mock_fn = mock.Mock()
423 mock_fn.return_value = 0
424 infer_fsid = cd.infer_fsid(mock_fn)
425
426 # mock the ceph.conf file content
427 if ceph_conf:
428 f = cephadm_fs.create_file('ceph.conf', contents=ceph_conf)
429 ctx.config = f.path
430
431 # test
432 with mock.patch('cephadm.list_daemons', return_value=list_daemons):
433 if err:
434 with pytest.raises(cd.Error, match=err):
435 infer_fsid(ctx)
436 else:
437 infer_fsid(ctx)
438 assert ctx.fsid == result
439
440 @pytest.mark.parametrize('fsid, config, name, list_daemons, result, ',
441 [
442 (
443 None,
444 '/foo/bar.conf',
445 None,
446 [],
447 '/foo/bar.conf',
448 ),
449 (
450 '00000000-0000-0000-0000-0000deadbeef',
451 None,
452 None,
453 [],
454 cd.SHELL_DEFAULT_CONF,
455 ),
456 (
457 '00000000-0000-0000-0000-0000deadbeef',
458 None,
459 None,
460 [{'name': 'mon.a'}],
461 '/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/mon.a/config',
462 ),
463 (
464 '00000000-0000-0000-0000-0000deadbeef',
465 None,
466 None,
467 [{'name': 'osd.0'}],
468 cd.SHELL_DEFAULT_CONF,
469 ),
470 (
471 '00000000-0000-0000-0000-0000deadbeef',
472 '/foo/bar.conf',
473 'mon.a',
474 [{'name': 'mon.a'}],
475 '/foo/bar.conf',
476 ),
477 (
478 '00000000-0000-0000-0000-0000deadbeef',
479 None,
480 'mon.a',
481 [],
482 '/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/mon.a/config',
483 ),
484 (
485 '00000000-0000-0000-0000-0000deadbeef',
486 None,
487 'osd.0',
488 [],
489 '/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/osd.0/config',
490 ),
491 (
492 None,
493 None,
494 None,
495 [],
496 cd.SHELL_DEFAULT_CONF,
497 ),
498 ])
499 @mock.patch('cephadm.call')
500 def test_infer_config(self, _call, fsid, config, name, list_daemons, result, cephadm_fs):
501 # build the context
502 ctx = cd.CephadmContext()
503 ctx.fsid = fsid
504 ctx.config = config
505 ctx.name = name
506
507 # mock the decorator
508 mock_fn = mock.Mock()
509 mock_fn.return_value = 0
510 infer_config = cd.infer_config(mock_fn)
511
512 # mock the shell config
513 cephadm_fs.create_file(cd.SHELL_DEFAULT_CONF)
514
515 # test
516 with mock.patch('cephadm.list_daemons', return_value=list_daemons):
517 infer_config(ctx)
518 assert ctx.config == result
519
520
f91f0fd5
TL
521class TestCustomContainer(unittest.TestCase):
522 cc: cd.CustomContainer
523
524 def setUp(self):
525 self.cc = cd.CustomContainer(
526 'e863154d-33c7-4350-bca5-921e0467e55b',
527 'container',
528 config_json={
529 'entrypoint': 'bash',
530 'gid': 1000,
531 'args': [
532 '--no-healthcheck',
533 '-p 6800:6800'
534 ],
535 'envs': ['SECRET=password'],
536 'ports': [8080, 8443],
537 'volume_mounts': {
538 '/CONFIG_DIR': '/foo/conf',
539 'bar/config': '/bar:ro'
540 },
541 'bind_mounts': [
542 [
543 'type=bind',
544 'source=/CONFIG_DIR',
545 'destination=/foo/conf',
546 ''
547 ],
548 [
549 'type=bind',
550 'source=bar/config',
551 'destination=/bar:ro',
552 'ro=true'
553 ]
554 ]
555 },
556 image='docker.io/library/hello-world:latest'
557 )
558
559 def test_entrypoint(self):
560 self.assertEqual(self.cc.entrypoint, 'bash')
561
562 def test_uid_gid(self):
563 self.assertEqual(self.cc.uid, 65534)
564 self.assertEqual(self.cc.gid, 1000)
565
566 def test_ports(self):
567 self.assertEqual(self.cc.ports, [8080, 8443])
568
569 def test_get_container_args(self):
570 result = self.cc.get_container_args()
571 self.assertEqual(result, [
572 '--no-healthcheck',
573 '-p 6800:6800'
574 ])
575
576 def test_get_container_envs(self):
577 result = self.cc.get_container_envs()
578 self.assertEqual(result, ['SECRET=password'])
579
580 def test_get_container_mounts(self):
581 result = self.cc.get_container_mounts('/xyz')
582 self.assertDictEqual(result, {
583 '/CONFIG_DIR': '/foo/conf',
584 '/xyz/bar/config': '/bar:ro'
585 })
586
587 def test_get_container_binds(self):
588 result = self.cc.get_container_binds('/xyz')
589 self.assertEqual(result, [
590 [
591 'type=bind',
592 'source=/CONFIG_DIR',
593 'destination=/foo/conf',
594 ''
595 ],
596 [
597 'type=bind',
598 'source=/xyz/bar/config',
599 'destination=/bar:ro',
600 'ro=true'
601 ]
602 ])
f67539c2
TL
603
604
605class TestCephadmExporter(object):
606 exporter: cd.CephadmDaemon
607 files_created: List[str] = []
608 crt = """-----BEGIN CERTIFICATE-----
609MIIC1zCCAb8CEFHoZE2MfUVzo53fzzBKAT0wDQYJKoZIhvcNAQENBQAwKjENMAsG
610A1UECgwEQ2VwaDEZMBcGA1UECwwQY2VwaGFkbS1leHBvcnRlcjAeFw0yMDExMjUy
611MzEwNTVaFw0zMDExMjMyMzEwNTVaMCoxDTALBgNVBAoMBENlcGgxGTAXBgNVBAsM
612EGNlcGhhZG0tZXhwb3J0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
613AQCsTfcJcXbREqfx1zTUuEmK+lJn9WWjk0URRF1Z+QgPkascNdkX16PnvhbGwXmF
614BTdAcNl7V0U+z4EsGJ7hJsB7qTq6Rb6wNl7r0OxjeWOmB9xbF4Q/KR5yrbM1DA9A
615B5fNswrUXViku5Y2jlOAz+ZMBhYxMx0edqhxSn297j04Z6RF4Mvkc43v0FH7Ju7k
616O5+0VbdzcOdu37DFpoE4Ll2MZ/GuAHcJ8SD06sEdzFEjRCraav976743XcUlhZGX
617ZTTG/Zf/a+wuCjtMG3od7vRFfuRrM5oTE133DuQ5deR7ybcZNDyopDjHF8xB1bAk
618IOz4SbP6Q25K99Czm1K+3kMLAgMBAAEwDQYJKoZIhvcNAQENBQADggEBACmtvZb8
619dJGHx/WC0/JHxnEJCJM2qnn87ELzbbIQL1w1Yb/I6JQYPgq+WiQPaHaLL9eYsm0l
620dFwvrh+WC0JpXDfADnUnkTSB/WpZ2nC+2JxBptrQEuIcqNXpcJd0bKDiHunv04JI
621uEVpTAK05dBV38qNmIlu4HyB4OEnuQpyOr9xpIhdxuJ95O9K0j5BIw98ZaEwYNUP
622Rm3YlQwfS6R5xaBvL9kyfxyAD2joNj44q6w/5zj4egXVIA5VpkQm8DmMtu0Pd2NG
623dzfYRmqrDolh+rty8HiyIxzeDJQ5bj6LKbUkmABvX50nDySVyMfHmt461/n7W65R
624CHFLoOmfJJik+Uc=\n-----END CERTIFICATE-----
625"""
626 key = """-----BEGIN PRIVATE KEY-----
627MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCsTfcJcXbREqfx
6281zTUuEmK+lJn9WWjk0URRF1Z+QgPkascNdkX16PnvhbGwXmFBTdAcNl7V0U+z4Es
629GJ7hJsB7qTq6Rb6wNl7r0OxjeWOmB9xbF4Q/KR5yrbM1DA9AB5fNswrUXViku5Y2
630jlOAz+ZMBhYxMx0edqhxSn297j04Z6RF4Mvkc43v0FH7Ju7kO5+0VbdzcOdu37DF
631poE4Ll2MZ/GuAHcJ8SD06sEdzFEjRCraav976743XcUlhZGXZTTG/Zf/a+wuCjtM
632G3od7vRFfuRrM5oTE133DuQ5deR7ybcZNDyopDjHF8xB1bAkIOz4SbP6Q25K99Cz
633m1K+3kMLAgMBAAECggEASnAwToMXWsGdjqxzpYasNv9oBIOO0nk4OHp5ffpJUjiT
634XM+ip1tA80g7HMjPD/mt4gge3NtaDgWlf4Bve0O7mnEE7x5cgFIs9eG/jkYOF9eD
635ilMBjivcfJywNDWujPH60iIMhqyBNEHaZl1ck+S9UJC8m6rCZLvMj40n/5riFfBy
6361sjf2uOwcfWrjSj9Ju4wlMI6khSSz2aYC7glQQ/fo2+YArbEUcy60iloPQ6wEgZK
637okoVWZA9AehwLcnRjkwd9EVmMMtRGPE/AcP4s/kKA0tRDRicPLN727Ke/yxv+Ppo
638hbIZIcOn7soOFAENcodJ4YRSCd++QfCNaVAi7vwWWQKBgQDeBY4vvr+H0brbSjQg
639O7Fpqub/fxZY3UoHWDqWs2X4o3qhDqaTQODpuYtCm8YQE//55JoLWKAD0evq5dLS
640YLrtC1Vyxf+TA7opCUjWBe+liyndbJdB5q0zF7qdWUtQKGVSWyUWhK8gHa6M64fP
641oi83DD7F0OGusTWGtfbceErk/wKBgQDGrJLRo/5xnAH5VmPfNu+S6h0M2qM6CYwe
642Y5wHFG2uQQct73adf53SkhvZVmOzJsWQbVnlDOKMhqazcs+7VWRgO5X3naWVcctE
643Hggw9MgpbXAWFOI5sNYsCYE58E+fTHjE6O4A3MhMCsze+CIC3sKuPQBBiL9bWSOX
6448POswqfl9QKBgDe/nVxPwTgRaaH2l/AgDQRDbY1qE+psZlJBzTRaB5jPM9ONIjaH
645a/JELLuk8a7H1tagmC2RK1zKMTriSnWY5FbxKZuQLAR2QyBavHdBNlOTBggbZD+f
6469I2Hv8wSx95wxkBPsphc6Lxft5ya55czWjewU3LIaGK9DHuu5TWm3udxAoGBAJGP
647PsJ59KIoOwoDUYjpJv3sqPwR9CVBeXeKY3aMcQ+KdUgiejVKmsb8ZYsG0GUhsv3u
648ID7BAfsTbG9tXuVR2wjmnymcRwUHKnXtyvKTZVN06vpCsryx4zjAff2FI9ECpjke
649r8HSAK41+4QhKEoSC3C9IMLi/dBfrsRTtTSOKZVBAoGBAI2dl5HEIFpufaI4toWM
650LO5HFrlXgRDGoc/+Byr5/8ZZpYpU115Ol/q6M+l0koV2ygJ9jeJJEllFWykIDS6F
651XxazFI74swAqobHb2ZS/SLhoVxE82DdSeXrjkTvUjNtrW5zs1gIMKBR4nD6H8AqL
652iMN28C2bKGao5UHvdER1rGy7
653-----END PRIVATE KEY-----
654"""
655 token = "MyAccessToken"
656
657 @classmethod
658 def setup_class(cls):
659 # create the ssl files
660 fname = os.path.join(os.getcwd(), 'crt')
661 with open(fname, 'w') as crt:
662 crt.write(cls.crt)
663 cls.files_created.append(fname)
664 fname = os.path.join(os.getcwd(), 'key')
665 with open(fname, 'w') as crt:
666 crt.write(cls.key)
667 cls.files_created.append(fname)
668 fname = os.path.join(os.getcwd(), 'token')
669 with open(fname, 'w') as crt:
670 crt.write(cls.token)
671 cls.files_created.append(fname)
672 # start a simple http instance to test the requesthandler
673 cls.server = HTTPServer(('0.0.0.0', 9443), cd.CephadmDaemonHandler)
674 cls.server.cephadm_cache = cd.CephadmCache()
675 cls.server.token = cls.token
676 t = threading.Thread(target=cls.server.serve_forever)
677 t.daemon = True
678 t.start()
679
680 @classmethod
681 def teardown_class(cls):
682 cls.server.shutdown()
683 assert len(cls.files_created) > 0
684 for f in cls.files_created:
685 os.remove(f)
686
687 def setup_method(self):
688 # re-init the cache for every test
689 TestCephadmExporter.server.cephadm_cache = cd.CephadmCache()
690
691 def teardown_method(self):
692 pass
693
694 def test_files_ready(self):
695 assert os.path.exists(os.path.join(os.getcwd(), 'crt'))
696 assert os.path.exists(os.path.join(os.getcwd(), 'key'))
697 assert os.path.exists(os.path.join(os.getcwd(), 'token'))
698
699 def test_can_run(self, exporter):
700 assert exporter.can_run
701
702 def test_token_valid(self, exporter):
703 assert exporter.token == self.token
704
705 def test_unit_name(self,exporter):
706 assert exporter.unit_name
707 assert exporter.unit_name == "ceph-foobar-cephadm-exporter.test.service"
708
709 def test_unit_run(self,exporter):
710 assert exporter.unit_run
711 lines = exporter.unit_run.split('\n')
712 assert len(lines) == 2
713 assert "cephadm exporter --fsid foobar --id test --port 9443 &" in lines[1]
714
715 def test_binary_path(self, exporter):
716 assert os.path.isfile(exporter.binary_path)
717
718 def test_systemd_unit(self, exporter):
719 assert exporter.unit_file
720
721 def test_validate_passes(self, exporter):
722 config = {
723 "crt": self.crt,
724 "key": self.key,
725 "token": self.token,
726 }
727 cd.CephadmDaemon.validate_config(config)
728
729 def test_validate_fails(self, exporter):
730 config = {
731 "key": self.key,
732 "token": self.token,
733 }
734 with pytest.raises(cd.Error):
735 cd.CephadmDaemon.validate_config(config)
736
737 def test_port_active(self, exporter):
738 assert exporter.port_active == True
739
740 def test_rqst_health_200(self):
741 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
742 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs)
743 r = urlopen(req)
744 assert r.status == 200
745
746 def test_rqst_all_inactive_500(self):
747 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
748 req=Request("http://localhost:9443/v1/metadata",headers=hdrs)
749 try:
750 r = urlopen(req)
751 except HTTPError as e:
752 assert e.code == 500
753
754 def test_rqst_no_auth_401(self):
755 req=Request("http://localhost:9443/v1/metadata")
756 try:
757 urlopen(req)
758 except HTTPError as e:
759 assert e.code == 401
760
761 def test_rqst_bad_auth_401(self):
762 hdrs={"Authorization":f"Bearer BogusAuthToken"}
763 req=Request("http://localhost:9443/v1/metadata",headers=hdrs)
764 try:
765 urlopen(req)
766 except HTTPError as e:
767 assert e.code == 401
768
769 def test_rqst_badURL_404(self):
770 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
771 req=Request("http://localhost:9443/v1/metazoic",headers=hdrs)
772 try:
773 urlopen(req)
774 except HTTPError as e:
775 assert e.code == 404
776
777 def test_rqst_inactive_task_204(self):
778 # all tasks initialise as inactive, and then 'go' active as their thread starts
779 # so we can pick any task to check for an inactive response (no content)
780 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
781 req=Request("http://localhost:9443/v1/metadata/disks",headers=hdrs)
782 r = urlopen(req)
783 assert r.status == 204
784
785 def test_rqst_active_task_200(self):
786 TestCephadmExporter.server.cephadm_cache.tasks['host'] = 'active'
787 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
788 req=Request("http://localhost:9443/v1/metadata/host",headers=hdrs)
789 r = urlopen(req)
790 assert r.status == 200
791
792 def test_rqst_all_206(self):
793 TestCephadmExporter.server.cephadm_cache.tasks['disks'] = 'active'
794 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
795 req=Request("http://localhost:9443/v1/metadata",headers=hdrs)
796 r = urlopen(req)
797 assert r.status == 206
798
799 def test_rqst_disks_200(self):
800 TestCephadmExporter.server.cephadm_cache.tasks['disks'] = 'active'
801 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
802 req=Request("http://localhost:9443/v1/metadata/disks",headers=hdrs)
803 r = urlopen(req)
804 assert r.status == 200
805
806 def test_thread_exception(self, exporter):
807 # run is patched to invoke a mocked scrape_host thread that will raise so
808 # we check here that the exception handler updates the cache object as we'd
809 # expect with the error
810 exporter.run()
811 assert exporter.cephadm_cache.host['scrape_errors']
812 assert exporter.cephadm_cache.host['scrape_errors'] == ['ValueError exception: wah']
813 assert exporter.cephadm_cache.errors == ['host thread stopped']
814
815 # Test the requesthandler does the right thing with invalid methods...
816 # ie. return a "501" - Not Implemented / Unsupported Method
817 def test_invalid_method_HEAD(self):
818 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
819 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="HEAD")
820 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
821 urlopen(req)
822
823 def test_invalid_method_DELETE(self):
824 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
825 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="DELETE")
826 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
827 urlopen(req)
828
829 def test_invalid_method_POST(self):
830 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
831 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="POST")
832 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
833 urlopen(req)
834
835 def test_invalid_method_PUT(self):
836 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
837 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="PUT")
838 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
839 urlopen(req)
840
841 def test_invalid_method_CONNECT(self):
842 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
843 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="CONNECT")
844 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
845 urlopen(req)
846
847 def test_invalid_method_TRACE(self):
848 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
849 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="TRACE")
850 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
851 urlopen(req)
852
853 def test_invalid_method_OPTIONS(self):
854 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
855 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="OPTIONS")
856 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
857 urlopen(req)
858
859 def test_invalid_method_PATCH(self):
860 hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"}
861 req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs, method="PATCH")
862 with pytest.raises(HTTPError, match=r"HTTP Error 501: .*") as e:
863 urlopen(req)
864
865 def test_ipv4_subnet(self):
866 rc, v, msg = cd.check_subnet('192.168.1.0/24')
867 assert rc == 0 and v[0] == 4
868
869 def test_ipv4_subnet_list(self):
870 rc, v, msg = cd.check_subnet('192.168.1.0/24,10.90.90.0/24')
871 assert rc == 0 and not msg
872
873 def test_ipv4_subnet_badlist(self):
874 rc, v, msg = cd.check_subnet('192.168.1.0/24,192.168.1.1')
875 assert rc == 1 and msg
876
877 def test_ipv4_subnet_mixed(self):
878 rc, v, msg = cd.check_subnet('192.168.100.0/24,fe80::/64')
879 assert rc == 0 and v == [4,6]
880
881 def test_ipv6_subnet(self):
882 rc, v, msg = cd.check_subnet('fe80::/64')
883 assert rc == 0 and v[0] == 6
884
885 def test_subnet_mask_missing(self):
886 rc, v, msg = cd.check_subnet('192.168.1.58')
887 assert rc == 1 and msg
888
889 def test_subnet_mask_junk(self):
890 rc, v, msg = cd.check_subnet('wah')
891 assert rc == 1 and msg
892
893
894class TestMaintenance:
895 systemd_target = "ceph.00000000-0000-0000-0000-000000c0ffee.target"
896
897 def test_systemd_target_OK(self, tmp_path):
898 base = tmp_path
899 wants = base / "ceph.target.wants"
900 wants.mkdir()
901 target = wants / TestMaintenance.systemd_target
902 target.touch()
903 cd.UNIT_DIR = str(base)
904
905 assert cd.systemd_target_state(target.name)
906
907 def test_systemd_target_NOTOK(self, tmp_path):
908 base = tmp_path
909 cd.UNIT_DIR = str(base)
910 assert not cd.systemd_target_state(TestMaintenance.systemd_target)
911
912 def test_parser_OK(self):
913 args = cd._parse_args(['host-maintenance', 'enter'])
914 assert args.maintenance_action == 'enter'
915
916 def test_parser_BAD(self):
917 with pytest.raises(SystemExit):
918 cd._parse_args(['host-maintenance', 'wah'])
919
920
921class TestMonitoring(object):
922 @mock.patch('cephadm.call')
923 def test_get_version_alertmanager(self, _call):
522d829b
TL
924 ctx = cd.CephadmContext()
925 ctx.container_engine = mock_podman()
f67539c2
TL
926 daemon_type = 'alertmanager'
927
928 # binary `prometheus`
929 _call.return_value = '', '{}, version 0.16.1'.format(daemon_type), 0
930 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
931 assert version == '0.16.1'
932
933 # binary `prometheus-alertmanager`
934 _call.side_effect = (
935 ('', '', 1),
936 ('', '{}, version 0.16.1'.format(daemon_type), 0),
937 )
938 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
939 assert version == '0.16.1'
940
941 @mock.patch('cephadm.call')
942 def test_get_version_prometheus(self, _call):
522d829b
TL
943 ctx = cd.CephadmContext()
944 ctx.container_engine = mock_podman()
f67539c2
TL
945 daemon_type = 'prometheus'
946 _call.return_value = '', '{}, version 0.16.1'.format(daemon_type), 0
947 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
948 assert version == '0.16.1'
949
950 @mock.patch('cephadm.call')
951 def test_get_version_node_exporter(self, _call):
522d829b
TL
952 ctx = cd.CephadmContext()
953 ctx.container_engine = mock_podman()
f67539c2
TL
954 daemon_type = 'node-exporter'
955 _call.return_value = '', '{}, version 0.16.1'.format(daemon_type.replace('-', '_')), 0
956 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
957 assert version == '0.16.1'
b3b6e05e 958
522d829b 959 def test_create_daemon_dirs_prometheus(self, cephadm_fs):
b3b6e05e
TL
960 """
961 Ensures the required and optional files given in the configuration are
962 created and mapped correctly inside the container. Tests absolute and
963 relative file paths given in the configuration.
964 """
965
966 fsid = 'aaf5a720-13fe-4a3b-82b9-2d99b7fd9704'
967 daemon_type = 'prometheus'
968 uid, gid = 50, 50
969 daemon_id = 'home'
522d829b 970 ctx = cd.CephadmContext()
b3b6e05e 971 ctx.data_dir = '/somedir'
522d829b 972 ctx.config_json = json.dumps({
b3b6e05e
TL
973 'files': {
974 'prometheus.yml': 'foo',
975 '/etc/prometheus/alerting/ceph_alerts.yml': 'bar'
976 }
522d829b 977 })
b3b6e05e
TL
978
979 cd.create_daemon_dirs(ctx,
980 fsid,
981 daemon_type,
982 daemon_id,
983 uid,
984 gid,
985 config=None,
986 keyring=None)
987
988 prefix = '{data_dir}/{fsid}/{daemon_type}.{daemon_id}'.format(
989 data_dir=ctx.data_dir,
990 fsid=fsid,
991 daemon_type=daemon_type,
992 daemon_id=daemon_id
993 )
522d829b
TL
994
995 expected = {
996 'etc/prometheus/prometheus.yml': 'foo',
997 'etc/prometheus/alerting/ceph_alerts.yml': 'bar',
998 }
999
1000 for file,content in expected.items():
1001 file = os.path.join(prefix, file)
1002 assert os.path.exists(file)
1003 with open(file) as f:
1004 assert f.read() == content
b3b6e05e
TL
1005
1006
1007class TestBootstrap(object):
1008
1009 @staticmethod
1010 def _get_cmd(*args):
1011 return [
1012 'bootstrap',
1013 '--allow-mismatched-release',
1014 '--skip-prepare-host',
1015 '--skip-dashboard',
1016 *args,
1017 ]
1018
1019 def test_config(self, cephadm_fs):
1020 conf_file = 'foo'
1021 cmd = self._get_cmd(
1022 '--mon-ip', '192.168.1.1',
1023 '--skip-mon-network',
1024 '--config', conf_file,
1025 )
1026
1027 with with_cephadm_ctx(cmd) as ctx:
1028 msg = r'No such file or directory'
1029 with pytest.raises(cd.Error, match=msg):
1030 cd.command_bootstrap(ctx)
1031
1032 cephadm_fs.create_file(conf_file)
1033 with with_cephadm_ctx(cmd) as ctx:
1034 retval = cd.command_bootstrap(ctx)
1035 assert retval == 0
1036
1037 def test_no_mon_addr(self, cephadm_fs):
1038 cmd = self._get_cmd()
1039 with with_cephadm_ctx(cmd) as ctx:
1040 msg = r'must specify --mon-ip or --mon-addrv'
1041 with pytest.raises(cd.Error, match=msg):
1042 cd.command_bootstrap(ctx)
1043
1044 def test_skip_mon_network(self, cephadm_fs):
1045 cmd = self._get_cmd('--mon-ip', '192.168.1.1')
1046
1047 with with_cephadm_ctx(cmd, list_networks={}) as ctx:
1048 msg = r'--skip-mon-network'
1049 with pytest.raises(cd.Error, match=msg):
1050 cd.command_bootstrap(ctx)
1051
1052 cmd += ['--skip-mon-network']
1053 with with_cephadm_ctx(cmd, list_networks={}) as ctx:
1054 retval = cd.command_bootstrap(ctx)
1055 assert retval == 0
1056
1057 @pytest.mark.parametrize('mon_ip, list_networks, result',
1058 [
1059 # IPv4
1060 (
1061 'eth0',
1062 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1063 False,
1064 ),
1065 (
1066 '0.0.0.0',
1067 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1068 False,
1069 ),
1070 (
1071 '192.168.1.0',
1072 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1073 False,
1074 ),
1075 (
1076 '192.168.1.1',
1077 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1078 True,
1079 ),
1080 (
1081 '192.168.1.1:1234',
1082 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1083 True,
1084 ),
522d829b
TL
1085 (
1086 '192.168.1.1:0123',
1087 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1088 True,
1089 ),
b3b6e05e
TL
1090 # IPv6
1091 (
1092 '::',
1093 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1094 False,
1095 ),
1096 (
1097 '::ffff:192.168.1.0',
1098 {"ffff::/64": {"eth0": ["::ffff:c0a8:101"]}},
1099 False,
1100 ),
1101 (
1102 '::ffff:192.168.1.1',
1103 {"ffff::/64": {"eth0": ["::ffff:c0a8:101"]}},
1104 True,
1105 ),
1106 (
1107 '::ffff:c0a8:101',
1108 {"ffff::/64": {"eth0": ["::ffff:c0a8:101"]}},
1109 True,
1110 ),
522d829b
TL
1111 (
1112 '[::ffff:c0a8:101]:1234',
1113 {"ffff::/64": {"eth0": ["::ffff:c0a8:101"]}},
1114 True,
1115 ),
1116 (
1117 '[::ffff:c0a8:101]:0123',
1118 {"ffff::/64": {"eth0": ["::ffff:c0a8:101"]}},
1119 True,
1120 ),
b3b6e05e
TL
1121 (
1122 '0000:0000:0000:0000:0000:FFFF:C0A8:0101',
1123 {"ffff::/64": {"eth0": ["::ffff:c0a8:101"]}},
1124 True,
1125 ),
1126 ])
1127 def test_mon_ip(self, mon_ip, list_networks, result, cephadm_fs):
1128 cmd = self._get_cmd('--mon-ip', mon_ip)
1129 if not result:
1130 with with_cephadm_ctx(cmd, list_networks=list_networks) as ctx:
1131 msg = r'--skip-mon-network'
1132 with pytest.raises(cd.Error, match=msg):
1133 cd.command_bootstrap(ctx)
1134 else:
1135 with with_cephadm_ctx(cmd, list_networks=list_networks) as ctx:
1136 retval = cd.command_bootstrap(ctx)
1137 assert retval == 0
1138
522d829b
TL
1139 @pytest.mark.parametrize('mon_addrv, list_networks, err',
1140 [
1141 # IPv4
1142 (
1143 '192.168.1.1',
1144 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1145 r'must use square backets',
1146 ),
1147 (
1148 '[192.168.1.1]',
1149 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1150 r'must include port number',
1151 ),
1152 (
1153 '[192.168.1.1:1234]',
1154 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1155 None,
1156 ),
1157 (
1158 '[192.168.1.1:0123]',
1159 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1160 None,
1161 ),
1162 (
1163 '[v2:192.168.1.1:3300,v1:192.168.1.1:6789]',
1164 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1165 None,
1166 ),
1167 # IPv6
1168 (
1169 '[::ffff:192.168.1.1:1234]',
1170 {'ffff::/64': {'eth0': ['::ffff:c0a8:101']}},
1171 None,
1172 ),
1173 (
1174 '[::ffff:192.168.1.1:0123]',
1175 {'ffff::/64': {'eth0': ['::ffff:c0a8:101']}},
1176 None,
1177 ),
1178 (
1179 '[0000:0000:0000:0000:0000:FFFF:C0A8:0101:1234]',
1180 {'ffff::/64': {'eth0': ['::ffff:c0a8:101']}},
1181 None,
1182 ),
1183 (
1184 '[v2:0000:0000:0000:0000:0000:FFFF:C0A8:0101:3300,v1:0000:0000:0000:0000:0000:FFFF:C0A8:0101:6789]',
1185 {'ffff::/64': {'eth0': ['::ffff:c0a8:101']}},
1186 None,
1187 ),
1188 ])
1189 def test_mon_addrv(self, mon_addrv, list_networks, err, cephadm_fs):
1190 cmd = self._get_cmd('--mon-addrv', mon_addrv)
1191 if err:
1192 with with_cephadm_ctx(cmd, list_networks=list_networks) as ctx:
1193 with pytest.raises(cd.Error, match=err):
1194 cd.command_bootstrap(ctx)
1195 else:
1196 with with_cephadm_ctx(cmd, list_networks=list_networks) as ctx:
1197 retval = cd.command_bootstrap(ctx)
1198 assert retval == 0
1199
b3b6e05e
TL
1200 def test_allow_fqdn_hostname(self, cephadm_fs):
1201 hostname = 'foo.bar'
1202 cmd = self._get_cmd(
1203 '--mon-ip', '192.168.1.1',
1204 '--skip-mon-network',
1205 )
1206
1207 with with_cephadm_ctx(cmd, hostname=hostname) as ctx:
1208 msg = r'--allow-fqdn-hostname'
1209 with pytest.raises(cd.Error, match=msg):
1210 cd.command_bootstrap(ctx)
1211
1212 cmd += ['--allow-fqdn-hostname']
1213 with with_cephadm_ctx(cmd, hostname=hostname) as ctx:
1214 retval = cd.command_bootstrap(ctx)
1215 assert retval == 0
1216
1217 @pytest.mark.parametrize('fsid, err',
1218 [
1219 ('', None),
1220 ('00000000-0000-0000-0000-0000deadbeef', None),
1221 ('00000000-0000-0000-0000-0000deadbeez', 'not an fsid'),
1222 ])
1223 def test_fsid(self, fsid, err, cephadm_fs):
1224 cmd = self._get_cmd(
1225 '--mon-ip', '192.168.1.1',
1226 '--skip-mon-network',
1227 '--fsid', fsid,
1228 )
1229
1230 with with_cephadm_ctx(cmd) as ctx:
1231 if err:
1232 with pytest.raises(cd.Error, match=err):
1233 cd.command_bootstrap(ctx)
1234 else:
1235 retval = cd.command_bootstrap(ctx)
1236 assert retval == 0
522d829b
TL
1237
1238
1239class TestShell(object):
1240
1241 def test_fsid(self, cephadm_fs):
1242 fsid = '00000000-0000-0000-0000-0000deadbeef'
1243
1244 cmd = ['shell', '--fsid', fsid]
1245 with with_cephadm_ctx(cmd) as ctx:
1246 retval = cd.command_shell(ctx)
1247 assert retval == 0
1248 assert ctx.fsid == fsid
1249
1250 cmd = ['shell', '--fsid', '00000000-0000-0000-0000-0000deadbeez']
1251 with with_cephadm_ctx(cmd) as ctx:
1252 err = 'not an fsid'
1253 with pytest.raises(cd.Error, match=err):
1254 retval = cd.command_shell(ctx)
1255 assert retval == 1
1256 assert ctx.fsid == None
1257
1258 s = get_ceph_conf(fsid=fsid)
1259 f = cephadm_fs.create_file('ceph.conf', contents=s)
1260
1261 cmd = ['shell', '--fsid', fsid, '--config', f.path]
1262 with with_cephadm_ctx(cmd) as ctx:
1263 retval = cd.command_shell(ctx)
1264 assert retval == 0
1265 assert ctx.fsid == fsid
1266
1267 cmd = ['shell', '--fsid', '10000000-0000-0000-0000-0000deadbeef', '--config', f.path]
1268 with with_cephadm_ctx(cmd) as ctx:
1269 err = 'fsid does not match ceph.conf'
1270 with pytest.raises(cd.Error, match=err):
1271 retval = cd.command_shell(ctx)
1272 assert retval == 1
1273 assert ctx.fsid == None
1274
1275 def test_name(self, cephadm_fs):
1276 cmd = ['shell', '--name', 'foo']
1277 with with_cephadm_ctx(cmd) as ctx:
1278 retval = cd.command_shell(ctx)
1279 assert retval == 0
1280
1281 cmd = ['shell', '--name', 'foo.bar']
1282 with with_cephadm_ctx(cmd) as ctx:
1283 err = r'must pass --fsid'
1284 with pytest.raises(cd.Error, match=err):
1285 retval = cd.command_shell(ctx)
1286 assert retval == 1
1287
1288 fsid = '00000000-0000-0000-0000-0000deadbeef'
1289 cmd = ['shell', '--name', 'foo.bar', '--fsid', fsid]
1290 with with_cephadm_ctx(cmd) as ctx:
1291 retval = cd.command_shell(ctx)
1292 assert retval == 0
1293
1294 def test_config(self, cephadm_fs):
1295 cmd = ['shell']
1296 with with_cephadm_ctx(cmd) as ctx:
1297 retval = cd.command_shell(ctx)
1298 assert retval == 0
1299 assert ctx.config == None
1300
1301 cephadm_fs.create_file(cd.SHELL_DEFAULT_CONF)
1302 with with_cephadm_ctx(cmd) as ctx:
1303 retval = cd.command_shell(ctx)
1304 assert retval == 0
1305 assert ctx.config == cd.SHELL_DEFAULT_CONF
1306
1307 cmd = ['shell', '--config', 'foo']
1308 with with_cephadm_ctx(cmd) as ctx:
1309 retval = cd.command_shell(ctx)
1310 assert retval == 0
1311 assert ctx.config == 'foo'
1312
1313 def test_keyring(self, cephadm_fs):
1314 cmd = ['shell']
1315 with with_cephadm_ctx(cmd) as ctx:
1316 retval = cd.command_shell(ctx)
1317 assert retval == 0
1318 assert ctx.keyring == None
1319
1320 cephadm_fs.create_file(cd.SHELL_DEFAULT_KEYRING)
1321 with with_cephadm_ctx(cmd) as ctx:
1322 retval = cd.command_shell(ctx)
1323 assert retval == 0
1324 assert ctx.keyring == cd.SHELL_DEFAULT_KEYRING
1325
1326 cmd = ['shell', '--keyring', 'foo']
1327 with with_cephadm_ctx(cmd) as ctx:
1328 retval = cd.command_shell(ctx)
1329 assert retval == 0
1330 assert ctx.keyring == 'foo'
1331
1332
1333class TestCephVolume(object):
1334
1335 @staticmethod
1336 def _get_cmd(*args):
1337 return [
1338 'ceph-volume',
1339 *args,
1340 '--', 'inventory', '--format', 'json'
1341 ]
1342
1343 def test_noop(self, cephadm_fs):
1344 cmd = self._get_cmd()
1345 with with_cephadm_ctx(cmd) as ctx:
1346 cd.command_ceph_volume(ctx)
1347 assert ctx.fsid == None
1348 assert ctx.config == None
1349 assert ctx.keyring == None
1350 assert ctx.config_json == None
1351
1352 def test_fsid(self, cephadm_fs):
1353 fsid = '00000000-0000-0000-0000-0000deadbeef'
1354
1355 cmd = self._get_cmd('--fsid', fsid)
1356 with with_cephadm_ctx(cmd) as ctx:
1357 cd.command_ceph_volume(ctx)
1358 assert ctx.fsid == fsid
1359
1360 cmd = self._get_cmd('--fsid', '00000000-0000-0000-0000-0000deadbeez')
1361 with with_cephadm_ctx(cmd) as ctx:
1362 err = 'not an fsid'
1363 with pytest.raises(cd.Error, match=err):
1364 retval = cd.command_shell(ctx)
1365 assert retval == 1
1366 assert ctx.fsid == None
1367
1368 s = get_ceph_conf(fsid=fsid)
1369 f = cephadm_fs.create_file('ceph.conf', contents=s)
1370
1371 cmd = self._get_cmd('--fsid', fsid, '--config', f.path)
1372 with with_cephadm_ctx(cmd) as ctx:
1373 cd.command_ceph_volume(ctx)
1374 assert ctx.fsid == fsid
1375
1376 cmd = self._get_cmd('--fsid', '10000000-0000-0000-0000-0000deadbeef', '--config', f.path)
1377 with with_cephadm_ctx(cmd) as ctx:
1378 err = 'fsid does not match ceph.conf'
1379 with pytest.raises(cd.Error, match=err):
1380 cd.command_ceph_volume(ctx)
1381 assert ctx.fsid == None
1382
1383 def test_config(self, cephadm_fs):
1384 cmd = self._get_cmd('--config', 'foo')
1385 with with_cephadm_ctx(cmd) as ctx:
1386 err = r'No such file or directory'
1387 with pytest.raises(cd.Error, match=err):
1388 cd.command_ceph_volume(ctx)
1389
1390 cephadm_fs.create_file('bar')
1391 cmd = self._get_cmd('--config', 'bar')
1392 with with_cephadm_ctx(cmd) as ctx:
1393 cd.command_ceph_volume(ctx)
1394 assert ctx.config == 'bar'
1395
1396 def test_keyring(self, cephadm_fs):
1397 cmd = self._get_cmd('--keyring', 'foo')
1398 with with_cephadm_ctx(cmd) as ctx:
1399 err = r'No such file or directory'
1400 with pytest.raises(cd.Error, match=err):
1401 cd.command_ceph_volume(ctx)
1402
1403 cephadm_fs.create_file('bar')
1404 cmd = self._get_cmd('--keyring', 'bar')
1405 with with_cephadm_ctx(cmd) as ctx:
1406 cd.command_ceph_volume(ctx)
1407 assert ctx.keyring == 'bar'
1408
1409
1410class TestIscsi:
1411 def test_unit_run(self, cephadm_fs):
1412 fsid = '9b9d7609-f4d5-4aba-94c8-effa764d96c9'
1413 config_json = {
1414 'files': {'iscsi-gateway.cfg': ''}
1415 }
1416 with with_cephadm_ctx(['--image=ceph/ceph'], list_networks={}) as ctx:
1417 import json
1418 ctx.config_json = json.dumps(config_json)
1419 ctx.fsid = fsid
1420 cd.get_parm.return_value = config_json
1421 iscsi = cd.CephIscsi(ctx, '9b9d7609-f4d5-4aba-94c8-effa764d96c9', 'daemon_id', config_json)
1422 c = iscsi.get_tcmu_runner_container()
1423
1424 cd.make_data_dir(ctx, fsid, 'iscsi', 'daemon_id')
1425 cd.deploy_daemon_units(
1426 ctx,
1427 fsid,
1428 0, 0,
1429 'iscsi',
1430 'daemon_id',
1431 c,
1432 True, True
1433 )
1434
1435 with open('/var/lib/ceph/9b9d7609-f4d5-4aba-94c8-effa764d96c9/iscsi.daemon_id/unit.run') as f:
1436 assert f.read() == """set -e
1437if ! grep -qs /var/lib/ceph/9b9d7609-f4d5-4aba-94c8-effa764d96c9/iscsi.daemon_id/configfs /proc/mounts; then mount -t configfs none /var/lib/ceph/9b9d7609-f4d5-4aba-94c8-effa764d96c9/iscsi.daemon_id/configfs; fi
1438# iscsi tcmu-runnter container
1439! /usr/bin/podman rm -f ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi.daemon_id-tcmu 2> /dev/null
1440! /usr/bin/podman rm -f ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi-daemon_id-tcmu 2> /dev/null
1441/usr/bin/podman run --rm --ipc=host --stop-signal=SIGTERM --net=host --entrypoint /usr/bin/tcmu-runner --privileged --group-add=disk --init --name ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi-daemon_id-tcmu -e CONTAINER_IMAGE=ceph/ceph -e NODE_NAME=host1 -e CEPH_USE_RANDOM_NONCE=1 -v /var/lib/ceph/9b9d7609-f4d5-4aba-94c8-effa764d96c9/iscsi.daemon_id/config:/etc/ceph/ceph.conf:z -v /var/lib/ceph/9b9d7609-f4d5-4aba-94c8-effa764d96c9/iscsi.daemon_id/keyring:/etc/ceph/keyring:z -v /var/lib/ceph/9b9d7609-f4d5-4aba-94c8-effa764d96c9/iscsi.daemon_id/iscsi-gateway.cfg:/etc/ceph/iscsi-gateway.cfg:z -v /var/lib/ceph/9b9d7609-f4d5-4aba-94c8-effa764d96c9/iscsi.daemon_id/configfs:/sys/kernel/config -v /var/log/ceph/9b9d7609-f4d5-4aba-94c8-effa764d96c9:/var/log/rbd-target-api:z -v /dev:/dev --mount type=bind,source=/lib/modules,destination=/lib/modules,ro=true ceph/ceph &
1442# iscsi.daemon_id
1443! /usr/bin/podman rm -f ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi.daemon_id-tcmu 2> /dev/null
1444! /usr/bin/podman rm -f ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi-daemon_id-tcmu 2> /dev/null
1445/usr/bin/podman run --rm --ipc=host --stop-signal=SIGTERM --net=host --entrypoint /usr/bin/tcmu-runner --privileged --group-add=disk --init --name ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi-daemon_id-tcmu -e CONTAINER_IMAGE=ceph/ceph -e NODE_NAME=host1 -e CEPH_USE_RANDOM_NONCE=1 -v /var/lib/ceph/9b9d7609-f4d5-4aba-94c8-effa764d96c9/iscsi.daemon_id/config:/etc/ceph/ceph.conf:z -v /var/lib/ceph/9b9d7609-f4d5-4aba-94c8-effa764d96c9/iscsi.daemon_id/keyring:/etc/ceph/keyring:z -v /var/lib/ceph/9b9d7609-f4d5-4aba-94c8-effa764d96c9/iscsi.daemon_id/iscsi-gateway.cfg:/etc/ceph/iscsi-gateway.cfg:z -v /var/lib/ceph/9b9d7609-f4d5-4aba-94c8-effa764d96c9/iscsi.daemon_id/configfs:/sys/kernel/config -v /var/log/ceph/9b9d7609-f4d5-4aba-94c8-effa764d96c9:/var/log/rbd-target-api:z -v /dev:/dev --mount type=bind,source=/lib/modules,destination=/lib/modules,ro=true ceph/ceph
1446"""
1447
1448 def test_get_container(self):
1449 """
1450 Due to a combination of socket.getfqdn() and podman's behavior to
1451 add the container name into the /etc/hosts file, we cannot use periods
1452 in container names. But we need to be able to detect old existing containers.
1453 Assert this behaviour. I think we can remove this in Ceph R
1454 """
1455 fsid = '9b9d7609-f4d5-4aba-94c8-effa764d96c9'
1456 with with_cephadm_ctx(['--image=ceph/ceph'], list_networks={}) as ctx:
1457 ctx.fsid = fsid
1458 c = cd.get_container(ctx, fsid, 'iscsi', 'something')
1459 assert c.cname == 'ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi-something'
1460 assert c.old_cname == 'ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi.something'
1461
1462
1463