diff --git a/tobiko/openstack/tests/_neutron.py b/tobiko/openstack/tests/_neutron.py index 816e94eab..1561547e1 100644 --- a/tobiko/openstack/tests/_neutron.py +++ b/tobiko/openstack/tests/_neutron.py @@ -73,11 +73,12 @@ def test_neutron_agents_are_alive(timeout=300., interval=5.) \ def ovn_dbs_are_synchronized(test_case): from tobiko.tripleo import containers # declare commands + runtime_name = containers.get_container_runtime_name() search_container_cmd = ( "%s ps --format '{{.Names}}' -f name=ovn-dbs-bundle" % - containers.container_runtime_name) + runtime_name) container_cmd_prefix = ('%s exec -uroot {container}' % - containers.container_runtime_name) + runtime_name) ovndb_sync_cmd = ('ovs-appctl -t /var/run/openvswitch/{ovndb_ctl_file} ' 'ovsdb-server/sync-status') ovndb_show_cmd = '{ovndb} show' diff --git a/tobiko/tests/faults/neutron/test_agents.py b/tobiko/tests/faults/neutron/test_agents.py index 28609ecbe..5271f9613 100644 --- a/tobiko/tests/faults/neutron/test_agents.py +++ b/tobiko/tests/faults/neutron/test_agents.py @@ -69,7 +69,7 @@ class BaseAgentTest(testtools.TestCase): @property def container_runtime_name(self): if overcloud.has_overcloud(): - return containers.container_runtime_name + return containers.get_container_runtime_name() else: return 'docker' diff --git a/tobiko/tests/functional/tripleo/test_containers.py b/tobiko/tests/functional/tripleo/test_containers.py new file mode 100644 index 000000000..032773af0 --- /dev/null +++ b/tobiko/tests/functional/tripleo/test_containers.py @@ -0,0 +1,31 @@ +# Copyright 2021 Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import absolute_import + +import testtools + +from tobiko.tripleo import containers + + +@containers.skip_unless_has_container_runtime() +class RuntimeRuntimeTest(testtools.TestCase): + + def test_get_container_runtime(self): + runtime = containers.get_container_runtime() + self.assertIsInstance(runtime, containers.ContainerRuntime) + + def test_list_containers(self): + containers_list = containers.list_containers() + for container in containers_list: + self.assertIsNotNone(container) diff --git a/tobiko/tripleo/containers.py b/tobiko/tripleo/containers.py index b62904939..fe2c63644 100644 --- a/tobiko/tripleo/containers.py +++ b/tobiko/tripleo/containers.py @@ -1,13 +1,14 @@ from __future__ import absolute_import +import abc import functools import os +import re import time import typing from oslo_log import log import pandas -import docker as dockerlib import tobiko from tobiko import podman @@ -23,57 +24,136 @@ from tobiko.tripleo import topology as tripleo_topology LOG = log.getLogger(__name__) -def get_container_runtime_module(): - """check what container runtime is running - and return a handle to it""" - # TODO THIS LOCKS SSH CLIENT TO CONTROLLER - ssh_client = topology.list_openstack_nodes(group='controller')[ - 0].ssh_client - if docker.is_docker_running(ssh_client=ssh_client): - return docker - else: - return podman +class ContainerRuntime(abc.ABC): + runtime_name: str + version_pattern: typing.Pattern + + def match_version(self, version: str) -> bool: + for version_line in version.splitlines(): + if self.version_pattern.match(version_line) is not None: + return True + return False + + def get_client(self, ssh_client): + for attempt in tobiko.retry(timeout=60.0, + interval=5.0): + try: + client = self._get_client(ssh_client=ssh_client) + break + # TODO chose a better exception type + except Exception: + if attempt.is_last: + raise + LOG.debug('Unable to connect to docker server', + exc_info=1) + ssh.reset_default_ssh_port_forward_manager() + else: + raise RuntimeError("Broken retry loop") + return client + + def _get_client(self, ssh_client): + raise NotImplementedError + + def list_containers(self, ssh_client): + raise NotImplementedError -def get_container_runtime_name(): - return container_runtime_module.__name__.rsplit('.', 1)[1] +class DockerContainerRuntime(ContainerRuntime): + runtime_name = 'docker' + version_pattern = re.compile('Docker version .*') + + def _get_client(self, ssh_client): + return docker.get_docker_client(ssh_client=ssh_client, + sudo=True).connect() + + def list_containers(self, ssh_client): + client = self.get_client(ssh_client=ssh_client) + return docker.list_docker_containers(client=client) -if overcloud.has_overcloud(): - container_runtime_module = get_container_runtime_module() - container_runtime_name = get_container_runtime_name() +class PodmanContainerRuntime(ContainerRuntime): + runtime_name = 'podman' + version_pattern = re.compile('Podman version .*') + + def _get_client(self, ssh_client): + return podman.get_podman_client(ssh_client=ssh_client).connect() + + def list_containers(self, ssh_client): + client = self.get_client(ssh_client=ssh_client) + return podman.list_podman_containers(client=client) + + +DOCKER_RUNTIME = DockerContainerRuntime() +PODMAN_RUNTIME = PodmanContainerRuntime() +CONTAINER_RUNTIMES = [PODMAN_RUNTIME, DOCKER_RUNTIME] + + +class ContainerRuntimeFixture(tobiko.SharedFixture): + + runtime: typing.Optional[ContainerRuntime] = None + + def setup_fixture(self): + if overcloud.has_overcloud(): + self.runtime = self.get_runtime() + + def cleanup_fixture(self): + self.runtime = None + + @staticmethod + def get_runtime() -> typing.Optional[ContainerRuntime]: + """check what container runtime is running + and return a handle to it""" + # TODO THIS LOCKS SSH CLIENT TO CONTROLLER + for node in topology.list_openstack_nodes(group='controller'): + try: + result = sh.execute('podman --version || docker --version', + ssh_client=node.ssh_client) + except sh.ShellCommandFailed: + continue + for runtime in CONTAINER_RUNTIMES: + for version in [result.stdout, result.stderr]: + if runtime.match_version(version): + return runtime + raise RuntimeError( + "Unable to find any container runtime in any overcloud " + "controller node") + + +def get_container_runtime() -> ContainerRuntime: + runtime = tobiko.setup_fixture(ContainerRuntimeFixture).runtime + return runtime + + +def get_container_runtime_name() -> str: + return get_container_runtime().runtime_name + + +def is_docker() -> bool: + return get_container_runtime().runtime_name == 'docker' + + +def is_podman() -> bool: + return get_container_runtime().runtime_name == 'podman' + + +def has_container_runtime() -> bool: + return get_container_runtime() is not None + + +def skip_unless_has_container_runtime(): + return tobiko.skip_unless('Container runtime not found', + has_container_runtime) @functools.lru_cache() def list_node_containers(ssh_client): """returns a list of containers and their run state""" - client = get_container_client(ssh_client=ssh_client) - if container_runtime_module == podman: - return container_runtime_module.list_podman_containers(client=client) - - elif container_runtime_module == docker: - return container_runtime_module.list_docker_containers(client=client) + return get_container_runtime().list_containers(ssh_client=ssh_client) def get_container_client(ssh_client=None): """returns a list of containers and their run state""" - - for attempt in tobiko.retry( - timeout=60.0, - interval=5.0): - try: - if container_runtime_module == podman: - return container_runtime_module.get_podman_client( - ssh_client=ssh_client).connect() - elif container_runtime_module == docker: - return container_runtime_module.get_docker_client( - ssh_client=ssh_client).connect() - except dockerlib.errors.DockerException: - LOG.debug('Unable to connect to docker API') - attempt.check_limits() - ssh.reset_default_ssh_port_forward_manager() - # no successful connection to docker/podman API has been performed - raise RuntimeError('Unable to connect to container mgmt tool') + return get_container_runtime().get_client(ssh_client=ssh_client) def list_containers_df(group=None): @@ -93,13 +173,13 @@ def list_containers(group=None): # AttributeError: module 'tobiko.openstack.topology' has no # attribute 'container_runtime' + if group is None: + group = 'overcloud' containers_list = tobiko.Selection() - if group: - openstack_nodes = topology.list_openstack_nodes(group=group) - else: - openstack_nodes = topology.list_openstack_nodes(group='overcloud') + openstack_nodes = topology.list_openstack_nodes(group=group) for node in openstack_nodes: + LOG.debug(f"List containers for node {node.name}") node_containers_list = list_node_containers(ssh_client=node.ssh_client) containers_list.extend(node_containers_list) return containers_list @@ -222,6 +302,7 @@ def assert_all_tripleo_containers_running(): def assert_ovn_containers_running(): # specific OVN verifications if neutron.has_ovn(): + container_runtime_name = get_container_runtime_name() ovn_controller_containers = ['ovn_controller', 'ovn-dbs-bundle-{}-'. format(container_runtime_name)] @@ -275,6 +356,7 @@ def run_container_config_validations(): 'expected_value': 'openvswitch'}]}] config_checkings += ovs_config_checkings + container_runtime_name = get_container_runtime_name() for config_check in config_checkings: for node in topology.list_openstack_nodes( group=config_check['node_group']): @@ -320,18 +402,17 @@ def comparable_container_keys(container, include_container_objects=False): container._client._context.hostname), # pylint: disable=W0212 container.data['names'], container.data['status']) - if container_runtime_module == podman and include_container_objects: + if is_podman() and include_container_objects: return con_host_name_stat_obj_tuple - - elif container_runtime_module == podman: + elif is_podman(): return con_host_name_stat_tuple - elif container_runtime_module == docker and include_container_objects: + elif is_docker() and include_container_objects: return (container.attrs['Config']['Hostname'], container.attrs['Name'].strip('/'), container.attrs['State']['Status'], container) - elif container_runtime_module == docker: + elif is_docker() == docker: return (container.attrs['Config']['Hostname'], container.attrs['Name'].strip('/'), container.attrs['State']['Status'])