diff --git a/src/cephadm/cephadm b/src/cephadm/cephadm index e78ef5f4f88..0de6dc4e523 100755 --- a/src/cephadm/cephadm +++ b/src/cephadm/cephadm @@ -113,6 +113,7 @@ cached_stdin = None class EndPoint: """EndPoint representing an ip:port format""" + def __init__(self, ip: str, port: int) -> None: self.ip = ip self.port = port @@ -124,6 +125,28 @@ class EndPoint: return f'{self.ip}:{self.port}' +class ContainerInfo: + def __init__(self, container_id: str, + image_name: str, + image_id: str, + start: str, + version: str) -> None: + self.container_id = container_id + self.image_name = image_name + self.image_id = image_id + self.start = start + self.version = version + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, ContainerInfo): + return NotImplemented + return (self.container_id == other.container_id + and self.image_name == other.image_name + and self.image_id == other.image_id + and self.start == other.start + and self.version == other.version) + + class BaseConfig: def __init__(self) -> None: @@ -330,7 +353,7 @@ class UnauthorizedRegistryError(Error): class Ceph(object): - daemons = ('mon', 'mgr', 'mds', 'osd', 'rgw', 'rbd-mirror', + daemons = ('mon', 'mgr', 'osd', 'mds', 'rgw', 'rbd-mirror', 'crash', 'cephfs-mirror') ################################## @@ -2034,7 +2057,7 @@ def infer_image(func: FuncT) -> FuncT: if not ctx.image: ctx.image = os.environ.get('CEPHADM_IMAGE') if not ctx.image: - ctx.image = get_last_local_ceph_image(ctx, ctx.container_engine.path) + ctx.image = infer_local_ceph_image(ctx, ctx.container_engine.path) if not ctx.image: ctx.image = _get_default_image(ctx) return func(ctx) @@ -2066,24 +2089,70 @@ def default_image(func: FuncT) -> FuncT: return cast(FuncT, _default_image) -def get_last_local_ceph_image(ctx: CephadmContext, container_path: str) -> Optional[str]: +def get_container_info(ctx: CephadmContext, daemon_filter: str, by_name: bool) -> Optional[ContainerInfo]: """ + :param ctx: Cephadm context + :param daemon_filter: daemon name or type + :param by_name: must be set to True if daemon name is provided + :return: Container information or None + """ + def daemon_name_or_type(daemon: Dict[str, str]) -> str: + return daemon['name'] if by_name else daemon['name'].split('.', 1)[0] + + if by_name and '.' not in daemon_filter: + logger.warning(f'Trying to get container info using invalid daemon name {daemon_filter}') + return None + daemons = list_daemons(ctx, detail=False) + matching_daemons = [d for d in daemons if daemon_name_or_type(d) == daemon_filter and d['fsid'] == ctx.fsid] + if matching_daemons: + d_type, d_id = matching_daemons[0]['name'].split('.', 1) + out, _, code = get_container_stats(ctx, ctx.container_engine.path, ctx.fsid, d_type, d_id) + if not code: + (container_id, image_name, image_id, start, version) = out.strip().split(',') + return ContainerInfo(container_id, image_name, image_id, start, version) + return None + + +def infer_local_ceph_image(ctx: CephadmContext, container_path: str) -> Optional[str]: + """ + Infer the local ceph image based on the following priority criteria: + 1- the image specified by --image arg (if provided). + 2- the same image as the daemon container specified by --name arg (if provided). + 3- image used by any ceph container running on the host. In this case we use daemon types. + 4- if no container is found then we use the most ceph recent image on the host. + + Note: any selected container must have the same fsid inferred previously. + :return: The most recent local ceph image (already pulled) """ + # '|' special character is used to separate the output fields into: + # - Repository@digest + # - Image Id + # - Image Tag + # - Image creation date out, _, _ = call_throws(ctx, [container_path, 'images', '--filter', 'label=ceph=True', '--filter', 'dangling=false', - '--format', '{{.Repository}}@{{.Digest}}']) - return _filter_last_local_ceph_image(out) + '--format', '{{.Repository}}@{{.Digest}}|{{.ID}}|{{.Tag}}|{{.CreatedAt}}']) + container_info = None + daemon_name = ctx.name if ('name' in ctx and ctx.name and '.' in ctx.name) else None + daemons_ls = [daemon_name] if daemon_name is not None else Ceph.daemons # daemon types: 'mon', 'mgr', etc + for daemon in daemons_ls: + container_info = get_container_info(ctx, daemon, daemon_name is not None) + if container_info is not None: + logger.debug(f"Using container info for daemon '{daemon}'") + break -def _filter_last_local_ceph_image(out): - # type: (str) -> Optional[str] for image in out.splitlines(): - if image and not image.endswith('@'): - logger.info('Using recent ceph image %s' % image) - return image + if image and not image.isspace(): + (digest, image_id, tag, created_date) = image.lstrip().split('|') + if container_info is not None and image_id not in container_info.image_id: + continue + if digest and not digest.endswith('@'): + logger.info(f"Using ceph image with id '{image_id}' and tag '{tag}' created on {created_date}\n{digest}") + return digest return None diff --git a/src/cephadm/tests/fixtures.py b/src/cephadm/tests/fixtures.py index 44f792f39cb..1f5f1712148 100644 --- a/src/cephadm/tests/fixtures.py +++ b/src/cephadm/tests/fixtures.py @@ -98,6 +98,7 @@ def with_cephadm_ctx( mock.patch('cephadm.call_timeout', return_value=0), \ mock.patch('cephadm.find_executable', return_value='foo'), \ mock.patch('cephadm.is_available', return_value=True), \ + mock.patch('cephadm.get_container_info', return_value=None), \ mock.patch('cephadm.json_loads_retry', return_value={'epoch' : 1}), \ mock.patch('socket.gethostname', return_value=hostname): ctx: cd.CephadmContext = cd.cephadm_init_ctx(cmd) diff --git a/src/cephadm/tests/test_cephadm.py b/src/cephadm/tests/test_cephadm.py index adac2d8c125..cd946cff14e 100644 --- a/src/cephadm/tests/test_cephadm.py +++ b/src/cephadm/tests/test_cephadm.py @@ -348,14 +348,203 @@ class TestCephAdm(object): result = cd.dict_get_join({'a': 1}, 'a') assert result == 1 - def test_last_local_images(self): - out = ''' -docker.io/ceph/daemon-base@ -docker.io/ceph/ceph:v15.2.5 -docker.io/ceph/daemon-base:octopus - ''' - image = cd._filter_last_local_ceph_image(out) - assert image == 'docker.io/ceph/ceph:v15.2.5' + @mock.patch('os.listdir', return_value=[]) + @mock.patch('cephadm.logger') + def test_infer_local_ceph_image(self, _logger, _listdir): + ctx = cd.CephadmContext() + ctx.fsid = '00000000-0000-0000-0000-0000deadbeez' + ctx.container_engine = mock_podman() + + # make sure the right image is selected when container is found + cinfo = cd.ContainerInfo('935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972', + 'registry.hub.docker.com/rkachach/ceph:custom-v0.5', + '514e6a882f6e74806a5856468489eeff8d7106095557578da96935e4d0ba4d9d', + '2022-04-19 13:45:20.97146228 +0000 UTC', + '') + out = '''quay.ceph.io/ceph-ci/ceph@sha256:87f200536bb887b36b959e887d5984dd7a3f008a23aa1f283ab55d48b22c6185|dad864ee21e9|master|2022-03-23 16:29:19 +0000 UTC + quay.ceph.io/ceph-ci/ceph@sha256:b50b130fcda2a19f8507ddde3435bb4722266956e1858ac395c838bc1dcf1c0e|514e6a882f6e|pacific|2022-03-23 15:58:34 +0000 UTC + docker.io/ceph/ceph@sha256:939a46c06b334e094901560c8346de33c00309e3e3968a2db240eb4897c6a508|666bbfa87e8d|v15.2.5|2020-09-16 14:15:15 +0000 UTC''' + with mock.patch('cephadm.call_throws', return_value=(out, '', '')): + with mock.patch('cephadm.get_container_info', return_value=cinfo): + image = cd.infer_local_ceph_image(ctx, ctx.container_engine) + assert image == 'quay.ceph.io/ceph-ci/ceph@sha256:b50b130fcda2a19f8507ddde3435bb4722266956e1858ac395c838bc1dcf1c0e' + + # make sure first valid image is used when no container_info is found + out = '''quay.ceph.io/ceph-ci/ceph@sha256:87f200536bb887b36b959e887d5984dd7a3f008a23aa1f283ab55d48b22c6185|dad864ee21e9|master|2022-03-23 16:29:19 +0000 UTC + quay.ceph.io/ceph-ci/ceph@sha256:b50b130fcda2a19f8507ddde3435bb4722266956e1858ac395c838bc1dcf1c0e|514e6a882f6e|pacific|2022-03-23 15:58:34 +0000 UTC + docker.io/ceph/ceph@sha256:939a46c06b334e094901560c8346de33c00309e3e3968a2db240eb4897c6a508|666bbfa87e8d|v15.2.5|2020-09-16 14:15:15 +0000 UTC''' + with mock.patch('cephadm.call_throws', return_value=(out, '', '')): + with mock.patch('cephadm.get_container_info', return_value=None): + image = cd.infer_local_ceph_image(ctx, ctx.container_engine) + assert image == 'quay.ceph.io/ceph-ci/ceph@sha256:87f200536bb887b36b959e887d5984dd7a3f008a23aa1f283ab55d48b22c6185' + + # make sure images without digest are discarded (no container_info is found) + out = '''quay.ceph.io/ceph-ci/ceph@||| + docker.io/ceph/ceph@||| + docker.io/ceph/ceph@sha256:939a46c06b334e094901560c8346de33c00309e3e3968a2db240eb4897c6a508|666bbfa87e8d|v15.2.5|2020-09-16 14:15:15 +0000 UTC''' + with mock.patch('cephadm.call_throws', return_value=(out, '', '')): + with mock.patch('cephadm.get_container_info', return_value=None): + image = cd.infer_local_ceph_image(ctx, ctx.container_engine) + assert image == 'docker.io/ceph/ceph@sha256:939a46c06b334e094901560c8346de33c00309e3e3968a2db240eb4897c6a508' + + + + @pytest.mark.parametrize('daemon_filter, by_name, daemon_list, container_stats, output', + [ + # get container info by type ('mon') + ( + 'mon', + False, + [ + {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}, + {'name': 'mgr.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}, + ], + ("935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972,registry.hub.docker.com/rkachach/ceph:custom-v0.5,666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4,2022-04-19 13:45:20.97146228 +0000 UTC,", + "", + 0), + cd.ContainerInfo('935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972', + 'registry.hub.docker.com/rkachach/ceph:custom-v0.5', + '666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4', + '2022-04-19 13:45:20.97146228 +0000 UTC', + '') + ), + # get container info by name ('mon.ceph-node-0') + ( + 'mon.ceph-node-0', + True, + [ + {'name': 'mgr.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}, + {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}, + ], + ("935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972,registry.hub.docker.com/rkachach/ceph:custom-v0.5,666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4,2022-04-19 13:45:20.97146228 +0000 UTC,", + "", + 0), + cd.ContainerInfo('935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972', + 'registry.hub.docker.com/rkachach/ceph:custom-v0.5', + '666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4', + '2022-04-19 13:45:20.97146228 +0000 UTC', + '') + ), + # get container info by name (same daemon but two different fsids) + ( + 'mon.ceph-node-0', + True, + [ + {'name': 'mon.ceph-node-0', 'fsid': '10000000-0000-0000-0000-0000deadbeef'}, + {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}, + ], + ("935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972,registry.hub.docker.com/rkachach/ceph:custom-v0.5,666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4,2022-04-19 13:45:20.97146228 +0000 UTC,", + "", + 0), + cd.ContainerInfo('935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972', + 'registry.hub.docker.com/rkachach/ceph:custom-v0.5', + '666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4', + '2022-04-19 13:45:20.97146228 +0000 UTC', + '') + ), + # get container info by type (bad container stats: 127 code) + ( + 'mon', + False, + [ + {'name': 'mon.ceph-node-0', 'fsid': '00000000-FFFF-0000-0000-0000deadbeef'}, + {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}, + ], + ("", + "", + 127), + None + ), + # get container info by name (bad container stats: 127 code) + ( + 'mon.ceph-node-0', + True, + [ + {'name': 'mgr.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}, + {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}, + ], + ("", + "", + 127), + None + ), + # get container info by invalid name (doens't contain '.') + ( + 'mon-ceph-node-0', + True, + [ + {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}, + {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}, + ], + ("935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972,registry.hub.docker.com/rkachach/ceph:custom-v0.5,666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4,2022-04-19 13:45:20.97146228 +0000 UTC,", + "", + 0), + None + ), + # get container info by invalid name (empty) + ( + '', + True, + [ + {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}, + {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}, + ], + ("935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972,registry.hub.docker.com/rkachach/ceph:custom-v0.5,666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4,2022-04-19 13:45:20.97146228 +0000 UTC,", + "", + 0), + None + ), + # get container info by invalid type (empty) + ( + '', + False, + [ + {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}, + {'name': 'mon.ceph-node-0', 'fsid': '00000000-0000-0000-0000-0000deadbeef'}, + ], + ("935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972,registry.hub.docker.com/rkachach/ceph:custom-v0.5,666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4,2022-04-19 13:45:20.97146228 +0000 UTC,", + "", + 0), + None + ), + # get container info by name: no match (invalid fsid) + ( + 'mon', + False, + [ + {'name': 'mon.ceph-node-0', 'fsid': '00000000-1111-0000-0000-0000deadbeef'}, + {'name': 'mon.ceph-node-0', 'fsid': '00000000-2222-0000-0000-0000deadbeef'}, + ], + ("935b549714b8f007c6a4e29c758689cf9e8e69f2e0f51180506492974b90a972,registry.hub.docker.com/rkachach/ceph:custom-v0.5,666bbfa87e8df05702d6172cae11dd7bc48efb1d94f1b9e492952f19647199a4,2022-04-19 13:45:20.97146228 +0000 UTC,", + "", + 0), + None + ), + # get container info by name: no match + ( + 'mon.ceph-node-0', + True, + [], + None, + None + ), + # get container info by type: no match + ( + 'mgr', + False, + [], + None, + None + ), + ]) + def test_get_container_info(self, daemon_filter, by_name, daemon_list, container_stats, output): + cd.logger = mock.Mock() + ctx = cd.CephadmContext() + ctx.fsid = '00000000-0000-0000-0000-0000deadbeef' + ctx.container_engine = mock_podman() + with mock.patch('cephadm.list_daemons', return_value=daemon_list): + with mock.patch('cephadm.get_container_stats', return_value=container_stats): + assert cd.get_container_info(ctx, daemon_filter, by_name) == output def test_should_log_to_journald(self): ctx = cd.CephadmContext() @@ -1796,8 +1985,8 @@ class TestPull: @mock.patch('cephadm.logger') @mock.patch('cephadm.get_image_info_from_inspect', return_value={}) - @mock.patch('cephadm.get_last_local_ceph_image', return_value='last_local_ceph_image') - def test_image(self, get_last_local_ceph_image, get_image_info_from_inspect, logger): + @mock.patch('cephadm.infer_local_ceph_image', return_value='last_local_ceph_image') + def test_image(self, infer_local_ceph_image, get_image_info_from_inspect, logger): cmd = ['pull'] with with_cephadm_ctx(cmd) as ctx: retval = cd.command_pull(ctx)