From 008c9fcca094f7b0b6575c337f40b8c51318c47e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Beraud?= Date: Wed, 11 Dec 2019 17:01:53 +0100 Subject: [PATCH] Podman integration Design and implement the podman client module These changes introduce: - the podman module - the podman requirement setup to python-podman version 1.6.0 - related unit tests - related functional tests Change-Id: I9f81d086d66812ff3e7c2493d68e6cedb7f7d9bd Co-authored-by: pkomarov --- requirements.txt | 1 + tobiko/podman/__init__.py | 32 +++++++ tobiko/podman/_client.py | 94 +++++++++++++++++++ tobiko/podman/_exception.py | 26 +++++ tobiko/podman/_shell.py | 43 +++++++++ tobiko/podman/config.py | 16 ++++ tobiko/tests/functional/podman/__init__.py | 0 tobiko/tests/functional/podman/test_client.py | 71 ++++++++++++++ tobiko/tests/unit/__init__.py | 6 ++ tobiko/tests/unit/podman/__init__.py | 0 tobiko/tests/unit/podman/_mocked_service.py | 60 ++++++++++++ tobiko/tests/unit/podman/test_client.py | 50 ++++++++++ tobiko/tests/unit/podman/test_shell.py | 88 +++++++++++++++++ 13 files changed, 487 insertions(+) create mode 100644 tobiko/podman/__init__.py create mode 100644 tobiko/podman/_client.py create mode 100644 tobiko/podman/_exception.py create mode 100644 tobiko/podman/_shell.py create mode 100644 tobiko/podman/config.py create mode 100644 tobiko/tests/functional/podman/__init__.py create mode 100644 tobiko/tests/functional/podman/test_client.py create mode 100644 tobiko/tests/unit/podman/__init__.py create mode 100644 tobiko/tests/unit/podman/_mocked_service.py create mode 100644 tobiko/tests/unit/podman/test_client.py create mode 100644 tobiko/tests/unit/podman/test_shell.py diff --git a/requirements.txt b/requirements.txt index 89a36a5be..5f5a2abbb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ ansible>=2.4.0,<2.8.0 # GPLv3 docker>=4.0 # Apache-2.0 +podman>=1.6.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/podman/__init__.py b/tobiko/podman/__init__.py new file mode 100644 index 000000000..f5dca0307 --- /dev/null +++ b/tobiko/podman/__init__.py @@ -0,0 +1,32 @@ +# 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.podman import _client +from tobiko.podman import _shell +from tobiko.podman import _exception + + +PodmanClientFixture = _client.PodmanClientFixture +get_podman_client = _client.get_podman_client +list_podman_containers = _client.list_podman_containers +podman_client = _client.podman_client + +discover_podman_socket = _shell.discover_podman_socket +is_podman_running = _shell.is_podman_running + +PodmanError = _exception.PodmanError +PodmanSocketNotFoundError = _exception.PodmanSocketNotFoundError diff --git a/tobiko/podman/_client.py b/tobiko/podman/_client.py new file mode 100644 index 000000000..4a377450f --- /dev/null +++ b/tobiko/podman/_client.py @@ -0,0 +1,94 @@ +# 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 six + +import podman + +import tobiko +from tobiko.podman import _exception +from tobiko.podman import _shell +from tobiko.shell import ssh + + +def get_podman_client(ssh_client=None): + return PodmanClientFixture(ssh_client=ssh_client) + + +def list_podman_containers(client=None, **kwargs): + try: + containers = podman_client(client).containers.list(**kwargs) + except _exception.PodmanSocketNotFoundError: + return tobiko.Selection() + else: + return tobiko.select(containers) + + +def podman_client(obj=None): + if obj is None: + obj = get_podman_client() + if tobiko.is_fixture(obj): + obj = tobiko.setup_fixture(obj).client + if isinstance(obj, podman.Client): + return obj + raise TypeError('Cannot obtain a Podman client from {!r}'.format(obj)) + + +class PodmanClientFixture(tobiko.SharedFixture): + + client = None + ssh_client = None + + def __init__(self, ssh_client=None): + if six.PY2: + raise _exception.PodmanError( + "Podman isn't compatible with python 2.7") + super(PodmanClientFixture, self).__init__() + if ssh_client: + self.ssh_client = ssh_client + + def setup_fixture(self): + self.setup_ssh_client() + 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_client(self): + client = self.client + if client is None: + self.client = client = self.create_client() + return client + + def create_client(self): + uri = self.discover_podman_socket() + if self.ssh_client: + uri = ssh.get_port_forward_url(ssh_client=self.ssh_client, url=uri) + client = podman.Client(uri=uri) + client.system.ping() + return client + + def connect(self): + return tobiko.setup_fixture(self).client + + def discover_podman_socket(self): + return _shell.discover_podman_socket(ssh_client=self.ssh_client) diff --git a/tobiko/podman/_exception.py b/tobiko/podman/_exception.py new file mode 100644 index 000000000..fb2b1443c --- /dev/null +++ b/tobiko/podman/_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 PodmanError(tobiko.TobikoException): + message = '{error!}' + + +class PodmanSocketNotFoundError(tobiko.TobikoException): + message = 'Socket not found: {details}' diff --git a/tobiko/podman/_shell.py b/tobiko/podman/_shell.py new file mode 100644 index 000000000..63397a508 --- /dev/null +++ b/tobiko/podman/_shell.py @@ -0,0 +1,43 @@ +# 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.podman import _exception +from tobiko.shell import sh + + +def discover_podman_socket(**execute_params): + cmd = "systemctl list-sockets | grep podman | awk '{print $1}'" + result = sh.execute(cmd, stdin=False, stdout=True, stderr=True, + expect_exit_status=None, **execute_params) + if result.exit_status or not result.stdout: + raise _exception.PodmanSocketNotFoundError(details=result.stderr) + try: + socket = result.stdout.splitlines()[0] + except IndexError: + raise _exception.PodmanSocketNotFoundError(details=result.stderr) + if '0 sockets listed' in socket: + raise _exception.PodmanSocketNotFoundError(details=socket) + return socket + + +def is_podman_running(ssh_client=None, **execute_params): + try: + discover_podman_socket(ssh_client=ssh_client, **execute_params) + except _exception.PodmanSocketNotFoundError: + return False + else: + return True diff --git a/tobiko/podman/config.py b/tobiko/podman/config.py new file mode 100644 index 000000000..7af0b1e60 --- /dev/null +++ b/tobiko/podman/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/tests/functional/podman/__init__.py b/tobiko/tests/functional/podman/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tobiko/tests/functional/podman/test_client.py b/tobiko/tests/functional/podman/test_client.py new file mode 100644 index 000000000..077b97c9e --- /dev/null +++ b/tobiko/tests/functional/podman/test_client.py @@ -0,0 +1,71 @@ +# 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 + +import six + +# We need to ignore this code under py2 +# it's not compatible and parser will failed even if we use +# the `unittest.skipIf` decorator, because during the test discovery +# stestr and unittest will load this test +# module before running it and it will load podman +# too which isn't compatible in version leather than python 3 +# Also the varlink mock module isn't compatible with py27, is using +# annotations syntaxe to generate varlink interface for the mocked service +# and it will raise related exceptions too. +# For all these reasons we can't run podman tests under a python 2 environment +if six.PY3: + from podman import client as podman_client + from podman.libs import containers + + from tobiko import podman + from tobiko.openstack import topology + + class PodmanClientTest(testtools.TestCase): + + ssh_client = None + + def setUp(self): + super(PodmanClientTest, 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 podman.is_podman_running(ssh_client=ssh_client): + self.skip('Podman server is not running') + + def test_get_podman_client(self): + client = podman.get_podman_client(ssh_client=self.ssh_client) + self.assertIsInstance(client, podman.PodmanClientFixture) + + def test_connect_podman_client(self): + client = podman.get_podman_client( + ssh_client=self.ssh_client).connect() + podman_clients_valid_types = ( + podman_client.LocalClient, + podman_client.RemoteClient + ) + self.assertIsInstance(client, podman_clients_valid_types) + client.ping() + + def test_list_podman_containers(self): + for container in podman.list_podman_containers( + ssh_client=self.ssh_client): + self.assertIsInstance(container, containers.Container) diff --git a/tobiko/tests/unit/__init__.py b/tobiko/tests/unit/__init__.py index 22c7b6332..dbd13e4a2 100644 --- a/tobiko/tests/unit/__init__.py +++ b/tobiko/tests/unit/__init__.py @@ -13,11 +13,17 @@ # under the License. from __future__ import absolute_import +import six + from tobiko.tests.unit import _case from tobiko.tests.unit import _patch TobikoUnitTest = _case.TobikoUnitTest +if six.PY3: + from tobiko.tests.unit.podman import _mocked_service + mocked_service = _mocked_service + PatchFixture = _patch.PatchFixture PatchMixin = _patch.PatchMixin diff --git a/tobiko/tests/unit/podman/__init__.py b/tobiko/tests/unit/podman/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tobiko/tests/unit/podman/_mocked_service.py b/tobiko/tests/unit/podman/_mocked_service.py new file mode 100644 index 000000000..233839d5d --- /dev/null +++ b/tobiko/tests/unit/podman/_mocked_service.py @@ -0,0 +1,60 @@ +types = """ +type ListPodData ( + id: string, + name: string, + createdat: string, + cgroup: string, + status: string, + labels: [string]string, + numberofcontainers: string, + containersinfo: []ListPodContainerInfo +) +type ListPodContainerInfo ( + name: string, + id: string, + status: string +) +""" + + +class ServicePod: + + def StartPod(self, name: str) -> str: + """return pod""" + return { # type: ignore + "pod": "135d71b9495f7c3967f536edad57750bfd" + "b569336cd107d8aabab45565ffcfb6", + "name": name + } + + def GetPod(self, name: str) -> str: + """return pod: ListPodData""" + return { # type: ignore + "pod": { + "cgroup": "machine.slice", + "containersinfo": [ + { + "id": "1840835294cf076a822e4e12ba4152411f131" + "bd869e7f6a4e8b16df9b0ea5c7f", + "name": "1840835294cf-infra", + "status": "running" + }, + { + "id": "49a5cce72093a5ca47c6de86f10ad7bb36391e2" + "d89cef765f807e460865a0ec6", + "name": "upbeat_murdock", + "status": "running" + } + ], + "createdat": "2018-12-07 13:10:15.014139258 -0600 CST", + "id": "135d71b9495f7c3967f536edad57750bfdb569336cd" + "107d8aabab45565ffcfb6", + "name": name, + "numberofcontainers": "2", + "status": "Running" + } + } + + def GetVersion(self) -> str: + """return version""" + return {"version": "testing"} # type: ignore diff --git a/tobiko/tests/unit/podman/test_client.py b/tobiko/tests/unit/podman/test_client.py new file mode 100644 index 000000000..c2d5d782e --- /dev/null +++ b/tobiko/tests/unit/podman/test_client.py @@ -0,0 +1,50 @@ +# Copyright 2018 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 mock +import six + +# We need to ignore this code under py2 +# it's not compatible and parser will failed even if we use +# the `unittest.skipIf` decorator, because during the test discovery +# stestr and unittest will load this test +# module before running it and it will load podman +# too which isn't compatible in version leather than python 3 +# Also the varlink mock module isn't compatible with py27, is using +# annotations syntaxe to generate varlink interface for the mocked service +# and it will raise related exceptions too. +# For all these reasons we can't run podman tests under a python 2 environment +if six.PY3: + from tobiko import podman + from tobiko.tests import unit + + from varlink import mock as varlink_mock + + class TestPodmanClient(unit.TobikoUnitTest): + + @varlink_mock.mockedservice( + fake_service=unit.mocked_service.ServicePod, + fake_types=unit.mocked_service.types, + name='io.podman', + address='unix:@podmantests' + ) + @mock.patch( + 'tobiko.podman._client.PodmanClientFixture.discover_podman_socket' + ) + def test_init(self, mocked_discover_podman_socket): + mocked_discover_podman_socket.return_value = 'unix:@podmantests' + client = podman.get_podman_client().connect() + pods = client.pods.get('135d71b9495f') + self.assertEqual(pods["numberofcontainers"], "2") diff --git a/tobiko/tests/unit/podman/test_shell.py b/tobiko/tests/unit/podman/test_shell.py new file mode 100644 index 000000000..0ca7a01bd --- /dev/null +++ b/tobiko/tests/unit/podman/test_shell.py @@ -0,0 +1,88 @@ +# Copyright 2018 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 mock + +import six + +# We need to ignore this code under py2 +# it's not compatible and parser will failed even if we use +# the `unittest.skipIf` decorator, because during the test discovery +# stestr and unittest will load this test +# module before running it and it will load podman +# too which isn't compatible in version leather than python 3 +# Also the varlink mock module isn't compatible with py27, is using +# annotations syntaxe to generate varlink interface for the mocked service +# and it will raise related exceptions too. +# For all these reasons we can't run podman tests under a python 2 environment +if six.PY3: + from tobiko import podman + from tobiko.tests import unit + + class TestShell(unit.TobikoUnitTest): + + @mock.patch('tobiko.shell.sh.execute') + def test_discover_podman_socket(self, mock_execute): + class FakeProcess: + exit_status = 0 + stdout = '/run/podman/io.podman' + stderr = '' + mock_execute.return_value = FakeProcess() + self.assertEqual( + podman.discover_podman_socket(), + '/run/podman/io.podman' + ) + + @mock.patch('tobiko.shell.sh.execute') + def test_discover_podman_socket_none_result(self, mock_execute): + class FakeProcess: + exit_status = 1 + stdout = '' + stderr = 'boom' + mock_execute.return_value = FakeProcess() + self.assertRaises( + podman.PodmanSocketNotFoundError, + podman.discover_podman_socket + ) + + @mock.patch('tobiko.shell.sh.execute') + def test_discover_podman_socket_with_exit_code(self, mock_execute): + class FakeProcess: + exit_status = 0 + stdout = '' + stderr = 'boom' + mock_execute.return_value = FakeProcess() + self.assertRaises( + podman.PodmanSocketNotFoundError, + podman.discover_podman_socket + ) + + @mock.patch('tobiko.shell.sh.execute') + def test_is_podman_running(self, mock_execute): + class FakeProcess: + exit_status = 0 + stdout = '/run/podman/io.podman' + stderr = '' + mock_execute.return_value = FakeProcess() + self.assertEqual(podman.is_podman_running(), True) + + @mock.patch('tobiko.shell.sh.execute') + def test_is_podman_running_without_socket(self, mock_execute): + class FakeProcess: + exit_status = 1 + stdout = '' + stderr = 'boom' + mock_execute.return_value = FakeProcess() + self.assertEqual(podman.is_podman_running(), False)