kolla-ansible/ansible/library/kolla_toolbox.py
Roman Krček c803413c52 Move kolla_toolbox to high level client
Move from APIClient to PodmanClient and DockerClient clients.

Signed-off-by: Roman Krček <roman.krcek@tietoevry.com>
Change-Id: I6c5734b6fff1bd42929851a263620bb4d959ac25
2025-03-31 09:33:56 +00:00

292 lines
9.4 KiB
Python

# Copyright 2016 99cloud Inc.
#
# 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 json
import traceback
from ansible.module_utils.basic import AnsibleModule
DOCUMENTATION = '''
---
module: kolla_toolbox
short_description: >
Module for invoking ansible module in kolla_toolbox container.
description:
- A module targerting at invoking ansible module in kolla_toolbox
container as used by Kolla project.
options:
container_engine:
description:
- Name of container engine to use
required: True
type: str
choices: ['docker', 'podman']
module_name:
description:
- The module name to invoke
required: True
type: str
module_args:
description:
- The module args use by the module
required: False
type: dict
module_extra_vars:
description:
- The extra variables used by the module
required: False
type: dict
user:
description:
- The user to execute Ansible inside kolla_toolbox with
required: False
type: str
api_version:
description:
- 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 client when contacting container API
required: False
type: int
default: 180
authors: Jeffrey Zhang, Roman Krček
'''
EXAMPLES = '''
- hosts: controller
tasks:
- name: Ensure the directory is removed
kolla_toolbox:
container_engine: podman
module_name: file
module_args:
path: /tmp/a
state: absent
- name: Create mysql database
kolla_toolbox:
container_engine: docker
module_name: mysql_db
module_args:
login_host: 192.168.1.10
login_user: root
login_password: admin
name: testdb
- name: Create default user role
kolla_toolbox:
container_engine: docker
module_name: openstack.cloud.identity_role
module_args:
name: member
auth: "{{ '{{ openstack_keystone_auth }}' }}"
module_extra_vars:
openstack_keystone_auth:
auth_url: http://127.0.0.1:5000
username: admin
password: password
project_name: "admin"
domain_name: "default"
'''
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 _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:
_, 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>
# }
# }
# }
# ]
# {
# ]
# }
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}'
)
return result
def main(self) -> None:
"""Run command inside the kolla_toolbox container with defined args."""
kolla_toolbox = self._get_toolbox_container()
command = self._generate_command()
environment = {'ANSIBLE_STDOUT_CALLBACK': 'json',
'ANSIBLE_LOAD_CALLBACK_PLUGINS': 'True'}
output_raw = self._run_command(kolla_toolbox,
command,
environment=environment,
tty=True,
user=self.module.params.get('user'))
self.result = self._process_container_output(output_raw)
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')
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'),
)
return AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
def main():
module = create_ansible_module()
client, container_errors = create_container_client(module)
ktbw = KollaToolboxWorker(module, client, container_errors)
try:
ktbw.main()
module.exit_json(**ktbw.result)
except Exception:
module.fail_json(changed=True,
msg=repr(traceback.format_exc()))
if __name__ == '__main__':
main()