aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2025-04-01 08:20:44 +0000
committerGerrit Code Review <review@openstack.org>2025-04-01 08:20:44 +0000
commit53b5d8eeb03ec22f8c6527cecc78a14c49142b0f (patch)
treee0908d1fc5fd493338b3ca63566a77fac631f8da
parent810940dc3191dc5f5c27545f297f4a960c28b517 (diff)
parentc803413c52e2c91e48e275aaadff6d1e26a45771 (diff)
downloadkolla-ansible-53b5d8eeb03ec22f8c6527cecc78a14c49142b0f.tar.gz
kolla-ansible-53b5d8eeb03ec22f8c6527cecc78a14c49142b0f.zip
Merge "Move kolla_toolbox to high level client"
-rw-r--r--ansible/library/kolla_toolbox.py346
-rw-r--r--tests/kolla-toolbox-testsuite.yml89
-rw-r--r--tests/test-kolla-toolbox.yml12
-rw-r--r--tests/test_kolla_toolbox.py479
4 files changed, 658 insertions, 268 deletions
diff --git a/ansible/library/kolla_toolbox.py b/ansible/library/kolla_toolbox.py
index d0493a24f..d368eb67d 100644
--- a/ansible/library/kolla_toolbox.py
+++ b/ansible/library/kolla_toolbox.py
@@ -13,13 +13,10 @@
# limitations under the License.
import json
-import re
+import traceback
from ansible.module_utils.basic import AnsibleModule
-from ast import literal_eval
-from shlex import split
-
DOCUMENTATION = '''
---
module: kolla_toolbox
@@ -34,6 +31,7 @@ options:
- Name of container engine to use
required: True
type: str
+ choices: ['docker', 'podman']
module_name:
description:
- The module name to invoke
@@ -43,12 +41,12 @@ options:
description:
- The module args use by the module
required: False
- type: str or dict
+ type: dict
module_extra_vars:
description:
- The extra variables used by the module
required: False
- type: str or dict
+ type: dict
user:
description:
- The user to execute Ansible inside kolla_toolbox with
@@ -56,27 +54,29 @@ options:
type: str
api_version:
description:
- - The version of the API for docker-py to use when contacting Docker
+ - The version of the API for client to use when contacting container API
required: False
type: str
default: auto
timeout:
description:
- - The default timeout for docker-py client when contacting Docker API
+ - The default timeout for client when contacting container API
required: False
type: int
default: 180
-author: Jeffrey Zhang
+authors: Jeffrey Zhang, Roman KrĨek
'''
EXAMPLES = '''
- hosts: controller
tasks:
- - name: Ensure the direct absent
+ - name: Ensure the directory is removed
kolla_toolbox:
- container_engine: docker
+ container_engine: podman
module_name: file
- module_args: path=/tmp/a state=absent
+ module_args:
+ path: /tmp/a
+ state: absent
- name: Create mysql database
kolla_toolbox:
container_engine: docker
@@ -86,7 +86,7 @@ EXAMPLES = '''
login_user: root
login_password: admin
name: testdb
- - name: Creating default user role
+ - name: Create default user role
kolla_toolbox:
container_engine: docker
module_name: openstack.cloud.identity_role
@@ -103,177 +103,189 @@ EXAMPLES = '''
'''
-JSON_REG = re.compile(r'^(?P<host>\w+) \| (?P<status>\w+)!? =>(?P<stdout>.*)$',
- re.MULTILINE | re.DOTALL)
-NON_JSON_REG = re.compile((r'^(?P<host>\w+) \| (?P<status>\w+)!? \| '
- r'rc=(?P<exit_code>\d+) >>\n(?P<stdout>.*)\n$'),
- re.MULTILINE | re.DOTALL)
+class KollaToolboxWorker():
+ def __init__(self, module, client, container_errors) -> None:
+ self.module = module
+ self.client = client
+ self.container_errors = container_errors
+ self.result = dict()
-
-def gen_commandline(params):
- command = ['ansible', 'localhost']
- if params.get('module_name'):
- command.extend(['-m', params.get('module_name')])
- if params.get('module_args'):
+ def _get_toolbox_container(self):
+ """Get the kolla_toolbox container object, if up and running."""
+ cont = self.client.containers.list(
+ filters=dict(name='kolla_toolbox', status='running')
+ )
+ if not cont:
+ self.module.fail_json(
+ msg='kolla_toolbox container is missing or not running!'
+ )
+ return cont[0]
+
+ def _format_module_args(self, module_args: dict) -> list:
+ """Format dict of module parameters into list of 'key=value' pairs."""
+ pairs = list()
+ for key, value in module_args.items():
+ if isinstance(value, dict):
+ value_json = json.dumps(value)
+ pairs.append(f"{key}='{value_json}'")
+ else:
+ pairs.append(f"{key}='{value}'")
+ return pairs
+
+ def _generate_command(self) -> list:
+ """Generate the command that will be executed inside kolla_toolbox."""
+ args_formatted = self._format_module_args(
+ self.module.params.get('module_args'))
+ extra_vars_formatted = self._format_module_args(
+ self.module.params.get('module_extra_vars'))
+
+ command = ['ansible', 'localhost']
+ command.extend(['-m', self.module.params.get('module_name')])
+ if args_formatted:
+ command.extend(['-a', ' '.join(args_formatted)])
+ if extra_vars_formatted:
+ command.extend(['-e', ' '.join(extra_vars_formatted)])
+ if self.module.check_mode:
+ command.append('--check')
+
+ return command
+
+ def _run_command(self, kolla_toolbox, command, *args, **kwargs) -> bytes:
try:
- module_args = literal_eval(params.get('module_args'))
- except SyntaxError:
- if not isinstance(params.get('module_args'), str):
- raise
-
- # account for string arguments
- module_args = split(params.get('module_args'))
- if isinstance(module_args, dict):
- module_args = ' '.join("{}='{}'".format(key, value)
- for key, value in module_args.items())
- if isinstance(module_args, list):
- module_args = ' '.join(module_args)
- command.extend(['-a', module_args])
- if params.get('module_extra_vars'):
- extra_vars = params.get('module_extra_vars')
- if isinstance(extra_vars, dict):
- extra_vars = json.dumps(extra_vars)
- command.extend(['--extra-vars', extra_vars])
- return command
-
-
-def get_docker_client():
- import docker
- return docker.APIClient
-
+ _, output_raw = kolla_toolbox.exec_run(command,
+ *args,
+ **kwargs)
+ except self.container_errors.APIError as e:
+ self.module.fail_json(
+ msg='Container engine client encountered API error: '
+ f'{e.explanation}'
+ )
+ return output_raw
+
+ def _process_container_output(self, output_raw: bytes) -> dict:
+ """Convert raw bytes output from container.exec_run into dictionary."""
+ try:
+ output_json = json.loads(output_raw.decode('utf-8'))
+ except json.JSONDecodeError as e:
+ self.module.fail_json(
+ msg=f'Parsing kolla_toolbox JSON output failed: {e}'
+ )
+
+ # Expected format for the output is the following:
+ # {
+ # "plays": [
+ # {
+ # "tasks": [
+ # {
+ # "hosts": {
+ # "localhost": {
+ # <module result>
+ # }
+ # }
+ # }
+ # ]
+ # {
+ # ]
+ # }
-def use_docker(module):
- client = get_docker_client()(
- version=module.params.get('api_version'),
- timeout=module.params.get('timeout'))
- command_line = gen_commandline(module.params)
- kolla_toolbox = client.containers(filters=dict(name='kolla_toolbox',
- status='running'))
- if not kolla_toolbox:
- module.fail_json(msg='kolla_toolbox container is not running.')
+ try:
+ result = output_json['plays'][0]['tasks'][0]['hosts']['localhost']
+ result.pop('_ansible_no_log', None)
+ except KeyError:
+ self.module.fail_json(
+ msg=f'Ansible JSON output has unexpected format: {output_json}'
+ )
- kolla_toolbox = kolla_toolbox[0]
- kwargs = {}
- if 'user' in module.params:
- kwargs['user'] = module.params['user']
+ return result
- # Use the JSON output formatter, so that we can parse it.
- environment = {"ANSIBLE_STDOUT_CALLBACK": "json",
- "ANSIBLE_LOAD_CALLBACK_PLUGINS": "True"}
- job = client.exec_create(kolla_toolbox, command_line,
- environment=environment, **kwargs)
- json_output, error = client.exec_start(job, demux=True)
- if error:
- module.log(msg='Inner module stderr: %s' % error)
+ def main(self) -> None:
+ """Run command inside the kolla_toolbox container with defined args."""
- try:
- output = json.loads(json_output)
- except Exception:
- module.fail_json(
- msg='Can not parse the inner module output: %s' % json_output)
+ kolla_toolbox = self._get_toolbox_container()
+ command = self._generate_command()
+ environment = {'ANSIBLE_STDOUT_CALLBACK': 'json',
+ 'ANSIBLE_LOAD_CALLBACK_PLUGINS': 'True'}
- # Expected format is the following:
- # {
- # "plays": [
- # {
- # "tasks": [
- # {
- # "hosts": {
- # "localhost": {
- # <module result>
- # }
- # }
- # }
- # ]
- # {
- # ]
- # }
- try:
- ret = output['plays'][0]['tasks'][0]['hosts']['localhost']
- except (KeyError, IndexError):
- module.fail_json(
- msg='Ansible JSON output has unexpected format: %s' % output)
+ output_raw = self._run_command(kolla_toolbox,
+ command,
+ environment=environment,
+ tty=True,
+ user=self.module.params.get('user'))
- # Remove Ansible's internal variables from returned fields.
- ret.pop('_ansible_no_log', None)
- return ret
+ self.result = self._process_container_output(output_raw)
-def get_kolla_toolbox():
- from podman import PodmanClient
+def create_container_client(module: AnsibleModule):
+ """Return container engine client based on the parameters."""
+ container_engine = module.params.get('container_engine')
+ api_version = module.params.get('api_version')
+ timeout = module.params.get('timeout')
- with PodmanClient(base_url="http+unix:/run/podman/podman.sock") as client:
- for cont in client.containers.list(all=True):
- cont.reload()
- if cont.name == 'kolla_toolbox' and cont.status == 'running':
- return cont
-
-
-def use_podman(module):
- from podman.errors.exceptions import APIError
+ if container_engine == 'docker':
+ try:
+ import docker
+ import docker.errors as container_errors
+ except ImportError:
+ module.fail_json(
+ msg='The docker library could not be imported!'
+ )
+ client = docker.DockerClient(
+ base_url='http+unix:/var/run/docker.sock',
+ version=api_version,
+ timeout=timeout)
+ else:
+ try:
+ import podman
+ import podman.errors as container_errors
+ except ImportError:
+ module.fail_json(
+ msg='The podman library could not be imported!'
+ )
+ # NOTE(r-krcek): PodmanClient has a glitch in which when you pass
+ # 'auto' to api_version, it literally creates an url of /vauto
+ # for API calls, instead of actually finding the compatible version
+ # like /v5.0.0, this leads to 404 Error when accessing the API.
+ if api_version == 'auto':
+ client = podman.PodmanClient(
+ base_url='http+unix:/run/podman/podman.sock',
+ timeout=timeout)
+ else:
+ client = podman.PodmanClient(
+ base_url='http+unix:/run/podman/podman.sock',
+ version=api_version,
+ timeout=timeout)
+ return client, container_errors
+
+
+def create_ansible_module() -> AnsibleModule:
+ argument_spec = dict(
+ container_engine=dict(type='str',
+ choices=['podman', 'docker'],
+ required=True),
+ module_name=dict(type='str', required=True),
+ module_args=dict(type='dict', default=dict()),
+ module_extra_vars=dict(type='dict', default=dict()),
+ api_version=dict(type='str', default='auto'),
+ timeout=dict(type='int', default=180),
+ user=dict(type='str'),
+ )
- try:
- kolla_toolbox = get_kolla_toolbox()
- if not kolla_toolbox:
- module.fail_json(msg='kolla_toolbox container is not running.')
+ return AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True)
- kwargs = {}
- if 'user' in module.params:
- kwargs['user'] = module.params['user']
- environment = {"ANSIBLE_STDOUT_CALLBACK": "json",
- "ANSIBLE_LOAD_CALLBACK_PLUGINS": "True"}
- command_line = gen_commandline(module.params)
- _, raw_output = kolla_toolbox.exec_run(
- command_line,
- environment=environment,
- tty=True,
- **kwargs
- )
- except APIError as e:
- module.fail_json(msg=f'Encountered Podman API error: {e.explanation}')
+def main():
+ module = create_ansible_module()
+ client, container_errors = create_container_client(module)
+ ktbw = KollaToolboxWorker(module, client, container_errors)
try:
- json_output = raw_output.decode('utf-8')
- output = json.loads(json_output)
+ ktbw.main()
+ module.exit_json(**ktbw.result)
except Exception:
- module.fail_json(
- msg='Can not parse the inner module output: %s' % json_output)
-
- try:
- ret = output['plays'][0]['tasks'][0]['hosts']['localhost']
- except (KeyError, IndexError):
- module.fail_json(
- msg='Ansible JSON output has unexpected format: %s' % output)
-
- # Remove Ansible's internal variables from returned fields.
- ret.pop('_ansible_no_log', None)
-
- return ret
-
-
-def main():
- specs = dict(
- container_engine=dict(required=True, type='str'),
- module_name=dict(required=True, type='str'),
- module_args=dict(type='str'),
- module_extra_vars=dict(type='json'),
- api_version=dict(required=False, type='str', default='auto'),
- timeout=dict(required=False, type='int', default=180),
- user=dict(required=False, type='str'),
- )
- module = AnsibleModule(argument_spec=specs, bypass_checks=True)
-
- container_engine = module.params.get('container_engine').lower()
- if container_engine == 'docker':
- result = use_docker(module)
- elif container_engine == 'podman':
- result = use_podman(module)
- else:
- module.fail_json(msg='Missing or invalid container engine.')
-
- module.exit_json(**result)
+ module.fail_json(changed=True,
+ msg=repr(traceback.format_exc()))
-if __name__ == "__main__":
+if __name__ == '__main__':
main()
diff --git a/tests/kolla-toolbox-testsuite.yml b/tests/kolla-toolbox-testsuite.yml
deleted file mode 100644
index 23a3fb08f..000000000
--- a/tests/kolla-toolbox-testsuite.yml
+++ /dev/null
@@ -1,89 +0,0 @@
----
-- name: Test successful & unchanged
- kolla_toolbox:
- common_options:
- container_engine: "{{ item }}"
- module_name: debug
- module_args:
- msg: hi
- register: result
-
-- name: Assert result is successful
- assert:
- that: result is successful
-
-- name: Assert result is not changed
- assert:
- that: result is not changed
-
-- name: Test successful & changed
- kolla_toolbox:
- common_options:
- container_engine: "{{ item }}"
- module_name: command
- module_args:
- echo hi
- register: result
-
-- name: Assert result is successful
- assert:
- that: result is successful
-
-- name: Assert result is changed
- assert:
- that: result is changed
-
-- name: Test unsuccessful
- kolla_toolbox:
- common_options:
- container_engine: "{{ item }}"
- module_name: command
- module_args:
- foo
- register: result
- ignore_errors: true
-
-- name: Assert result is failed
- assert:
- that: result is failed
-
-- name: Test invalid module parameters
- kolla_toolbox:
- common_options:
- container_engine: "{{ item }}"
- module_name: debug
- module_args:
- foo: bar
- register: result
- ignore_errors: true
-
-- name: Assert result is failed
- assert:
- that: result is failed
-
-- name: Setup for Test successful & changed (JSON format)
- kolla_toolbox:
- common_options:
- container_engine: "{{ item }}"
- module_name: file
- module_args:
- path: /tmp/foo
- state: absent
-
-- name: Test successful & changed (JSON format)
- kolla_toolbox:
- common_options:
- container_engine: "{{ item }}"
- module_name: file
- module_args:
- path: /tmp/foo
- state: directory
- register: result
-
-- name: Assert result is successful
- assert:
- that: result is successful
-
-- name: Assert result is changed
- assert:
- that: result is changed
diff --git a/tests/test-kolla-toolbox.yml b/tests/test-kolla-toolbox.yml
deleted file mode 100644
index 566d53220..000000000
--- a/tests/test-kolla-toolbox.yml
+++ /dev/null
@@ -1,12 +0,0 @@
----
-- name: Test the kolla_toolbox module
- hosts: localhost
- gather_facts: false
- vars:
- container_engines:
- - "docker"
- - "podman"
- tasks:
- - name: Test kolla-toolbox for each container engine
- include_tasks: kolla-toolbox-testsuite.yml
- with_items: "{{ container_engines }}"
diff --git a/tests/test_kolla_toolbox.py b/tests/test_kolla_toolbox.py
new file mode 100644
index 000000000..112bd403c
--- /dev/null
+++ b/tests/test_kolla_toolbox.py
@@ -0,0 +1,479 @@
+# Copyright 2024 Tietoevry
+#
+# 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.
+
+import builtins
+import json
+import os
+import sys
+
+from ansible.module_utils import basic
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.text.converters import to_bytes
+from importlib.machinery import SourceFileLoader
+from oslotest import base
+from unittest import mock
+
+# Import kolla_toolbox module using SourceFileLoader
+this_dir = os.path.dirname(sys.modules[__name__].__file__)
+ansible_dir = os.path.join(this_dir, '..', 'ansible')
+kolla_toolbox_file = os.path.join(ansible_dir, 'library', 'kolla_toolbox.py')
+
+kolla_toolbox = SourceFileLoader('kolla_toolbox',
+ kolla_toolbox_file).load_module()
+
+
+def set_module_args(args):
+ """Prepare arguments so they will be picked up during module creation."""
+
+ args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
+ basic._ANSIBLE_ARGS = to_bytes(args)
+
+
+class AnsibleExitJson(BaseException):
+ """Exception to be raised by module.exit_json and caught by a test case."""
+
+ def __init__(self, kwargs):
+ super().__init__(kwargs)
+ self.result = kwargs
+
+
+class AnsibleFailJson(BaseException):
+ """Exception to be raised by module.fail_json and caught by a test case."""
+
+ def __init__(self, kwargs):
+ super().__init__(kwargs)
+ self.result = kwargs
+
+
+class MockAPIError(Exception):
+ """Mock exception to be raised to simulate engine client APIError."""
+
+ def __init__(self, message, explanation=None):
+ super().__init__(message)
+ self.explanation = explanation
+
+
+class TestKollaToolboxModule(base.BaseTestCase):
+ """Base class for the module's tests.
+
+ Sets up methods that patch over the module's fail_json and exit_json,
+ so that they dont just call sys.exit() and instead they return
+ value of the result.
+ """
+
+ def setUp(self):
+ super().setUp()
+
+ self.fail_json_patch = mock.patch(
+ 'ansible.module_utils.basic.AnsibleModule.fail_json',
+ side_effect=self.fail_json)
+ self.exit_json_patch = mock.patch(
+ 'ansible.module_utils.basic.AnsibleModule.exit_json',
+ side_effect=self.exit_json)
+
+ self.fail_json_mock = self.fail_json_patch.start()
+ self.exit_json_mock = self.exit_json_patch.start()
+
+ def tearDown(self):
+ super().tearDown()
+ self.fail_json_patch.stop()
+ self.exit_json_patch.stop()
+
+ def exit_json(self, *args, **kwargs):
+ raise AnsibleExitJson(kwargs)
+
+ def fail_json(self, *args, **kwargs):
+ raise AnsibleFailJson(kwargs)
+
+
+class TestKollaToolboxMethods(TestKollaToolboxModule):
+ """Class focused on testing the methods of KollaToolboxWorker."""
+
+ def setUp(self):
+ super().setUp()
+
+ # Mock container client
+ self.mock_container_client = mock.MagicMock()
+ self.mock_container_errors = mock.MagicMock()
+ self.mock_container_errors.APIError = MockAPIError
+
+ # Mock Ansible module
+ self.mock_ansible_module = mock.MagicMock()
+ self.mock_ansible_module.fail_json.side_effect = self.fail_json
+ self.mock_ansible_module.exit_json.side_effect = self.exit_json
+
+ # Fake Kolla Toolbox Worker
+ self.fake_ktbw = kolla_toolbox.KollaToolboxWorker(
+ self.mock_ansible_module,
+ self.mock_container_client,
+ self.mock_container_errors)
+
+ def test_ktb_container_missing_or_not_running(self):
+ self.mock_container_client.containers.list.return_value = []
+
+ error = self.assertRaises(AnsibleFailJson,
+ self.fake_ktbw._get_toolbox_container)
+ self.assertIn("kolla_toolbox container is missing or not running!",
+ error.result["msg"])
+
+ def test_get_ktb_container_success(self):
+ ktb_container = mock.MagicMock()
+ other_container = mock.MagicMock()
+ self.mock_container_client.containers.list.return_value = [
+ ktb_container, other_container]
+
+ ktb_container_returned = self.fake_ktbw._get_toolbox_container()
+
+ self.assertEqual(ktb_container, ktb_container_returned)
+
+ def test_format_module_args(self):
+ module_args = [
+ {
+ 'module_args': {},
+ 'expected_output': []
+ },
+ {
+ 'module_args': {
+ 'path': '/some/folder',
+ 'state': 'absent'},
+ 'expected_output': ["path='/some/folder'", "state='absent'"]
+ }
+ ]
+
+ for args in module_args:
+ formatted_args = self.fake_ktbw._format_module_args(
+ args['module_args'])
+
+ self.assertEqual(args['expected_output'], formatted_args)
+
+ @mock.patch('kolla_toolbox.KollaToolboxWorker._format_module_args')
+ def test_generate_correct_ktb_command(self, mock_formatter):
+ fake_module_params = {
+ 'module_args': {
+ 'path': '/some/folder',
+ 'state': 'absent'
+ },
+ 'module_extra_vars': {
+ 'variable': {
+ 'key': 'pair',
+ 'list': ['item1', 'item2']
+ }
+ },
+ 'user': 'root',
+ 'module_name': 'file'
+ }
+
+ mock_params = mock.MagicMock()
+ mock_params.get.side_effect = lambda key: fake_module_params.get(key)
+ self.mock_ansible_module.params = mock_params
+
+ mock_formatter.side_effect = [
+ ["path='/some/folder'", "state='absent'"],
+ ['variable=\'{"key": "pair", "list": ["item1", "item2"]}\'']
+ ]
+
+ expected_command = ['ansible', 'localhost', '-m', 'file',
+ '-a', "path='/some/folder' state='absent'",
+ '-e', 'variable=\'{"key": "pair", '
+ '"list": ["item1", "item2"]}\'',
+ '--check']
+
+ generated_command = self.fake_ktbw._generate_command()
+
+ self.assertEqual(expected_command, generated_command)
+ mock_formatter.assert_has_calls([
+ mock.call(fake_module_params['module_args']),
+ mock.call(fake_module_params['module_extra_vars'])
+ ])
+
+ def test_run_command_raises_apierror(self):
+ ktb_container = mock.MagicMock()
+ api_error = self.mock_container_errors.APIError(
+ 'API error occurred', explanation='Error explanation')
+ ktb_container.exec_run.side_effect = api_error
+
+ error = self.assertRaises(AnsibleFailJson,
+ self.fake_ktbw._run_command,
+ ktb_container,
+ 'some_command')
+ self.assertIn('Container engine client encountered API error',
+ error.result['msg'])
+
+ def test_run_command_success(self):
+ exec_return_value = (0, b'data')
+ ktb_container = mock.MagicMock()
+ ktb_container.exec_run.return_value = exec_return_value
+ self.mock_container_client.containers.list.return_value = [
+ ktb_container]
+
+ command_output = self.fake_ktbw._run_command(
+ ktb_container, 'some_command')
+
+ self.assertEqual(exec_return_value[1], command_output)
+ self.assertIsInstance(command_output, bytes)
+ ktb_container.exec_run.assert_called_once_with('some_command')
+
+ def test_process_container_output_invalid_json(self):
+ invalid_json = b'this is no json'
+
+ error = self.assertRaises(AnsibleFailJson,
+ self.fake_ktbw._process_container_output,
+ invalid_json)
+ self.assertIn('Parsing kolla_toolbox JSON output failed',
+ error.result['msg'])
+
+ def test_process_container_output_invalid_structure(self):
+ wrong_output_json = {
+ 'plays': [
+ {
+ 'tasks': [
+ {
+ 'wrong': {
+ 'control_node': {
+ 'pong': 'ping'
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ encoded_json = json.dumps(wrong_output_json).encode('utf-8')
+
+ error = self.assertRaises(AnsibleFailJson,
+ self.fake_ktbw._process_container_output,
+ encoded_json)
+ self.assertIn('Ansible JSON output has unexpected format',
+ error.result['msg'])
+
+ def test_process_container_output_success(self):
+ container_output_json = {
+ 'custom_stats': {},
+ 'global_custom_stats': {},
+ 'plays': [
+ {
+ 'tasks': [
+ {
+ 'hosts': {
+ 'localhost': {
+ '_ansible_no_log': False,
+ 'action': 'ping',
+ 'changed': False,
+ 'invocation': {
+ 'module_args': {
+ 'data': 'pong'
+ }
+ },
+ 'ping': 'pong'
+ }
+ },
+ }
+ ]
+ }
+ ],
+ }
+ container_encoded_json = json.dumps(
+ container_output_json).encode('utf-8')
+
+ expected_output = {
+ 'action': 'ping',
+ 'changed': False,
+ 'invocation': {
+ 'module_args': {
+ 'data': 'pong'
+ }
+ },
+ 'ping': 'pong'
+ }
+ generated_module_output = self.fake_ktbw._process_container_output(
+ container_encoded_json)
+
+ self.assertNotIn('_ansible_no_log', generated_module_output)
+ self.assertEqual(expected_output, generated_module_output)
+
+
+class TestModuleInteraction(TestKollaToolboxModule):
+ """Class focused on testing user input data from playbook."""
+
+ def test_create_ansible_module_missing_required_module_name(self):
+ set_module_args({
+ 'container_engine': 'docker'
+ })
+
+ error = self.assertRaises(AnsibleFailJson,
+ kolla_toolbox.create_ansible_module)
+ self.assertIn('missing required arguments: module_name',
+ error.result['msg'])
+
+ def test_create_ansible_module_missing_required_container_engine(self):
+ set_module_args({
+ 'module_name': 'url'
+ })
+
+ error = self.assertRaises(AnsibleFailJson,
+ kolla_toolbox.create_ansible_module)
+ self.assertIn('missing required arguments: container_engine',
+ error.result['msg'])
+
+ def test_create_ansible_module_invalid_container_engine(self):
+ set_module_args({
+ 'module_name': 'url',
+ 'container_engine': 'podmano'
+ })
+
+ error = self.assertRaises(AnsibleFailJson,
+ kolla_toolbox.create_ansible_module)
+ self.assertIn(
+ 'value of container_engine must be one of: podman, docker',
+ error.result['msg']
+ )
+
+ def test_create_ansible_module_success(self):
+ args = {
+ 'container_engine': 'docker',
+ 'module_name': 'file',
+ 'module_args': {
+ 'path': '/some/folder',
+ 'state': 'absent'
+ },
+ 'module_extra_vars': {
+ 'variable': {
+ 'key': 'pair',
+ 'list': ['item1', 'item2']
+ }
+ },
+ 'user': 'root',
+ 'timeout': 180,
+ 'api_version': '1.5'
+ }
+ set_module_args(args)
+
+ module = kolla_toolbox.create_ansible_module()
+
+ self.assertIsInstance(module, AnsibleModule)
+ self.assertEqual(args, module.params)
+
+
+class TestContainerEngineClientIntraction(TestKollaToolboxModule):
+ """Class focused on testing container engine client creation."""
+
+ def setUp(self):
+ super().setUp()
+ self.module_to_mock_import = ''
+ self.original_import = builtins.__import__
+
+ def mock_import_error(self, name, globals, locals, fromlist, level):
+ """Mock import function to raise ImportError for a specific module."""
+
+ if name == self.module_to_mock_import:
+ raise ImportError(f'No module named {name}')
+ return self.original_import(name, globals, locals, fromlist, level)
+
+ def test_podman_client_params(self):
+ set_module_args({
+ 'module_name': 'ping',
+ 'container_engine': 'podman',
+ 'api_version': '1.47',
+ 'timeout': 155
+ })
+
+ module = kolla_toolbox.create_ansible_module()
+ mock_podman = mock.MagicMock()
+ mock_podman_errors = mock.MagicMock()
+ import_dict = {'podman': mock_podman,
+ 'podman.errors': mock_podman_errors}
+
+ with mock.patch.dict('sys.modules', import_dict):
+ kolla_toolbox.create_container_client(module)
+ mock_podman.PodmanClient.assert_called_with(
+ base_url='http+unix:/run/podman/podman.sock',
+ version='1.47',
+ timeout=155
+ )
+
+ def test_docker_client_params(self):
+ set_module_args({
+ 'module_name': 'ping',
+ 'container_engine': 'docker',
+ 'api_version': '1.47',
+ 'timeout': 155
+ })
+
+ module = kolla_toolbox.create_ansible_module()
+ mock_docker = mock.MagicMock()
+ mock_docker_errors = mock.MagicMock()
+ import_dict = {'docker': mock_docker,
+ 'docker.errors': mock_docker_errors}
+
+ with mock.patch.dict('sys.modules', import_dict):
+ kolla_toolbox.create_container_client(module)
+ mock_docker.DockerClient.assert_called_with(
+ base_url='http+unix:/var/run/docker.sock',
+ version='1.47',
+ timeout=155
+ )
+
+ def test_create_container_client_podman_not_called_with_auto(self):
+ set_module_args({
+ 'module_name': 'ping',
+ 'container_engine': 'podman',
+ 'api_version': 'auto',
+ 'timeout': 90
+ })
+
+ module = kolla_toolbox.create_ansible_module()
+ mock_podman = mock.MagicMock()
+ mock_podman_errors = mock.MagicMock()
+ import_dict = {'podman': mock_podman,
+ 'podman.errors': mock_podman_errors}
+
+ with mock.patch.dict('sys.modules', import_dict):
+ kolla_toolbox.create_container_client(module)
+ mock_podman.PodmanClient.assert_called_with(
+ base_url='http+unix:/run/podman/podman.sock',
+ timeout=90
+ )
+
+ def test_create_container_client_podman_importerror(self):
+ set_module_args({
+ 'module_name': 'ping',
+ 'container_engine': 'podman'
+ })
+ self.module_to_mock_import = 'podman'
+ module = kolla_toolbox.create_ansible_module()
+
+ with mock.patch('builtins.__import__',
+ side_effect=self.mock_import_error):
+ error = self.assertRaises(AnsibleFailJson,
+ kolla_toolbox.create_container_client,
+ module)
+ self.assertIn('The podman library could not be imported!',
+ error.result['msg'])
+
+ def test_create_container_client_docker_importerror(self):
+ set_module_args({
+ 'module_name': 'ping',
+ 'container_engine': 'docker'
+ })
+
+ self.module_to_mock_import = 'docker'
+ module = kolla_toolbox.create_ansible_module()
+
+ with mock.patch('builtins.__import__',
+ side_effect=self.mock_import_error):
+ error = self.assertRaises(AnsibleFailJson,
+ kolla_toolbox.create_container_client,
+ module)
+ self.assertIn('The docker library could not be imported!',
+ error.result['msg'])