From b6ca89d194aeb428e58f680a43c4bef700b96831 Mon Sep 17 00:00:00 2001 From: Sagi Shnaidman Date: Wed, 21 Oct 2020 15:08:10 +0300 Subject: [PATCH] Update for a podman_container from collection And removing docker.io from podman container tests. Change-Id: I29f94783637264b6938c1f03925ed700bd8acafc --- .../module_utils/podman/common.py | 28 + .../podman/podman_container_lib.py | 1452 +++++++++++++++++ .../modules/podman_container.py | 1347 +-------------- .../molecule/podman_container/converge.yml | 54 +- 4 files changed, 1549 insertions(+), 1332 deletions(-) create mode 100644 tripleo_ansible/ansible_plugins/module_utils/podman/common.py create mode 100644 tripleo_ansible/ansible_plugins/module_utils/podman/podman_container_lib.py diff --git a/tripleo_ansible/ansible_plugins/module_utils/podman/common.py b/tripleo_ansible/ansible_plugins/module_utils/podman/common.py new file mode 100644 index 000000000..d7b0ed790 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/module_utils/podman/common.py @@ -0,0 +1,28 @@ +#!/usr/bin/python +# Copyright (c) 2020 Red Hat +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +def run_podman_command(module, executable='podman', args=None, expected_rc=0, ignore_errors=False): + if not isinstance(executable, list): + command = [executable] + if args is not None: + command.extend(args) + rc, out, err = module.run_command(command) + if not ignore_errors and rc != expected_rc: + module.fail_json( + msg='Failed to run {command} {args}: {err}'.format( + command=command, args=args, err=err)) + return rc, out, err + + +def lower_keys(x): + if isinstance(x, list): + return [lower_keys(v) for v in x] + elif isinstance(x, dict): + return dict((k.lower(), lower_keys(v)) for k, v in x.items()) + else: + return x diff --git a/tripleo_ansible/ansible_plugins/module_utils/podman/podman_container_lib.py b/tripleo_ansible/ansible_plugins/module_utils/podman/podman_container_lib.py new file mode 100644 index 000000000..004196ab2 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/module_utils/podman/podman_container_lib.py @@ -0,0 +1,1452 @@ +#!/usr/bin/python +# Copyright (c) 2019 OpenStack Foundation +# 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, division, print_function) +import json # noqa: F402 +from distutils.version import LooseVersion # noqa: F402 + +from ansible.module_utils._text import to_bytes, to_native # noqa: F402 +from ansible.module_utils.podman.common import lower_keys + +__metaclass__ = type + +ARGUMENTS_SPEC_CONTAINER = dict( + name=dict(required=True, type='str'), + executable=dict(default='podman', type='str'), + state=dict(type='str', default='started', choices=[ + 'absent', 'present', 'stopped', 'started']), + image=dict(type='str'), + annotation=dict(type='dict'), + authfile=dict(type='path'), + blkio_weight=dict(type='int'), + blkio_weight_device=dict(type='dict'), + cap_add=dict(type='list', elements='str', aliases=['capabilities']), + cap_drop=dict(type='list', elements='str'), + cgroup_parent=dict(type='path'), + cgroupns=dict(type='str'), + cgroups=dict(type='str', choices=['default', 'disabled']), + cidfile=dict(type='path'), + cmd_args=dict(type='list', elements='str'), + conmon_pidfile=dict(type='path'), + command=dict(type='raw'), + cpu_period=dict(type='int'), + cpu_rt_period=dict(type='int'), + cpu_rt_runtime=dict(type='int'), + cpu_shares=dict(type='int'), + cpus=dict(type='str'), + cpuset_cpus=dict(type='str'), + cpuset_mems=dict(type='str'), + detach=dict(type='bool', default=True), + debug=dict(type='bool', default=False), + detach_keys=dict(type='str'), + device=dict(type='list', elements='str'), + device_read_bps=dict(type='list'), + device_read_iops=dict(type='list'), + device_write_bps=dict(type='list'), + device_write_iops=dict(type='list'), + dns=dict(type='list', elements='str', aliases=['dns_servers']), + dns_option=dict(type='str', aliases=['dns_opts']), + dns_search=dict(type='str', aliases=['dns_search_domains']), + entrypoint=dict(type='str'), + env=dict(type='dict'), + env_file=dict(type='path'), + env_host=dict(type='bool'), + etc_hosts=dict(type='dict', aliases=['add_hosts']), + expose=dict(type='list', elements='str', aliases=[ + 'exposed', 'exposed_ports']), + force_restart=dict(type='bool', default=False, + aliases=['restart']), + gidmap=dict(type='str'), + group_add=dict(type='list', aliases=['groups']), + healthcheck=dict(type='str'), + healthcheck_interval=dict(type='str'), + healthcheck_retries=dict(type='int'), + healthcheck_start_period=dict(type='str'), + healthcheck_timeout=dict(type='str'), + hostname=dict(type='str'), + http_proxy=dict(type='bool'), + image_volume=dict(type='str', choices=['bind', 'tmpfs', 'ignore']), + image_strict=dict(type='bool', default=False), + init=dict(type='bool'), + init_path=dict(type='str'), + interactive=dict(type='bool'), + ip=dict(type='str'), + ipc=dict(type='str', aliases=['ipc_mode']), + kernel_memory=dict(type='str'), + label=dict(type='dict', aliases=['labels']), + label_file=dict(type='str'), + log_driver=dict(type='str', choices=[ + 'k8s-file', 'journald', 'json-file']), + log_level=dict( + type='str', + choices=["debug", "info", "warn", "error", "fatal", "panic"]), + log_opt=dict(type='str', aliases=['log_options']), + mac_address=dict(type='str'), + memory=dict(type='str'), + memory_reservation=dict(type='str'), + memory_swap=dict(type='str'), + memory_swappiness=dict(type='int'), + mount=dict(type='str'), + network=dict(type='list', elements='str', aliases=['net', 'network_mode']), + no_hosts=dict(type='bool'), + oom_kill_disable=dict(type='bool'), + oom_score_adj=dict(type='int'), + pid=dict(type='str', aliases=['pid_mode']), + pids_limit=dict(type='str'), + pod=dict(type='str'), + privileged=dict(type='bool'), + publish=dict(type='list', elements='str', aliases=[ + 'ports', 'published', 'published_ports']), + publish_all=dict(type='bool'), + read_only=dict(type='bool'), + read_only_tmpfs=dict(type='bool'), + recreate=dict(type='bool', default=False), + restart_policy=dict(type='str'), + rm=dict(type='bool', aliases=['remove', 'auto_remove']), + rootfs=dict(type='bool'), + security_opt=dict(type='list', elements='str'), + shm_size=dict(type='str'), + sig_proxy=dict(type='bool'), + stop_signal=dict(type='int'), + stop_timeout=dict(type='int'), + subgidname=dict(type='str'), + subuidname=dict(type='str'), + sysctl=dict(type='dict'), + systemd=dict(type='bool'), + tmpfs=dict(type='dict'), + tty=dict(type='bool'), + uidmap=dict(type='list'), + ulimit=dict(type='list', aliases=['ulimits']), + user=dict(type='str'), + userns=dict(type='str', aliases=['userns_mode']), + uts=dict(type='str'), + volume=dict(type='list', elements='str', aliases=['volumes']), + volumes_from=dict(type='list', elements='str'), + workdir=dict(type='str', aliases=['working_dir']) +) + + +class PodmanModuleParams: + """Creates list of arguments for podman CLI command. + + Arguments: + action {str} -- action type from 'run', 'stop', 'create', 'delete', + 'start' + params {dict} -- dictionary of module parameters + + """ + + def __init__(self, action, params, podman_version, module): + self.params = params + self.action = action + self.podman_version = podman_version + self.module = module + + def construct_command_from_params(self): + """Create a podman command from given module parameters. + + Returns: + list -- list of byte strings for Popen command + """ + if self.action in ['start', 'stop', 'delete']: + return self.start_stop_delete() + if self.action in ['create', 'run']: + cmd = [self.action, '--name', self.params['name']] + all_param_methods = [func for func in dir(self) + if callable(getattr(self, func)) + and func.startswith("addparam")] + params_set = (i for i in self.params if self.params[i] is not None) + for param in params_set: + func_name = "_".join(["addparam", param]) + if func_name in all_param_methods: + cmd = getattr(self, func_name)(cmd) + cmd.append(self.params['image']) + if self.params['command']: + if isinstance(self.params['command'], list): + cmd += self.params['command'] + else: + cmd += self.params['command'].split() + return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] + + def start_stop_delete(self): + + if self.action in ['stop', 'start']: + cmd = [self.action, self.params['name']] + return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] + + if self.action == 'delete': + cmd = ['rm', '-f', self.params['name']] + return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] + + def check_version(self, param, minv=None, maxv=None): + if minv and LooseVersion(minv) > LooseVersion( + self.podman_version): + self.module.fail_json(msg="Parameter %s is supported from podman " + "version %s only! Current version is %s" % ( + param, minv, self.podman_version)) + if maxv and LooseVersion(maxv) < LooseVersion( + self.podman_version): + self.module.fail_json(msg="Parameter %s is supported till podman " + "version %s only! Current version is %s" % ( + param, minv, self.podman_version)) + + def addparam_annotation(self, c): + for annotate in self.params['annotation'].items(): + c += ['--annotation', '='.join(annotate)] + return c + + def addparam_authfile(self, c): + return c + ['--authfile', self.params['authfile']] + + def addparam_blkio_weight(self, c): + return c + ['--blkio-weight', self.params['blkio_weight']] + + def addparam_blkio_weight_device(self, c): + for blkio in self.params['blkio_weight_device'].items(): + c += ['--blkio-weight-device', ':'.join(blkio)] + return c + + def addparam_cap_add(self, c): + for cap_add in self.params['cap_add']: + c += ['--cap-add', cap_add] + return c + + def addparam_cap_drop(self, c): + for cap_drop in self.params['cap_drop']: + c += ['--cap-drop', cap_drop] + return c + + def addparam_cgroups(self, c): + self.check_version('--cgroups', minv='1.6.0') + return c + ['--cgroups=%s' % self.params['cgroups']] + + def addparam_cgroupns(self, c): + self.check_version('--cgroupns', minv='1.6.2') + return c + ['--cgroupns=%s' % self.params['cgroupns']] + + def addparam_cgroup_parent(self, c): + return c + ['--cgroup-parent', self.params['cgroup_parent']] + + def addparam_cidfile(self, c): + return c + ['--cidfile', self.params['cidfile']] + + def addparam_conmon_pidfile(self, c): + return c + ['--conmon-pidfile', self.params['conmon_pidfile']] + + def addparam_cpu_period(self, c): + return c + ['--cpu-period', self.params['cpu_period']] + + def addparam_cpu_rt_period(self, c): + return c + ['--cpu-rt-period', self.params['cpu_rt_period']] + + def addparam_cpu_rt_runtime(self, c): + return c + ['--cpu-rt-runtime', self.params['cpu_rt_runtime']] + + def addparam_cpu_shares(self, c): + return c + ['--cpu-shares', self.params['cpu_shares']] + + def addparam_cpus(self, c): + return c + ['--cpus', self.params['cpus']] + + def addparam_cpuset_cpus(self, c): + return c + ['--cpuset-cpus', self.params['cpuset_cpus']] + + def addparam_cpuset_mems(self, c): + return c + ['--cpuset-mems', self.params['cpuset_mems']] + + def addparam_detach(self, c): + return c + ['--detach=%s' % self.params['detach']] + + def addparam_detach_keys(self, c): + return c + ['--detach-keys', self.params['detach_keys']] + + def addparam_device(self, c): + for dev in self.params['device']: + c += ['--device', dev] + return c + + def addparam_device_read_bps(self, c): + for dev in self.params['device_read_bps']: + c += ['--device-read-bps', dev] + return c + + def addparam_device_read_iops(self, c): + for dev in self.params['device_read_iops']: + c += ['--device-read-iops', dev] + return c + + def addparam_device_write_bps(self, c): + for dev in self.params['device_write_bps']: + c += ['--device-write-bps', dev] + return c + + def addparam_device_write_iops(self, c): + for dev in self.params['device_write_iops']: + c += ['--device-write-iops', dev] + return c + + def addparam_dns(self, c): + return c + ['--dns', ','.join(self.params['dns'])] + + def addparam_dns_option(self, c): + return c + ['--dns-option', self.params['dns_option']] + + def addparam_dns_search(self, c): + return c + ['--dns-search', self.params['dns_search']] + + def addparam_entrypoint(self, c): + return c + ['--entrypoint', self.params['entrypoint']] + + def addparam_env(self, c): + for env_value in self.params['env'].items(): + c += ['--env', + b"=".join([to_bytes(k, errors='surrogate_or_strict') + for k in env_value])] + return c + + def addparam_env_file(self, c): + return c + ['--env-file', self.params['env_file']] + + def addparam_env_host(self, c): + self.check_version('--env-host', minv='1.5.0') + return c + ['--env-host=%s' % self.params['env_host']] + + def addparam_etc_hosts(self, c): + for host_ip in self.params['etc_hosts'].items(): + c += ['--add-host', ':'.join(host_ip)] + return c + + def addparam_expose(self, c): + for exp in self.params['expose']: + c += ['--expose', exp] + return c + + def addparam_gidmap(self, c): + return c + ['--gidmap', self.params['gidmap']] + + def addparam_group_add(self, c): + for g in self.params['group_add']: + c += ['--group-add', g] + return c + + def addparam_healthcheck(self, c): + return c + ['--healthcheck-command', self.params['healthcheck']] + + def addparam_healthcheck_interval(self, c): + return c + ['--healthcheck-interval', + self.params['healthcheck_interval']] + + def addparam_healthcheck_retries(self, c): + return c + ['--healthcheck-retries', + self.params['healthcheck_retries']] + + def addparam_healthcheck_start_period(self, c): + return c + ['--healthcheck-start-period', + self.params['healthcheck_start_period']] + + def addparam_healthcheck_timeout(self, c): + return c + ['--healthcheck-timeout', + self.params['healthcheck_timeout']] + + def addparam_hostname(self, c): + return c + ['--hostname', self.params['hostname']] + + def addparam_http_proxy(self, c): + return c + ['--http-proxy=%s' % self.params['http_proxy']] + + def addparam_image_volume(self, c): + return c + ['--image-volume', self.params['image_volume']] + + def addparam_init(self, c): + if self.params['init']: + c += ['--init'] + return c + + def addparam_init_path(self, c): + return c + ['--init-path', self.params['init_path']] + + def addparam_interactive(self, c): + return c + ['--interactive=%s' % self.params['interactive']] + + def addparam_ip(self, c): + return c + ['--ip', self.params['ip']] + + def addparam_ipc(self, c): + return c + ['--ipc', self.params['ipc']] + + def addparam_kernel_memory(self, c): + return c + ['--kernel-memory', self.params['kernel_memory']] + + def addparam_label(self, c): + for label in self.params['label'].items(): + c += ['--label', b'='.join([to_bytes(la, errors='surrogate_or_strict') + for la in label])] + return c + + def addparam_label_file(self, c): + return c + ['--label-file', self.params['label_file']] + + def addparam_log_driver(self, c): + return c + ['--log-driver', self.params['log_driver']] + + def addparam_log_opt(self, c): + return c + ['--log-opt', self.params['log_opt']] + + def addparam_log_level(self, c): + return c + ['--log-level', self.params['log_level']] + + def addparam_mac_address(self, c): + return c + ['--mac-address', self.params['mac_address']] + + def addparam_memory(self, c): + return c + ['--memory', self.params['memory']] + + def addparam_memory_reservation(self, c): + return c + ['--memory-reservation', self.params['memory_reservation']] + + def addparam_memory_swap(self, c): + return c + ['--memory-swap', self.params['memory_swap']] + + def addparam_memory_swappiness(self, c): + return c + ['--memory-swappiness', self.params['memory_swappiness']] + + def addparam_mount(self, c): + return c + ['--mount', self.params['mount']] + + def addparam_network(self, c): + return c + ['--network', ",".join(self.params['network'])] + + def addparam_no_hosts(self, c): + return c + ['--no-hosts=%s' % self.params['no_hosts']] + + def addparam_oom_kill_disable(self, c): + return c + ['--oom-kill-disable=%s' % self.params['oom_kill_disable']] + + def addparam_oom_score_adj(self, c): + return c + ['--oom-score-adj', self.params['oom_score_adj']] + + def addparam_pid(self, c): + return c + ['--pid', self.params['pid']] + + def addparam_pids_limit(self, c): + return c + ['--pids-limit', self.params['pids_limit']] + + def addparam_pod(self, c): + return c + ['--pod', self.params['pod']] + + def addparam_privileged(self, c): + return c + ['--privileged=%s' % self.params['privileged']] + + def addparam_publish(self, c): + for pub in self.params['publish']: + c += ['--publish', pub] + return c + + def addparam_publish_all(self, c): + return c + ['--publish-all=%s' % self.params['publish_all']] + + def addparam_read_only(self, c): + return c + ['--read-only=%s' % self.params['read_only']] + + def addparam_read_only_tmpfs(self, c): + return c + ['--read-only-tmpfs=%s' % self.params['read_only_tmpfs']] + + def addparam_restart_policy(self, c): + return c + ['--restart=%s' % self.params['restart_policy']] + + def addparam_rm(self, c): + if self.params['rm']: + c += ['--rm'] + return c + + def addparam_rootfs(self, c): + return c + ['--rootfs=%s' % self.params['rootfs']] + + def addparam_security_opt(self, c): + for secopt in self.params['security_opt']: + c += ['--security-opt', secopt] + return c + + def addparam_shm_size(self, c): + return c + ['--shm-size', self.params['shm_size']] + + def addparam_sig_proxy(self, c): + return c + ['--sig-proxy=%s' % self.params['sig_proxy']] + + def addparam_stop_signal(self, c): + return c + ['--stop-signal', self.params['stop_signal']] + + def addparam_stop_timeout(self, c): + return c + ['--stop-timeout', self.params['stop_timeout']] + + def addparam_subgidname(self, c): + return c + ['--subgidname', self.params['subgidname']] + + def addparam_subuidname(self, c): + return c + ['--subuidname', self.params['subuidname']] + + def addparam_sysctl(self, c): + for sysctl in self.params['sysctl'].items(): + c += ['--sysctl', + b"=".join([to_bytes(k, errors='surrogate_or_strict') + for k in sysctl])] + return c + + def addparam_systemd(self, c): + return c + ['--systemd=%s' % self.params['systemd']] + + def addparam_tmpfs(self, c): + for tmpfs in self.params['tmpfs'].items(): + c += ['--tmpfs', ':'.join(tmpfs)] + return c + + def addparam_tty(self, c): + return c + ['--tty=%s' % self.params['tty']] + + def addparam_uidmap(self, c): + for uidmap in self.params['uidmap']: + c += ['--uidmap', uidmap] + return c + + def addparam_ulimit(self, c): + for u in self.params['ulimit']: + c += ['--ulimit', u] + return c + + def addparam_user(self, c): + return c + ['--user', self.params['user']] + + def addparam_userns(self, c): + return c + ['--userns', self.params['userns']] + + def addparam_uts(self, c): + return c + ['--uts', self.params['uts']] + + def addparam_volume(self, c): + for vol in self.params['volume']: + if vol: + c += ['--volume', vol] + return c + + def addparam_volumes_from(self, c): + for vol in self.params['volumes_from']: + c += ['--volumes-from', vol] + return c + + def addparam_workdir(self, c): + return c + ['--workdir', self.params['workdir']] + + # Add your own args for podman command + def addparam_cmd_args(self, c): + return c + self.params['cmd_args'] + + +class PodmanDefaults: + def __init__(self, image_info, podman_version): + self.version = podman_version + self.image_info = image_info + self.defaults = { + "blkio_weight": 0, + "cgroups": "default", + "cidfile": "", + "cpus": 0.0, + "cpu_shares": 0, + "cpu_quota": 0, + "cpu_period": 0, + "cpu_rt_runtime": 0, + "cpu_rt_period": 0, + "cpuset_cpus": "", + "cpuset_mems": "", + "detach": True, + "device": [], + "env_host": False, + "etc_hosts": {}, + "group_add": [], + "ipc": "", + "kernelmemory": "0", + "log_driver": "k8s-file", + "log_level": "error", + "memory": "0", + "memory_swap": "0", + "memory_reservation": "0", + # "memory_swappiness": -1, + "no_hosts": False, + # libpod issue with networks in inspection + "oom_score_adj": 0, + "pid": "", + "privileged": False, + "rm": False, + "security_opt": [], + "stop_signal": self.image_info['config'].get('stopsignal', "15"), + "tty": False, + "user": self.image_info.get('user', ''), + "workdir": self.image_info['config'].get('workingdir', '/'), + "uts": "", + } + + def default_dict(self): + # make here any changes to self.defaults related to podman version + # https://github.com/containers/libpod/pull/5669 + if (LooseVersion(self.version) >= LooseVersion('1.8.0') + and LooseVersion(self.version) < LooseVersion('1.9.0')): + self.defaults['cpu_shares'] = 1024 + if (LooseVersion(self.version) >= LooseVersion('2.0.0')): + self.defaults['network'] = ["slirp4netns"] + self.defaults['ipc'] = "private" + self.defaults['uts'] = "private" + self.defaults['pid'] = "private" + return self.defaults + + +class PodmanContainerDiff: + def __init__(self, module, module_params, info, image_info, podman_version): + self.module = module + self.module_params = module_params + self.version = podman_version + self.default_dict = None + self.info = lower_keys(info) + self.image_info = lower_keys(image_info) + self.params = self.defaultize() + self.diff = {'before': {}, 'after': {}} + self.non_idempotent = { + 'env_file', # We can't get env vars from file to check + 'env_host', + } + + def defaultize(self): + params_with_defaults = {} + self.default_dict = PodmanDefaults( + self.image_info, self.version).default_dict() + for p in self.module_params: + if self.module_params[p] is None and p in self.default_dict: + params_with_defaults[p] = self.default_dict[p] + else: + params_with_defaults[p] = self.module_params[p] + return params_with_defaults + + def _diff_update_and_compare(self, param_name, before, after): + if before != after: + self.diff['before'].update({param_name: before}) + self.diff['after'].update({param_name: after}) + return True + return False + + def diffparam_annotation(self): + before = self.info['config']['annotations'] or {} + after = before.copy() + if self.module_params['annotation'] is not None: + after.update(self.params['annotation']) + return self._diff_update_and_compare('annotation', before, after) + + def diffparam_env_host(self): + # It's impossible to get from inspest, recreate it if not default + before = False + after = self.params['env_host'] + return self._diff_update_and_compare('env_host', before, after) + + def diffparam_blkio_weight(self): + before = self.info['hostconfig']['blkioweight'] + after = self.params['blkio_weight'] + return self._diff_update_and_compare('blkio_weight', before, after) + + def diffparam_blkio_weight_device(self): + before = self.info['hostconfig']['blkioweightdevice'] + if before == [] and self.module_params['blkio_weight_device'] is None: + after = [] + else: + after = self.params['blkio_weight_device'] + return self._diff_update_and_compare('blkio_weight_device', before, after) + + def diffparam_cap_add(self): + before = self.info['effectivecaps'] or [] + after = [] + if self.module_params['cap_add'] is not None: + after += ["cap_" + i.lower() + for i in self.module_params['cap_add']] + after += before + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('cap_add', before, after) + + def diffparam_cap_drop(self): + before = self.info['effectivecaps'] or [] + after = before[:] + if self.module_params['cap_drop'] is not None: + for c in ["cap_" + i.lower() for i in self.module_params['cap_drop']]: + if c in after: + after.remove(c) + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('cap_drop', before, after) + + def diffparam_cgroup_parent(self): + before = self.info['hostconfig']['cgroupparent'] + after = self.params['cgroup_parent'] + if after is None: + after = before + return self._diff_update_and_compare('cgroup_parent', before, after) + + def diffparam_cgroups(self): + # Cgroups output is not supported in all versions + if 'cgroups' in self.info['hostconfig']: + before = self.info['hostconfig']['cgroups'] + after = self.params['cgroups'] + return self._diff_update_and_compare('cgroups', before, after) + return False + + def diffparam_cidfile(self): + before = self.info['hostconfig']['containeridfile'] + after = self.params['cidfile'] + return self._diff_update_and_compare('cidfile', before, after) + + def diffparam_command(self): + # TODO(sshnaidm): to inspect image to get the default command + if self.module_params['command'] is not None: + before = self.info['config']['cmd'] + after = self.params['command'] + if isinstance(after, str): + after = [i.lower() for i in after.split()] + elif isinstance(after, list): + after = [i.lower() for i in after] + return self._diff_update_and_compare('command', before, after) + return False + + def diffparam_conmon_pidfile(self): + before = self.info['conmonpidfile'] + if self.module_params['conmon_pidfile'] is None: + after = before + else: + after = self.params['conmon_pidfile'] + return self._diff_update_and_compare('conmon_pidfile', before, after) + + def diffparam_cpu_period(self): + before = self.info['hostconfig']['cpuperiod'] + after = self.params['cpu_period'] + return self._diff_update_and_compare('cpu_period', before, after) + + def diffparam_cpu_rt_period(self): + before = self.info['hostconfig']['cpurealtimeperiod'] + after = self.params['cpu_rt_period'] + return self._diff_update_and_compare('cpu_rt_period', before, after) + + def diffparam_cpu_rt_runtime(self): + before = self.info['hostconfig']['cpurealtimeruntime'] + after = self.params['cpu_rt_runtime'] + return self._diff_update_and_compare('cpu_rt_runtime', before, after) + + def diffparam_cpu_shares(self): + before = self.info['hostconfig']['cpushares'] + after = self.params['cpu_shares'] + return self._diff_update_and_compare('cpu_shares', before, after) + + def diffparam_cpus(self): + before = int(self.info['hostconfig']['nanocpus']) / 1000000000 + after = self.params['cpus'] + return self._diff_update_and_compare('cpus', before, after) + + def diffparam_cpuset_cpus(self): + before = self.info['hostconfig']['cpusetcpus'] + after = self.params['cpuset_cpus'] + return self._diff_update_and_compare('cpuset_cpus', before, after) + + def diffparam_cpuset_mems(self): + before = self.info['hostconfig']['cpusetmems'] + after = self.params['cpuset_mems'] + return self._diff_update_and_compare('cpuset_mems', before, after) + + def diffparam_device(self): + before = [":".join([i['pathonhost'], i['pathincontainer']]) + for i in self.info['hostconfig']['devices']] + after = [":".join(i.split(":")[:2]) for i in self.params['device']] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('devices', before, after) + + def diffparam_device_read_bps(self): + before = self.info['hostconfig']['blkiodevicereadbps'] or [] + before = ["%s:%s" % (i['path'], i['rate']) for i in before] + after = self.params['device_read_bps'] or [] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('device_read_bps', before, after) + + def diffparam_device_read_iops(self): + before = self.info['hostconfig']['blkiodevicereadiops'] or [] + before = ["%s:%s" % (i['path'], i['rate']) for i in before] + after = self.params['device_read_iops'] or [] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('device_read_iops', before, after) + + def diffparam_device_write_bps(self): + before = self.info['hostconfig']['blkiodevicewritebps'] or [] + before = ["%s:%s" % (i['path'], i['rate']) for i in before] + after = self.params['device_write_bps'] or [] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('device_write_bps', before, after) + + def diffparam_device_write_iops(self): + before = self.info['hostconfig']['blkiodevicewriteiops'] or [] + before = ["%s:%s" % (i['path'], i['rate']) for i in before] + after = self.params['device_write_iops'] or [] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('device_write_iops', before, after) + + # Limited idempotency, it can't guess default values + def diffparam_env(self): + env_before = self.info['config']['env'] or {} + before = {i.split("=")[0]: "=".join(i.split("=")[1:]) + for i in env_before} + after = before.copy() + if self.params['env']: + after.update({ + k: v + for k, v in self.params['env'].items() + }) + return self._diff_update_and_compare('env', before, after) + + def diffparam_etc_hosts(self): + if self.info['hostconfig']['extrahosts']: + before = dict([i.split(":") + for i in self.info['hostconfig']['extrahosts']]) + else: + before = {} + after = self.params['etc_hosts'] + return self._diff_update_and_compare('etc_hosts', before, after) + + def diffparam_group_add(self): + before = self.info['hostconfig']['groupadd'] + after = self.params['group_add'] + return self._diff_update_and_compare('group_add', before, after) + + # Healthcheck is only defined in container config if a healthcheck + # was configured; otherwise the config key isn't part of the config. + def diffparam_healthcheck(self): + if 'healthcheck' in self.info['config']: + # the "test" key is a list of 2 items where the first one is + # "CMD-SHELL" and the second one is the actual healthcheck command. + before = self.info['config']['healthcheck']['test'][1] + else: + before = '' + after = self.params['healthcheck'] or before + return self._diff_update_and_compare('healthcheck', before, after) + + # Because of hostname is random generated, this parameter has partial idempotency only. + def diffparam_hostname(self): + before = self.info['config']['hostname'] + after = self.params['hostname'] or before + return self._diff_update_and_compare('hostname', before, after) + + def diffparam_image(self): + before_id = self.info['image'] + after_id = self.image_info['id'] + if before_id == after_id: + return self._diff_update_and_compare('image', before_id, after_id) + before = self.info['config']['image'] + after = self.params['image'] + mode = self.params['image_strict'] + if mode is None or not mode: + # In a idempotency 'lite mode' assume all images from different registries are the same + before = before.replace(":latest", "") + after = after.replace(":latest", "") + before = before.split("/")[-1] + after = after.split("/")[-1] + else: + return self._diff_update_and_compare('image', before_id, after_id) + return self._diff_update_and_compare('image', before, after) + + def diffparam_ipc(self): + before = self.info['hostconfig']['ipcmode'] + after = self.params['ipc'] + if self.params['pod'] and not self.module_params['ipc']: + after = before + return self._diff_update_and_compare('ipc', before, after) + + def diffparam_label(self): + before = self.info['config']['labels'] or {} + after = self.image_info.get('labels') or {} + if self.params['label']: + after.update({ + str(k).lower(): str(v).lower() + for k, v in self.params['label'].items() + }) + return self._diff_update_and_compare('label', before, after) + + def diffparam_log_driver(self): + before = self.info['hostconfig']['logconfig']['type'] + after = self.params['log_driver'] + return self._diff_update_and_compare('log_driver', before, after) + + def diffparam_log_level(self): + excom = self.info['exitcommand'] + if '--log-level' in excom: + before = excom[excom.index('--log-level') + 1].lower() + else: + before = self.params['log_level'] + after = self.params['log_level'] + return self._diff_update_and_compare('log_level', before, after) + + # Parameter has limited idempotency, unable to guess the default log_path + def diffparam_log_opt(self): + before = self.info['logpath'] + if self.module_params['log_opt'] in [None, '']: + after = before + else: + after = self.params['log_opt'].split("=")[1] + return self._diff_update_and_compare('log_opt', before, after) + + def diffparam_memory(self): + before = str(self.info['hostconfig']['memory']) + after = self.params['memory'] + return self._diff_update_and_compare('memory', before, after) + + def diffparam_memory_swap(self): + # By default it's twice memory parameter + before = str(self.info['hostconfig']['memoryswap']) + after = self.params['memory_swap'] + if (self.module_params['memory_swap'] is None + and self.params['memory'] != 0 + and self.params['memory'].isdigit()): + after = str(int(self.params['memory']) * 2) + return self._diff_update_and_compare('memory_swap', before, after) + + def diffparam_memory_reservation(self): + before = str(self.info['hostconfig']['memoryreservation']) + after = self.params['memory_reservation'] + return self._diff_update_and_compare('memory_reservation', before, after) + + def diffparam_network(self): + net_mode_before = self.info['hostconfig']['networkmode'] + net_mode_after = '' + before = list(self.info['networksettings'].get('networks', {})) + after = self.params['network'] or [] + # If container is in pod and no networks are provided + if not self.module_params['network'] and self.params['pod']: + after = before + return self._diff_update_and_compare('network', before, after) + # Check special network modes + if after in [['bridge'], ['host'], ['slirp4netns'], ['none']]: + net_mode_after = after[0] + # If changes are only for network mode and container has no networks + if net_mode_after and not before: + # Remove differences between v1 and v2 + net_mode_after = net_mode_after.replace('bridge', 'default') + net_mode_after = net_mode_after.replace('slirp4netns', 'default') + net_mode_before = net_mode_before.replace('bridge', 'default') + net_mode_before = net_mode_before.replace('slirp4netns', 'default') + return self._diff_update_and_compare('network', net_mode_before, net_mode_after) + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('network', before, after) + + def diffparam_no_hosts(self): + before = not bool(self.info['hostspath']) + after = self.params['no_hosts'] + if self.params['network'] == ['none']: + after = True + return self._diff_update_and_compare('no_hosts', before, after) + + def diffparam_oom_score_adj(self): + before = self.info['hostconfig']['oomscoreadj'] + after = self.params['oom_score_adj'] + return self._diff_update_and_compare('oom_score_adj', before, after) + + def diffparam_privileged(self): + before = self.info['hostconfig']['privileged'] + after = self.params['privileged'] + return self._diff_update_and_compare('privileged', before, after) + + def diffparam_pid(self): + before = self.info['hostconfig']['pidmode'] + after = self.params['pid'] + return self._diff_update_and_compare('pid', before, after) + + # TODO(sshnaidm) Need to add port ranges support + def diffparam_publish(self): + ports = self.info['hostconfig']['portbindings'] + before = [":".join([ + j[0]['hostip'], + str(j[0]["hostport"]), + i.replace('/tcp', '') + ]).strip(':') for i, j in ports.items()] + after = self.params['publish'] or [] + if self.params['publish_all']: + image_ports = self.image_info['config'].get('exposedports', {}) + if image_ports: + after += list(image_ports.keys()) + after = [i.replace("/tcp", "") for i in after] + # No support for port ranges yet + for ports in after: + if "-" in ports: + return self._diff_update_and_compare('publish', '', '') + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('publish', before, after) + + def diffparam_rm(self): + before = self.info['hostconfig']['autoremove'] + after = self.params['rm'] + return self._diff_update_and_compare('rm', before, after) + + def diffparam_security_opt(self): + before = self.info['hostconfig']['securityopt'] + # In rootful containers with apparmor there is a default security opt + before = [o for o in before if 'apparmor=containers-default' not in o] + after = self.params['security_opt'] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('security_opt', before, after) + + def diffparam_stop_signal(self): + signals = { + "sighup": "1", + "sigint": "2", + "sigquit": "3", + "sigill": "4", + "sigtrap": "5", + "sigabrt": "6", + "sigiot": "6", + "sigbus": "7", + "sigfpe": "8", + "sigkill": "9", + "sigusr1": "10", + "sigsegv": "11", + "sigusr2": "12", + "sigpipe": "13", + "sigalrm": "14", + "sigterm": "15", + "sigstkflt": "16", + "sigchld": "17", + "sigcont": "18", + "sigstop": "19", + "sigtstp": "20", + "sigttin": "21", + "sigttou": "22", + "sigurg": "23", + "sigxcpu": "24", + "sigxfsz": "25", + "sigvtalrm": "26", + "sigprof": "27", + "sigwinch": "28", + "sigio": "29", + "sigpwr": "30", + "sigsys": "31" + } + before = str(self.info['config']['stopsignal']) + if not before.isdigit(): + before = signals[before.lower()] + after = str(self.params['stop_signal']) + if not after.isdigit(): + after = signals[after.lower()] + return self._diff_update_and_compare('stop_signal', before, after) + + def diffparam_tty(self): + before = self.info['config']['tty'] + after = self.params['tty'] + return self._diff_update_and_compare('tty', before, after) + + def diffparam_user(self): + before = self.info['config']['user'] + after = self.params['user'] + return self._diff_update_and_compare('user', before, after) + + def diffparam_ulimit(self): + after = self.params['ulimit'] or [] + # In case of latest podman + if 'createcommand' in self.info['config']: + ulimits = [] + for k, c in enumerate(self.info['config']['createcommand']): + if c == '--ulimit': + ulimits.append(self.info['config']['createcommand'][k + 1]) + before = ulimits + before, after = sorted(before), sorted(after) + return self._diff_update_and_compare('ulimit', before, after) + if after: + ulimits = self.info['hostconfig']['ulimits'] + before = { + u['name'].replace('rlimit_', ''): "%s:%s" % (u['soft'], u['hard']) for u in ulimits} + after = {i.split('=')[0]: i.split('=')[1] + for i in self.params['ulimit']} + new_before = [] + new_after = [] + for u in list(after.keys()): + # We don't support unlimited ulimits because it depends on platform + if u in before and "-1" not in after[u]: + new_before.append([u, before[u]]) + new_after.append([u, after[u]]) + return self._diff_update_and_compare('ulimit', new_before, new_after) + return self._diff_update_and_compare('ulimit', '', '') + + def diffparam_uts(self): + before = self.info['hostconfig']['utsmode'] + after = self.params['uts'] + if self.params['pod'] and not self.module_params['uts']: + after = before + return self._diff_update_and_compare('uts', before, after) + + def diffparam_volume(self): + def clean_volume(x): + '''Remove trailing and double slashes from volumes.''' + return x.replace("//", "/").rstrip("/") + + before = self.info['mounts'] + before_local_vols = [] + if before: + volumes = [] + local_vols = [] + for m in before: + if m['type'] != 'volume': + volumes.append([m['source'], m['destination']]) + elif m['type'] == 'volume': + local_vols.append([m['name'], m['destination']]) + before = [":".join(v) for v in volumes] + before_local_vols = [":".join(v) for v in local_vols] + if self.params['volume'] is not None: + after = [":".join( + [clean_volume(i) for i in v.split(":")[:2]] + ) for v in self.params['volume']] + else: + after = [] + if before_local_vols: + after = list(set(after).difference(before_local_vols)) + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('volume', before, after) + + def diffparam_volumes_from(self): + # Possibly volumesfrom is not in config + before = self.info['hostconfig'].get('volumesfrom', []) or [] + after = self.params['volumes_from'] or [] + return self._diff_update_and_compare('volumes_from', before, after) + + def diffparam_workdir(self): + before = self.info['config']['workingdir'] + after = self.params['workdir'] + return self._diff_update_and_compare('workdir', before, after) + + def is_different(self): + diff_func_list = [func for func in dir(self) + if callable(getattr(self, func)) and func.startswith( + "diffparam")] + fail_fast = not bool(self.module._diff) + different = False + for func_name in diff_func_list: + dff_func = getattr(self, func_name) + if dff_func(): + if fail_fast: + return True + different = True + # Check non idempotent parameters + for p in self.non_idempotent: + if self.module_params[p] is not None and self.module_params[p] not in [{}, [], '']: + different = True + return different + + +def ensure_image_exists(module, image, module_params): + """If image is passed, ensure it exists, if not - pull it or fail. + + Arguments: + module {obj} -- ansible module object + image {str} -- name of image + + Returns: + list -- list of image actions - if it pulled or nothing was done + """ + image_actions = [] + module_exec = module_params['executable'] + if not image: + return image_actions + rc, out, err = module.run_command([module_exec, 'image', 'exists', image]) + if rc == 0: + return image_actions + rc, out, err = module.run_command([module_exec, 'image', 'pull', image]) + if rc != 0: + module.fail_json(msg="Can't pull image %s" % image, stdout=out, + stderr=err) + image_actions.append("pulled image %s" % image) + return image_actions + + +class PodmanContainer: + """Perform container tasks. + + Manages podman container, inspects it and checks its current state + """ + + def __init__(self, module, name, module_params): + """Initialize PodmanContainer class. + + Arguments: + module {obj} -- ansible module object + name {str} -- name of container + """ + + self.module = module + self.module_params = module_params + self.name = name + self.stdout, self.stderr = '', '' + self.info = self.get_info() + self.version = self._get_podman_version() + self.diff = {} + self.actions = [] + + @property + def exists(self): + """Check if container exists.""" + return bool(self.info != {}) + + @property + def different(self): + """Check if container is different.""" + diffcheck = PodmanContainerDiff( + self.module, + self.module_params, + self.info, + self.get_image_info(), + self.version) + is_different = diffcheck.is_different() + diffs = diffcheck.diff + if self.module._diff and is_different and diffs['before'] and diffs['after']: + self.diff['before'] = "\n".join( + ["%s - %s" % (k, v) for k, v in sorted( + diffs['before'].items())]) + "\n" + self.diff['after'] = "\n".join( + ["%s - %s" % (k, v) for k, v in sorted( + diffs['after'].items())]) + "\n" + return is_different + + @property + def running(self): + """Return True if container is running now.""" + return self.exists and self.info['State']['Running'] + + @property + def stopped(self): + """Return True if container exists and is not running now.""" + return self.exists and not self.info['State']['Running'] + + def get_info(self): + """Inspect container and gather info about it.""" + # pylint: disable=unused-variable + rc, out, err = self.module.run_command( + [self.module_params['executable'], b'container', b'inspect', self.name]) + return json.loads(out)[0] if rc == 0 else {} + + def get_image_info(self): + """Inspect container image and gather info about it.""" + # pylint: disable=unused-variable + rc, out, err = self.module.run_command( + [self.module_params['executable'], b'image', b'inspect', self.module_params['image']]) + return json.loads(out)[0] if rc == 0 else {} + + def _get_podman_version(self): + # pylint: disable=unused-variable + rc, out, err = self.module.run_command( + [self.module_params['executable'], b'--version']) + if rc != 0 or not out or "version" not in out: + self.module.fail_json(msg="%s run failed!" % + self.module_params['executable']) + return out.split("version")[1].strip() + + def _perform_action(self, action): + """Perform action with container. + + Arguments: + action {str} -- action to perform - start, create, stop, run, + delete + """ + def clean_stderr(err): + # Inspect STDERR for logs to avoid modules failures in case of + # increased log level verbosity + return "\n".join( + [line for line in err.splitlines() if 'level=' not in line]).strip() + + b_command = PodmanModuleParams(action, + self.module_params, + self.version, + self.module, + ).construct_command_from_params() + full_cmd = " ".join([self.module_params['executable']] + + [to_native(i) for i in b_command]) + self.actions.append(full_cmd) + if self.module.check_mode: + self.module.log( + "PODMAN-CONTAINER-DEBUG (check_mode): %s" % full_cmd) + else: + rc, out, err = self.module.run_command( + [self.module_params['executable'], b'container'] + b_command, + expand_user_and_vars=False) + self.module.log("PODMAN-CONTAINER-DEBUG: %s" % full_cmd) + if self.module_params['debug']: + self.module.log("PODMAN-CONTAINER-DEBUG STDOUT: %s" % out) + self.module.log("PODMAN-CONTAINER-DEBUG STDERR: %s" % err) + self.module.log("PODMAN-CONTAINER-DEBUG RC: %s" % rc) + self.stdout = out + self.stderr = err + if rc != 0 or clean_stderr(err) != '': + self.module.fail_json( + msg="Failed %s container %s" % (action, self.name), + stdout=out, stderr=err) + + def run(self): + """Run the container.""" + self._perform_action('run') + + def delete(self): + """Delete the container.""" + self._perform_action('delete') + + def stop(self): + """Stop the container.""" + self._perform_action('stop') + + def start(self): + """Start the container.""" + self._perform_action('start') + + def create(self): + """Create the container.""" + self._perform_action('create') + + def recreate(self): + """Recreate the container.""" + self.delete() + self.run() + + def restart(self): + """Restart the container.""" + self.stop() + self.start() + + +class PodmanManager: + """Module manager class. + + Defines according to parameters what actions should be applied to container + """ + + def __init__(self, module, params): + """Initialize PodmanManager class. + + Arguments: + module {obj} -- ansible module object + """ + + self.module = module + self.results = { + 'changed': False, + 'actions': [], + 'container': {}, + } + self.module_params = params + self.name = self.module_params['name'] + self.executable = \ + self.module.get_bin_path(self.module_params['executable'], + required=True) + self.image = self.module_params['image'] + image_actions = ensure_image_exists( + self.module, self.image, self.module_params) + self.results['actions'] += image_actions + self.state = self.module_params['state'] + self.restart = self.module_params['force_restart'] + self.recreate = self.module_params['recreate'] + self.container = PodmanContainer( + self.module, self.name, self.module_params) + + def update_container_result(self, changed=True): + """Inspect the current container, update results with last info, exit. + + Keyword Arguments: + changed {bool} -- whether any action was performed + (default: {True}) + """ + facts = self.container.get_info() if changed else self.container.info + out, err = self.container.stdout, self.container.stderr + self.results.update({'changed': changed, 'container': facts, + 'podman_actions': self.container.actions}, + stdout=out, stderr=err) + if self.container.diff: + self.results.update({'diff': self.container.diff}) + if self.module.params['debug'] or self.module_params['debug']: + self.results.update({'podman_version': self.container.version}) + + def make_started(self): + """Run actions if desired state is 'started'.""" + if self.container.running and \ + (self.container.different or self.recreate): + self.container.recreate() + self.results['actions'].append('recreated %s' % + self.container.name) + self.update_container_result() + return + elif self.container.running and not self.container.different: + if self.restart: + self.container.restart() + self.results['actions'].append('restarted %s' % + self.container.name) + self.update_container_result() + return + self.update_container_result(changed=False) + return + elif not self.container.exists: + self.container.run() + self.results['actions'].append('started %s' % self.container.name) + self.update_container_result() + return + elif self.container.stopped and self.container.different: + self.container.recreate() + self.results['actions'].append('recreated %s' % + self.container.name) + self.update_container_result() + return + elif self.container.stopped and not self.container.different: + self.container.start() + self.results['actions'].append('started %s' % self.container.name) + self.update_container_result() + return + + def make_stopped(self): + """Run actions if desired state is 'stopped'.""" + if not self.container.exists and not self.image: + self.module.fail_json(msg='Cannot create container when image' + ' is not specified!') + if not self.container.exists: + self.container.create() + self.results['actions'].append('created %s' % self.container.name) + self.update_container_result() + return + if self.container.stopped: + self.update_container_result(changed=False) + return + elif self.container.running: + self.container.stop() + self.results['actions'].append('stopped %s' % self.container.name) + self.update_container_result() + return + + def make_absent(self): + """Run actions if desired state is 'absent'.""" + if not self.container.exists: + self.results.update({'changed': False}) + elif self.container.exists: + self.container.delete() + self.results['actions'].append('deleted %s' % self.container.name) + self.results.update({'changed': True}) + self.results.update({'container': {}, + 'podman_actions': self.container.actions}) + + def execute(self): + """Execute the desired action according to map of actions & states.""" + states_map = { + 'present': self.make_started, + 'started': self.make_started, + 'absent': self.make_absent, + 'stopped': self.make_stopped + } + process_action = states_map[self.state] + process_action() + return self.results diff --git a/tripleo_ansible/ansible_plugins/modules/podman_container.py b/tripleo_ansible/ansible_plugins/modules/podman_container.py index 4ab642938..351e53163 100644 --- a/tripleo_ansible/ansible_plugins/modules/podman_container.py +++ b/tripleo_ansible/ansible_plugins/modules/podman_container.py @@ -13,20 +13,14 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - # flake8: noqa: E501 +from __future__ import absolute_import, division, print_function + __metaclass__ = type -import json -from distutils.version import LooseVersion -import yaml -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_bytes, to_native - - -DOCUMENTATION = """ +DOCUMENTATION = r""" module: podman_container author: - "Sagi Shnaidman (@sshnaidm)" @@ -36,7 +30,7 @@ notes: [] description: - Start, stop, restart and manage Podman containers requirements: - - "Podman installed on host" + - podman options: name: description: @@ -110,6 +104,8 @@ options: - List of capabilities to add to the container. type: list elements: str + aliases: + - capabilities cap_drop: description: - List of capabilities to drop from the container. @@ -239,15 +235,21 @@ options: - Set custom DNS servers type: list elements: str + aliases: + - dns_servers dns_option: description: - Set custom DNS options type: str + aliases: + - dns_opts dns_search: description: - Set custom DNS search domains (Use dns_search with '' if you don't wish to set the search domain) type: str + aliases: + - dns_search_domains entrypoint: description: - Overwrite the default ENTRYPOINT of the image @@ -300,6 +302,8 @@ options: description: - Add additional groups to run as type: list + aliases: + - groups healthcheck: description: - Set or alter a healthcheck command for a container. @@ -359,8 +363,8 @@ options: init: description: - Run an init inside the container that forwards signals and reaps - processes. - type: str + processes. The default is false. + type: bool init_path: description: - Path to the container-init binary. @@ -386,6 +390,8 @@ options: - Default is to create a private IPC namespace (POSIX SysV IPC) for the container type: str + aliases: + - ipc_mode kernel_memory: description: - Kernel memory limit @@ -395,6 +401,8 @@ options: label: description: - Add metadata to a container, pass dictionary of label names and values + aliases: + - labels type: dict label_file: description: @@ -429,6 +437,12 @@ options: type: str aliases: - log_options + mac_address: + description: + - Specify a MAC address for the container, for example + '92:d0:c6:0a:29:33'. + Don't forget that it must be unique within one Ethernet network. + type: str memory: description: - Memory limit (format 10k, where unit = b, k, m or g) @@ -473,6 +487,7 @@ options: elements: str aliases: - net + - network_mode no_hosts: description: - Do not create /etc/hosts for the container @@ -491,9 +506,11 @@ options: description: - Set the PID mode for the container type: str + aliases: + - pid_mode pids_limit: description: - - Tune the container's PIDs limit. Set 0 to have unlimited PIDs for the + - Tune the container's PIDs limit. Set -1 to have unlimited PIDs for the container. type: str pod: @@ -511,6 +528,8 @@ options: - Publish a container's port, or range of ports, to the host. Format - ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort + In case of only containerPort is set, the hostPort will chosen + randomly by Podman. type: list elements: str aliases: @@ -555,12 +574,12 @@ options: type: bool aliases: - remove + - auto_remove rootfs: description: - If true, the first argument refers to an exploded container on the file system. The default is false. type: bool - default: False security_opt: description: - Security Options. For example security_opt "seccomp=unconfined" @@ -623,6 +642,8 @@ options: description: - Ulimit options type: list + aliases: + - ulimits user: description: - Sets the username or UID used and optionally the groupname or GID for @@ -634,6 +655,8 @@ options: It defaults to the PODMAN_USERNS environment variable. An empty value means user namespaces are disabled. type: str + aliases: + - userns_mode uts: description: - Set the UTS mode for the container @@ -658,9 +681,11 @@ options: The default working directory for running binaries within a container is the root directory (/). type: str + aliases: + - working_dir """ -EXAMPLES = """ +EXAMPLES = r""" - name: Run container podman_container: name: container @@ -736,7 +761,7 @@ EXAMPLES = """ log_driver: k8s-file """ -RETURN = """ +RETURN = r""" container: description: - Facts representing the current state of the container. Matches the @@ -856,1304 +881,28 @@ container: }' """ - -class PodmanModuleParams: - """Creates list of arguments for podman CLI command. - - Arguments: - action {str} -- action type from 'run', 'stop', 'create', 'delete', - 'start' - params {dict} -- dictionary of module parameters - - """ - - def __init__(self, action, params, podman_version, module): - self.params = params - self.action = action - self.podman_version = podman_version - self.module = module - - def construct_command_from_params(self): - """Create a podman command from given module parameters. - - Returns: - list -- list of byte strings for Popen command - """ - if self.action in ['start', 'stop', 'delete']: - return self.start_stop_delete() - if self.action in ['create', 'run']: - cmd = [self.action, '--name', self.params['name']] - all_param_methods = [func for func in dir(self) - if callable(getattr(self, func)) - and func.startswith("addparam")] - params_set = (i for i in self.params if self.params[i] is not None) - for param in params_set: - func_name = "_".join(["addparam", param]) - if func_name in all_param_methods: - cmd = getattr(self, func_name)(cmd) - cmd.append(self.params['image']) - if self.params['command']: - if isinstance(self.params['command'], list): - cmd += self.params['command'] - else: - cmd += self.params['command'].split() - return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] - - def start_stop_delete(self): - - if self.action in ['stop', 'start']: - cmd = [self.action, self.params['name']] - return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] - - if self.action == 'delete': - cmd = ['rm', '-f', self.params['name']] - return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] - - def check_version(self, param, minv=None, maxv=None): - if minv and LooseVersion(minv) > LooseVersion( - self.podman_version): - self.module.fail_json(msg="Parameter %s is supported from podman " - "version %s only! Current version is %s" % ( - param, minv, self.podman_version)) - if maxv and LooseVersion(maxv) < LooseVersion( - self.podman_version): - self.module.fail_json(msg="Parameter %s is supported till podman " - "version %s only! Current version is %s" % ( - param, minv, self.podman_version)) - - def addparam_annotation(self, c): - for annotate in self.params['annotation'].items(): - c += ['--annotation', '='.join(annotate)] - return c - - def addparam_authfile(self, c): - return c + ['--authfile', self.params['authfile']] - - def addparam_blkio_weight(self, c): - return c + ['--blkio-weight', self.params['blkio_weight']] - - def addparam_blkio_weight_device(self, c): - for blkio in self.params['blkio_weight_device'].items(): - c += ['--blkio-weight-device', ':'.join(blkio)] - return c - - def addparam_cap_add(self, c): - for cap_add in self.params['cap_add']: - c += ['--cap-add', cap_add] - return c - - def addparam_cap_drop(self, c): - for cap_drop in self.params['cap_drop']: - c += ['--cap-drop', cap_drop] - return c - - def addparam_cgroups(self, c): - self.check_version('--cgroups', minv='1.6.0') - return c + ['--cgroups=%s' % self.params['cgroups']] - - def addparam_cgroupns(self, c): - self.check_version('--cgroupns', minv='1.6.2') - return c + ['--cgroupns=%s' % self.params['cgroupns']] - - def addparam_cgroup_parent(self, c): - return c + ['--cgroup-parent', self.params['cgroup_parent']] - - def addparam_cidfile(self, c): - return c + ['--cidfile', self.params['cidfile']] - - def addparam_conmon_pidfile(self, c): - return c + ['--conmon-pidfile', self.params['conmon_pidfile']] - - def addparam_cpu_period(self, c): - return c + ['--cpu-period', self.params['cpu_period']] - - def addparam_cpu_rt_period(self, c): - return c + ['--cpu-rt-period', self.params['cpu_rt_period']] - - def addparam_cpu_rt_runtime(self, c): - return c + ['--cpu-rt-runtime', self.params['cpu_rt_runtime']] - - def addparam_cpu_shares(self, c): - return c + ['--cpu-shares', self.params['cpu_shares']] - - def addparam_cpus(self, c): - return c + ['--cpus', self.params['cpus']] - - def addparam_cpuset_cpus(self, c): - return c + ['--cpuset-cpus', self.params['cpuset_cpus']] - - def addparam_cpuset_mems(self, c): - return c + ['--cpuset-mems', self.params['cpuset_mems']] - - def addparam_detach(self, c): - return c + ['--detach=%s' % self.params['detach']] - - def addparam_detach_keys(self, c): - return c + ['--detach-keys', self.params['detach_keys']] - - def addparam_device(self, c): - for dev in self.params['device']: - c += ['--device', dev] - return c - - def addparam_device_read_bps(self, c): - for dev in self.params['device_read_bps']: - c += ['--device-read-bps', dev] - return c - - def addparam_device_read_iops(self, c): - for dev in self.params['device_read_iops']: - c += ['--device-read-iops', dev] - return c - - def addparam_device_write_bps(self, c): - for dev in self.params['device_write_bps']: - c += ['--device-write-bps', dev] - return c - - def addparam_device_write_iops(self, c): - for dev in self.params['device_write_iops']: - c += ['--device-write-iops', dev] - return c - - def addparam_dns(self, c): - return c + ['--dns', ','.join(self.params['dns'])] - - def addparam_dns_option(self, c): - return c + ['--dns-option', self.params['dns_option']] - - def addparam_dns_search(self, c): - return c + ['--dns-search', self.params['dns_search']] - - def addparam_entrypoint(self, c): - return c + ['--entrypoint', self.params['entrypoint']] - - def addparam_env(self, c): - for env_value in self.params['env'].items(): - c += ['--env', - b"=".join([to_bytes(k, errors='surrogate_or_strict') - for k in env_value])] - return c - - def addparam_env_file(self, c): - return c + ['--env-file', self.params['env_file']] - - def addparam_env_host(self, c): - self.check_version('--env-host', minv='1.5.0') - return c + ['--env-host=%s' % self.params['env_host']] - - def addparam_etc_hosts(self, c): - for host_ip in self.params['etc_hosts'].items(): - c += ['--add-host', ':'.join(host_ip)] - return c - - def addparam_expose(self, c): - for exp in self.params['expose']: - c += ['--expose', exp] - return c - - def addparam_gidmap(self, c): - return c + ['--gidmap', self.params['gidmap']] - - def addparam_group_add(self, c): - for g in self.params['group_add']: - c += ['--group-add', g] - return c - - def addparam_healthcheck(self, c): - return c + ['--healthcheck-command', self.params['healthcheck']] - - def addparam_healthcheck_interval(self, c): - return c + ['--healthcheck-interval', - self.params['healthcheck_interval']] - - def addparam_healthcheck_retries(self, c): - return c + ['--healthcheck-retries', - self.params['healthcheck_retries']] - - def addparam_healthcheck_start_period(self, c): - return c + ['--healthcheck-start-period', - self.params['healthcheck_start_period']] - - def addparam_healthcheck_timeout(self, c): - return c + ['--healthcheck-timeout', - self.params['healthcheck_timeout']] - - def addparam_hostname(self, c): - return c + ['--hostname', self.params['hostname']] - - def addparam_http_proxy(self, c): - return c + ['--http-proxy=%s' % self.params['http_proxy']] - - def addparam_image_volume(self, c): - return c + ['--image-volume', self.params['image_volume']] - - def addparam_init(self, c): - return c + ['--init', self.params['init']] - - def addparam_init_path(self, c): - return c + ['--init-path', self.params['init_path']] - - def addparam_interactive(self, c): - return c + ['--interactive=%s' % self.params['interactive']] - - def addparam_ip(self, c): - return c + ['--ip', self.params['ip']] - - def addparam_ipc(self, c): - return c + ['--ipc', self.params['ipc']] - - def addparam_kernel_memory(self, c): - return c + ['--kernel-memory', self.params['kernel_memory']] - - def addparam_label(self, c): - for label in self.params['label'].items(): - c += ['--label', b'='.join([to_bytes(l, errors='surrogate_or_strict') - for l in label])] - return c - - def addparam_label_file(self, c): - return c + ['--label-file', self.params['label_file']] - - def addparam_log_driver(self, c): - return c + ['--log-driver', self.params['log_driver']] - - def addparam_log_opt(self, c): - return c + ['--log-opt', self.params['log_opt']] - - def addparam_log_level(self, c): - return c + ['--log-level', self.params['log_level']] - - def addparam_memory(self, c): - return c + ['--memory', self.params['memory']] - - def addparam_memory_reservation(self, c): - return c + ['--memory-reservation', self.params['memory_reservation']] - - def addparam_memory_swap(self, c): - return c + ['--memory-swap', self.params['memory_swap']] - - def addparam_memory_swappiness(self, c): - return c + ['--memory-swappiness', self.params['memory_swappiness']] - - def addparam_mount(self, c): - return c + ['--mount', self.params['mount']] - - def addparam_network(self, c): - return c + ['--network', ",".join(self.params['network'])] - - def addparam_no_hosts(self, c): - return c + ['--no-hosts=%s' % self.params['no_hosts']] - - def addparam_oom_kill_disable(self, c): - return c + ['--oom-kill-disable=%s' % self.params['oom_kill_disable']] - - def addparam_oom_score_adj(self, c): - return c + ['--oom-score-adj', self.params['oom_score_adj']] - - def addparam_pid(self, c): - return c + ['--pid', self.params['pid']] - - def addparam_pids_limit(self, c): - return c + ['--pids-limit', self.params['pids_limit']] - - def addparam_pod(self, c): - return c + ['--pod', self.params['pod']] - - def addparam_privileged(self, c): - return c + ['--privileged=%s' % self.params['privileged']] - - def addparam_publish(self, c): - for pub in self.params['publish']: - c += ['--publish', pub] - return c - - def addparam_publish_all(self, c): - return c + ['--publish-all=%s' % self.params['publish_all']] - - def addparam_read_only(self, c): - return c + ['--read-only=%s' % self.params['read_only']] - - def addparam_read_only_tmpfs(self, c): - return c + ['--read-only-tmpfs=%s' % self.params['read_only_tmpfs']] - - def addparam_restart_policy(self, c): - return c + ['--restart=%s' % self.params['restart_policy']] - - def addparam_rm(self, c): - if self.params['rm']: - c += ['--rm'] - return c - - def addparam_rootfs(self, c): - return c + ['--rootfs=%s' % self.params['rootfs']] - - def addparam_security_opt(self, c): - for secopt in self.params['security_opt']: - c += ['--security-opt', secopt] - return c - - def addparam_shm_size(self, c): - return c + ['--shm-size', self.params['shm_size']] - - def addparam_sig_proxy(self, c): - return c + ['--sig-proxy=%s' % self.params['sig_proxy']] - - def addparam_stop_signal(self, c): - return c + ['--stop-signal', self.params['stop_signal']] - - def addparam_stop_timeout(self, c): - return c + ['--stop-timeout', self.params['stop_timeout']] - - def addparam_subgidname(self, c): - return c + ['--subgidname', self.params['subgidname']] - - def addparam_subuidname(self, c): - return c + ['--subuidname', self.params['subuidname']] - - def addparam_sysctl(self, c): - for sysctl in self.params['sysctl'].items(): - c += ['--sysctl', - b"=".join([to_bytes(k, errors='surrogate_or_strict') - for k in sysctl])] - return c - - def addparam_systemd(self, c): - return c + ['--systemd=%s' % self.params['systemd']] - - def addparam_tmpfs(self, c): - for tmpfs in self.params['tmpfs'].items(): - c += ['--tmpfs', ':'.join(tmpfs)] - return c - - def addparam_tty(self, c): - return c + ['--tty=%s' % self.params['tty']] - - def addparam_uidmap(self, c): - for uidmap in self.params['uidmap']: - c += ['--uidmap', uidmap] - return c - - def addparam_ulimit(self, c): - for u in self.params['ulimit']: - c += ['--ulimit', u] - return c - - def addparam_user(self, c): - return c + ['--user', self.params['user']] - - def addparam_userns(self, c): - return c + ['--userns', self.params['userns']] - - def addparam_uts(self, c): - return c + ['--uts', self.params['uts']] - - def addparam_volume(self, c): - for vol in self.params['volume']: - if vol: - c += ['--volume', vol] - return c - - def addparam_volumes_from(self, c): - for vol in self.params['volumes_from']: - c += ['--volumes-from', vol] - return c - - def addparam_workdir(self, c): - return c + ['--workdir', self.params['workdir']] - - # Add your own args for podman command - def addparam_cmd_args(self, c): - return c + self.params['cmd_args'] - - -class PodmanDefaults: - def __init__(self, module, image_info, podman_version): - self.module = module - self.version = podman_version - self.image_info = image_info - self.defaults = { - "blkio_weight": 0, - "cgroups": "default", - "cidfile": "", - "cpus": 0.0, - "cpu_shares": 0, - "cpu_quota": 0, - "cpu_period": 0, - "cpu_rt_runtime": 0, - "cpu_rt_period": 0, - "cpuset_cpus": "", - "cpuset_mems": "", - "detach": True, - "device": [], - "env_host": False, - "etc_hosts": {}, - "group_add": [], - "ipc": "", - "kernelmemory": "0", - "log_driver": "k8s-file", - "log_level": "error", - "memory": "0", - "memory_swap": "0", - "memory_reservation": "0", - # "memory_swappiness": -1, - "no_hosts": False, - # libpod issue with networks in inspection - "network": ["default"], - "oom_score_adj": 0, - "pid": "", - "privileged": False, - "rm": False, - "security_opt": [], - "stop_signal": self.image_info['config'].get('stopsignal', "15"), - "tty": False, - "user": self.image_info.get('user', ''), - "workdir": self.image_info['config'].get('workingdir', '/'), - "uts": "", - } - - def default_dict(self): - # make here any changes to self.defaults related to podman version - # https://github.com/containers/libpod/pull/5669 - if (LooseVersion(self.version) >= LooseVersion('1.8.0') - and LooseVersion(self.version) < LooseVersion('1.9.0')): - self.defaults['cpu_shares'] = 1024 - if (LooseVersion(self.version) >= LooseVersion('2.0.0')): - self.defaults['network'] = ["slirp4netns"] - self.defaults['ipc'] = "private" - self.defaults['uts'] = "private" - self.defaults['pid'] = "private" - return self.defaults - - -class PodmanContainerDiff: - def __init__(self, module, info, image_info, podman_version): - self.module = module - self.version = podman_version - self.default_dict = None - self.info = yaml.safe_load(json.dumps(info).lower()) - self.image_info = yaml.safe_load(json.dumps(image_info).lower()) - self.params = self.defaultize() - self.diff = {'before': {}, 'after': {}} - self.non_idempotent = { - 'env_file', # We can't get env vars from file to check - 'env_host', - } - - def defaultize(self): - params_with_defaults = {} - self.default_dict = PodmanDefaults( - self.module, self.image_info, self.version).default_dict() - for p in self.module.params: - if self.module.params[p] is None and p in self.default_dict: - params_with_defaults[p] = self.default_dict[p] - else: - params_with_defaults[p] = self.module.params[p] - return params_with_defaults - - def _diff_update_and_compare(self, param_name, before, after): - if before != after: - self.diff['before'].update({param_name: before}) - self.diff['after'].update({param_name: after}) - return True - return False - - def diffparam_annotation(self): - before = self.info['config']['annotations'] or {} - after = before.copy() - if self.module.params['annotation'] is not None: - after.update(self.params['annotation']) - return self._diff_update_and_compare('annotation', before, after) - - def diffparam_env_host(self): - # It's impossible to get from inspest, recreate it if not default - before = False - after = self.params['env_host'] - return self._diff_update_and_compare('env_host', before, after) - - def diffparam_blkio_weight(self): - before = self.info['hostconfig']['blkioweight'] - after = self.params['blkio_weight'] - return self._diff_update_and_compare('blkio_weight', before, after) - - def diffparam_blkio_weight_device(self): - before = self.info['hostconfig']['blkioweightdevice'] - if before == [] and self.module.params['blkio_weight_device'] is None: - after = [] - else: - after = self.params['blkio_weight_device'] - return self._diff_update_and_compare('blkio_weight_device', before, after) - - def diffparam_cap_add(self): - before = self.info['effectivecaps'] or [] - after = [] - if self.module.params['cap_add'] is not None: - after += ["cap_" + i.lower() - for i in self.module.params['cap_add']] - after += before - before, after = sorted(list(set(before))), sorted(list(set(after))) - return self._diff_update_and_compare('cap_add', before, after) - - def diffparam_cap_drop(self): - before = self.info['effectivecaps'] or [] - after = before[:] - if self.module.params['cap_drop'] is not None: - for c in ["cap_" + i.lower() for i in self.module.params['cap_drop']]: - if c in after: - after.remove(c) - before, after = sorted(list(set(before))), sorted(list(set(after))) - return self._diff_update_and_compare('cap_drop', before, after) - - def diffparam_cgroup_parent(self): - before = self.info['hostconfig']['cgroupparent'] - after = self.params['cgroup_parent'] - if after is None: - after = before - return self._diff_update_and_compare('cgroup_parent', before, after) - - def diffparam_cgroups(self): - # Cgroups output is not supported in all versions - if 'cgroups' in self.info['hostconfig']: - before = self.info['hostconfig']['cgroups'] - after = self.params['cgroups'] - return self._diff_update_and_compare('cgroups', before, after) - return False - - def diffparam_cidfile(self): - before = self.info['hostconfig']['containeridfile'] - after = self.params['cidfile'] - return self._diff_update_and_compare('cidfile', before, after) - - def diffparam_command(self): - # TODO(sshnaidm): to inspect image to get the default command - if self.module.params['command'] is not None: - before = self.info['config']['cmd'] - after = self.params['command'] - if isinstance(after, str): - after = [i.lower() for i in after.split()] - elif isinstance(after, list): - after = [i.lower() for i in after] - return self._diff_update_and_compare('command', before, after) - return False - - def diffparam_conmon_pidfile(self): - before = self.info['conmonpidfile'] - if self.module.params['conmon_pidfile'] is None: - after = before - else: - after = self.params['conmon_pidfile'] - return self._diff_update_and_compare('conmon_pidfile', before, after) - - def diffparam_cpu_period(self): - before = self.info['hostconfig']['cpuperiod'] - after = self.params['cpu_period'] - return self._diff_update_and_compare('cpu_period', before, after) - - def diffparam_cpu_rt_period(self): - before = self.info['hostconfig']['cpurealtimeperiod'] - after = self.params['cpu_rt_period'] - return self._diff_update_and_compare('cpu_rt_period', before, after) - - def diffparam_cpu_rt_runtime(self): - before = self.info['hostconfig']['cpurealtimeruntime'] - after = self.params['cpu_rt_runtime'] - return self._diff_update_and_compare('cpu_rt_runtime', before, after) - - def diffparam_cpu_shares(self): - before = self.info['hostconfig']['cpushares'] - after = self.params['cpu_shares'] - return self._diff_update_and_compare('cpu_shares', before, after) - - def diffparam_cpus(self): - before = int(self.info['hostconfig']['nanocpus']) / 1000000000 - after = self.params['cpus'] - return self._diff_update_and_compare('cpus', before, after) - - def diffparam_cpuset_cpus(self): - before = self.info['hostconfig']['cpusetcpus'] - after = self.params['cpuset_cpus'] - return self._diff_update_and_compare('cpuset_cpus', before, after) - - def diffparam_cpuset_mems(self): - before = self.info['hostconfig']['cpusetmems'] - after = self.params['cpuset_mems'] - return self._diff_update_and_compare('cpuset_mems', before, after) - - def diffparam_device(self): - before = [":".join([i['pathonhost'], i['pathincontainer']]) - for i in self.info['hostconfig']['devices']] - after = [":".join(i.split(":")[:2]) for i in self.params['device']] - before, after = sorted(list(set(before))), sorted(list(set(after))) - return self._diff_update_and_compare('devices', before, after) - - def diffparam_device_read_bps(self): - before = self.info['hostconfig']['blkiodevicereadbps'] or [] - before = ["%s:%s" % (i['path'], i['rate']) for i in before] - after = self.params['device_read_bps'] or [] - before, after = sorted(list(set(before))), sorted(list(set(after))) - return self._diff_update_and_compare('device_read_bps', before, after) - - def diffparam_device_read_iops(self): - before = self.info['hostconfig']['blkiodevicereadiops'] or [] - before = ["%s:%s" % (i['path'], i['rate']) for i in before] - after = self.params['device_read_iops'] or [] - before, after = sorted(list(set(before))), sorted(list(set(after))) - return self._diff_update_and_compare('device_read_iops', before, after) - - def diffparam_device_write_bps(self): - before = self.info['hostconfig']['blkiodevicewritebps'] or [] - before = ["%s:%s" % (i['path'], i['rate']) for i in before] - after = self.params['device_write_bps'] or [] - before, after = sorted(list(set(before))), sorted(list(set(after))) - return self._diff_update_and_compare('device_write_bps', before, after) - - def diffparam_device_write_iops(self): - before = self.info['hostconfig']['blkiodevicewriteiops'] or [] - before = ["%s:%s" % (i['path'], i['rate']) for i in before] - after = self.params['device_write_iops'] or [] - before, after = sorted(list(set(before))), sorted(list(set(after))) - return self._diff_update_and_compare('device_write_iops', before, after) - - # Limited idempotency, it can't guess default values - def diffparam_env(self): - env_before = self.info['config']['env'] or {} - before = {i.split("=")[0]: i.split("=")[1] for i in env_before} - after = before.copy() - if self.params['env']: - after.update({ - str(k).lower(): str(v).lower() - for k, v in self.params['env'].items() - }) - return self._diff_update_and_compare('env', before, after) - - def diffparam_etc_hosts(self): - if self.info['hostconfig']['extrahosts']: - before = dict([i.split(":") for i in self.info['hostconfig']['extrahosts']]) - else: - before = {} - after = self.params['etc_hosts'] - return self._diff_update_and_compare('etc_hosts', before, after) - - def diffparam_group_add(self): - before = self.info['hostconfig']['groupadd'] - after = self.params['group_add'] - return self._diff_update_and_compare('group_add', before, after) - - # Healthcheck is only defined in container config if a healthcheck - # was configured; otherwise the config key isn't part of the config. - def diffparam_healthcheck(self): - if 'healthcheck' in self.info['config']: - # the "test" key is a list of 2 items where the first one is - # "CMD-SHELL" and the second one is the actual healthcheck command. - before = self.info['config']['healthcheck']['test'][1] - else: - before = '' - after = self.params['healthcheck'] or before - return self._diff_update_and_compare('healthcheck', before, after) - - # Because of hostname is random generated, this parameter has partial idempotency only. - def diffparam_hostname(self): - before = self.info['config']['hostname'] - after = self.params['hostname'] or before - return self._diff_update_and_compare('hostname', before, after) - - def diffparam_image(self): - # TODO(sshnaidm): for strict image compare mode use SHAs - before = self.info['config']['image'] - after = self.params['image'] - mode = self.params['image_strict'] - if mode is None or not mode: - # In a idempotency 'lite mode' assume all images from different registries are the same - before = before.replace(":latest", "") - after = after.replace(":latest", "") - before = before.split("/")[-1] - after = after.split("/")[-1] - # the image in the config has changed (e.g. new tag) - if self._diff_update_and_compare('image', before, after): - return True - # compare image id on disk to container to see if the image has been updated - before = self.info['image'] - after = self.image_info['id'] - return self._diff_update_and_compare('image_id', before, after) - - def diffparam_ipc(self): - before = self.info['hostconfig']['ipcmode'] - after = self.params['ipc'] - if self.params['pod'] and not after: - after = before - return self._diff_update_and_compare('ipc', before, after) - - def diffparam_label(self): - before = self.info['config']['labels'] or {} - after = self.image_info.get('labels') or {} - if self.params['label']: - after.update({ - str(k).lower(): str(v).lower() - for k, v in self.params['label'].items() - }) - return self._diff_update_and_compare('label', before, after) - - def diffparam_log_driver(self): - before = self.info['hostconfig']['logconfig']['type'] - after = self.params['log_driver'] - return self._diff_update_and_compare('log_driver', before, after) - - def diffparam_log_level(self): - excom = self.info['exitcommand'] - if '--log-level' in excom: - before = excom[excom.index('--log-level') + 1] - else: - before = self.params['log_level'] - after = self.params['log_level'] - return self._diff_update_and_compare('log_level', before, after) - - # Parameter has limited idempotency, unable to guess the default log_path - def diffparam_log_opt(self): - before = self.info['logpath'] - if self.module.params['log_opt'] in [None, '']: - after = before - else: - after = self.params['log_opt'].split("=")[1] - return self._diff_update_and_compare('log_opt', before, after) - - def diffparam_memory(self): - before = str(self.info['hostconfig']['memory']) - after = self.params['memory'] - return self._diff_update_and_compare('memory', before, after) - - def diffparam_memory_swap(self): - # By default it's twice memory parameter - before = str(self.info['hostconfig']['memoryswap']) - after = self.params['memory_swap'] - if (self.module.params['memory_swap'] is None - and self.params['memory'] != 0 - and self.params['memory'].isdigit()): - after = str(int(self.params['memory']) * 2) - return self._diff_update_and_compare('memory_swap', before, after) - - def diffparam_memory_reservation(self): - before = str(self.info['hostconfig']['memoryreservation']) - after = self.params['memory_reservation'] - return self._diff_update_and_compare('memory_reservation', before, after) - - def diffparam_network(self): - before = [self.info['hostconfig']['networkmode']] - after = self.params['network'] - if self.params['pod'] and not self.module.params['network']: - after = before - return self._diff_update_and_compare('network', before, after) - - def diffparam_no_hosts(self): - before = not bool(self.info['hostspath']) - after = self.params['no_hosts'] - if self.params['network'] == ['none']: - after = True - return self._diff_update_and_compare('no_hosts', before, after) - - def diffparam_oom_score_adj(self): - before = self.info['hostconfig']['oomscoreadj'] - after = self.params['oom_score_adj'] - return self._diff_update_and_compare('oom_score_adj', before, after) - - def diffparam_privileged(self): - before = self.info['hostconfig']['privileged'] - after = self.params['privileged'] - return self._diff_update_and_compare('privileged', before, after) - - def diffparam_pid(self): - before = self.info['hostconfig']['pidmode'] - after = self.params['pid'] - return self._diff_update_and_compare('pid', before, after) - - # TODO(sshnaidm) Need to add port ranges support - def diffparam_publish(self): - ports = self.info['hostconfig']['portbindings'] - before = [":".join([ - j[0]['hostip'], - str(j[0]["hostport"]), - i.replace('/tcp', '') - ]).strip(':') for i, j in ports.items()] - after = self.params['publish'] or [] - if self.params['publish_all']: - image_ports = self.image_info['config'].get('exposedports', {}) - if image_ports: - after += list(image_ports.keys()) - after = [i.replace("/tcp", "") for i in after] - # No support for port ranges yet - for ports in after: - if "-" in ports: - return self._diff_update_and_compare('publish', '', '') - before, after = sorted(list(set(before))), sorted(list(set(after))) - return self._diff_update_and_compare('publish', before, after) - - def diffparam_rm(self): - before = self.info['hostconfig']['autoremove'] - after = self.params['rm'] - return self._diff_update_and_compare('rm', before, after) - - def diffparam_security_opt(self): - before = self.info['hostconfig']['securityopt'] - after = self.params['security_opt'] - before, after = sorted(list(set(before))), sorted(list(set(after))) - return self._diff_update_and_compare('security_opt', before, after) - - def diffparam_stop_signal(self): - signals = { - "sighup": "1", - "sigint": "2", - "sigquit": "3", - "sigill": "4", - "sigtrap": "5", - "sigabrt": "6", - "sigiot": "6", - "sigbus": "7", - "sigfpe": "8", - "sigkill": "9", - "sigusr1": "10", - "sigsegv": "11", - "sigusr2": "12", - "sigpipe": "13", - "sigalrm": "14", - "sigterm": "15", - "sigstkflt": "16", - "sigchld": "17", - "sigcont": "18", - "sigstop": "19", - "sigtstp": "20", - "sigttin": "21", - "sigttou": "22", - "sigurg": "23", - "sigxcpu": "24", - "sigxfsz": "25", - "sigvtalrm": "26", - "sigprof": "27", - "sigwinch": "28", - "sigio": "29", - "sigpwr": "30", - "sigsys": "31" - } - before = str(self.info['config']['stopsignal']) - if not before.isdigit(): - before = signals[before] - after = str(self.params['stop_signal']) - if not after.isdigit(): - after = signals[after] - return self._diff_update_and_compare('stop_signal', before, after) - - def diffparam_tty(self): - before = self.info['config']['tty'] - after = self.params['tty'] - return self._diff_update_and_compare('tty', before, after) - - def diffparam_user(self): - before = self.info['config']['user'] - after = self.params['user'] - return self._diff_update_and_compare('user', before, after) - - def diffparam_ulimit(self): - after = self.params['ulimit'] or [] - # In case of latest podman - if 'createcommand' in self.info['config']: - ulimits = [] - for k, c in enumerate(self.info['config']['createcommand']): - if c == '--ulimit': - ulimits.append(self.info['config']['createcommand'][k + 1]) - before = ulimits - before, after = sorted(before), sorted(after) - return self._diff_update_and_compare('ulimit', before, after) - if after: - ulimits = self.info['hostconfig']['ulimits'] - before = { - u['name'].replace('rlimit_', ''): "%s:%s" % (u['soft'], u['hard']) for u in ulimits} - after = {i.split('=')[0]: i.split('=')[1] for i in self.params['ulimit']} - new_before = [] - new_after = [] - for u in list(after.keys()): - # We don't support unlimited ulimits because it depends on platform - if u in before and "-1" not in after[u]: - new_before.append([u, before[u]]) - new_after.append([u, after[u]]) - return self._diff_update_and_compare('ulimit', new_before, new_after) - return self._diff_update_and_compare('ulimit', '', '') - - def diffparam_uts(self): - before = self.info['hostconfig']['utsmode'] - after = self.params['uts'] - if self.params['pod'] and not after: - after = before - return self._diff_update_and_compare('uts', before, after) - - def diffparam_volume(self): - def clean_volume(x): - '''Remove trailing and double slashes from volumes.''' - return x.replace("//", "/").rstrip("/") - - before = self.info['mounts'] - before_local_vols = [] - if before: - volumes = [] - local_vols = [] - for m in before: - if m['type'] != 'volume': - volumes.append([m['source'], m['destination']]) - elif m['type'] == 'volume': - local_vols.append([m['name'], m['destination']]) - before = [":".join(v) for v in volumes] - before_local_vols = [":".join(v) for v in local_vols] - if self.params['volume'] is not None: - after = [":".join( - [clean_volume(i) for i in v.split(":")[:2]] - ) for v in self.params['volume']] - else: - after = [] - if before_local_vols: - after = list(set(after).difference(before_local_vols)) - before, after = sorted(list(set(before))), sorted(list(set(after))) - return self._diff_update_and_compare('volume', before, after) - - def diffparam_volumes_from(self): - # Possibly volumesfrom is not in config - before = self.info['hostconfig'].get('volumesfrom', []) or [] - after = self.params['volumes_from'] or [] - return self._diff_update_and_compare('volumes_from', before, after) - - def diffparam_workdir(self): - before = self.info['config']['workingdir'] - after = self.params['workdir'] - return self._diff_update_and_compare('workdir', before, after) - - def is_different(self): - diff_func_list = [func for func in dir(self) - if callable(getattr(self, func)) and func.startswith( - "diffparam")] - fail_fast = not bool(self.module._diff) - different = False - for func_name in diff_func_list: - dff_func = getattr(self, func_name) - if dff_func(): - if fail_fast: - return True - else: - different = True - # Check non idempotent parameters - for p in self.non_idempotent: - if self.module.params[p] is not None and self.module.params[p] not in [{}, [], '']: - different = True - return different - - -def ensure_image_exists(module, image): - """If image is passed, ensure it exists, if not - pull it or fail. - - Arguments: - module {obj} -- ansible module object - image {str} -- name of image - - Returns: - list -- list of image actions - if it pulled or nothing was done - """ - image_actions = [] - module_exec = module.params['executable'] - if not image: - return image_actions - rc, out, err = module.run_command([module_exec, 'image', 'exists', image]) - if rc == 0: - return image_actions - rc, out, err = module.run_command([module_exec, 'image', 'pull', image]) - if rc != 0: - module.fail_json(msg="Can't pull image %s" % image, stdout=out, - stderr=err) - image_actions.append("pulled image %s" % image) - return image_actions - - -class PodmanContainer: - """Perform container tasks. - - Manages podman container, inspects it and checks its current state - """ - - def __init__(self, module, name): - """Initialize PodmanContainer class. - - Arguments: - module {obj} -- ansible module object - name {str} -- name of container - """ - - super(PodmanContainer, self).__init__() - self.module = module - self.name = name - self.stdout, self.stderr = '', '' - self.info = self.get_info() - self.version = self._get_podman_version() - self.diff = {} - self.actions = [] - - @property - def exists(self): - """Check if container exists.""" - return bool(self.info != {}) - - @property - def different(self): - """Check if container is different.""" - diffcheck = PodmanContainerDiff( - self.module, - self.info, - self.get_image_info(), - self.version) - is_different = diffcheck.is_different() - diffs = diffcheck.diff - if self.module._diff and is_different and diffs['before'] and diffs['after']: - self.diff['before'] = "\n".join( - ["%s - %s" % (k, v) for k, v in sorted( - diffs['before'].items())]) + "\n" - self.diff['after'] = "\n".join( - ["%s - %s" % (k, v) for k, v in sorted( - diffs['after'].items())]) + "\n" - return is_different - - @property - def running(self): - """Return True if container is running now.""" - return self.exists and self.info['State']['Running'] - - @property - def stopped(self): - """Return True if container exists and is not running now.""" - return self.exists and not self.info['State']['Running'] - - def get_info(self): - """Inspect container and gather info about it.""" - # pylint: disable=unused-variable - rc, out, err = self.module.run_command( - [self.module.params['executable'], b'container', b'inspect', self.name]) - return json.loads(out)[0] if rc == 0 else {} - - def get_image_info(self): - """Inspect container image and gather info about it.""" - # pylint: disable=unused-variable - rc, out, err = self.module.run_command( - [self.module.params['executable'], b'image', b'inspect', self.module.params['image']]) - return json.loads(out)[0] if rc == 0 else {} - - def _get_podman_version(self): - # pylint: disable=unused-variable - rc, out, err = self.module.run_command( - [self.module.params['executable'], b'--version']) - if rc != 0 or not out or "version" not in out: - self.module.fail_json(msg="%s run failed!" % self.module.params['executable']) - return out.split("version")[1].strip() - - def _perform_action(self, action): - """Perform action with container. - - Arguments: - action {str} -- action to perform - start, create, stop, run, - delete - """ - - def clean_stderr(err): - # Inspect STDERR for logs to avoid modules failures in case of - # increased log level verbosity - return "\n".join( - [line for line in err.splitlines() if 'level=' not in line]).strip() - - b_command = PodmanModuleParams(action, - self.module.params, - self.version, - self.module, - ).construct_command_from_params() - full_cmd = " ".join([self.module.params['executable']] - + [to_native(i) for i in b_command]) - self.actions.append(full_cmd) - if self.module.check_mode: - self.module.log("PODMAN-CONTAINER-DEBUG (check_mode): %s" % full_cmd) - else: - rc, out, err = self.module.run_command( - [self.module.params['executable'], b'container'] + b_command, - expand_user_and_vars=False) - self.module.log("PODMAN-CONTAINER-DEBUG: %s" % full_cmd) - if self.module.params['debug']: - self.module.log("PODMAN-CONTAINER-DEBUG STDOUT: %s" % out) - self.module.log("PODMAN-CONTAINER-DEBUG STDERR: %s" % err) - self.module.log("PODMAN-CONTAINER-DEBUG RC: %s" % rc) - self.stdout = out - self.stderr = err - if rc != 0 or clean_stderr(err) != '': - self.module.fail_json( - msg="Failed %s container %s" % (action, self.name), - stdout=out, stderr=err) - - def run(self): - """Run the container.""" - self._perform_action('run') - - def delete(self): - """Delete the container.""" - self._perform_action('delete') - - def stop(self): - """Stop the container.""" - self._perform_action('stop') - - def start(self): - """Start the container.""" - self._perform_action('start') - - def create(self): - """Create the container.""" - self._perform_action('create') - - def recreate(self): - """Recreate the container.""" - self.delete() - self.run() - - def restart(self): - """Restart the container.""" - self.stop() - self.run() - - -class PodmanManager: - """Module manager class. - - Defines according to parameters what actions should be applied to container - """ - - def __init__(self, module): - """Initialize PodmanManager class. - - Arguments: - module {obj} -- ansible module object - """ - - super(PodmanManager, self).__init__() - - self.module = module - self.results = { - 'changed': False, - 'actions': [], - 'container': {}, - } - self.name = self.module.params['name'] - self.executable = \ - self.module.get_bin_path(self.module.params['executable'], - required=True) - self.image = self.module.params['image'] - image_actions = ensure_image_exists(self.module, self.image) - self.results['actions'] += image_actions - self.state = self.module.params['state'] - self.restart = self.module.params['force_restart'] - self.recreate = self.module.params['recreate'] - self.container = PodmanContainer(self.module, self.name) - - def update_container_result(self, changed=True): - """Inspect the current container, update results with last info, exit. - - Keyword Arguments: - changed {bool} -- whether any action was performed - (default: {True}) - """ - facts = self.container.get_info() if changed else self.container.info - out, err = self.container.stdout, self.container.stderr - self.results.update({'changed': changed, 'container': facts, - 'podman_actions': self.container.actions}, - stdout=out, stderr=err) - if self.container.diff: - self.results.update({'diff': self.container.diff}) - if self.module.params['debug']: - self.results.update({'podman_version': self.container.version}) - self.module.exit_json(**self.results) - - def make_started(self): - """Run actions if desired state is 'started'.""" - if self.container.running and \ - (self.container.different or self.recreate): - self.container.recreate() - self.results['actions'].append('recreated %s' % - self.container.name) - self.update_container_result() - elif self.container.running and not self.container.different: - if self.restart: - self.container.restart() - self.results['actions'].append('restarted %s' % - self.container.name) - self.update_container_result() - self.update_container_result(changed=False) - elif not self.container.exists: - self.container.run() - self.results['actions'].append('started %s' % self.container.name) - self.update_container_result() - elif self.container.stopped and self.container.different: - self.container.recreate() - self.results['actions'].append('recreated %s' % - self.container.name) - self.update_container_result() - elif self.container.stopped and not self.container.different: - self.container.start() - self.results['actions'].append('started %s' % self.container.name) - self.update_container_result() - - def make_stopped(self): - """Run actions if desired state is 'stopped'.""" - if not self.container.exists and not self.image: - self.module.fail_json(msg='Cannot create container when image' - ' is not specified!') - if not self.container.exists: - self.container.create() - self.results['actions'].append('created %s' % self.container.name) - self.update_container_result() - if self.container.stopped: - self.update_container_result(changed=False) - elif self.container.running: - self.container.stop() - self.results['actions'].append('stopped %s' % self.container.name) - self.update_container_result() - - def make_absent(self): - """Run actions if desired state is 'absent'.""" - if not self.container.exists: - self.results.update({'changed': False}) - elif self.container.exists: - self.container.delete() - self.results['actions'].append('deleted %s' % self.container.name) - self.results.update({'changed': True}) - self.results.update({'container': {}, - 'podman_actions': self.container.actions}) - self.module.exit_json(**self.results) - - def execute(self): - """Execute the desired action according to map of actions & states.""" - states_map = { - 'present': self.make_started, - 'started': self.make_started, - 'absent': self.make_absent, - 'stopped': self.make_stopped - } - process_action = states_map[self.state] - process_action() - self.module.fail_json(msg="Unexpected logic error happened, " - "please contact maintainers ASAP!") +from ansible.module_utils.basic import AnsibleModule # noqa: F402 +from ..module_utils.podman.podman_container_lib import PodmanManager # noqa: F402 +from ..module_utils.podman.podman_container_lib import ARGUMENTS_SPEC_CONTAINER # noqa: F402 def main(): module = AnsibleModule( - argument_spec=yaml.safe_load(DOCUMENTATION)['options'], + argument_spec=ARGUMENTS_SPEC_CONTAINER, mutually_exclusive=( ['no_hosts', 'etc_hosts'], ), supports_check_mode=True, ) + # work on input vars if module.params['state'] in ['started', 'present'] and \ not module.params['image']: module.fail_json(msg="State '%s' required image to be configured!" % module.params['state']) - PodmanManager(module).execute() + results = PodmanManager(module, module.params).execute() + module.exit_json(**results) if __name__ == '__main__': diff --git a/tripleo_ansible/ansible_plugins/tests/molecule/podman_container/converge.yml b/tripleo_ansible/ansible_plugins/tests/molecule/podman_container/converge.yml index 64e88d716..b02935243 100644 --- a/tripleo_ansible/ansible_plugins/tests/molecule/podman_container/converge.yml +++ b/tripleo_ansible/ansible_plugins/tests/molecule/podman_container/converge.yml @@ -10,7 +10,7 @@ name: "{{ item }}" state: absent loop: - - "alpine:3.7" + - "ubi-minimal" - "container" - "container2" @@ -48,13 +48,13 @@ - name: Ensure image doesn't exist podman_image: - name: alpine:3.7 + name: registry.access.redhat.com/ubi8/ubi-minimal state: absent - name: Check pulling image podman_container: name: container - image: alpine:3.7 + image: registry.access.redhat.com/ubi8/ubi-minimal state: present command: sleep 1d register: image @@ -62,7 +62,7 @@ - name: Check using already pulled image podman_container: name: container2 - image: alpine:3.7 + image: registry.access.redhat.com/ubi8/ubi-minimal state: present command: sleep 1d register: image2 @@ -73,12 +73,12 @@ - image is changed - image.container is defined - image.container['State']['Running'] - - "'pulled image alpine:3.7' in image.actions" + - "'pulled image registry.access.redhat.com/ubi8/ubi-minimal' in image.actions" - "'started container' in image.actions" - image2 is changed - image2.container is defined - image2.container['State']['Running'] - - "'pulled image alpine:3.7' not in image2.actions" + - "'pulled image registry.access.redhat.com/ubi8/ubi-minimal' not in image2.actions" - "'started container2' in image2.actions" fail_msg: Pulling image test failed! success_msg: Pulling image test passed! @@ -102,7 +102,7 @@ - name: Force container recreate podman_container: name: container - image: alpine + image: registry.access.redhat.com/ubi8/ubi-minimal state: present command: sleep 1d recreate: true @@ -173,7 +173,7 @@ - name: Create container, but don't run podman_container: name: container - image: alpine:3.7 + image: registry.access.redhat.com/ubi8/ubi-minimal state: stopped command: sleep 1d register: created @@ -197,7 +197,7 @@ - name: Start container that was deleted podman_container: name: container - image: alpine:3.7 + image: registry.access.redhat.com/ubi8/ubi-minimal state: started command: sleep 1d register: started @@ -208,7 +208,7 @@ - started is changed - started.container is defined - started.container['State']['Running'] - - "'pulled image alpine:3.7' not in started.actions" + - "'pulled image registry.access.redhat.com/ubi8/ubi-minimal' not in started.actions" - name: Delete started container podman_container: @@ -239,7 +239,7 @@ - name: Recreate container with parameters podman_container: name: container - image: docker.io/alpine:3.7 + image: registry.access.redhat.com/ubi8/ubi-minimal state: started command: sleep 1d recreate: true @@ -306,7 +306,7 @@ - "'TEST=1' in test.container['Config']['Env']" - "'BOOL=False' in test.container['Config']['Env']" # test labels - - test.container['Config']['Labels'] | length == 2 + - test.container['Config']['Labels'] | length >= 2 - test.container['Config']['Labels']['somelabel'] == "labelvalue" - test.container['Config']['Labels']['otheralbe'] == "othervalue" # test mounts @@ -326,14 +326,14 @@ - name: Check basic idempotency of running container podman_container: name: testidem - image: docker.io/alpine + image: registry.access.redhat.com/ubi8/ubi-minimal state: present command: sleep 20m - name: Check basic idempotency of running container - run it again podman_container: name: testidem - image: alpine:latest + image: ubi-minimal:latest state: present command: sleep 20m register: idem @@ -346,7 +346,7 @@ - name: Run changed container (with tty enabled) podman_container: name: testidem - image: alpine + image: ubi-minimal state: present command: sleep 20m tty: true @@ -360,7 +360,7 @@ - name: Run changed container without specifying an option, use defaults podman_container: name: testidem - image: alpine + image: ubi-minimal state: present command: sleep 20m register: idem2 @@ -384,7 +384,7 @@ - name: Check basic idempotency of pod container podman_container: name: testidem-pod - image: docker.io/alpine + image: registry.access.redhat.com/ubi8/ubi-minimal state: present command: sleep 20m pod: "new:testidempod" @@ -392,7 +392,7 @@ - name: Check basic idempotency of pod container - run it again podman_container: name: testidem-pod - image: alpine:latest + image: ubi-minimal:latest state: present command: sleep 20m pod: testidempod @@ -406,7 +406,7 @@ - name: Run changed pod container (with tty enabled) podman_container: name: testidem-pod - image: alpine + image: ubi-minimal state: present command: sleep 20m tty: true @@ -423,30 +423,18 @@ name: testidem-pod state: absent - - name: Run three containers at once - podman_containers: - containers: - - name: alpine - image: alpine - command: sleep 1d - - name: web - image: nginx - - name: test - image: python:3-alpine - command: python -V - always: - name: Delete all container leftovers from tests podman_container: name: "{{ item }}" state: absent loop: - - "alpine:3.7" - "container" - "container2" - - "alpine" + - "ubi-minimal" - "web" - "test" + - "testidem" - name: Remove pod shell: podman pod rm -f testidempod