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