From 7dfeb04fcf615fa102502e07045ca1b25e976b9a Mon Sep 17 00:00:00 2001 From: Federico Ressi Date: Thu, 10 Oct 2019 12:20:02 +0200 Subject: [PATCH] Add docker client module Change-Id: I27e87cc601b08fa44e7b4c1a0b8c007f4708b59c --- requirements.txt | 1 + tobiko/docker/__init__.py | 31 +++++ tobiko/docker/_client.py | 111 ++++++++++++++++++ tobiko/docker/_exception.py | 26 ++++ tobiko/docker/_shell.py | 55 +++++++++ tobiko/docker/config.py | 16 +++ tobiko/openstack/topology/_topology.py | 11 ++ tobiko/tests/functional/docker/__init__.py | 0 tobiko/tests/functional/docker/test_client.py | 54 +++++++++ 9 files changed, 305 insertions(+) create mode 100644 tobiko/docker/__init__.py create mode 100644 tobiko/docker/_client.py create mode 100644 tobiko/docker/_exception.py create mode 100644 tobiko/docker/_shell.py create mode 100644 tobiko/docker/config.py create mode 100644 tobiko/tests/functional/docker/__init__.py create mode 100644 tobiko/tests/functional/docker/test_client.py diff --git a/requirements.txt b/requirements.txt index 78f4668b3..de9257530 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ # Tobiko framework requirements ansible>=2.4.0,<2.8.0 # GPLv3 +docker>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD keystoneauth1>=3.4.0 # Apache-2.0 Jinja2>=2.8.0 # BSD diff --git a/tobiko/docker/__init__.py b/tobiko/docker/__init__.py new file mode 100644 index 000000000..240f8115c --- /dev/null +++ b/tobiko/docker/__init__.py @@ -0,0 +1,31 @@ +# Copyright (c) 2019 Red Hat, Inc. +# +# All Rights Reserved. +# +# 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 + +from tobiko.docker import _client +from tobiko.docker import _shell +from tobiko.docker import _exception + + +DockerClientFixture = _client.DockerClientFixture +get_docker_client = _client.get_docker_client +list_docker_containers = _client.list_docker_containers + +discover_docker_urls = _shell.discover_docker_urls +is_docker_running = _shell.is_docker_running + +DockerError = _exception.DockerError +DockerUrlNotFoundError = _exception.DockerUrlNotFoundError diff --git a/tobiko/docker/_client.py b/tobiko/docker/_client.py new file mode 100644 index 000000000..62a486b7f --- /dev/null +++ b/tobiko/docker/_client.py @@ -0,0 +1,111 @@ +# Copyright (c) 2019 Red Hat, Inc. +# +# All Rights Reserved. +# +# 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 docker + +import tobiko +from tobiko.docker import _exception +from tobiko.docker import _shell +from tobiko.shell import ssh + + +def get_docker_client(base_urls=None, ssh_client=None): + return DockerClientFixture(base_urls=base_urls, + ssh_client=ssh_client) + + +def list_docker_containers(client=None, **kwargs): + try: + containers = docker_client(client).containers.list(**kwargs) + except _exception.DockerUrlNotFoundError: + return tobiko.Selection() + else: + return tobiko.select(containers) + + +def docker_client(obj=None): + if obj is None: + obj = get_docker_client() + if tobiko.is_fixture(obj): + obj = tobiko.setup_fixture(obj).client + if isinstance(obj, docker.DockerClient): + return obj + raise TypeError('Cannot obtain a DockerClient from {!r}'.format(obj)) + + +class DockerClientFixture(tobiko.SharedFixture): + + base_urls = None + client = None + ssh_client = None + + def __init__(self, base_urls=None, ssh_client=None): + super(DockerClientFixture, self).__init__() + if base_urls: + self.base_urls = list(base_urls) + if ssh_client: + self.ssh_client = ssh_client + + def setup_fixture(self): + self.setup_ssh_client() + self.setup_base_urls() + self.setup_client() + + def setup_ssh_client(self): + ssh_client = self.ssh_client + if ssh_client is None: + self.ssh_client = ssh_client = ssh.ssh_proxy_client() or False + if ssh_client: + tobiko.setup_fixture(ssh_client) + return ssh_client + + def setup_base_urls(self): + base_urls = self.base_urls + if base_urls is None: + self.base_urls = base_urls = self.discover_docker_urls() + return base_urls + + def setup_client(self): + client = self.client + if client is None: + self.client = client = self.create_client() + return client + + def create_client(self): + exc_info = None + for base_url in self.base_urls: + if self.ssh_client: + base_url = ssh.get_port_forward_url(ssh_client=self.ssh_client, + url=base_url) + client = docker.DockerClient(base_url=base_url) + try: + client.ping() + except Exception: + exc_info = exc_info or tobiko.exc_info() + else: + return client + + if exc_info: + exc_info.reraise() + else: + raise _exception.DockerError('Unable to create docker client') + + def connect(self): + return tobiko.setup_fixture(self).client + + def discover_docker_urls(self): + return _shell.discover_docker_urls(ssh_client=self.ssh_client) diff --git a/tobiko/docker/_exception.py b/tobiko/docker/_exception.py new file mode 100644 index 000000000..d19625745 --- /dev/null +++ b/tobiko/docker/_exception.py @@ -0,0 +1,26 @@ +# Copyright (c) 2019 Red Hat, Inc. +# +# All Rights Reserved. +# +# 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 tobiko + + +class DockerError(tobiko.TobikoException): + message = '{error!}' + + +class DockerUrlNotFoundError(tobiko.TobikoException): + message = 'URL not found: {details}' diff --git a/tobiko/docker/_shell.py b/tobiko/docker/_shell.py new file mode 100644 index 000000000..caba966ba --- /dev/null +++ b/tobiko/docker/_shell.py @@ -0,0 +1,55 @@ +# Copyright (c) 2019 Red Hat, Inc. +# +# All Rights Reserved. +# +# 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 + +from tobiko.docker import _exception +from tobiko.shell import sh + + +def discover_docker_urls(**execute_params): + result = sh.execute('ps aux | grep dockerd', stdin=False, stdout=True, + stderr=True, expect_exit_status=None, **execute_params) + if result.exit_status or not result.stdout: + raise _exception.DockerUrlNotFoundError(details=result.stderr) + + urls = [] + for line in result.stdout.splitlines(): + fields = line.strip().split() + if fields: + offset = 0 + while True: + try: + offset = fields.index('-H', offset) + url = fields[offset + 1] + except (ValueError, IndexError): + break + else: + urls.append(url) + offset += 2 + + if not urls: + raise _exception.DockerUrlNotFoundError(details='\n' + result.stdout) + + return urls + + +def is_docker_running(ssh_client=None, **execute_params): + try: + discover_docker_urls(ssh_client=ssh_client, **execute_params) + except _exception.DockerUrlNotFoundError: + return False + else: + return True diff --git a/tobiko/docker/config.py b/tobiko/docker/config.py new file mode 100644 index 000000000..7af0b1e60 --- /dev/null +++ b/tobiko/docker/config.py @@ -0,0 +1,16 @@ +# Copyright (c) 2019 Red Hat, Inc. +# +# All Rights Reserved. +# +# 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 diff --git a/tobiko/openstack/topology/_topology.py b/tobiko/openstack/topology/_topology.py index 9bd8233a8..055e1a6b0 100644 --- a/tobiko/openstack/topology/_topology.py +++ b/tobiko/openstack/topology/_topology.py @@ -23,6 +23,7 @@ import six from six.moves.urllib import parse import tobiko +from tobiko import docker from tobiko.shell import ip from tobiko.shell import ping from tobiko.shell import sh @@ -91,6 +92,8 @@ def set_default_openstack_topology_class(topology_class): class OpenStackTopologyNode(object): + _docker_client = None + def __init__(self, topology, name, public_ip, ssh_client): self._topology = weakref.ref(topology) self.name = name @@ -109,6 +112,14 @@ class OpenStackTopologyNode(object): def ssh_parameters(self): return self.ssh_client.setup_connect_parameters() + @property + def docker_client(self): + docker_client = self._docker_client + if not docker_client: + self._docker_client = docker_client = docker.get_docker_client( + ssh_client=self.ssh_client) + return docker_client + def __repr__(self): return "{cls!s}".format(cls=type(self).__name__, name=self.name) diff --git a/tobiko/tests/functional/docker/__init__.py b/tobiko/tests/functional/docker/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tobiko/tests/functional/docker/test_client.py b/tobiko/tests/functional/docker/test_client.py new file mode 100644 index 000000000..e4cc9f419 --- /dev/null +++ b/tobiko/tests/functional/docker/test_client.py @@ -0,0 +1,54 @@ +# Copyright (c) 2019 Red Hat, Inc. +# +# All Rights Reserved. +# +# 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 docker import client as docker_client +from docker.models import containers + +from tobiko import docker +from tobiko.openstack import topology + + +class DockerClientTest(testtools.TestCase): + + ssh_client = None + + def setUp(self): + super(DockerClientTest, self).setUp() + for node in topology.list_openstack_nodes(group='controller'): + self.ssh_client = ssh_client = node.ssh_client + break + else: + self.skip('Any controller node found from OpenStack topology') + + if not docker.is_docker_running(ssh_client=ssh_client): + self.skip('Docker server is not running') + + def test_get_docker_client(self): + client = docker.get_docker_client(ssh_client=self.ssh_client) + self.assertIsInstance(client, docker.DockerClientFixture) + + def test_connect_docker_client(self): + client = docker.get_docker_client(ssh_client=self.ssh_client).connect() + self.assertIsInstance(client, docker_client.DockerClient) + client.ping() + + def test_list_docker_containers(self): + for container in docker.list_docker_containers( + ssh_client=self.ssh_client): + self.assertIsInstance(container, containers.Container)