]> git.proxmox.com Git - ceph.git/blob - ceph/src/cephadm/tests/test_cephadm.py
import ceph quincy 17.2.6
[ceph.git] / ceph / src / cephadm / tests / test_cephadm.py
1 # type: ignore
2
3 import errno
4 import json
5 import mock
6 import os
7 import pytest
8 import socket
9 import unittest
10 from textwrap import dedent
11
12 from .fixtures import (
13 cephadm_fs,
14 mock_docker,
15 mock_podman,
16 with_cephadm_ctx,
17 mock_bad_firewalld,
18 )
19
20 from pyfakefs import fake_filesystem
21 from pyfakefs import fake_filesystem_unittest
22
23 with mock.patch('builtins.open', create=True):
24 from importlib.machinery import SourceFileLoader
25 cd = SourceFileLoader('cephadm', 'cephadm').load_module()
26
27
28 def get_ceph_conf(
29 fsid='00000000-0000-0000-0000-0000deadbeef',
30 mon_host='[v2:192.168.1.1:3300/0,v1:192.168.1.1:6789/0]'):
31 return f'''
32 # minimal ceph.conf for {fsid}
33 [global]
34 fsid = {fsid}
35 mon_host = {mon_host}
36 '''
37
38 class TestCephAdm(object):
39
40 def test_docker_unit_file(self):
41 ctx = cd.CephadmContext()
42 ctx.container_engine = mock_docker()
43 r = cd.get_unit_file(ctx, '9b9d7609-f4d5-4aba-94c8-effa764d96c9')
44 assert 'Requires=docker.service' in r
45 ctx.container_engine = mock_podman()
46 r = cd.get_unit_file(ctx, '9b9d7609-f4d5-4aba-94c8-effa764d96c9')
47 assert 'Requires=docker.service' not in r
48
49 @mock.patch('cephadm.logger')
50 def test_attempt_bind(self, logger):
51 ctx = None
52 address = None
53 port = 0
54
55 def os_error(errno):
56 _os_error = OSError()
57 _os_error.errno = errno
58 return _os_error
59
60 for side_effect, expected_exception in (
61 (os_error(errno.EADDRINUSE), cd.PortOccupiedError),
62 (os_error(errno.EAFNOSUPPORT), cd.Error),
63 (os_error(errno.EADDRNOTAVAIL), cd.Error),
64 (None, None),
65 ):
66 _socket = mock.Mock()
67 _socket.bind.side_effect = side_effect
68 try:
69 cd.attempt_bind(ctx, _socket, address, port)
70 except Exception as e:
71 assert isinstance(e, expected_exception)
72 else:
73 if expected_exception is not None:
74 assert False
75
76 @mock.patch('cephadm.attempt_bind')
77 @mock.patch('cephadm.logger')
78 def test_port_in_use(self, logger, attempt_bind):
79 empty_ctx = None
80
81 assert cd.port_in_use(empty_ctx, 9100) == False
82
83 attempt_bind.side_effect = cd.PortOccupiedError('msg')
84 assert cd.port_in_use(empty_ctx, 9100) == True
85
86 os_error = OSError()
87 os_error.errno = errno.EADDRNOTAVAIL
88 attempt_bind.side_effect = os_error
89 assert cd.port_in_use(empty_ctx, 9100) == False
90
91 os_error = OSError()
92 os_error.errno = errno.EAFNOSUPPORT
93 attempt_bind.side_effect = os_error
94 assert cd.port_in_use(empty_ctx, 9100) == False
95
96 @mock.patch('socket.socket')
97 @mock.patch('cephadm.logger')
98 def test_check_ip_port_success(self, logger, _socket):
99 ctx = cd.CephadmContext()
100 ctx.skip_ping_check = False # enables executing port check with `check_ip_port`
101
102 for address, address_family in (
103 ('0.0.0.0', socket.AF_INET),
104 ('::', socket.AF_INET6),
105 ):
106 try:
107 cd.check_ip_port(ctx, cd.EndPoint(address, 9100))
108 except:
109 assert False
110 else:
111 assert _socket.call_args == mock.call(address_family, socket.SOCK_STREAM)
112
113 @mock.patch('socket.socket')
114 @mock.patch('cephadm.logger')
115 def test_check_ip_port_failure(self, logger, _socket):
116 ctx = cd.CephadmContext()
117 ctx.skip_ping_check = False # enables executing port check with `check_ip_port`
118
119 def os_error(errno):
120 _os_error = OSError()
121 _os_error.errno = errno
122 return _os_error
123
124 for address, address_family in (
125 ('0.0.0.0', socket.AF_INET),
126 ('::', socket.AF_INET6),
127 ):
128 for side_effect, expected_exception in (
129 (os_error(errno.EADDRINUSE), cd.PortOccupiedError),
130 (os_error(errno.EADDRNOTAVAIL), cd.Error),
131 (os_error(errno.EAFNOSUPPORT), cd.Error),
132 (None, None),
133 ):
134 mock_socket_obj = mock.Mock()
135 mock_socket_obj.bind.side_effect = side_effect
136 _socket.return_value = mock_socket_obj
137 try:
138 cd.check_ip_port(ctx, cd.EndPoint(address, 9100))
139 except Exception as e:
140 assert isinstance(e, expected_exception)
141 else:
142 if side_effect is not None:
143 assert False
144
145
146 def test_is_not_fsid(self):
147 assert not cd.is_fsid('no-uuid')
148
149 def test_is_fsid(self):
150 assert cd.is_fsid('e863154d-33c7-4350-bca5-921e0467e55b')
151
152 def test__get_parser_image(self):
153 args = cd._parse_args(['--image', 'foo', 'version'])
154 assert args.image == 'foo'
155
156 def test_parse_mem_usage(self):
157 cd.logger = mock.Mock()
158 len, summary = cd._parse_mem_usage(0, 'c6290e3f1489,-- / --')
159 assert summary == {}
160
161 def test_CustomValidation(self):
162 assert cd._parse_args(['deploy', '--name', 'mon.a', '--fsid', 'fsid'])
163
164 with pytest.raises(SystemExit):
165 cd._parse_args(['deploy', '--name', 'wrong', '--fsid', 'fsid'])
166
167 @pytest.mark.parametrize("test_input, expected", [
168 ("1.6.2", (1,6,2)),
169 ("1.6.2-stable2", (1,6,2)),
170 ])
171 def test_parse_podman_version(self, test_input, expected):
172 assert cd._parse_podman_version(test_input) == expected
173
174 def test_parse_podman_version_invalid(self):
175 with pytest.raises(ValueError) as res:
176 cd._parse_podman_version('inval.id')
177 assert 'inval' in str(res.value)
178
179 def test_is_ipv6(self):
180 cd.logger = mock.Mock()
181 for good in ("[::1]", "::1",
182 "fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"):
183 assert cd.is_ipv6(good)
184 for bad in ("127.0.0.1",
185 "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffg",
186 "1:2:3:4:5:6:7:8:9", "fd00::1::1", "[fg::1]"):
187 assert not cd.is_ipv6(bad)
188
189 def test_unwrap_ipv6(self):
190 def unwrap_test(address, expected):
191 assert cd.unwrap_ipv6(address) == expected
192
193 tests = [
194 ('::1', '::1'), ('[::1]', '::1'),
195 ('[fde4:8dba:82e1:0:5054:ff:fe6a:357]', 'fde4:8dba:82e1:0:5054:ff:fe6a:357'),
196 ('can actually be any string', 'can actually be any string'),
197 ('[but needs to be stripped] ', '[but needs to be stripped] ')]
198 for address, expected in tests:
199 unwrap_test(address, expected)
200
201 def test_wrap_ipv6(self):
202 def wrap_test(address, expected):
203 assert cd.wrap_ipv6(address) == expected
204
205 tests = [
206 ('::1', '[::1]'), ('[::1]', '[::1]'),
207 ('fde4:8dba:82e1:0:5054:ff:fe6a:357',
208 '[fde4:8dba:82e1:0:5054:ff:fe6a:357]'),
209 ('myhost.example.com', 'myhost.example.com'),
210 ('192.168.0.1', '192.168.0.1'),
211 ('', ''), ('fd00::1::1', 'fd00::1::1')]
212 for address, expected in tests:
213 wrap_test(address, expected)
214
215 @mock.patch('cephadm.Firewalld', mock_bad_firewalld)
216 @mock.patch('cephadm.logger')
217 def test_skip_firewalld(self, logger, cephadm_fs):
218 """
219 test --skip-firewalld actually skips changing firewall
220 """
221
222 ctx = cd.CephadmContext()
223 with pytest.raises(Exception):
224 cd.update_firewalld(ctx, 'mon')
225
226 ctx.skip_firewalld = True
227 cd.update_firewalld(ctx, 'mon')
228
229 ctx.skip_firewalld = False
230 with pytest.raises(Exception):
231 cd.update_firewalld(ctx, 'mon')
232
233 ctx = cd.CephadmContext()
234 ctx.ssl_dashboard_port = 8888
235 ctx.dashboard_key = None
236 ctx.dashboard_password_noupdate = True
237 ctx.initial_dashboard_password = 'password'
238 ctx.initial_dashboard_user = 'User'
239 with pytest.raises(Exception):
240 cd.prepare_dashboard(ctx, 0, 0, lambda _, extra_mounts=None, ___=None : '5', lambda : None)
241
242 ctx.skip_firewalld = True
243 cd.prepare_dashboard(ctx, 0, 0, lambda _, extra_mounts=None, ___=None : '5', lambda : None)
244
245 ctx.skip_firewalld = False
246 with pytest.raises(Exception):
247 cd.prepare_dashboard(ctx, 0, 0, lambda _, extra_mounts=None, ___=None : '5', lambda : None)
248
249 @mock.patch('cephadm.logger')
250 @mock.patch('cephadm.get_custom_config_files')
251 @mock.patch('cephadm.get_container')
252 def test_get_deployment_container(self, _get_container, _get_config, logger):
253 """
254 test get_deployment_container properly makes use of extra container args and custom conf files
255 """
256
257 ctx = cd.CephadmContext()
258 ctx.config_json = '-'
259 ctx.extra_container_args = [
260 '--pids-limit=12345',
261 '--something',
262 ]
263 ctx.data_dir = 'data'
264 _get_config.return_value = {'custom_config_files': [
265 {
266 'mount_path': '/etc/testing.str',
267 'content': 'this\nis\na\nstring',
268 }
269 ]}
270 _get_container.return_value = cd.CephContainer.for_daemon(
271 ctx,
272 fsid='9b9d7609-f4d5-4aba-94c8-effa764d96c9',
273 daemon_type='grafana',
274 daemon_id='host1',
275 entrypoint='',
276 args=[],
277 container_args=[],
278 volume_mounts={},
279 bind_mounts=[],
280 envs=[],
281 privileged=False,
282 ptrace=False,
283 host_network=True,
284 )
285 c = cd.get_deployment_container(ctx,
286 '9b9d7609-f4d5-4aba-94c8-effa764d96c9',
287 'grafana',
288 'host1',)
289
290 assert '--pids-limit=12345' in c.container_args
291 assert '--something' in c.container_args
292 assert os.path.join('data', '9b9d7609-f4d5-4aba-94c8-effa764d96c9', 'custom_config_files', 'grafana.host1', 'testing.str') in c.volume_mounts
293 assert c.volume_mounts[os.path.join('data', '9b9d7609-f4d5-4aba-94c8-effa764d96c9', 'custom_config_files', 'grafana.host1', 'testing.str')] == '/etc/testing.str'
294
295 @mock.patch('cephadm.logger')
296 @mock.patch('cephadm.get_custom_config_files')
297 def test_write_custom_conf_files(self, _get_config, logger, cephadm_fs):
298 """
299 test _write_custom_conf_files writes the conf files correctly
300 """
301
302 ctx = cd.CephadmContext()
303 ctx.config_json = '-'
304 ctx.data_dir = cd.DATA_DIR
305 _get_config.return_value = {'custom_config_files': [
306 {
307 'mount_path': '/etc/testing.str',
308 'content': 'this\nis\na\nstring',
309 },
310 {
311 'mount_path': '/etc/testing.conf',
312 'content': 'very_cool_conf_setting: very_cool_conf_value\nx: y',
313 },
314 {
315 'mount_path': '/etc/no-content.conf',
316 },
317 ]}
318 cd._write_custom_conf_files(ctx, 'mon', 'host1', 'fsid', 0, 0)
319 with open(os.path.join(cd.DATA_DIR, 'fsid', 'custom_config_files', 'mon.host1', 'testing.str'), 'r') as f:
320 assert 'this\nis\na\nstring' == f.read()
321 with open(os.path.join(cd.DATA_DIR, 'fsid', 'custom_config_files', 'mon.host1', 'testing.conf'), 'r') as f:
322 assert 'very_cool_conf_setting: very_cool_conf_value\nx: y' == f.read()
323 with pytest.raises(FileNotFoundError):
324 open(os.path.join(cd.DATA_DIR, 'fsid', 'custom_config_files', 'mon.host1', 'no-content.conf'), 'r')
325
326 @mock.patch('cephadm.call_throws')
327 @mock.patch('cephadm.get_parm')
328 def test_registry_login(self, get_parm, call_throws):
329 # test normal valid login with url, username and password specified
330 call_throws.return_value = '', '', 0
331 ctx: cd.CephadmContext = cd.cephadm_init_ctx(
332 ['registry-login', '--registry-url', 'sample-url',
333 '--registry-username', 'sample-user', '--registry-password',
334 'sample-pass'])
335 ctx.container_engine = mock_docker()
336 retval = cd.command_registry_login(ctx)
337 assert retval == 0
338
339 # test bad login attempt with invalid arguments given
340 ctx: cd.CephadmContext = cd.cephadm_init_ctx(
341 ['registry-login', '--registry-url', 'bad-args-url'])
342 with pytest.raises(Exception) as e:
343 assert cd.command_registry_login(ctx)
344 assert str(e.value) == ('Invalid custom registry arguments received. To login to a custom registry include '
345 '--registry-url, --registry-username and --registry-password options or --registry-json option')
346
347 # test normal valid login with json file
348 get_parm.return_value = {"url": "sample-url", "username": "sample-username", "password": "sample-password"}
349 ctx: cd.CephadmContext = cd.cephadm_init_ctx(
350 ['registry-login', '--registry-json', 'sample-json'])
351 ctx.container_engine = mock_docker()
352 retval = cd.command_registry_login(ctx)
353 assert retval == 0
354
355 # test bad login attempt with bad json file
356 get_parm.return_value = {"bad-json": "bad-json"}
357 ctx: cd.CephadmContext = cd.cephadm_init_ctx(
358 ['registry-login', '--registry-json', 'sample-json'])
359 with pytest.raises(Exception) as e:
360 assert cd.command_registry_login(ctx)
361 assert str(e.value) == ("json provided for custom registry login did not include all necessary fields. "
362 "Please setup json file as\n"
363 "{\n"
364 " \"url\": \"REGISTRY_URL\",\n"
365 " \"username\": \"REGISTRY_USERNAME\",\n"
366 " \"password\": \"REGISTRY_PASSWORD\"\n"
367 "}\n")
368
369 # test login attempt with valid arguments where login command fails
370 call_throws.side_effect = Exception
371 ctx: cd.CephadmContext = cd.cephadm_init_ctx(
372 ['registry-login', '--registry-url', 'sample-url',
373 '--registry-username', 'sample-user', '--registry-password',
374 'sample-pass'])
375 with pytest.raises(Exception) as e:
376 cd.command_registry_login(ctx)
377 assert str(e.value) == "Failed to login to custom registry @ sample-url as sample-user with given password"
378
379 def test_get_image_info_from_inspect(self):
380 # podman
381 out = """204a01f9b0b6710dd0c0af7f37ce7139c47ff0f0105d778d7104c69282dfbbf1,[docker.io/ceph/ceph@sha256:1cc9b824e1b076cdff52a9aa3f0cc8557d879fb2fbbba0cafed970aca59a3992]"""
382 r = cd.get_image_info_from_inspect(out, 'registry/ceph/ceph:latest')
383 print(r)
384 assert r == {
385 'image_id': '204a01f9b0b6710dd0c0af7f37ce7139c47ff0f0105d778d7104c69282dfbbf1',
386 'repo_digests': ['docker.io/ceph/ceph@sha256:1cc9b824e1b076cdff52a9aa3f0cc8557d879fb2fbbba0cafed970aca59a3992']
387 }
388
389 # docker
390 out = """sha256:16f4549cf7a8f112bbebf7946749e961fbbd1b0838627fe619aab16bc17ce552,[quay.ceph.io/ceph-ci/ceph@sha256:4e13da36c1bd6780b312a985410ae678984c37e6a9493a74c87e4a50b9bda41f]"""
391 r = cd.get_image_info_from_inspect(out, 'registry/ceph/ceph:latest')
392 assert r == {
393 'image_id': '16f4549cf7a8f112bbebf7946749e961fbbd1b0838627fe619aab16bc17ce552',
394 'repo_digests': ['quay.ceph.io/ceph-ci/ceph@sha256:4e13da36c1bd6780b312a985410ae678984c37e6a9493a74c87e4a50b9bda41f']
395 }
396
397 # multiple digests (podman)
398 out = """e935122ab143a64d92ed1fbb27d030cf6e2f0258207be1baf1b509c466aeeb42,[docker.io/prom/prometheus@sha256:e4ca62c0d62f3e886e684806dfe9d4e0cda60d54986898173c1083856cfda0f4 docker.io/prom/prometheus@sha256:efd99a6be65885c07c559679a0df4ec709604bcdd8cd83f0d00a1a683b28fb6a]"""
399 r = cd.get_image_info_from_inspect(out, 'registry/prom/prometheus:latest')
400 assert r == {
401 'image_id': 'e935122ab143a64d92ed1fbb27d030cf6e2f0258207be1baf1b509c466aeeb42',
402 'repo_digests': [
403 'docker.io/prom/prometheus@sha256:e4ca62c0d62f3e886e684806dfe9d4e0cda60d54986898173c1083856cfda0f4',
404 'docker.io/prom/prometheus@sha256:efd99a6be65885c07c559679a0df4ec709604bcdd8cd83f0d00a1a683b28fb6a',
405 ]
406 }
407
408
409 def test_dict_get(self):
410 result = cd.dict_get({'a': 1}, 'a', require=True)
411 assert result == 1
412 result = cd.dict_get({'a': 1}, 'b')
413 assert result is None
414 result = cd.dict_get({'a': 1}, 'b', default=2)
415 assert result == 2
416
417 def test_dict_get_error(self):
418 with pytest.raises(cd.Error):
419 cd.dict_get({'a': 1}, 'b', require=True)
420
421 def test_dict_get_join(self):
422 result = cd.dict_get_join({'foo': ['a', 'b']}, 'foo')
423 assert result == 'a\nb'
424 result = cd.dict_get_join({'foo': [1, 2]}, 'foo')
425 assert result == '1\n2'
426 result = cd.dict_get_join({'bar': 'a'}, 'bar')
427 assert result == 'a'
428 result = cd.dict_get_join({'a': 1}, 'a')
429 assert result == 1
430
431 @mock.patch('os.listdir', return_value=[])
432 @mock.patch('cephadm.logger')
433 def test_infer_local_ceph_image(self, _logger, _listdir):
434 ctx = cd.CephadmContext()
435 ctx.fsid = '00000000-0000-0000-0000-0000deadbeez'
436 ctx.container_engine = mock_podman()
437
438 # make sure the right image is selected when container is found
439 cinfo = cd.ContainerInfo('935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972',
440 'registry.hub.docker.com/rkachach/ceph:custom-v0.5',
441 '514e6a882f6e74806a5856468489eeff8d7106095557578da96935e4d0ba4d9d',
442 '2022-04-19 13:45:20.97146228 +0000 UTC',
443 '')
444 out = '''quay.ceph.io/ceph-ci/ceph@sha256:87f200536bb887b36b959e887d5984dd7a3f008a23aa1f283ab55d48b22c6185|dad864ee21e9|main|2022-03-23 16:29:19 +0000 UTC
445 quay.ceph.io/ceph-ci/ceph@sha256:b50b130fcda2a19f8507ddde3435bb4722266956e1858ac395c838bc1dcf1c0e|514e6a882f6e|pacific|2022-03-23 15:58:34 +0000 UTC
446 docker.io/ceph/ceph@sha256:939a46c06b334e094901560c8346de33c00309e3e3968a2db240eb4897c6a508|666bbfa87e8d|v15.2.5|2020-09-16 14:15:15 +0000 UTC'''
447 with mock.patch('cephadm.call_throws', return_value=(out, '', '')):
448 with mock.patch('cephadm.get_container_info', return_value=cinfo):
449 image = cd.infer_local_ceph_image(ctx, ctx.container_engine)
450 assert image == 'quay.ceph.io/ceph-ci/ceph@sha256:b50b130fcda2a19f8507ddde3435bb4722266956e1858ac395c838bc1dcf1c0e'
451
452 # make sure first valid image is used when no container_info is found
453 out = '''quay.ceph.io/ceph-ci/ceph@sha256:87f200536bb887b36b959e887d5984dd7a3f008a23aa1f283ab55d48b22c6185|dad864ee21e9|main|2022-03-23 16:29:19 +0000 UTC
454 quay.ceph.io/ceph-ci/ceph@sha256:b50b130fcda2a19f8507ddde3435bb4722266956e1858ac395c838bc1dcf1c0e|514e6a882f6e|pacific|2022-03-23 15:58:34 +0000 UTC
455 docker.io/ceph/ceph@sha256:939a46c06b334e094901560c8346de33c00309e3e3968a2db240eb4897c6a508|666bbfa87e8d|v15.2.5|2020-09-16 14:15:15 +0000 UTC'''
456 with mock.patch('cephadm.call_throws', return_value=(out, '', '')):
457 with mock.patch('cephadm.get_container_info', return_value=None):
458 image = cd.infer_local_ceph_image(ctx, ctx.container_engine)
459 assert image == 'quay.ceph.io/ceph-ci/ceph@sha256:87f200536bb887b36b959e887d5984dd7a3f008a23aa1f283ab55d48b22c6185'
460
461 # make sure images without digest are discarded (no container_info is found)
462 out = '''quay.ceph.io/ceph-ci/ceph@|||
463 docker.io/ceph/ceph@|||
464 docker.io/ceph/ceph@sha256:939a46c06b334e094901560c8346de33c00309e3e3968a2db240eb4897c6a508|666bbfa87e8d|v15.2.5|2020-09-16 14:15:15 +0000 UTC'''
465 with mock.patch('cephadm.call_throws', return_value=(out, '', '')):
466 with mock.patch('cephadm.get_container_info', return_value=None):
467 image = cd.infer_local_ceph_image(ctx, ctx.container_engine)
468 assert image == 'docker.io/ceph/ceph@sha256:939a46c06b334e094901560c8346de33c00309e3e3968a2db240eb4897c6a508'
469
470
471
472 @pytest.mark.parametrize('daemon_filter, by_name, daemon_list, container_stats, output',
473 [
474 # get container info by type ('mon')
475 (
476 'mon',
477 False,
478 [
479 {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'},
480 {'name': 'mgr.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'},
481 ],
482 ("935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972,registry.hub.docker.com/rkachach/ceph:custom-v0.5,666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4,2022-04-19 13:45:20.97146228 +0000 UTC,",
483 "",
484 0),
485 cd.ContainerInfo('935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972',
486 'registry.hub.docker.com/rkachach/ceph:custom-v0.5',
487 '666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4',
488 '2022-04-19 13:45:20.97146228 +0000 UTC',
489 '')
490 ),
491 # get container info by name ('mon.ceph-node-0')
492 (
493 'mon.ceph-node-0',
494 True,
495 [
496 {'name': 'mgr.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'},
497 {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'},
498 ],
499 ("935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972,registry.hub.docker.com/rkachach/ceph:custom-v0.5,666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4,2022-04-19 13:45:20.97146228 +0000 UTC,",
500 "",
501 0),
502 cd.ContainerInfo('935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972',
503 'registry.hub.docker.com/rkachach/ceph:custom-v0.5',
504 '666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4',
505 '2022-04-19 13:45:20.97146228 +0000 UTC',
506 '')
507 ),
508 # get container info by name (same daemon but two different fsids)
509 (
510 'mon.ceph-node-0',
511 True,
512 [
513 {'name': 'mon.ceph-node-0', 'fsid': '10000000-0000-0000-0000-0000deadbeef'},
514 {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'},
515 ],
516 ("935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972,registry.hub.docker.com/rkachach/ceph:custom-v0.5,666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4,2022-04-19 13:45:20.97146228 +0000 UTC,",
517 "",
518 0),
519 cd.ContainerInfo('935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972',
520 'registry.hub.docker.com/rkachach/ceph:custom-v0.5',
521 '666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4',
522 '2022-04-19 13:45:20.97146228 +0000 UTC',
523 '')
524 ),
525 # get container info by type (bad container stats: 127 code)
526 (
527 'mon',
528 False,
529 [
530 {'name': 'mon.ceph-node-0', 'fsid': '00000000-FFFF-0000-0000-0000deadbeef'},
531 {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'},
532 ],
533 ("",
534 "",
535 127),
536 None
537 ),
538 # get container info by name (bad container stats: 127 code)
539 (
540 'mon.ceph-node-0',
541 True,
542 [
543 {'name': 'mgr.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'},
544 {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'},
545 ],
546 ("",
547 "",
548 127),
549 None
550 ),
551 # get container info by invalid name (doens't contain '.')
552 (
553 'mon-ceph-node-0',
554 True,
555 [
556 {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'},
557 {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'},
558 ],
559 ("935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972,registry.hub.docker.com/rkachach/ceph:custom-v0.5,666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4,2022-04-19 13:45:20.97146228 +0000 UTC,",
560 "",
561 0),
562 None
563 ),
564 # get container info by invalid name (empty)
565 (
566 '',
567 True,
568 [
569 {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'},
570 {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'},
571 ],
572 ("935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972,registry.hub.docker.com/rkachach/ceph:custom-v0.5,666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4,2022-04-19 13:45:20.97146228 +0000 UTC,",
573 "",
574 0),
575 None
576 ),
577 # get container info by invalid type (empty)
578 (
579 '',
580 False,
581 [
582 {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'},
583 {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'},
584 ],
585 ("935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972,registry.hub.docker.com/rkachach/ceph:custom-v0.5,666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4,2022-04-19 13:45:20.97146228 +0000 UTC,",
586 "",
587 0),
588 None
589 ),
590 # get container info by name: no match (invalid fsid)
591 (
592 'mon',
593 False,
594 [
595 {'name': 'mon.ceph-node-0', 'fsid': '00000000-1111-0000-0000-0000deadbeef'},
596 {'name': 'mon.ceph-node-0', 'fsid': '00000000-2222-0000-0000-0000deadbeef'},
597 ],
598 ("935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972,registry.hub.docker.com/rkachach/ceph:custom-v0.5,666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4,2022-04-19 13:45:20.97146228 +0000 UTC,",
599 "",
600 0),
601 None
602 ),
603 # get container info by name: no match
604 (
605 'mon.ceph-node-0',
606 True,
607 [],
608 None,
609 None
610 ),
611 # get container info by type: no match
612 (
613 'mgr',
614 False,
615 [],
616 None,
617 None
618 ),
619 ])
620 def test_get_container_info(self, daemon_filter, by_name, daemon_list, container_stats, output):
621 cd.logger = mock.Mock()
622 ctx = cd.CephadmContext()
623 ctx.fsid = '00000000-0000-0000-0000-0000deadbeef'
624 ctx.container_engine = mock_podman()
625 with mock.patch('cephadm.list_daemons', return_value=daemon_list):
626 with mock.patch('cephadm.get_container_stats', return_value=container_stats):
627 assert cd.get_container_info(ctx, daemon_filter, by_name) == output
628
629 def test_should_log_to_journald(self):
630 ctx = cd.CephadmContext()
631 # explicit
632 ctx.log_to_journald = True
633 assert cd.should_log_to_journald(ctx)
634
635 ctx.log_to_journald = None
636 # enable if podman support --cgroup=split
637 ctx.container_engine = mock_podman()
638 ctx.container_engine.version = (2, 1, 0)
639 assert cd.should_log_to_journald(ctx)
640
641 # disable on old podman
642 ctx.container_engine.version = (2, 0, 0)
643 assert not cd.should_log_to_journald(ctx)
644
645 # disable on docker
646 ctx.container_engine = mock_docker()
647 assert not cd.should_log_to_journald(ctx)
648
649 def test_normalize_image_digest(self):
650 s = 'myhostname:5000/ceph/ceph@sha256:753886ad9049004395ae990fbb9b096923b5a518b819283141ee8716ddf55ad1'
651 assert cd.normalize_image_digest(s) == s
652
653 s = 'ceph/ceph:latest'
654 assert cd.normalize_image_digest(s) == f'{cd.DEFAULT_REGISTRY}/{s}'
655
656 @pytest.mark.parametrize('fsid, ceph_conf, list_daemons, result, err, ',
657 [
658 (
659 None,
660 None,
661 [],
662 None,
663 None,
664 ),
665 (
666 '00000000-0000-0000-0000-0000deadbeef',
667 None,
668 [],
669 '00000000-0000-0000-0000-0000deadbeef',
670 None,
671 ),
672 (
673 '00000000-0000-0000-0000-0000deadbeef',
674 None,
675 [
676 {'fsid': '10000000-0000-0000-0000-0000deadbeef'},
677 {'fsid': '20000000-0000-0000-0000-0000deadbeef'},
678 ],
679 '00000000-0000-0000-0000-0000deadbeef',
680 None,
681 ),
682 (
683 None,
684 None,
685 [
686 {'fsid': '00000000-0000-0000-0000-0000deadbeef'},
687 ],
688 '00000000-0000-0000-0000-0000deadbeef',
689 None,
690 ),
691 (
692 None,
693 None,
694 [
695 {'fsid': '10000000-0000-0000-0000-0000deadbeef'},
696 {'fsid': '20000000-0000-0000-0000-0000deadbeef'},
697 ],
698 None,
699 r'Cannot infer an fsid',
700 ),
701 (
702 None,
703 get_ceph_conf(fsid='00000000-0000-0000-0000-0000deadbeef'),
704 [],
705 '00000000-0000-0000-0000-0000deadbeef',
706 None,
707 ),
708 (
709 None,
710 get_ceph_conf(fsid='00000000-0000-0000-0000-0000deadbeef'),
711 [
712 {'fsid': '00000000-0000-0000-0000-0000deadbeef'},
713 ],
714 '00000000-0000-0000-0000-0000deadbeef',
715 None,
716 ),
717 (
718 None,
719 get_ceph_conf(fsid='00000000-0000-0000-0000-0000deadbeef'),
720 [
721 {'fsid': '10000000-0000-0000-0000-0000deadbeef'},
722 {'fsid': '20000000-0000-0000-0000-0000deadbeef'},
723 ],
724 None,
725 r'Cannot infer an fsid',
726 ),
727 ])
728 @mock.patch('cephadm.call')
729 def test_infer_fsid(self, _call, fsid, ceph_conf, list_daemons, result, err, cephadm_fs):
730 # build the context
731 ctx = cd.CephadmContext()
732 ctx.fsid = fsid
733
734 # mock the decorator
735 mock_fn = mock.Mock()
736 mock_fn.return_value = 0
737 infer_fsid = cd.infer_fsid(mock_fn)
738
739 # mock the ceph.conf file content
740 if ceph_conf:
741 f = cephadm_fs.create_file('ceph.conf', contents=ceph_conf)
742 ctx.config = f.path
743
744 # test
745 with mock.patch('cephadm.list_daemons', return_value=list_daemons):
746 if err:
747 with pytest.raises(cd.Error, match=err):
748 infer_fsid(ctx)
749 else:
750 infer_fsid(ctx)
751 assert ctx.fsid == result
752
753 @pytest.mark.parametrize('fsid, other_conf_files, config, name, list_daemons, result, ',
754 [
755 # per cluster conf has more precedence than default conf
756 (
757 '00000000-0000-0000-0000-0000deadbeef',
758 [cd.CEPH_DEFAULT_CONF],
759 None,
760 None,
761 [],
762 '/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/config/ceph.conf',
763 ),
764 # mon daemon conf has more precedence than cluster conf and default conf
765 (
766 '00000000-0000-0000-0000-0000deadbeef',
767 ['/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/config/ceph.conf',
768 cd.CEPH_DEFAULT_CONF],
769 None,
770 None,
771 [{'name': 'mon.a', 'fsid': '00000000-0000-0000-0000-0000deadbeef', 'style': 'cephadm:v1'}],
772 '/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/mon.a/config',
773 ),
774 # daemon conf (--name option) has more precedence than cluster, default and mon conf
775 (
776 '00000000-0000-0000-0000-0000deadbeef',
777 ['/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/config/ceph.conf',
778 '/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/mon.a/config',
779 cd.CEPH_DEFAULT_CONF],
780 None,
781 'osd.0',
782 [{'name': 'mon.a', 'fsid': '00000000-0000-0000-0000-0000deadbeef', 'style': 'cephadm:v1'},
783 {'name': 'osd.0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}],
784 '/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/osd.0/config',
785 ),
786 # user provided conf ('/foo/ceph.conf') more precedence than any other conf
787 (
788 '00000000-0000-0000-0000-0000deadbeef',
789 ['/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/config/ceph.conf',
790 cd.CEPH_DEFAULT_CONF,
791 '/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/mon.a/config'],
792 '/foo/ceph.conf',
793 None,
794 [{'name': 'mon.a', 'fsid': '00000000-0000-0000-0000-0000deadbeef', 'style': 'cephadm:v1'}],
795 '/foo/ceph.conf',
796 ),
797 ])
798 @mock.patch('cephadm.call')
799 @mock.patch('cephadm.logger')
800 def test_infer_config_precedence(self, logger, _call, other_conf_files, fsid, config, name, list_daemons, result, cephadm_fs):
801 # build the context
802 ctx = cd.CephadmContext()
803 ctx.fsid = fsid
804 ctx.config = config
805 ctx.name = name
806
807 # mock the decorator
808 mock_fn = mock.Mock()
809 mock_fn.return_value = 0
810 infer_config = cd.infer_config(mock_fn)
811
812 # mock the config file
813 cephadm_fs.create_file(result)
814
815 # mock other potential config files
816 for f in other_conf_files:
817 cephadm_fs.create_file(f)
818
819 # test
820 with mock.patch('cephadm.list_daemons', return_value=list_daemons):
821 infer_config(ctx)
822 assert ctx.config == result
823
824 @pytest.mark.parametrize('fsid, config, name, list_daemons, result, ',
825 [
826 (
827 None,
828 '/foo/bar.conf',
829 None,
830 [],
831 '/foo/bar.conf',
832 ),
833 (
834 '00000000-0000-0000-0000-0000deadbeef',
835 None,
836 None,
837 [],
838 cd.CEPH_DEFAULT_CONF,
839 ),
840 (
841 '00000000-0000-0000-0000-0000deadbeef',
842 None,
843 None,
844 [],
845 '/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/config/ceph.conf',
846 ),
847 (
848 '00000000-0000-0000-0000-0000deadbeef',
849 None,
850 None,
851 [{'name': 'mon.a', 'fsid': '00000000-0000-0000-0000-0000deadbeef', 'style': 'cephadm:v1'}],
852 '/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/mon.a/config',
853 ),
854 (
855 '00000000-0000-0000-0000-0000deadbeef',
856 None,
857 None,
858 [{'name': 'mon.a', 'fsid': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'style': 'cephadm:v1'}],
859 cd.CEPH_DEFAULT_CONF,
860 ),
861 (
862 '00000000-0000-0000-0000-0000deadbeef',
863 None,
864 None,
865 [{'name': 'mon.a', 'fsid': '00000000-0000-0000-0000-0000deadbeef', 'style': 'legacy'}],
866 cd.CEPH_DEFAULT_CONF,
867 ),
868 (
869 '00000000-0000-0000-0000-0000deadbeef',
870 None,
871 None,
872 [{'name': 'osd.0'}],
873 cd.CEPH_DEFAULT_CONF,
874 ),
875 (
876 '00000000-0000-0000-0000-0000deadbeef',
877 '/foo/bar.conf',
878 'mon.a',
879 [{'name': 'mon.a', 'style': 'cephadm:v1'}],
880 '/foo/bar.conf',
881 ),
882 (
883 '00000000-0000-0000-0000-0000deadbeef',
884 None,
885 'mon.a',
886 [],
887 '/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/mon.a/config',
888 ),
889 (
890 '00000000-0000-0000-0000-0000deadbeef',
891 None,
892 'osd.0',
893 [],
894 '/var/lib/ceph/00000000-0000-0000-0000-0000deadbeef/osd.0/config',
895 ),
896 (
897 None,
898 None,
899 None,
900 [],
901 cd.CEPH_DEFAULT_CONF,
902 ),
903 ])
904 @mock.patch('cephadm.call')
905 @mock.patch('cephadm.logger')
906 def test_infer_config(self, logger, _call, fsid, config, name, list_daemons, result, cephadm_fs):
907 # build the context
908 ctx = cd.CephadmContext()
909 ctx.fsid = fsid
910 ctx.config = config
911 ctx.name = name
912
913 # mock the decorator
914 mock_fn = mock.Mock()
915 mock_fn.return_value = 0
916 infer_config = cd.infer_config(mock_fn)
917
918 # mock the config file
919 cephadm_fs.create_file(result)
920
921 # test
922 with mock.patch('cephadm.list_daemons', return_value=list_daemons):
923 infer_config(ctx)
924 assert ctx.config == result
925
926 @mock.patch('cephadm.call')
927 def test_extract_uid_gid_fail(self, _call):
928 err = """Error: container_linux.go:370: starting container process caused: process_linux.go:459: container init caused: process_linux.go:422: setting cgroup config for procHooks process caused: Unit libpod-056038e1126191fba41d8a037275136f2d7aeec9710b9ee
929 ff792c06d8544b983.scope not found.: OCI runtime error"""
930 _call.return_value = ('', err, 127)
931 ctx = cd.CephadmContext()
932 ctx.container_engine = mock_podman()
933 with pytest.raises(cd.Error, match='OCI'):
934 cd.extract_uid_gid(ctx)
935
936 @pytest.mark.parametrize('test_input, expected', [
937 ([cd.make_fsid(), cd.make_fsid(), cd.make_fsid()], 3),
938 ([cd.make_fsid(), 'invalid-fsid', cd.make_fsid(), '0b87e50c-8e77-11ec-b890-'], 2),
939 (['f6860ec2-8e76-11ec-', '0b87e50c-8e77-11ec-b890-', ''], 0),
940 ([], 0),
941 ])
942 def test_get_ceph_cluster_count(self, test_input, expected):
943 ctx = cd.CephadmContext()
944 with mock.patch('os.listdir', return_value=test_input):
945 assert cd.get_ceph_cluster_count(ctx) == expected
946
947 def test_set_image_minimize_config(self):
948 def throw_cmd(cmd):
949 raise cd.Error(' '.join(cmd))
950 ctx = cd.CephadmContext()
951 ctx.image = 'test_image'
952 ctx.no_minimize_config = True
953 fake_cli = lambda cmd, __=None, ___=None: throw_cmd(cmd)
954 with pytest.raises(cd.Error, match='config set global container_image test_image'):
955 cd.finish_bootstrap_config(
956 ctx=ctx,
957 fsid=cd.make_fsid(),
958 config='',
959 mon_id='a', mon_dir='mon_dir',
960 mon_network=None, ipv6=False,
961 cli=fake_cli,
962 cluster_network=None,
963 ipv6_cluster_network=False
964 )
965
966
967 class TestCustomContainer(unittest.TestCase):
968 cc: cd.CustomContainer
969
970 def setUp(self):
971 self.cc = cd.CustomContainer(
972 'e863154d-33c7-4350-bca5-921e0467e55b',
973 'container',
974 config_json={
975 'entrypoint': 'bash',
976 'gid': 1000,
977 'args': [
978 '--no-healthcheck',
979 '-p 6800:6800'
980 ],
981 'envs': ['SECRET=password'],
982 'ports': [8080, 8443],
983 'volume_mounts': {
984 '/CONFIG_DIR': '/foo/conf',
985 'bar/config': '/bar:ro'
986 },
987 'bind_mounts': [
988 [
989 'type=bind',
990 'source=/CONFIG_DIR',
991 'destination=/foo/conf',
992 ''
993 ],
994 [
995 'type=bind',
996 'source=bar/config',
997 'destination=/bar:ro',
998 'ro=true'
999 ]
1000 ]
1001 },
1002 image='docker.io/library/hello-world:latest'
1003 )
1004
1005 def test_entrypoint(self):
1006 self.assertEqual(self.cc.entrypoint, 'bash')
1007
1008 def test_uid_gid(self):
1009 self.assertEqual(self.cc.uid, 65534)
1010 self.assertEqual(self.cc.gid, 1000)
1011
1012 def test_ports(self):
1013 self.assertEqual(self.cc.ports, [8080, 8443])
1014
1015 def test_get_container_args(self):
1016 result = self.cc.get_container_args()
1017 self.assertEqual(result, [
1018 '--no-healthcheck',
1019 '-p 6800:6800'
1020 ])
1021
1022 def test_get_container_envs(self):
1023 result = self.cc.get_container_envs()
1024 self.assertEqual(result, ['SECRET=password'])
1025
1026 def test_get_container_mounts(self):
1027 result = self.cc.get_container_mounts('/xyz')
1028 self.assertDictEqual(result, {
1029 '/CONFIG_DIR': '/foo/conf',
1030 '/xyz/bar/config': '/bar:ro'
1031 })
1032
1033 def test_get_container_binds(self):
1034 result = self.cc.get_container_binds('/xyz')
1035 self.assertEqual(result, [
1036 [
1037 'type=bind',
1038 'source=/CONFIG_DIR',
1039 'destination=/foo/conf',
1040 ''
1041 ],
1042 [
1043 'type=bind',
1044 'source=/xyz/bar/config',
1045 'destination=/bar:ro',
1046 'ro=true'
1047 ]
1048 ])
1049
1050
1051 class TestMaintenance:
1052 systemd_target = "ceph.00000000-0000-0000-0000-000000c0ffee.target"
1053 fsid = '0ea8cdd0-1bbf-11ec-a9c7-5254002763fa'
1054
1055 def test_systemd_target_OK(self, tmp_path):
1056 base = tmp_path
1057 wants = base / "ceph.target.wants"
1058 wants.mkdir()
1059 target = wants / TestMaintenance.systemd_target
1060 target.touch()
1061 ctx = cd.CephadmContext()
1062 ctx.unit_dir = str(base)
1063
1064 assert cd.systemd_target_state(ctx, target.name)
1065
1066 def test_systemd_target_NOTOK(self, tmp_path):
1067 base = tmp_path
1068 ctx = cd.CephadmContext()
1069 ctx.unit_dir = str(base)
1070 assert not cd.systemd_target_state(ctx, TestMaintenance.systemd_target)
1071
1072 def test_parser_OK(self):
1073 args = cd._parse_args(['host-maintenance', 'enter'])
1074 assert args.maintenance_action == 'enter'
1075
1076 def test_parser_BAD(self):
1077 with pytest.raises(SystemExit):
1078 cd._parse_args(['host-maintenance', 'wah'])
1079
1080 @mock.patch('os.listdir', return_value=[])
1081 @mock.patch('cephadm.call')
1082 @mock.patch('cephadm.systemd_target_state')
1083 def test_enter_failure_1(self, _target_state, _call, _listdir):
1084 _call.return_value = '', '', 999
1085 _target_state.return_value = True
1086 ctx: cd.CephadmContext = cd.cephadm_init_ctx(
1087 ['host-maintenance', 'enter', '--fsid', TestMaintenance.fsid])
1088 ctx.container_engine = mock_podman()
1089 retval = cd.command_maintenance(ctx)
1090 assert retval.startswith('failed')
1091
1092 @mock.patch('os.listdir', return_value=[])
1093 @mock.patch('cephadm.call')
1094 @mock.patch('cephadm.systemd_target_state')
1095 def test_enter_failure_2(self, _target_state, _call, _listdir):
1096 _call.side_effect = [('', '', 0), ('', '', 999), ('', '', 0), ('', '', 999)]
1097 _target_state.return_value = True
1098 ctx: cd.CephadmContext = cd.cephadm_init_ctx(
1099 ['host-maintenance', 'enter', '--fsid', TestMaintenance.fsid])
1100 ctx.container_engine = mock_podman()
1101 retval = cd.command_maintenance(ctx)
1102 assert retval.startswith('failed')
1103
1104 @mock.patch('os.listdir', return_value=[])
1105 @mock.patch('cephadm.call')
1106 @mock.patch('cephadm.systemd_target_state')
1107 @mock.patch('cephadm.target_exists')
1108 def test_exit_failure_1(self, _target_exists, _target_state, _call, _listdir):
1109 _call.return_value = '', '', 999
1110 _target_state.return_value = False
1111 _target_exists.return_value = True
1112 ctx: cd.CephadmContext = cd.cephadm_init_ctx(
1113 ['host-maintenance', 'exit', '--fsid', TestMaintenance.fsid])
1114 ctx.container_engine = mock_podman()
1115 retval = cd.command_maintenance(ctx)
1116 assert retval.startswith('failed')
1117
1118 @mock.patch('os.listdir', return_value=[])
1119 @mock.patch('cephadm.call')
1120 @mock.patch('cephadm.systemd_target_state')
1121 @mock.patch('cephadm.target_exists')
1122 def test_exit_failure_2(self, _target_exists, _target_state, _call, _listdir):
1123 _call.side_effect = [('', '', 0), ('', '', 999), ('', '', 0), ('', '', 999)]
1124 _target_state.return_value = False
1125 _target_exists.return_value = True
1126 ctx: cd.CephadmContext = cd.cephadm_init_ctx(
1127 ['host-maintenance', 'exit', '--fsid', TestMaintenance.fsid])
1128 ctx.container_engine = mock_podman()
1129 retval = cd.command_maintenance(ctx)
1130 assert retval.startswith('failed')
1131
1132
1133 class TestMonitoring(object):
1134 @mock.patch('cephadm.call')
1135 def test_get_version_alertmanager(self, _call):
1136 ctx = cd.CephadmContext()
1137 ctx.container_engine = mock_podman()
1138 daemon_type = 'alertmanager'
1139
1140 # binary `prometheus`
1141 _call.return_value = '', '{}, version 0.16.1'.format(daemon_type), 0
1142 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
1143 assert version == '0.16.1'
1144
1145 # binary `prometheus-alertmanager`
1146 _call.side_effect = (
1147 ('', '', 1),
1148 ('', '{}, version 0.16.1'.format(daemon_type), 0),
1149 )
1150 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
1151 assert version == '0.16.1'
1152
1153 @mock.patch('cephadm.call')
1154 def test_get_version_prometheus(self, _call):
1155 ctx = cd.CephadmContext()
1156 ctx.container_engine = mock_podman()
1157 daemon_type = 'prometheus'
1158 _call.return_value = '', '{}, version 0.16.1'.format(daemon_type), 0
1159 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
1160 assert version == '0.16.1'
1161
1162 def test_prometheus_external_url(self):
1163 ctx = cd.CephadmContext()
1164 ctx.config_json = json.dumps({'files': {}, 'retention_time': '15d'})
1165 daemon_type = 'prometheus'
1166 daemon_id = 'home'
1167 fsid = 'aaf5a720-13fe-4a3b-82b9-2d99b7fd9704'
1168 args = cd.get_daemon_args(ctx, fsid, daemon_type, daemon_id)
1169 assert any([x.startswith('--web.external-url=http://') for x in args])
1170
1171 @mock.patch('cephadm.call')
1172 def test_get_version_node_exporter(self, _call):
1173 ctx = cd.CephadmContext()
1174 ctx.container_engine = mock_podman()
1175 daemon_type = 'node-exporter'
1176 _call.return_value = '', '{}, version 0.16.1'.format(daemon_type.replace('-', '_')), 0
1177 version = cd.Monitoring.get_version(ctx, 'container_id', daemon_type)
1178 assert version == '0.16.1'
1179
1180 def test_create_daemon_dirs_prometheus(self, cephadm_fs):
1181 """
1182 Ensures the required and optional files given in the configuration are
1183 created and mapped correctly inside the container. Tests absolute and
1184 relative file paths given in the configuration.
1185 """
1186
1187 fsid = 'aaf5a720-13fe-4a3b-82b9-2d99b7fd9704'
1188 daemon_type = 'prometheus'
1189 uid, gid = 50, 50
1190 daemon_id = 'home'
1191 ctx = cd.CephadmContext()
1192 ctx.data_dir = '/somedir'
1193 ctx.config_json = json.dumps({
1194 'files': {
1195 'prometheus.yml': 'foo',
1196 '/etc/prometheus/alerting/ceph_alerts.yml': 'bar'
1197 }
1198 })
1199
1200 cd.create_daemon_dirs(ctx,
1201 fsid,
1202 daemon_type,
1203 daemon_id,
1204 uid,
1205 gid,
1206 config=None,
1207 keyring=None)
1208
1209 prefix = '{data_dir}/{fsid}/{daemon_type}.{daemon_id}'.format(
1210 data_dir=ctx.data_dir,
1211 fsid=fsid,
1212 daemon_type=daemon_type,
1213 daemon_id=daemon_id
1214 )
1215
1216 expected = {
1217 'etc/prometheus/prometheus.yml': 'foo',
1218 'etc/prometheus/alerting/ceph_alerts.yml': 'bar',
1219 }
1220
1221 for file,content in expected.items():
1222 file = os.path.join(prefix, file)
1223 assert os.path.exists(file)
1224 with open(file) as f:
1225 assert f.read() == content
1226
1227 # assert uid/gid after redeploy
1228 new_uid = uid+1
1229 new_gid = gid+1
1230 cd.create_daemon_dirs(ctx,
1231 fsid,
1232 daemon_type,
1233 daemon_id,
1234 new_uid,
1235 new_gid,
1236 config=None,
1237 keyring=None)
1238 for file,content in expected.items():
1239 file = os.path.join(prefix, file)
1240 assert os.stat(file).st_uid == new_uid
1241 assert os.stat(file).st_gid == new_gid
1242
1243
1244 class TestBootstrap(object):
1245
1246 @staticmethod
1247 def _get_cmd(*args):
1248 return [
1249 'bootstrap',
1250 '--allow-mismatched-release',
1251 '--skip-prepare-host',
1252 '--skip-dashboard',
1253 *args,
1254 ]
1255
1256
1257 ###############################################3
1258
1259 def test_config(self, cephadm_fs):
1260 conf_file = 'foo'
1261 cmd = self._get_cmd(
1262 '--mon-ip', '192.168.1.1',
1263 '--skip-mon-network',
1264 '--config', conf_file,
1265 )
1266
1267 with with_cephadm_ctx(cmd) as ctx:
1268 msg = r'No such file or directory'
1269 with pytest.raises(cd.Error, match=msg):
1270 cd.command_bootstrap(ctx)
1271
1272 cephadm_fs.create_file(conf_file)
1273 with with_cephadm_ctx(cmd) as ctx:
1274 retval = cd.command_bootstrap(ctx)
1275 assert retval == 0
1276
1277 def test_no_mon_addr(self, cephadm_fs):
1278 cmd = self._get_cmd()
1279 with with_cephadm_ctx(cmd) as ctx:
1280 msg = r'must specify --mon-ip or --mon-addrv'
1281 with pytest.raises(cd.Error, match=msg):
1282 cd.command_bootstrap(ctx)
1283
1284 def test_skip_mon_network(self, cephadm_fs):
1285 cmd = self._get_cmd('--mon-ip', '192.168.1.1')
1286
1287 with with_cephadm_ctx(cmd, list_networks={}) as ctx:
1288 msg = r'--skip-mon-network'
1289 with pytest.raises(cd.Error, match=msg):
1290 cd.command_bootstrap(ctx)
1291
1292 cmd += ['--skip-mon-network']
1293 with with_cephadm_ctx(cmd, list_networks={}) as ctx:
1294 retval = cd.command_bootstrap(ctx)
1295 assert retval == 0
1296
1297 @pytest.mark.parametrize('mon_ip, list_networks, result',
1298 [
1299 # IPv4
1300 (
1301 'eth0',
1302 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1303 False,
1304 ),
1305 (
1306 '0.0.0.0',
1307 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1308 False,
1309 ),
1310 (
1311 '192.168.1.0',
1312 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1313 False,
1314 ),
1315 (
1316 '192.168.1.1',
1317 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1318 True,
1319 ),
1320 (
1321 '192.168.1.1:1234',
1322 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1323 True,
1324 ),
1325 (
1326 '192.168.1.1:0123',
1327 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1328 True,
1329 ),
1330 # IPv6
1331 (
1332 '::',
1333 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1334 False,
1335 ),
1336 (
1337 '::ffff:192.168.1.0',
1338 {"ffff::/64": {"eth0": ["::ffff:c0a8:101"]}},
1339 False,
1340 ),
1341 (
1342 '::ffff:192.168.1.1',
1343 {"ffff::/64": {"eth0": ["::ffff:c0a8:101"]}},
1344 True,
1345 ),
1346 (
1347 '::ffff:c0a8:101',
1348 {"ffff::/64": {"eth0": ["::ffff:c0a8:101"]}},
1349 True,
1350 ),
1351 (
1352 '[::ffff:c0a8:101]:1234',
1353 {"ffff::/64": {"eth0": ["::ffff:c0a8:101"]}},
1354 True,
1355 ),
1356 (
1357 '[::ffff:c0a8:101]:0123',
1358 {"ffff::/64": {"eth0": ["::ffff:c0a8:101"]}},
1359 True,
1360 ),
1361 (
1362 '0000:0000:0000:0000:0000:FFFF:C0A8:0101',
1363 {"ffff::/64": {"eth0": ["::ffff:c0a8:101"]}},
1364 True,
1365 ),
1366 ])
1367 def test_mon_ip(self, mon_ip, list_networks, result, cephadm_fs):
1368 cmd = self._get_cmd('--mon-ip', mon_ip)
1369 if not result:
1370 with with_cephadm_ctx(cmd, list_networks=list_networks) as ctx:
1371 msg = r'--skip-mon-network'
1372 with pytest.raises(cd.Error, match=msg):
1373 cd.command_bootstrap(ctx)
1374 else:
1375 with with_cephadm_ctx(cmd, list_networks=list_networks) as ctx:
1376 retval = cd.command_bootstrap(ctx)
1377 assert retval == 0
1378
1379 @pytest.mark.parametrize('mon_addrv, list_networks, err',
1380 [
1381 # IPv4
1382 (
1383 '192.168.1.1',
1384 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1385 r'must use square backets',
1386 ),
1387 (
1388 '[192.168.1.1]',
1389 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1390 r'must include port number',
1391 ),
1392 (
1393 '[192.168.1.1:1234]',
1394 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1395 None,
1396 ),
1397 (
1398 '[192.168.1.1:0123]',
1399 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1400 None,
1401 ),
1402 (
1403 '[v2:192.168.1.1:3300,v1:192.168.1.1:6789]',
1404 {'192.168.1.0/24': {'eth0': ['192.168.1.1']}},
1405 None,
1406 ),
1407 # IPv6
1408 (
1409 '[::ffff:192.168.1.1:1234]',
1410 {'ffff::/64': {'eth0': ['::ffff:c0a8:101']}},
1411 None,
1412 ),
1413 (
1414 '[::ffff:192.168.1.1:0123]',
1415 {'ffff::/64': {'eth0': ['::ffff:c0a8:101']}},
1416 None,
1417 ),
1418 (
1419 '[0000:0000:0000:0000:0000:FFFF:C0A8:0101:1234]',
1420 {'ffff::/64': {'eth0': ['::ffff:c0a8:101']}},
1421 None,
1422 ),
1423 (
1424 '[v2:0000:0000:0000:0000:0000:FFFF:C0A8:0101:3300,v1:0000:0000:0000:0000:0000:FFFF:C0A8:0101:6789]',
1425 {'ffff::/64': {'eth0': ['::ffff:c0a8:101']}},
1426 None,
1427 ),
1428 ])
1429 def test_mon_addrv(self, mon_addrv, list_networks, err, cephadm_fs):
1430 cmd = self._get_cmd('--mon-addrv', mon_addrv)
1431 if err:
1432 with with_cephadm_ctx(cmd, list_networks=list_networks) as ctx:
1433 with pytest.raises(cd.Error, match=err):
1434 cd.command_bootstrap(ctx)
1435 else:
1436 with with_cephadm_ctx(cmd, list_networks=list_networks) as ctx:
1437 retval = cd.command_bootstrap(ctx)
1438 assert retval == 0
1439
1440 def test_allow_fqdn_hostname(self, cephadm_fs):
1441 hostname = 'foo.bar'
1442 cmd = self._get_cmd(
1443 '--mon-ip', '192.168.1.1',
1444 '--skip-mon-network',
1445 )
1446
1447 with with_cephadm_ctx(cmd, hostname=hostname) as ctx:
1448 msg = r'--allow-fqdn-hostname'
1449 with pytest.raises(cd.Error, match=msg):
1450 cd.command_bootstrap(ctx)
1451
1452 cmd += ['--allow-fqdn-hostname']
1453 with with_cephadm_ctx(cmd, hostname=hostname) as ctx:
1454 retval = cd.command_bootstrap(ctx)
1455 assert retval == 0
1456
1457 @pytest.mark.parametrize('fsid, err',
1458 [
1459 ('', None),
1460 ('00000000-0000-0000-0000-0000deadbeef', None),
1461 ('00000000-0000-0000-0000-0000deadbeez', 'not an fsid'),
1462 ])
1463 def test_fsid(self, fsid, err, cephadm_fs):
1464 cmd = self._get_cmd(
1465 '--mon-ip', '192.168.1.1',
1466 '--skip-mon-network',
1467 '--fsid', fsid,
1468 )
1469
1470 with with_cephadm_ctx(cmd) as ctx:
1471 if err:
1472 with pytest.raises(cd.Error, match=err):
1473 cd.command_bootstrap(ctx)
1474 else:
1475 retval = cd.command_bootstrap(ctx)
1476 assert retval == 0
1477
1478
1479 class TestShell(object):
1480
1481 def test_fsid(self, cephadm_fs):
1482 fsid = '00000000-0000-0000-0000-0000deadbeef'
1483
1484 cmd = ['shell', '--fsid', fsid]
1485 with with_cephadm_ctx(cmd) as ctx:
1486 retval = cd.command_shell(ctx)
1487 assert retval == 0
1488 assert ctx.fsid == fsid
1489
1490 cmd = ['shell', '--fsid', '00000000-0000-0000-0000-0000deadbeez']
1491 with with_cephadm_ctx(cmd) as ctx:
1492 err = 'not an fsid'
1493 with pytest.raises(cd.Error, match=err):
1494 retval = cd.command_shell(ctx)
1495 assert retval == 1
1496 assert ctx.fsid == None
1497
1498 s = get_ceph_conf(fsid=fsid)
1499 f = cephadm_fs.create_file('ceph.conf', contents=s)
1500
1501 cmd = ['shell', '--fsid', fsid, '--config', f.path]
1502 with with_cephadm_ctx(cmd) as ctx:
1503 retval = cd.command_shell(ctx)
1504 assert retval == 0
1505 assert ctx.fsid == fsid
1506
1507 cmd = ['shell', '--fsid', '10000000-0000-0000-0000-0000deadbeef', '--config', f.path]
1508 with with_cephadm_ctx(cmd) as ctx:
1509 err = 'fsid does not match ceph.conf'
1510 with pytest.raises(cd.Error, match=err):
1511 retval = cd.command_shell(ctx)
1512 assert retval == 1
1513 assert ctx.fsid == None
1514
1515 def test_name(self, cephadm_fs):
1516 cmd = ['shell', '--name', 'foo']
1517 with with_cephadm_ctx(cmd) as ctx:
1518 retval = cd.command_shell(ctx)
1519 assert retval == 0
1520
1521 cmd = ['shell', '--name', 'foo.bar']
1522 with with_cephadm_ctx(cmd) as ctx:
1523 err = r'must pass --fsid'
1524 with pytest.raises(cd.Error, match=err):
1525 retval = cd.command_shell(ctx)
1526 assert retval == 1
1527
1528 fsid = '00000000-0000-0000-0000-0000deadbeef'
1529 cmd = ['shell', '--name', 'foo.bar', '--fsid', fsid]
1530 with with_cephadm_ctx(cmd) as ctx:
1531 retval = cd.command_shell(ctx)
1532 assert retval == 0
1533
1534 def test_config(self, cephadm_fs):
1535 cmd = ['shell']
1536 with with_cephadm_ctx(cmd) as ctx:
1537 retval = cd.command_shell(ctx)
1538 assert retval == 0
1539 assert ctx.config == None
1540
1541 cephadm_fs.create_file(cd.CEPH_DEFAULT_CONF)
1542 with with_cephadm_ctx(cmd) as ctx:
1543 retval = cd.command_shell(ctx)
1544 assert retval == 0
1545 assert ctx.config == cd.CEPH_DEFAULT_CONF
1546
1547 cmd = ['shell', '--config', 'foo']
1548 with with_cephadm_ctx(cmd) as ctx:
1549 retval = cd.command_shell(ctx)
1550 assert retval == 0
1551 assert ctx.config == 'foo'
1552
1553 def test_keyring(self, cephadm_fs):
1554 cmd = ['shell']
1555 with with_cephadm_ctx(cmd) as ctx:
1556 retval = cd.command_shell(ctx)
1557 assert retval == 0
1558 assert ctx.keyring == None
1559
1560 cephadm_fs.create_file(cd.CEPH_DEFAULT_KEYRING)
1561 with with_cephadm_ctx(cmd) as ctx:
1562 retval = cd.command_shell(ctx)
1563 assert retval == 0
1564 assert ctx.keyring == cd.CEPH_DEFAULT_KEYRING
1565
1566 cmd = ['shell', '--keyring', 'foo']
1567 with with_cephadm_ctx(cmd) as ctx:
1568 retval = cd.command_shell(ctx)
1569 assert retval == 0
1570 assert ctx.keyring == 'foo'
1571
1572 @mock.patch('cephadm.CephContainer')
1573 def test_mount_no_dst(self, m_ceph_container, cephadm_fs):
1574 cmd = ['shell', '--mount', '/etc/foo']
1575 with with_cephadm_ctx(cmd) as ctx:
1576 retval = cd.command_shell(ctx)
1577 assert retval == 0
1578 assert m_ceph_container.call_args.kwargs['volume_mounts']['/etc/foo'] == '/mnt/foo'
1579
1580 @mock.patch('cephadm.CephContainer')
1581 def test_mount_with_dst_no_opt(self, m_ceph_container, cephadm_fs):
1582 cmd = ['shell', '--mount', '/etc/foo:/opt/foo/bar']
1583 with with_cephadm_ctx(cmd) as ctx:
1584 retval = cd.command_shell(ctx)
1585 assert retval == 0
1586 assert m_ceph_container.call_args.kwargs['volume_mounts']['/etc/foo'] == '/opt/foo/bar'
1587
1588 @mock.patch('cephadm.CephContainer')
1589 def test_mount_with_dst_and_opt(self, m_ceph_container, cephadm_fs):
1590 cmd = ['shell', '--mount', '/etc/foo:/opt/foo/bar:Z']
1591 with with_cephadm_ctx(cmd) as ctx:
1592 retval = cd.command_shell(ctx)
1593 assert retval == 0
1594 assert m_ceph_container.call_args.kwargs['volume_mounts']['/etc/foo'] == '/opt/foo/bar:Z'
1595
1596 class TestCephVolume(object):
1597
1598 @staticmethod
1599 def _get_cmd(*args):
1600 return [
1601 'ceph-volume',
1602 *args,
1603 '--', 'inventory', '--format', 'json'
1604 ]
1605
1606 def test_noop(self, cephadm_fs):
1607 cmd = self._get_cmd()
1608 with with_cephadm_ctx(cmd) as ctx:
1609 cd.command_ceph_volume(ctx)
1610 assert ctx.fsid == None
1611 assert ctx.config == None
1612 assert ctx.keyring == None
1613 assert ctx.config_json == None
1614
1615 def test_fsid(self, cephadm_fs):
1616 fsid = '00000000-0000-0000-0000-0000deadbeef'
1617
1618 cmd = self._get_cmd('--fsid', fsid)
1619 with with_cephadm_ctx(cmd) as ctx:
1620 cd.command_ceph_volume(ctx)
1621 assert ctx.fsid == fsid
1622
1623 cmd = self._get_cmd('--fsid', '00000000-0000-0000-0000-0000deadbeez')
1624 with with_cephadm_ctx(cmd) as ctx:
1625 err = 'not an fsid'
1626 with pytest.raises(cd.Error, match=err):
1627 retval = cd.command_shell(ctx)
1628 assert retval == 1
1629 assert ctx.fsid == None
1630
1631 s = get_ceph_conf(fsid=fsid)
1632 f = cephadm_fs.create_file('ceph.conf', contents=s)
1633
1634 cmd = self._get_cmd('--fsid', fsid, '--config', f.path)
1635 with with_cephadm_ctx(cmd) as ctx:
1636 cd.command_ceph_volume(ctx)
1637 assert ctx.fsid == fsid
1638
1639 cmd = self._get_cmd('--fsid', '10000000-0000-0000-0000-0000deadbeef', '--config', f.path)
1640 with with_cephadm_ctx(cmd) as ctx:
1641 err = 'fsid does not match ceph.conf'
1642 with pytest.raises(cd.Error, match=err):
1643 cd.command_ceph_volume(ctx)
1644 assert ctx.fsid == None
1645
1646 def test_config(self, cephadm_fs):
1647 cmd = self._get_cmd('--config', 'foo')
1648 with with_cephadm_ctx(cmd) as ctx:
1649 err = r'No such file or directory'
1650 with pytest.raises(cd.Error, match=err):
1651 cd.command_ceph_volume(ctx)
1652
1653 cephadm_fs.create_file('bar')
1654 cmd = self._get_cmd('--config', 'bar')
1655 with with_cephadm_ctx(cmd) as ctx:
1656 cd.command_ceph_volume(ctx)
1657 assert ctx.config == 'bar'
1658
1659 def test_keyring(self, cephadm_fs):
1660 cmd = self._get_cmd('--keyring', 'foo')
1661 with with_cephadm_ctx(cmd) as ctx:
1662 err = r'No such file or directory'
1663 with pytest.raises(cd.Error, match=err):
1664 cd.command_ceph_volume(ctx)
1665
1666 cephadm_fs.create_file('bar')
1667 cmd = self._get_cmd('--keyring', 'bar')
1668 with with_cephadm_ctx(cmd) as ctx:
1669 cd.command_ceph_volume(ctx)
1670 assert ctx.keyring == 'bar'
1671
1672
1673 class TestIscsi:
1674 def test_unit_run(self, cephadm_fs):
1675 fsid = '9b9d7609-f4d5-4aba-94c8-effa764d96c9'
1676 config_json = {
1677 'files': {'iscsi-gateway.cfg': ''}
1678 }
1679 with with_cephadm_ctx(['--image=ceph/ceph'], list_networks={}) as ctx:
1680 import json
1681 ctx.config_json = json.dumps(config_json)
1682 ctx.fsid = fsid
1683 cd.get_parm.return_value = config_json
1684 c = cd.get_container(ctx, fsid, 'iscsi', 'daemon_id')
1685
1686 cd.make_data_dir(ctx, fsid, 'iscsi', 'daemon_id')
1687 cd.deploy_daemon_units(
1688 ctx,
1689 fsid,
1690 0, 0,
1691 'iscsi',
1692 'daemon_id',
1693 c,
1694 True, True
1695 )
1696
1697 with open('/var/lib/ceph/9b9d7609-f4d5-4aba-94c8-effa764d96c9/iscsi.daemon_id/unit.run') as f:
1698 assert f.read() == """set -e
1699 if ! 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
1700 # iscsi tcmu-runner container
1701 ! /usr/bin/podman rm -f ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi.daemon_id-tcmu 2> /dev/null
1702 ! /usr/bin/podman rm -f ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi-daemon_id-tcmu 2> /dev/null
1703 /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 --pids-limit=0 -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:z -v /dev:/dev --mount type=bind,source=/lib/modules,destination=/lib/modules,ro=true ceph/ceph &
1704 # iscsi.daemon_id
1705 ! /usr/bin/podman rm -f ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi.daemon_id 2> /dev/null
1706 ! /usr/bin/podman rm -f ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi-daemon_id 2> /dev/null
1707 /usr/bin/podman run --rm --ipc=host --stop-signal=SIGTERM --net=host --entrypoint /usr/bin/rbd-target-api --privileged --group-add=disk --init --name ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi-daemon_id --pids-limit=0 -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:z -v /dev:/dev --mount type=bind,source=/lib/modules,destination=/lib/modules,ro=true ceph/ceph
1708 """
1709
1710 def test_get_container(self):
1711 """
1712 Due to a combination of socket.getfqdn() and podman's behavior to
1713 add the container name into the /etc/hosts file, we cannot use periods
1714 in container names. But we need to be able to detect old existing containers.
1715 Assert this behaviour. I think we can remove this in Ceph R
1716 """
1717 fsid = '9b9d7609-f4d5-4aba-94c8-effa764d96c9'
1718 with with_cephadm_ctx(['--image=ceph/ceph'], list_networks={}) as ctx:
1719 ctx.fsid = fsid
1720 c = cd.get_container(ctx, fsid, 'iscsi', 'something')
1721 assert c.cname == 'ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi-something'
1722 assert c.old_cname == 'ceph-9b9d7609-f4d5-4aba-94c8-effa764d96c9-iscsi.something'
1723
1724
1725 class TestCheckHost:
1726
1727 @mock.patch('cephadm.find_executable', return_value='foo')
1728 @mock.patch('cephadm.check_time_sync', return_value=True)
1729 def test_container_engine(self, find_executable, check_time_sync):
1730 ctx = cd.CephadmContext()
1731
1732 ctx.container_engine = None
1733 err = r'No container engine binary found'
1734 with pytest.raises(cd.Error, match=err):
1735 cd.command_check_host(ctx)
1736
1737 ctx.container_engine = mock_podman()
1738 cd.command_check_host(ctx)
1739
1740 ctx.container_engine = mock_docker()
1741 cd.command_check_host(ctx)
1742
1743
1744 class TestRmRepo:
1745
1746 @pytest.mark.parametrize('os_release',
1747 [
1748 # Apt
1749 dedent("""
1750 NAME="Ubuntu"
1751 VERSION="20.04 LTS (Focal Fossa)"
1752 ID=ubuntu
1753 ID_LIKE=debian
1754 PRETTY_NAME="Ubuntu 20.04 LTS"
1755 VERSION_ID="20.04"
1756 HOME_URL="https://www.ubuntu.com/"
1757 SUPPORT_URL="https://help.ubuntu.com/"
1758 BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
1759 PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
1760 VERSION_CODENAME=focal
1761 UBUNTU_CODENAME=focal
1762 """),
1763
1764 # YumDnf
1765 dedent("""
1766 NAME="CentOS Linux"
1767 VERSION="8 (Core)"
1768 ID="centos"
1769 ID_LIKE="rhel fedora"
1770 VERSION_ID="8"
1771 PLATFORM_ID="platform:el8"
1772 PRETTY_NAME="CentOS Linux 8 (Core)"
1773 ANSI_COLOR="0;31"
1774 CPE_NAME="cpe:/o:centos:centos:8"
1775 HOME_URL="https://www.centos.org/"
1776 BUG_REPORT_URL="https://bugs.centos.org/"
1777
1778 CENTOS_MANTISBT_PROJECT="CentOS-8"
1779 CENTOS_MANTISBT_PROJECT_VERSION="8"
1780 REDHAT_SUPPORT_PRODUCT="centos"
1781 REDHAT_SUPPORT_PRODUCT_VERSION="8"
1782 """),
1783
1784 # Zypper
1785 dedent("""
1786 NAME="openSUSE Tumbleweed"
1787 # VERSION="20210810"
1788 ID="opensuse-tumbleweed"
1789 ID_LIKE="opensuse suse"
1790 VERSION_ID="20210810"
1791 PRETTY_NAME="openSUSE Tumbleweed"
1792 ANSI_COLOR="0;32"
1793 CPE_NAME="cpe:/o:opensuse:tumbleweed:20210810"
1794 BUG_REPORT_URL="https://bugs.opensuse.org"
1795 HOME_URL="https://www.opensuse.org/"
1796 DOCUMENTATION_URL="https://en.opensuse.org/Portal:Tumbleweed"
1797 LOGO="distributor-logo"
1798 """),
1799 ])
1800 @mock.patch('cephadm.find_executable', return_value='foo')
1801 def test_container_engine(self, find_executable, os_release, cephadm_fs):
1802 cephadm_fs.create_file('/etc/os-release', contents=os_release)
1803 ctx = cd.CephadmContext()
1804
1805 ctx.container_engine = None
1806 cd.command_rm_repo(ctx)
1807
1808 ctx.container_engine = mock_podman()
1809 cd.command_rm_repo(ctx)
1810
1811 ctx.container_engine = mock_docker()
1812 cd.command_rm_repo(ctx)
1813
1814
1815 class TestValidateRepo:
1816
1817 @pytest.mark.parametrize('values',
1818 [
1819 # Apt - no checks
1820 dict(
1821 version="",
1822 release="pacific",
1823 err_text="",
1824 os_release=dedent("""
1825 NAME="Ubuntu"
1826 VERSION="20.04 LTS (Focal Fossa)"
1827 ID=ubuntu
1828 ID_LIKE=debian
1829 PRETTY_NAME="Ubuntu 20.04 LTS"
1830 VERSION_ID="20.04"
1831 HOME_URL="https://www.ubuntu.com/"
1832 SUPPORT_URL="https://help.ubuntu.com/"
1833 BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
1834 PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
1835 VERSION_CODENAME=focal
1836 UBUNTU_CODENAME=focal
1837 """)),
1838
1839 # YumDnf on Centos8 - OK
1840 dict(
1841 version="",
1842 release="pacific",
1843 err_text="",
1844 os_release=dedent("""
1845 NAME="CentOS Linux"
1846 VERSION="8 (Core)"
1847 ID="centos"
1848 ID_LIKE="rhel fedora"
1849 VERSION_ID="8"
1850 PLATFORM_ID="platform:el8"
1851 PRETTY_NAME="CentOS Linux 8 (Core)"
1852 ANSI_COLOR="0;31"
1853 CPE_NAME="cpe:/o:centos:centos:8"
1854 HOME_URL="https://www.centos.org/"
1855 BUG_REPORT_URL="https://bugs.centos.org/"
1856
1857 CENTOS_MANTISBT_PROJECT="CentOS-8"
1858 CENTOS_MANTISBT_PROJECT_VERSION="8"
1859 REDHAT_SUPPORT_PRODUCT="centos"
1860 REDHAT_SUPPORT_PRODUCT_VERSION="8"
1861 """)),
1862
1863 # YumDnf on Fedora - Fedora not supported
1864 dict(
1865 version="",
1866 release="pacific",
1867 err_text="does not build Fedora",
1868 os_release=dedent("""
1869 NAME="Fedora Linux"
1870 VERSION="35 (Cloud Edition)"
1871 ID=fedora
1872 VERSION_ID=35
1873 VERSION_CODENAME=""
1874 PLATFORM_ID="platform:f35"
1875 PRETTY_NAME="Fedora Linux 35 (Cloud Edition)"
1876 ANSI_COLOR="0;38;2;60;110;180"
1877 LOGO=fedora-logo-icon
1878 CPE_NAME="cpe:/o:fedoraproject:fedora:35"
1879 HOME_URL="https://fedoraproject.org/"
1880 DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f35/system-administrators-guide/"
1881 SUPPORT_URL="https://ask.fedoraproject.org/"
1882 BUG_REPORT_URL="https://bugzilla.redhat.com/"
1883 REDHAT_BUGZILLA_PRODUCT="Fedora"
1884 REDHAT_BUGZILLA_PRODUCT_VERSION=35
1885 REDHAT_SUPPORT_PRODUCT="Fedora"
1886 REDHAT_SUPPORT_PRODUCT_VERSION=35
1887 PRIVACY_POLICY_URL="https://fedoraproject.org/wiki/Legal:PrivacyPolicy"
1888 VARIANT="Cloud Edition"
1889 VARIANT_ID=cloud
1890 """)),
1891
1892 # YumDnf on Centos 7 - no pacific
1893 dict(
1894 version="",
1895 release="pacific",
1896 err_text="does not support pacific",
1897 os_release=dedent("""
1898 NAME="CentOS Linux"
1899 VERSION="7 (Core)"
1900 ID="centos"
1901 ID_LIKE="rhel fedora"
1902 VERSION_ID="7"
1903 PRETTY_NAME="CentOS Linux 7 (Core)"
1904 ANSI_COLOR="0;31"
1905 CPE_NAME="cpe:/o:centos:centos:7"
1906 HOME_URL="https://www.centos.org/"
1907 BUG_REPORT_URL="https://bugs.centos.org/"
1908
1909 CENTOS_MANTISBT_PROJECT="CentOS-7"
1910 CENTOS_MANTISBT_PROJECT_VERSION="7"
1911 REDHAT_SUPPORT_PRODUCT="centos"
1912 REDHAT_SUPPORT_PRODUCT_VERSION="7"
1913 """)),
1914
1915 # YumDnf on Centos 7 - nothing after pacific
1916 dict(
1917 version="",
1918 release="zillions",
1919 err_text="does not support pacific",
1920 os_release=dedent("""
1921 NAME="CentOS Linux"
1922 VERSION="7 (Core)"
1923 ID="centos"
1924 ID_LIKE="rhel fedora"
1925 VERSION_ID="7"
1926 PRETTY_NAME="CentOS Linux 7 (Core)"
1927 ANSI_COLOR="0;31"
1928 CPE_NAME="cpe:/o:centos:centos:7"
1929 HOME_URL="https://www.centos.org/"
1930 BUG_REPORT_URL="https://bugs.centos.org/"
1931
1932 CENTOS_MANTISBT_PROJECT="CentOS-7"
1933 CENTOS_MANTISBT_PROJECT_VERSION="7"
1934 REDHAT_SUPPORT_PRODUCT="centos"
1935 REDHAT_SUPPORT_PRODUCT_VERSION="7"
1936 """)),
1937
1938 # YumDnf on Centos 7 - nothing v16 or higher
1939 dict(
1940 version="v16.1.3",
1941 release="",
1942 err_text="does not support",
1943 os_release=dedent("""
1944 NAME="CentOS Linux"
1945 VERSION="7 (Core)"
1946 ID="centos"
1947 ID_LIKE="rhel fedora"
1948 VERSION_ID="7"
1949 PRETTY_NAME="CentOS Linux 7 (Core)"
1950 ANSI_COLOR="0;31"
1951 CPE_NAME="cpe:/o:centos:centos:7"
1952 HOME_URL="https://www.centos.org/"
1953 BUG_REPORT_URL="https://bugs.centos.org/"
1954
1955 CENTOS_MANTISBT_PROJECT="CentOS-7"
1956 CENTOS_MANTISBT_PROJECT_VERSION="7"
1957 REDHAT_SUPPORT_PRODUCT="centos"
1958 REDHAT_SUPPORT_PRODUCT_VERSION="7"
1959 """)),
1960 ])
1961 @mock.patch('cephadm.find_executable', return_value='foo')
1962 def test_distro_validation(self, find_executable, values, cephadm_fs):
1963 os_release = values['os_release']
1964 release = values['release']
1965 version = values['version']
1966 err_text = values['err_text']
1967
1968 cephadm_fs.create_file('/etc/os-release', contents=os_release)
1969 ctx = cd.CephadmContext()
1970 ctx.repo_url = 'http://localhost'
1971 pkg = cd.create_packager(ctx, stable=release, version=version)
1972
1973 if err_text:
1974 with pytest.raises(cd.Error, match=err_text):
1975 pkg.validate()
1976 else:
1977 with mock.patch('cephadm.urlopen', return_value=None):
1978 pkg.validate()
1979
1980 @pytest.mark.parametrize('values',
1981 [
1982 # Apt - not checked
1983 dict(
1984 version="",
1985 release="pacific",
1986 err_text="",
1987 os_release=dedent("""
1988 NAME="Ubuntu"
1989 VERSION="20.04 LTS (Focal Fossa)"
1990 ID=ubuntu
1991 ID_LIKE=debian
1992 PRETTY_NAME="Ubuntu 20.04 LTS"
1993 VERSION_ID="20.04"
1994 HOME_URL="https://www.ubuntu.com/"
1995 SUPPORT_URL="https://help.ubuntu.com/"
1996 BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
1997 PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
1998 VERSION_CODENAME=focal
1999 UBUNTU_CODENAME=focal
2000 """)),
2001
2002 # YumDnf on Centos8 - force failure
2003 dict(
2004 version="",
2005 release="foobar",
2006 err_text="failed to fetch repository metadata",
2007 os_release=dedent("""
2008 NAME="CentOS Linux"
2009 VERSION="8 (Core)"
2010 ID="centos"
2011 ID_LIKE="rhel fedora"
2012 VERSION_ID="8"
2013 PLATFORM_ID="platform:el8"
2014 PRETTY_NAME="CentOS Linux 8 (Core)"
2015 ANSI_COLOR="0;31"
2016 CPE_NAME="cpe:/o:centos:centos:8"
2017 HOME_URL="https://www.centos.org/"
2018 BUG_REPORT_URL="https://bugs.centos.org/"
2019
2020 CENTOS_MANTISBT_PROJECT="CentOS-8"
2021 CENTOS_MANTISBT_PROJECT_VERSION="8"
2022 REDHAT_SUPPORT_PRODUCT="centos"
2023 REDHAT_SUPPORT_PRODUCT_VERSION="8"
2024 """)),
2025 ])
2026 @mock.patch('cephadm.find_executable', return_value='foo')
2027 def test_http_validation(self, find_executable, values, cephadm_fs):
2028 from urllib.error import HTTPError
2029
2030 os_release = values['os_release']
2031 release = values['release']
2032 version = values['version']
2033 err_text = values['err_text']
2034
2035 cephadm_fs.create_file('/etc/os-release', contents=os_release)
2036 ctx = cd.CephadmContext()
2037 ctx.repo_url = 'http://localhost'
2038 pkg = cd.create_packager(ctx, stable=release, version=version)
2039
2040 with mock.patch('cephadm.urlopen') as _urlopen:
2041 _urlopen.side_effect = HTTPError(ctx.repo_url, 404, "not found", None, fp=None)
2042 if err_text:
2043 with pytest.raises(cd.Error, match=err_text):
2044 pkg.validate()
2045 else:
2046 pkg.validate()
2047
2048
2049 class TestPull:
2050
2051 @mock.patch('time.sleep')
2052 @mock.patch('cephadm.call', return_value=('', '', 0))
2053 @mock.patch('cephadm.get_image_info_from_inspect', return_value={})
2054 def test_error(self, get_image_info_from_inspect, call, sleep):
2055 ctx = cd.CephadmContext()
2056 ctx.container_engine = mock_podman()
2057 ctx.insecure = False
2058
2059 call.return_value = ('', '', 0)
2060 retval = cd.command_pull(ctx)
2061 assert retval == 0
2062
2063 err = 'maximum retries reached'
2064
2065 call.return_value = ('', 'foobar', 1)
2066 with pytest.raises(cd.Error) as e:
2067 cd.command_pull(ctx)
2068 assert err not in str(e.value)
2069
2070 call.return_value = ('', 'net/http: TLS handshake timeout', 1)
2071 with pytest.raises(cd.Error) as e:
2072 cd.command_pull(ctx)
2073 assert err in str(e.value)
2074
2075 @mock.patch('cephadm.logger')
2076 @mock.patch('cephadm.get_image_info_from_inspect', return_value={})
2077 @mock.patch('cephadm.infer_local_ceph_image', return_value='last_local_ceph_image')
2078 def test_image(self, infer_local_ceph_image, get_image_info_from_inspect, logger):
2079 cmd = ['pull']
2080 with with_cephadm_ctx(cmd) as ctx:
2081 retval = cd.command_pull(ctx)
2082 assert retval == 0
2083 assert ctx.image == cd.DEFAULT_IMAGE
2084
2085 with mock.patch.dict(os.environ, {"CEPHADM_IMAGE": 'cephadm_image_environ'}):
2086 cmd = ['pull']
2087 with with_cephadm_ctx(cmd) as ctx:
2088 retval = cd.command_pull(ctx)
2089 assert retval == 0
2090 assert ctx.image == 'cephadm_image_environ'
2091
2092 cmd = ['--image', 'cephadm_image_param', 'pull']
2093 with with_cephadm_ctx(cmd) as ctx:
2094 retval = cd.command_pull(ctx)
2095 assert retval == 0
2096 assert ctx.image == 'cephadm_image_param'
2097
2098
2099 class TestApplySpec:
2100
2101 def test_extract_host_info_from_applied_spec(self, cephadm_fs):
2102 yaml = '''---
2103 service_type: host
2104 hostname: vm-00
2105 addr: 192.168.122.44
2106 labels:
2107 - example1
2108 - example2
2109 ---
2110 service_type: host
2111 hostname: vm-01
2112 addr: 192.168.122.247
2113 labels:
2114 - grafana
2115 ---
2116 service_type: host
2117 hostname: vm-02
2118 ---
2119 ---
2120 service_type: rgw
2121 service_id: myrgw
2122 spec:
2123 rgw_frontend_ssl_certificate: |
2124 -----BEGIN PRIVATE KEY-----
2125 V2VyIGRhcyBsaWVzdCBpc3QgZG9vZi4gTG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFt
2126 ZXQsIGNvbnNldGV0dXIgc2FkaXBzY2luZyBlbGl0ciwgc2VkIGRpYW0gbm9udW15
2127 IGVpcm1vZCB0ZW1wb3IgaW52aWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWdu
2128 YSBhbGlxdXlhbSBlcmF0LCBzZWQgZGlhbSB2b2x1cHR1YS4gQXQgdmVybyBlb3Mg
2129 ZXQgYWNjdXNhbSBldCBqdXN0byBkdW8=
2130 -----END PRIVATE KEY-----
2131 -----BEGIN CERTIFICATE-----
2132 V2VyIGRhcyBsaWVzdCBpc3QgZG9vZi4gTG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFt
2133 ZXQsIGNvbnNldGV0dXIgc2FkaXBzY2luZyBlbGl0ciwgc2VkIGRpYW0gbm9udW15
2134 IGVpcm1vZCB0ZW1wb3IgaW52aWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWdu
2135 YSBhbGlxdXlhbSBlcmF0LCBzZWQgZGlhbSB2b2x1cHR1YS4gQXQgdmVybyBlb3Mg
2136 ZXQgYWNjdXNhbSBldCBqdXN0byBkdW8=
2137 -----END CERTIFICATE-----
2138 ssl: true
2139 ---
2140 '''
2141
2142 cephadm_fs.create_file('spec.yml', contents=yaml)
2143 retdic = [{'hostname': 'vm-00', 'addr': '192.168.122.44'},
2144 {'hostname': 'vm-01', 'addr': '192.168.122.247'},
2145 {'hostname': 'vm-02',}]
2146
2147 with open('spec.yml') as f:
2148 dic = cd._extract_host_info_from_applied_spec(f)
2149 assert dic == retdic
2150
2151 @mock.patch('cephadm.call', return_value=('', '', 0))
2152 def test_distribute_ssh_keys(self, call):
2153 ctx = cd.CephadmContext()
2154 ctx.ssh_public_key = None
2155 ctx.ssh_user = 'root'
2156
2157 host_spec = {'service_type': 'host', 'hostname': 'vm-02', 'addr': '192.168.122.165'}
2158
2159 retval = cd._distribute_ssh_keys(ctx, host_spec, 'bootstrap_hostname')
2160
2161 assert retval == 0
2162
2163 call.return_value = ('', '', 1)
2164
2165 retval = cd._distribute_ssh_keys(ctx, host_spec, 'bootstrap_hostname')
2166
2167 assert retval == 1
2168
2169
2170 class TestSNMPGateway:
2171 V2c_config = {
2172 'snmp_community': 'public',
2173 'destination': '192.168.1.10:162',
2174 'snmp_version': 'V2c',
2175 }
2176 V3_no_priv_config = {
2177 'destination': '192.168.1.10:162',
2178 'snmp_version': 'V3',
2179 'snmp_v3_auth_username': 'myuser',
2180 'snmp_v3_auth_password': 'mypassword',
2181 'snmp_v3_auth_protocol': 'SHA',
2182 'snmp_v3_engine_id': '8000C53F00000000',
2183 }
2184 V3_priv_config = {
2185 'destination': '192.168.1.10:162',
2186 'snmp_version': 'V3',
2187 'snmp_v3_auth_username': 'myuser',
2188 'snmp_v3_auth_password': 'mypassword',
2189 'snmp_v3_auth_protocol': 'SHA',
2190 'snmp_v3_priv_protocol': 'DES',
2191 'snmp_v3_priv_password': 'mysecret',
2192 'snmp_v3_engine_id': '8000C53F00000000',
2193 }
2194 no_destination_config = {
2195 'snmp_version': 'V3',
2196 'snmp_v3_auth_username': 'myuser',
2197 'snmp_v3_auth_password': 'mypassword',
2198 'snmp_v3_auth_protocol': 'SHA',
2199 'snmp_v3_priv_protocol': 'DES',
2200 'snmp_v3_priv_password': 'mysecret',
2201 'snmp_v3_engine_id': '8000C53F00000000',
2202 }
2203 bad_version_config = {
2204 'snmp_community': 'public',
2205 'destination': '192.168.1.10:162',
2206 'snmp_version': 'V1',
2207 }
2208
2209 def test_unit_run_V2c(self, cephadm_fs):
2210 fsid = 'ca734440-3dc6-11ec-9b98-5254002537a6'
2211 with with_cephadm_ctx(['--image=docker.io/maxwo/snmp-notifier:v1.2.1'], list_networks={}) as ctx:
2212 import json
2213 ctx.config_json = json.dumps(self.V2c_config)
2214 ctx.fsid = fsid
2215 ctx.tcp_ports = '9464'
2216 cd.get_parm.return_value = self.V2c_config
2217 c = cd.get_container(ctx, fsid, 'snmp-gateway', 'daemon_id')
2218
2219 cd.make_data_dir(ctx, fsid, 'snmp-gateway', 'daemon_id')
2220
2221 cd.create_daemon_dirs(ctx, fsid, 'snmp-gateway', 'daemon_id', 0, 0)
2222 with open(f'/var/lib/ceph/{fsid}/snmp-gateway.daemon_id/snmp-gateway.conf', 'r') as f:
2223 conf = f.read().rstrip()
2224 assert conf == 'SNMP_NOTIFIER_COMMUNITY=public'
2225
2226 cd.deploy_daemon_units(
2227 ctx,
2228 fsid,
2229 0, 0,
2230 'snmp-gateway',
2231 'daemon_id',
2232 c,
2233 True, True
2234 )
2235 with open(f'/var/lib/ceph/{fsid}/snmp-gateway.daemon_id/unit.run', 'r') as f:
2236 run_cmd = f.readlines()[-1].rstrip()
2237 assert run_cmd.endswith('docker.io/maxwo/snmp-notifier:v1.2.1 --web.listen-address=:9464 --snmp.destination=192.168.1.10:162 --snmp.version=V2c --log.level=info --snmp.trap-description-template=/etc/snmp_notifier/description-template.tpl')
2238
2239 def test_unit_run_V3_noPriv(self, cephadm_fs):
2240 fsid = 'ca734440-3dc6-11ec-9b98-5254002537a6'
2241 with with_cephadm_ctx(['--image=docker.io/maxwo/snmp-notifier:v1.2.1'], list_networks={}) as ctx:
2242 import json
2243 ctx.config_json = json.dumps(self.V3_no_priv_config)
2244 ctx.fsid = fsid
2245 ctx.tcp_ports = '9465'
2246 cd.get_parm.return_value = self.V3_no_priv_config
2247 c = cd.get_container(ctx, fsid, 'snmp-gateway', 'daemon_id')
2248
2249 cd.make_data_dir(ctx, fsid, 'snmp-gateway', 'daemon_id')
2250
2251 cd.create_daemon_dirs(ctx, fsid, 'snmp-gateway', 'daemon_id', 0, 0)
2252 with open(f'/var/lib/ceph/{fsid}/snmp-gateway.daemon_id/snmp-gateway.conf', 'r') as f:
2253 conf = f.read()
2254 assert conf == 'SNMP_NOTIFIER_AUTH_USERNAME=myuser\nSNMP_NOTIFIER_AUTH_PASSWORD=mypassword\n'
2255
2256 cd.deploy_daemon_units(
2257 ctx,
2258 fsid,
2259 0, 0,
2260 'snmp-gateway',
2261 'daemon_id',
2262 c,
2263 True, True
2264 )
2265 with open(f'/var/lib/ceph/{fsid}/snmp-gateway.daemon_id/unit.run', 'r') as f:
2266 run_cmd = f.readlines()[-1].rstrip()
2267 assert run_cmd.endswith('docker.io/maxwo/snmp-notifier:v1.2.1 --web.listen-address=:9465 --snmp.destination=192.168.1.10:162 --snmp.version=V3 --log.level=info --snmp.trap-description-template=/etc/snmp_notifier/description-template.tpl --snmp.authentication-enabled --snmp.authentication-protocol=SHA --snmp.security-engine-id=8000C53F00000000')
2268
2269 def test_unit_run_V3_Priv(self, cephadm_fs):
2270 fsid = 'ca734440-3dc6-11ec-9b98-5254002537a6'
2271 with with_cephadm_ctx(['--image=docker.io/maxwo/snmp-notifier:v1.2.1'], list_networks={}) as ctx:
2272 import json
2273 ctx.config_json = json.dumps(self.V3_priv_config)
2274 ctx.fsid = fsid
2275 ctx.tcp_ports = '9464'
2276 cd.get_parm.return_value = self.V3_priv_config
2277 c = cd.get_container(ctx, fsid, 'snmp-gateway', 'daemon_id')
2278
2279 cd.make_data_dir(ctx, fsid, 'snmp-gateway', 'daemon_id')
2280
2281 cd.create_daemon_dirs(ctx, fsid, 'snmp-gateway', 'daemon_id', 0, 0)
2282 with open(f'/var/lib/ceph/{fsid}/snmp-gateway.daemon_id/snmp-gateway.conf', 'r') as f:
2283 conf = f.read()
2284 assert conf == 'SNMP_NOTIFIER_AUTH_USERNAME=myuser\nSNMP_NOTIFIER_AUTH_PASSWORD=mypassword\nSNMP_NOTIFIER_PRIV_PASSWORD=mysecret\n'
2285
2286 cd.deploy_daemon_units(
2287 ctx,
2288 fsid,
2289 0, 0,
2290 'snmp-gateway',
2291 'daemon_id',
2292 c,
2293 True, True
2294 )
2295 with open(f'/var/lib/ceph/{fsid}/snmp-gateway.daemon_id/unit.run', 'r') as f:
2296 run_cmd = f.readlines()[-1].rstrip()
2297 assert run_cmd.endswith('docker.io/maxwo/snmp-notifier:v1.2.1 --web.listen-address=:9464 --snmp.destination=192.168.1.10:162 --snmp.version=V3 --log.level=info --snmp.trap-description-template=/etc/snmp_notifier/description-template.tpl --snmp.authentication-enabled --snmp.authentication-protocol=SHA --snmp.security-engine-id=8000C53F00000000 --snmp.private-enabled --snmp.private-protocol=DES')
2298
2299 def test_unit_run_no_dest(self, cephadm_fs):
2300 fsid = 'ca734440-3dc6-11ec-9b98-5254002537a6'
2301 with with_cephadm_ctx(['--image=docker.io/maxwo/snmp-notifier:v1.2.1'], list_networks={}) as ctx:
2302 import json
2303 ctx.config_json = json.dumps(self.no_destination_config)
2304 ctx.fsid = fsid
2305 ctx.tcp_ports = '9464'
2306 cd.get_parm.return_value = self.no_destination_config
2307
2308 with pytest.raises(Exception) as e:
2309 c = cd.get_container(ctx, fsid, 'snmp-gateway', 'daemon_id')
2310 assert str(e.value) == "config is missing destination attribute(<ip>:<port>) of the target SNMP listener"
2311
2312 def test_unit_run_bad_version(self, cephadm_fs):
2313 fsid = 'ca734440-3dc6-11ec-9b98-5254002537a6'
2314 with with_cephadm_ctx(['--image=docker.io/maxwo/snmp-notifier:v1.2.1'], list_networks={}) as ctx:
2315 import json
2316 ctx.config_json = json.dumps(self.bad_version_config)
2317 ctx.fsid = fsid
2318 ctx.tcp_ports = '9464'
2319 cd.get_parm.return_value = self.bad_version_config
2320
2321 with pytest.raises(Exception) as e:
2322 c = cd.get_container(ctx, fsid, 'snmp-gateway', 'daemon_id')
2323 assert str(e.value) == 'not a valid snmp version: V1'
2324
2325 class TestNetworkValidation:
2326
2327 def test_ipv4_subnet(self):
2328 rc, v, msg = cd.check_subnet('192.168.1.0/24')
2329 assert rc == 0 and v[0] == 4
2330
2331 def test_ipv4_subnet_list(self):
2332 rc, v, msg = cd.check_subnet('192.168.1.0/24,10.90.90.0/24')
2333 assert rc == 0 and not msg
2334
2335 def test_ipv4_subnet_list_with_spaces(self):
2336 rc, v, msg = cd.check_subnet('192.168.1.0/24, 10.90.90.0/24 ')
2337 assert rc == 0 and not msg
2338
2339 def test_ipv4_subnet_badlist(self):
2340 rc, v, msg = cd.check_subnet('192.168.1.0/24,192.168.1.1')
2341 assert rc == 1 and msg
2342
2343 def test_ipv4_subnet_mixed(self):
2344 rc, v, msg = cd.check_subnet('192.168.100.0/24,fe80::/64')
2345 assert rc == 0 and v == [4,6]
2346
2347 def test_ipv6_subnet(self):
2348 rc, v, msg = cd.check_subnet('fe80::/64')
2349 assert rc == 0 and v[0] == 6
2350
2351 def test_subnet_mask_missing(self):
2352 rc, v, msg = cd.check_subnet('192.168.1.58')
2353 assert rc == 1 and msg
2354
2355 def test_subnet_mask_junk(self):
2356 rc, v, msg = cd.check_subnet('wah')
2357 assert rc == 1 and msg
2358
2359 def test_ip_in_subnet(self):
2360 # valid ip and only one valid subnet
2361 rc = cd.ip_in_subnets('192.168.100.1', '192.168.100.0/24')
2362 assert rc is True
2363
2364 # valid ip and valid subnets list without spaces
2365 rc = cd.ip_in_subnets('192.168.100.1', '192.168.100.0/24,10.90.90.0/24')
2366 assert rc is True
2367
2368 # valid ip and valid subnets list with spaces
2369 rc = cd.ip_in_subnets('10.90.90.2', '192.168.1.0/24, 192.168.100.0/24, 10.90.90.0/24')
2370 assert rc is True
2371
2372 # valid ip that doesn't belong to any subnet
2373 rc = cd.ip_in_subnets('192.168.100.2', '192.168.50.0/24, 10.90.90.0/24')
2374 assert rc is False
2375
2376 # valid ip that doesn't belong to the subnet (only 14 hosts)
2377 rc = cd.ip_in_subnets('192.168.100.20', '192.168.100.0/28')
2378 assert rc is False
2379
2380 # valid ip and valid IPV6 network
2381 rc = cd.ip_in_subnets('fe80::5054:ff:fef4:873a', 'fe80::/64')
2382 assert rc is True
2383
2384 # valid wrapped ip and valid IPV6 network
2385 rc = cd.ip_in_subnets('[fe80::5054:ff:fef4:873a]', 'fe80::/64')
2386 assert rc is True
2387
2388 # valid ip and that doesn't belong to IPV6 network
2389 rc = cd.ip_in_subnets('fe80::5054:ff:fef4:873a', '2001:db8:85a3::/64')
2390 assert rc is False
2391
2392 # invalid IPv4 and valid subnets list
2393 with pytest.raises(Exception):
2394 rc = cd.ip_in_sublets('10.90.200.', '192.168.1.0/24, 192.168.100.0/24, 10.90.90.0/24')
2395
2396 # invalid IPv6 and valid subnets list
2397 with pytest.raises(Exception):
2398 rc = cd.ip_in_sublets('fe80:2030:31:24', 'fe80::/64')
2399
2400
2401 @pytest.mark.parametrize("conf", [
2402 """[global]
2403 public_network='1.1.1.0/24,2.2.2.0/24'
2404 cluster_network="3.3.3.0/24, 4.4.4.0/24"
2405 """,
2406 """[global]
2407 public_network=" 1.1.1.0/24,2.2.2.0/24 "
2408 cluster_network=3.3.3.0/24, 4.4.4.0/24
2409 """,
2410 """[global]
2411 public_network= 1.1.1.0/24, 2.2.2.0/24
2412 cluster_network='3.3.3.0/24,4.4.4.0/24'
2413 """])
2414 @mock.patch('cephadm.list_networks')
2415 def test_get_networks_from_conf(self, _list_networks, conf, cephadm_fs):
2416 cephadm_fs.create_file('ceph.conf', contents=conf)
2417 _list_networks.return_value = {'1.1.1.0/24': {'eth0': ['1.1.1.1']},
2418 '2.2.2.0/24': {'eth1': ['2.2.2.2']},
2419 '3.3.3.0/24': {'eth2': ['3.3.3.3']},
2420 '4.4.4.0/24': {'eth3': ['4.4.4.4']}}
2421 ctx = cd.CephadmContext()
2422 ctx.config = 'ceph.conf'
2423 ctx.mon_ip = '1.1.1.1'
2424 ctx.cluster_network = None
2425 # what the cephadm module does with the public network string is
2426 # [x.strip() for x in out.split(',')]
2427 # so we must make sure our output, through that alteration,
2428 # generates correctly formatted networks
2429 def _str_to_networks(s):
2430 return [x.strip() for x in s.split(',')]
2431 public_network = cd.get_public_net_from_cfg(ctx)
2432 assert _str_to_networks(public_network) == ['1.1.1.0/24', '2.2.2.0/24']
2433 cluster_network, ipv6 = cd.prepare_cluster_network(ctx)
2434 assert not ipv6
2435 assert _str_to_networks(cluster_network) == ['3.3.3.0/24', '4.4.4.0/24']
2436
2437 class TestRescan(fake_filesystem_unittest.TestCase):
2438
2439 def setUp(self):
2440 self.setUpPyfakefs()
2441 if not fake_filesystem.is_root():
2442 fake_filesystem.set_uid(0)
2443
2444 self.fs.create_dir('/sys/class')
2445 self.ctx = cd.CephadmContext()
2446 self.ctx.func = cd.command_rescan_disks
2447
2448 def test_no_hbas(self):
2449 out = cd.command_rescan_disks(self.ctx)
2450 assert out == 'Ok. No compatible HBAs found'
2451
2452 def test_success(self):
2453 self.fs.create_file('/sys/class/scsi_host/host0/scan')
2454 self.fs.create_file('/sys/class/scsi_host/host1/scan')
2455 out = cd.command_rescan_disks(self.ctx)
2456 assert out.startswith('Ok. 2 adapters detected: 2 rescanned, 0 skipped, 0 failed')
2457
2458 def test_skip_usb_adapter(self):
2459 self.fs.create_file('/sys/class/scsi_host/host0/scan')
2460 self.fs.create_file('/sys/class/scsi_host/host1/scan')
2461 self.fs.create_file('/sys/class/scsi_host/host1/proc_name', contents='usb-storage')
2462 out = cd.command_rescan_disks(self.ctx)
2463 assert out.startswith('Ok. 2 adapters detected: 1 rescanned, 1 skipped, 0 failed')
2464
2465 def test_skip_unknown_adapter(self):
2466 self.fs.create_file('/sys/class/scsi_host/host0/scan')
2467 self.fs.create_file('/sys/class/scsi_host/host1/scan')
2468 self.fs.create_file('/sys/class/scsi_host/host1/proc_name', contents='unknown')
2469 out = cd.command_rescan_disks(self.ctx)
2470 assert out.startswith('Ok. 2 adapters detected: 1 rescanned, 1 skipped, 0 failed')