diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/containers/podman/plugins/module_utils | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/containers/podman/plugins/module_utils')
5 files changed, 2808 insertions, 0 deletions
diff --git a/ansible_collections/containers/podman/plugins/module_utils/__init__.py b/ansible_collections/containers/podman/plugins/module_utils/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/containers/podman/plugins/module_utils/__init__.py diff --git a/ansible_collections/containers/podman/plugins/module_utils/podman/__init__.py b/ansible_collections/containers/podman/plugins/module_utils/podman/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/containers/podman/plugins/module_utils/podman/__init__.py diff --git a/ansible_collections/containers/podman/plugins/module_utils/podman/common.py b/ansible_collections/containers/podman/plugins/module_utils/podman/common.py new file mode 100644 index 000000000..dba3aff65 --- /dev/null +++ b/ansible_collections/containers/podman/plugins/module_utils/podman/common.py @@ -0,0 +1,232 @@ +# 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 + +import json +import os +import shutil + +from ansible.module_utils.six import raise_from +try: + from ansible.module_utils.compat.version import LooseVersion # noqa: F401 +except ImportError: + try: + from distutils.version import LooseVersion # noqa: F401 + except ImportError as exc: + raise_from(ImportError('To use this plugin or module with ansible-core' + ' < 2.11, you need to use Python < 3.12 with ' + 'distutils.version present'), exc) + + +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 run_generate_systemd_command(module, module_params, name, version): + """Generate systemd unit file.""" + command = [module_params['executable'], 'generate', 'systemd', + name, '--format', 'json'] + sysconf = module_params['generate_systemd'] + gt4ver = LooseVersion(version) >= LooseVersion('4.0.0') + if sysconf.get('restart_policy'): + if sysconf.get('restart_policy') not in [ + "no", "on-success", "on-failure", "on-abnormal", "on-watchdog", + "on-abort", "always"]: + module.fail_json( + 'Restart policy for systemd unit file is "%s" and must be one of: ' + '"no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", or "always"' % + sysconf.get('restart_policy')) + command.extend([ + '--restart-policy', + sysconf['restart_policy']]) + if sysconf.get('time'): + command.extend(['--time', str(sysconf['time'])]) + if sysconf.get('no_header'): + command.extend(['--no-header']) + if sysconf.get('names', True): + command.extend(['--name']) + if sysconf.get("new"): + command.extend(["--new"]) + if sysconf.get('container_prefix') is not None: + command.extend(['--container-prefix=%s' % sysconf['container_prefix']]) + if sysconf.get('pod_prefix') is not None: + command.extend(['--pod-prefix=%s' % sysconf['pod_prefix']]) + if sysconf.get('separator') is not None: + command.extend(['--separator=%s' % sysconf['separator']]) + if sysconf.get('after') is not None: + + sys_after = sysconf['after'] + if isinstance(sys_after, str): + sys_after = [sys_after] + for after in sys_after: + command.extend(['--after=%s' % after]) + if sysconf.get('wants') is not None: + sys_wants = sysconf['wants'] + if isinstance(sys_wants, str): + sys_wants = [sys_wants] + for want in sys_wants: + command.extend(['--wants=%s' % want]) + if sysconf.get('requires') is not None: + sys_req = sysconf['requires'] + if isinstance(sys_req, str): + sys_req = [sys_req] + for require in sys_req: + command.extend(['--requires=%s' % require]) + for param in ['after', 'wants', 'requires']: + if sysconf.get(param) is not None and not gt4ver: + module.fail_json(msg="Systemd parameter '%s' is supported from " + "podman version 4 only! Current version is %s" % ( + param, version)) + + if module.params['debug'] or module_params['debug']: + module.log("PODMAN-CONTAINER-DEBUG: systemd command: %s" % + " ".join(command)) + rc, systemd, err = module.run_command(command) + return rc, systemd, err + + +def generate_systemd(module, module_params, name, version): + empty = {} + sysconf = module_params['generate_systemd'] + rc, systemd, err = run_generate_systemd_command(module, module_params, name, version) + if rc != 0: + module.log( + "PODMAN-CONTAINER-DEBUG: Error generating systemd: %s" % err) + return empty + else: + try: + data = json.loads(systemd) + if sysconf.get('path'): + full_path = os.path.expanduser(sysconf['path']) + if not os.path.exists(full_path): + os.makedirs(full_path) + if not os.path.isdir(full_path): + module.fail_json("Path %s is not a directory! " + "Can not save systemd unit files there!" + % full_path) + for file_name, file_content in data.items(): + file_name += ".service" + with open(os.path.join(full_path, file_name), 'w') as f: + f.write(file_content) + return data + except Exception as e: + module.log( + "PODMAN-CONTAINER-DEBUG: Error writing systemd: %s" % e) + return empty + + +def delete_systemd(module, module_params, name, version): + sysconf = module_params['generate_systemd'] + if not sysconf.get('path'): + # We don't know where systemd files are located, nothing to delete + module.log( + "PODMAN-CONTAINER-DEBUG: Not deleting systemd file - no path!") + return + rc, systemd, err = run_generate_systemd_command(module, module_params, name, version) + if rc != 0: + module.log( + "PODMAN-CONTAINER-DEBUG: Error generating systemd: %s" % err) + return + else: + try: + data = json.loads(systemd) + for file_name in data.keys(): + file_name += ".service" + full_dir_path = os.path.expanduser(sysconf['path']) + file_path = os.path.join(full_dir_path, file_name) + if os.path.exists(file_path): + os.unlink(file_path) + return + except Exception as e: + module.log( + "PODMAN-CONTAINER-DEBUG: Error deleting systemd: %s" % e) + return + + +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 + + +def remove_file_or_dir(path): + if os.path.isfile(path): + os.unlink(path) + elif os.path.isdir(path): + shutil.rmtree(path) + else: + raise ValueError("file %s is not a file or dir." % path) + + +# Generated from https://github.com/containers/podman/blob/main/pkg/signal/signal_linux.go +# and https://github.com/containers/podman/blob/main/pkg/signal/signal_linux_mipsx.go +_signal_map = { + "ABRT": 6, + "ALRM": 14, + "BUS": 7, + "CHLD": 17, + "CLD": 17, + "CONT": 18, + "EMT": 7, + "FPE": 8, + "HUP": 1, + "ILL": 4, + "INT": 2, + "IO": 29, + "IOT": 6, + "KILL": 9, + "PIPE": 13, + "POLL": 29, + "PROF": 27, + "PWR": 30, + "QUIT": 3, + "RTMAX": 64, + "RTMIN": 34, + "SEGV": 11, + "STKFLT": 16, + "STOP": 19, + "SYS": 31, + "TERM": 15, + "TRAP": 5, + "TSTP": 20, + "TTIN": 21, + "TTOU": 22, + "URG": 23, + "USR1": 10, + "USR2": 12, + "VTALRM": 26, + "WINCH": 28, + "XCPU": 24, + "XFSZ": 25 +} + +for i in range(1, _signal_map['RTMAX'] - _signal_map['RTMIN'] + 1): + _signal_map['RTMIN+{0}'.format(i)] = _signal_map['RTMIN'] + i + _signal_map['RTMAX-{0}'.format(i)] = _signal_map['RTMAX'] - i + + +def normalize_signal(signal_name_or_number): + signal_name_or_number = str(signal_name_or_number) + if signal_name_or_number.isdigit(): + return signal_name_or_number + else: + signal_name = signal_name_or_number.upper() + if signal_name.startswith('SIG'): + signal_name = signal_name[3:] + if signal_name not in _signal_map: + raise RuntimeError("Unknown signal '{0}'".format(signal_name_or_number)) + return str(_signal_map[signal_name]) diff --git a/ansible_collections/containers/podman/plugins/module_utils/podman/podman_container_lib.py b/ansible_collections/containers/podman/plugins/module_utils/podman/podman_container_lib.py new file mode 100644 index 000000000..1ba28f4c8 --- /dev/null +++ b/ansible_collections/containers/podman/plugins/module_utils/podman/podman_container_lib.py @@ -0,0 +1,1696 @@ +from __future__ import (absolute_import, division, print_function) +import json # noqa: F402 +import os # noqa: F402 +import shlex # noqa: F402 + +from ansible.module_utils._text import to_bytes, to_native # noqa: F402 +from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion +from ansible_collections.containers.podman.plugins.module_utils.podman.common import lower_keys +from ansible_collections.containers.podman.plugins.module_utils.podman.common import generate_systemd +from ansible_collections.containers.podman.plugins.module_utils.podman.common import delete_systemd +from ansible_collections.containers.podman.plugins.module_utils.podman.common import normalize_signal + +__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', 'created']), + 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'), + 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', no_log=False), + device=dict(type='list', elements='str'), + device_read_bps=dict(type='list', elements='str'), + device_read_iops=dict(type='list', elements='str'), + device_write_bps=dict(type='list', elements='str'), + device_write_iops=dict(type='list', elements='str'), + 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']), + generate_systemd=dict(type='dict', default={}), + gidmap=dict(type='list', elements='str'), + group_add=dict(type='list', elements='str', 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'), + hooks_dir=dict(type='list', elements='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='dict', aliases=['log_options'], + options=dict( + max_size=dict(type='str'), + path=dict(type='str'), + tag=dict(type='str'))), + 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='list', elements='str', aliases=['mounts']), + network=dict(type='list', elements='str', aliases=['net', 'network_mode']), + network_aliases=dict(type='list', elements='str'), + 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), + requires=dict(type='list', elements='str'), + restart_policy=dict(type='str'), + rm=dict(type='bool', aliases=['remove', 'auto_remove']), + rootfs=dict(type='bool'), + secrets=dict(type='list', elements='str', no_log=True), + sdnotify=dict(type='str'), + 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='str'), + timezone=dict(type='str'), + tmpfs=dict(type='dict'), + tty=dict(type='bool'), + uidmap=dict(type='list', elements='str'), + ulimit=dict(type='list', elements='str', 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']) +) + + +def init_options(): + default = {} + opts = ARGUMENTS_SPEC_CONTAINER + for k, v in opts.items(): + if 'default' in v: + default[k] = v['default'] + else: + default[k] = None + return default + + +def update_options(opts_dict, container): + def to_bool(x): + return str(x).lower() not in ['no', 'false'] + + aliases = {} + for k, v in ARGUMENTS_SPEC_CONTAINER.items(): + if 'aliases' in v: + for alias in v['aliases']: + aliases[alias] = k + for k in list(container): + if k in aliases: + key = aliases[k] + container[key] = container.pop(k) + else: + key = k + if ARGUMENTS_SPEC_CONTAINER[key]['type'] == 'list' and not isinstance(container[key], list): + opts_dict[key] = [container[key]] + elif ARGUMENTS_SPEC_CONTAINER[key]['type'] == 'bool' and not isinstance(container[key], bool): + opts_dict[key] = to_bool(container[key]) + elif ARGUMENTS_SPEC_CONTAINER[key]['type'] == 'int' and not isinstance(container[key], int): + opts_dict[key] = int(container[key]) + else: + opts_dict[key] = container[key] + + return opts_dict + + +def set_container_opts(input_vars): + default_options_templ = init_options() + options_dict = update_options(default_options_templ, input_vars) + return options_dict + + +class PodmanModuleParams: + """Creates list of arguments for podman CLI command. + + Arguments: + action {str} -- action type from 'run', 'stop', 'create', 'delete', + 'start', 'restart' + 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', 'restart']: + 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', 'restart']: + 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): + for gidmap in self.params['gidmap']: + c += ['--gidmap', gidmap] + return c + + 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_hooks_dir(self, c): + for hook_dir in self.params['hooks_dir']: + c += ['--hooks-dir=%s' % hook_dir] + return c + + 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): + for k, v in self.params['log_opt'].items(): + if v is not None: + c += ['--log-opt', + b"=".join([to_bytes(k.replace('max_size', 'max-size'), + errors='surrogate_or_strict'), + to_bytes(v, + errors='surrogate_or_strict')])] + return c + + 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): + for mnt in self.params['mount']: + if mnt: + c += ['--mount', mnt] + return c + + def addparam_network(self, c): + if LooseVersion(self.podman_version) >= LooseVersion('4.0.0'): + for net in self.params['network']: + c += ['--network', net] + return c + return c + ['--network', ",".join(self.params['network'])] + + def addparam_network_aliases(self, c): + for alias in self.params['network_aliases']: + c += ['--network-alias', alias] + return c + + 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_requires(self, c): + return c + ['--requires', ",".join(self.params['requires'])] + + 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_sdnotify(self, c): + return c + ['--sdnotify=%s' % self.params['sdnotify']] + + def addparam_secrets(self, c): + for secret in self.params['secrets']: + c += ['--secret', secret] + return c + + 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' % str(self.params['systemd']).lower()] + + def addparam_tmpfs(self, c): + for tmpfs in self.params['tmpfs'].items(): + c += ['--tmpfs', ':'.join(tmpfs)] + return c + + def addparam_timezone(self, c): + return c + ['--tz=%s' % self.params['timezone']] + + 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_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, + "read_only": False, + "rm": False, + "security_opt": [], + "stop_signal": self.image_info.get('config', {}).get('stopsignal', "15"), + "tty": False, + "user": self.image_info.get('user', ''), + "workdir": self.image_info.get('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" + if (LooseVersion(self.version) >= LooseVersion('3.0.0')): + self.defaults['log_level'] = "warning" + if (LooseVersion(self.version) >= LooseVersion('4.1.0')): + self.defaults['ipc'] = "shareable" + 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 = {} + + 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 [] + before = [i.lower() for i in before] + after = [] + if self.module_params['cap_add'] is not None: + for cap in self.module_params['cap_add']: + cap = cap.lower() + cap = cap if cap.startswith('cap_') else 'cap_' + cap + after.append(cap) + 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 [] + before = [i.lower() for i in before] + after = before[:] + if self.module_params['cap_drop'] is not None: + for cap in self.module_params['cap_drop']: + cap = cap.lower() + cap = cap if cap.startswith('cap_') else 'cap_' + cap + if cap in after: + after.remove(cap) + 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'] + labels = self.info['config']['labels'] or {} + # Ignore cidfile that is coming from systemd files + # https://github.com/containers/ansible-podman-collections/issues/276 + if 'podman_systemd_unit' in labels: + after = before + 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 = shlex.split(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']] + if not before and 'createcommand' in self.info['config']: + cr_com = self.info['config']['createcommand'] + if '--device' in cr_com: + before = [cr_com[k + 1].lower() + for k, i in enumerate(cr_com) if i == '--device'] + before = [":".join((i, i)) + if len(i.split(":")) == 1 else i for i in before] + after = [":".join(i.split(":")[:2]) for i in self.params['device']] + after = [":".join((i, i)) + if len(i.split(":")) == 1 else i for i in after] + after = [i.lower() for i in after] + 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: str(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(":", 1) + 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'] or self.info['rootfs'] + after_id = self.image_info['id'] + if before_id == after_id: + return self._diff_update_and_compare('image', before_id, after_id) + is_rootfs = self.info['rootfs'] != '' or self.params['rootfs'] + before = self.info['config']['image'] or before_id + after = self.params['image'] + mode = self.params['image_strict'] or is_rootfs + 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) + for k, v in self.params['label'].items() + }) + # Strip out labels that are coming from systemd files + # https://github.com/containers/ansible-podman-collections/issues/276 + if 'podman_systemd_unit' in before: + after.pop('podman_systemd_unit', None) + before.pop('podman_systemd_unit', None) + return self._diff_update_and_compare('label', before, after) + + def diffparam_log_driver(self): + before = self.info['hostconfig']['logconfig']['type'] + if self.module_params['log_driver'] is not None: + after = self.params['log_driver'] + else: + after = before + return self._diff_update_and_compare('log_driver', before, after) + + # Parameter has limited idempotency, unable to guess the default log_path + def diffparam_log_opt(self): + before, after = {}, {} + + # Log path + path_before = None + if 'logpath' in self.info: + path_before = self.info['logpath'] + # For Podman v3 + if ('logconfig' in self.info['hostconfig'] and + 'path' in self.info['hostconfig']['logconfig']): + path_before = self.info['hostconfig']['logconfig']['path'] + if path_before is not None: + if (self.module_params['log_opt'] and + 'path' in self.module_params['log_opt'] and + self.module_params['log_opt']['path'] is not None): + path_after = self.params['log_opt']['path'] + else: + path_after = path_before + if path_before != path_after: + before.update({'log-path': path_before}) + after.update({'log-path': path_after}) + + # Log tag + tag_before = None + if 'logtag' in self.info: + tag_before = self.info['logtag'] + # For Podman v3 + if ('logconfig' in self.info['hostconfig'] and + 'tag' in self.info['hostconfig']['logconfig']): + tag_before = self.info['hostconfig']['logconfig']['tag'] + if tag_before is not None: + if (self.module_params['log_opt'] and + 'tag' in self.module_params['log_opt'] and + self.module_params['log_opt']['tag'] is not None): + tag_after = self.params['log_opt']['tag'] + else: + tag_after = '' + if tag_before != tag_after: + before.update({'log-tag': tag_before}) + after.update({'log-tag': tag_after}) + + # Log size + # For Podman v3 + # size_before = '0B' + # TODO(sshnaidm): integrate B/KB/MB/GB calculation for sizes + # if ('logconfig' in self.info['hostconfig'] and + # 'size' in self.info['hostconfig']['logconfig']): + # size_before = self.info['hostconfig']['logconfig']['size'] + # if size_before != '0B': + # if (self.module_params['log_opt'] and + # 'max_size' in self.module_params['log_opt'] and + # self.module_params['log_opt']['max_size'] is not None): + # size_after = self.params['log_opt']['max_size'] + # else: + # size_after = '' + # if size_before != size_after: + # before.update({'log-size': size_before}) + # after.update({'log-size': size_after}) + + return self._diff_update_and_compare('log_opt', before, after) + + def diffparam_mac_address(self): + before = str(self.info['networksettings']['macaddress']) + if not before and self.info['networksettings'].get('networks'): + nets = self.info['networksettings']['networks'] + macs = [ + nets[i]["macaddress"] for i in nets if nets[i]["macaddress"]] + if macs: + before = macs[0] + if not before and 'createcommand' in self.info['config']: + cr_com = self.info['config']['createcommand'] + if '--mac-address' in cr_com: + before = cr_com[cr_com.index('--mac-address') + 1].lower() + if self.module_params['mac_address'] is not None: + after = self.params['mac_address'] + else: + after = before + return self._diff_update_and_compare('mac_address', before, after) + + def diffparam_network(self): + net_mode_before = self.info['hostconfig']['networkmode'] + net_mode_after = '' + before = list(self.info['networksettings'].get('networks', {})) + # Remove default 'podman' network in v3 for comparison + if before == ['podman']: + before = [] + # Special case for options for slirp4netns rootless networking from v2 + if net_mode_before == 'slirp4netns' and 'createcommand' in self.info['config']: + cr_com = self.info['config']['createcommand'] + if '--network' in cr_com: + cr_net = cr_com[cr_com.index('--network') + 1].lower() + if 'slirp4netns:' in cr_net: + before = [cr_net] + 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) + # If container is attached to network of a different container + if "container" in net_mode_before: + for netw in after: + if "container" in netw: + before = after = netw + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('network', 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): + def compose(p, h): + s = ":".join( + [str(h["hostport"]), p.replace('/tcp', '')] + ).strip(":") + if h['hostip']: + return ":".join([h['hostip'], s]) + return s + + ports = self.info['hostconfig']['portbindings'] + before = [] + for port, hosts in ports.items(): + if hosts: + for h in hosts: + before.append(compose(port, h)) + after = self.params['publish'] or [] + if self.params['publish_all']: + image_ports = self.image_info.get('config', {}).get('exposedports', {}) + if image_ports: + after += list(image_ports.keys()) + after = [ + i.replace("/tcp", "").replace("[", "").replace("]", "") + 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_read_only(self): + before = self.info['hostconfig']['readonlyrootfs'] + after = self.params['read_only'] + return self._diff_update_and_compare('read_only', before, after) + + def diffparam_restart_policy(self): + before = self.info['hostconfig']['restartpolicy']['name'] + after = self.params['restart_policy'] or "" + return self._diff_update_and_compare('restart_policy', 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): + before = normalize_signal(self.info['config']['stopsignal']) + after = normalize_signal(self.params['stop_signal']) + return self._diff_update_and_compare('stop_signal', before, after) + + def diffparam_timezone(self): + before = self.info['config'].get('timezone') + after = self.params['timezone'] + return self._diff_update_and_compare('timezone', 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.''' + if not x.rstrip("/"): + return "/" + 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( + [ + clean_volume(m['source']), + clean_volume(m['destination']) + ]) + elif m['type'] == 'volume': + local_vols.append( + [m['name'], clean_volume(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'] + is_rootfs = module_params['rootfs'] + + if is_rootfs: + if not os.path.exists(image) or not os.path.isdir(image): + module.fail_json(msg="Image rootfs doesn't exist %s" % image) + return image_actions + 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 + is_rootfs = self.module_params['rootfs'] + if is_rootfs: + return {'Id': self.module_params['image']} + 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, restart + """ + b_command = PodmanModuleParams(action, + self.module_params, + self.version, + self.module, + ).construct_command_from_params() + if action == 'create': + b_command.remove(b'--detach=True') + 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: + self.module.fail_json( + msg="Can't %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 restart(self): + """Restart the container.""" + self._perform_action('restart') + + def create(self): + """Create the container.""" + self._perform_action('create') + + def recreate(self): + """Recreate the container.""" + if self.running: + self.stop() + if not self.info['HostConfig']['AutoRemove']: + self.delete() + self.create() + + def recreate_run(self): + """Recreate and run the container.""" + if self.running: + self.stop() + if not self.info['HostConfig']['AutoRemove']: + self.delete() + self.run() + + +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'] + + if self.module_params['generate_systemd'].get('new'): + self.module_params['rm'] = True + + 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}) + self.results.update( + {'podman_systemd': generate_systemd(self.module, + self.module_params, + self.name, + self.container.version)}) + + def make_started(self): + """Run actions if desired state is 'started'.""" + if not self.image: + if not self.container.exists: + self.module.fail_json(msg='Cannot start container when image' + ' is not specified!') + if self.restart: + self.container.restart() + self.results['actions'].append('restarted %s' % + self.container.name) + else: + self.container.start() + self.results['actions'].append('started %s' % + self.container.name) + self.update_container_result() + return + if self.container.exists and self.restart: + if self.container.running: + self.container.restart() + self.results['actions'].append('restarted %s' % + self.container.name) + else: + self.container.start() + self.results['actions'].append('started %s' % + self.container.name) + self.update_container_result() + return + if self.container.running and \ + (self.container.different or self.recreate): + self.container.recreate_run() + 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_run() + 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_created(self): + """Run actions if desired state is 'created'.""" + 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 + else: + if (self.container.different or self.recreate): + self.container.recreate() + self.results['actions'].append('recreated %s' % + self.container.name) + if self.container.running: + self.container.start() + self.results['actions'].append('started %s' % + self.container.name) + self.update_container_result() + return + elif self.restart: + if self.container.running: + self.container.restart() + self.results['actions'].append('restarted %s' % + self.container.name) + else: + self.container.start() + self.results['actions'].append('started %s' % + self.container.name) + self.update_container_result() + return + self.update_container_result(changed=False) + 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: + delete_systemd(self.module, + self.module_params, + self.name, + self.container.version) + 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_created, + 'started': self.make_started, + 'absent': self.make_absent, + 'stopped': self.make_stopped, + 'created': self.make_created, + } + process_action = states_map[self.state] + process_action() + return self.results diff --git a/ansible_collections/containers/podman/plugins/module_utils/podman/podman_pod_lib.py b/ansible_collections/containers/podman/plugins/module_utils/podman/podman_pod_lib.py new file mode 100644 index 000000000..0b4afc0bc --- /dev/null +++ b/ansible_collections/containers/podman/plugins/module_utils/podman/podman_pod_lib.py @@ -0,0 +1,880 @@ +from __future__ import (absolute_import, division, print_function) +import json + +from ansible.module_utils._text import to_bytes, to_native +from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion +from ansible_collections.containers.podman.plugins.module_utils.podman.common import lower_keys +from ansible_collections.containers.podman.plugins.module_utils.podman.common import generate_systemd +from ansible_collections.containers.podman.plugins.module_utils.podman.common import delete_systemd + + +__metaclass__ = type + +ARGUMENTS_SPEC_POD = dict( + state=dict( + type='str', + default="created", + choices=[ + 'created', + 'killed', + 'restarted', + 'absent', + 'started', + 'stopped', + 'paused', + 'unpaused', + ]), + recreate=dict(type='bool', default=False), + add_host=dict(type='list', required=False, elements='str'), + cgroup_parent=dict(type='str', required=False), + cpus=dict(type='str', required=False), + cpuset_cpus=dict(type='str', required=False), + device=dict(type='list', elements='str', required=False), + device_read_bps=dict(type='list', elements='str', required=False), + dns=dict(type='list', elements='str', required=False), + dns_opt=dict(type='list', elements='str', required=False), + dns_search=dict(type='list', elements='str', required=False), + generate_systemd=dict(type='dict', default={}), + gidmap=dict(type='list', elements='str', required=False), + hostname=dict(type='str', required=False), + infra=dict(type='bool', required=False), + infra_conmon_pidfile=dict(type='str', required=False), + infra_command=dict(type='str', required=False), + infra_image=dict(type='str', required=False), + infra_name=dict(type='str', required=False), + ip=dict(type='str', required=False), + label=dict(type='dict', required=False), + label_file=dict(type='str', required=False), + mac_address=dict(type='str', required=False), + name=dict(type='str', required=True), + network=dict(type='list', elements='str', required=False), + network_alias=dict(type='list', elements='str', required=False, + aliases=['network_aliases']), + no_hosts=dict(type='bool', required=False), + pid=dict(type='str', required=False), + pod_id_file=dict(type='str', required=False), + publish=dict(type='list', required=False, + elements='str', aliases=['ports']), + share=dict(type='str', required=False), + subgidname=dict(type='str', required=False), + subuidname=dict(type='str', required=False), + uidmap=dict(type='list', elements='str', required=False), + userns=dict(type='str', required=False), + volume=dict(type='list', elements='str', aliases=['volumes'], + required=False), + executable=dict(type='str', required=False, default='podman'), + debug=dict(type='bool', default=False), +) + + +class PodmanPodModuleParams: + """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', 'restart', 'stop', 'delete', 'pause', + 'unpause', 'kill']: + return self._simple_action() + if self.action in ['create']: + return self._create_action() + self.module.fail_json(msg="Unknown action %s" % self.action) + + def _simple_action(self): + if self.action in ['start', 'restart', 'stop', 'pause', 'unpause', 'kill']: + 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] + self.module.fail_json(msg="Unknown action %s" % self.action) + + def _create_action(self): + cmd = [self.action] + 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) + 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_add_host(self, c): + for g in self.params['add_host']: + c += ['--add-host', g] + return c + + def addparam_cgroup_parent(self, c): + return c + ['--cgroup-parent', self.params['cgroup_parent']] + + 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_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_dns(self, c): + for g in self.params['dns']: + c += ['--dns', g] + return c + + def addparam_dns_opt(self, c): + for g in self.params['dns_opt']: + c += ['--dns-opt', g] + return c + + def addparam_dns_search(self, c): + for g in self.params['dns_search']: + c += ['--dns-search', g] + return c + + def addparam_gidmap(self, c): + for gidmap in self.params['gidmap']: + c += ['--gidmap', gidmap] + return c + + def addparam_hostname(self, c): + return c + ['--hostname', self.params['hostname']] + + def addparam_infra(self, c): + return c + [b'='.join([b'--infra', + to_bytes(self.params['infra'], + errors='surrogate_or_strict')])] + + def addparam_infra_conmon_pidfile(self, c): + return c + ['--infra-conmon-pidfile', self.params['infra_conmon_pidfile']] + + def addparam_infra_command(self, c): + return c + ['--infra-command', self.params['infra_command']] + + def addparam_infra_image(self, c): + return c + ['--infra-image', self.params['infra_image']] + + def addparam_infra_name(self, c): + return c + ['--infra-name', self.params['infra_name']] + + def addparam_ip(self, c): + return c + ['--ip', self.params['ip']] + + def addparam_label(self, c): + for label in self.params['label'].items(): + c += ['--label', b'='.join( + [to_bytes(i, errors='surrogate_or_strict') for i in label])] + return c + + def addparam_label_file(self, c): + return c + ['--label-file', self.params['label_file']] + + def addparam_mac_address(self, c): + return c + ['--mac-address', self.params['mac_address']] + + def addparam_name(self, c): + return c + ['--name', self.params['name']] + + def addparam_network(self, c): + if LooseVersion(self.podman_version) >= LooseVersion('4.0.0'): + for net in self.params['network']: + c += ['--network', net] + return c + return c + ['--network', ",".join(self.params['network'])] + + def addparam_network_aliases(self, c): + for alias in self.params['network_aliases']: + c += ['--network-alias', alias] + return c + + def addparam_no_hosts(self, c): + return c + ["=".join('--no-hosts', self.params['no_hosts'])] + + def addparam_pid(self, c): + return c + ['--pid', self.params['pid']] + + def addparam_pod_id_file(self, c): + return c + ['--pod-id-file', self.params['pod_id_file']] + + def addparam_publish(self, c): + for g in self.params['publish']: + c += ['--publish', g] + return c + + def addparam_share(self, c): + return c + ['--share', self.params['share']] + + def addparam_subgidname(self, c): + return c + ['--subgidname', self.params['subgidname']] + + def addparam_subuidname(self, c): + return c + ['--subuidname', self.params['subuidname']] + + def addparam_uidmap(self, c): + for uidmap in self.params['uidmap']: + c += ['--uidmap', uidmap] + return c + + def addparam_userns(self, c): + return c + ['--userns', self.params['userns']] + + def addparam_volume(self, c): + for vol in self.params['volume']: + if vol: + c += ['--volume', vol] + return c + + +class PodmanPodDefaults: + def __init__(self, module, podman_version): + self.module = module + self.version = podman_version + self.defaults = { + 'add_host': [], + 'dns': [], + 'dns_opt': [], + 'dns_search': [], + 'infra': True, + 'label': {}, + } + + 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 + return self.defaults + + +class PodmanPodDiff: + def __init__(self, module, module_params, info, infra_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.infra_info = lower_keys(infra_info) + self.params = self.defaultize() + self.diff = {'before': {}, 'after': {}} + self.non_idempotent = {} + + def defaultize(self): + params_with_defaults = {} + self.default_dict = PodmanPodDefaults( + self.module, 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_add_host(self): + if not self.infra_info: + return self._diff_update_and_compare('add_host', '', '') + before = self.infra_info['hostconfig']['extrahosts'] or [] + after = self.params['add_host'] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('add_host', before, after) + + def diffparam_cgroup_parent(self): + if 'cgroupparent' in self.info: + before = self.info['cgroupparent'] + elif 'config' in self.info and self.info['config'].get('cgroupparent'): + before = self.info['config']['cgroupparent'] + after = self.params['cgroup_parent'] or before + return self._diff_update_and_compare('cgroup_parent', before, after) + + def diffparam_dns(self): + if not self.infra_info: + return self._diff_update_and_compare('dns', '', '') + before = self.infra_info['hostconfig']['dns'] or [] + after = self.params['dns'] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('dns', before, after) + + def diffparam_dns_opt(self): + if not self.infra_info: + return self._diff_update_and_compare('dns_opt', '', '') + before = self.infra_info['hostconfig']['dnsoptions'] or [] + after = self.params['dns_opt'] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('dns_opt', before, after) + + def diffparam_dns_search(self): + if not self.infra_info: + return self._diff_update_and_compare('dns_search', '', '') + before = self.infra_info['hostconfig']['dnssearch'] or [] + after = self.params['dns_search'] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('dns_search', before, after) + + def diffparam_hostname(self): + if not self.infra_info: + return self._diff_update_and_compare('hostname', '', '') + before = self.infra_info['config']['hostname'] + after = self.params['hostname'] or before + return self._diff_update_and_compare('hostname', before, after) + + # TODO(sshnaidm): https://github.com/containers/podman/issues/6968 + def diffparam_infra(self): + if 'state' in self.info and 'infracontainerid' in self.info['state']: + before = self.info['state']['infracontainerid'] != "" + else: + # TODO(sshnaidm): https://github.com/containers/podman/issues/6968 + before = 'infracontainerid' in self.info + after = self.params['infra'] + return self._diff_update_and_compare('infra', before, after) + + # TODO(sshnaidm): https://github.com/containers/podman/issues/6969 + # def diffparam_infra_command(self): + # before = str(self.info['hostconfig']['infra_command']) + # after = self.params['infra_command'] + # return self._diff_update_and_compare('infra_command', before, after) + + def diffparam_infra_image(self): + if not self.infra_info: + return self._diff_update_and_compare('infra_image', '', '') + before = str(self.infra_info['imagename']) + after = before + if self.module_params['infra_image']: + after = self.params['infra_image'] + before = before.replace(":latest", "") + after = after.replace(":latest", "") + before = before.split("/")[-1] # pylint: disable=W,C,R + after = after.split("/")[-1] # pylint: disable=W,C,R + return self._diff_update_and_compare('infra_image', before, after) + + # TODO(sshnaidm): https://github.com/containers/podman/pull/6956 + # def diffparam_ip(self): + # before = str(self.info['hostconfig']['ip']) + # after = self.params['ip'] + # return self._diff_update_and_compare('ip', before, after) + + def diffparam_label(self): + if 'config' in self.info and 'labels' in self.info['config']: + before = self.info['config'].get('labels') or {} + else: + before = self.info['labels'] if 'labels' in self.info else {} + after = self.params['label'] + # Strip out labels that are coming from systemd files + # https://github.com/containers/ansible-podman-collections/issues/276 + if 'podman_systemd_unit' in before: + after.pop('podman_systemd_unit', None) + before.pop('podman_systemd_unit', None) + return self._diff_update_and_compare('label', before, after) + + # TODO(sshnaidm): https://github.com/containers/podman/pull/6956 + # def diffparam_mac_address(self): + # before = str(self.info['hostconfig']['mac_address']) + # after = self.params['mac_address'] + # return self._diff_update_and_compare('mac_address', before, after) + + def diffparam_network(self): + if not self.infra_info: + return self._diff_update_and_compare('network', [], []) + net_mode_before = self.infra_info['hostconfig']['networkmode'] + net_mode_after = '' + before = list(self.infra_info['networksettings'].get('networks', {})) + # Remove default 'podman' network in v3 for comparison + if before == ['podman']: + before = [] + after = self.params['network'] or [] + # Special case for options for slirp4netns rootless networking from v2 + if net_mode_before == 'slirp4netns' and 'createcommand' in self.info: + cr_com = self.info['createcommand'] + if '--network' in cr_com: + cr_net = cr_com[cr_com.index('--network') + 1].lower() + if 'slirp4netns:' in cr_net: + before = [cr_net] + # Currently supported only 'host' and 'none' network modes idempotency + if after in [['bridge'], ['host'], ['slirp4netns']]: + net_mode_after = after[0] + + 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) + # For 4.4.0+ podman versions with no network specified + if not net_mode_after and net_mode_before == 'slirp4netns' and not after: + net_mode_after = 'slirp4netns' + if before == ['slirp4netns']: + after = ['slirp4netns'] + if not net_mode_after and net_mode_before == 'bridge' and not after: + net_mode_after = 'bridge' + if before == ['bridge']: + after = ['bridge'] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('network', before, after) + + # TODO(sshnaidm) + # def diffparam_no_hosts(self): + # before = str(self.info['hostconfig']['no_hosts']) + # after = self.params['no_hosts'] + # return self._diff_update_and_compare('no_hosts', before, after) + + # TODO(sshnaidm) Need to add port ranges support + def diffparam_publish(self): + def compose(p, h): + s = ":".join( + [str(h["hostport"]), p.replace('/tcp', '')] + ).strip(":") + if h['hostip']: + return ":".join([h['hostip'], s]) + return s + + if not self.infra_info: + return self._diff_update_and_compare('publish', '', '') + + ports = self.infra_info['hostconfig']['portbindings'] + before = [] + for port, hosts in ports.items(): + if hosts: + for h in hosts: + before.append(compose(port, h)) + after = self.params['publish'] or [] + after = [ + i.replace("/tcp", "").replace("[", "").replace("]", "") + 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_share(self): + if not self.infra_info: + return self._diff_update_and_compare('share', '', '') + if 'sharednamespaces' in self.info: + before = self.info['sharednamespaces'] + elif 'config' in self.info: + before = [ + i.split('shares')[1].lower() + for i in self.info['config'] if 'shares' in i] + # TODO(sshnaidm): to discover why in podman v1 'cgroup' appears + before.remove('cgroup') + else: + before = [] + if self.params['share'] is not None: + after = self.params['share'].split(",") + else: + after = ['uts', 'ipc', 'net'] + # TODO: find out why on Ubuntu the 'net' is not present + if 'net' not in before: + after.remove('net') + if self.params["uidmap"] or self.params["gidmap"]: + after.append('user') + + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('share', 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 + + +class PodmanPod: + """Perform pod tasks. + + Manages podman pod, inspects it and checks its current state + """ + + def __init__(self, module, name, module_params): + """Initialize PodmanPod class. + + Arguments: + module {obj} -- ansible module object + name {str} -- name of pod + """ + + self.module = module + self.module_params = module_params + self.name = name + self.stdout, self.stderr = '', '' + self.info = self.get_info() + self.infra_info = self.get_infra_info() + self.version = self._get_podman_version() + self.diff = {} + self.actions = [] + + @property + def exists(self): + """Check if pod exists.""" + return bool(self.info != {}) + + @property + def different(self): + """Check if pod is different.""" + diffcheck = PodmanPodDiff( + self.module, + self.module_params, + self.info, + self.infra_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 pod is running now.""" + if 'status' in self.info['State']: + return self.info['State']['status'] == 'Running' + # older podman versions (1.6.x) don't have status in 'podman pod inspect' + # if other methods fail, use 'podman pod ps' + ps_info = self.get_ps() + if 'status' in ps_info: + return ps_info['status'] == 'Running' + return self.info['State'] == 'Running' + + @property + def paused(self): + """Return True if pod is paused now.""" + if 'status' in self.info['State']: + return self.info['State']['status'] == 'Paused' + return self.info['State'] == 'Paused' + + @property + def stopped(self): + """Return True if pod exists and is not running now.""" + if not self.exists: + return False + if 'status' in self.info['State']: + return not (self.info['State']['status'] == 'Running') + return not (self.info['State'] == 'Running') + + def get_info(self): + """Inspect pod and gather info about it.""" + # pylint: disable=unused-variable + rc, out, err = self.module.run_command( + [self.module_params['executable'], b'pod', b'inspect', self.name]) + return json.loads(out) if rc == 0 else {} + + def get_ps(self): + """Inspect pod process and gather info about it.""" + # pylint: disable=unused-variable + rc, out, err = self.module.run_command( + [self.module_params['executable'], b'pod', b'ps', b'--format', b'json', b'--filter', 'name=' + self.name]) + return json.loads(out)[0] if rc == 0 else {} + + def get_infra_info(self): + """Inspect pod and gather info about it.""" + if not self.info: + return {} + if 'InfraContainerID' in self.info: + infra_container_id = self.info['InfraContainerID'] + elif 'State' in self.info and 'infraContainerID' in self.info['State']: + infra_container_id = self.info['State']['infraContainerID'] + else: + return {} + # pylint: disable=unused-variable + rc, out, err = self.module.run_command( + [self.module_params['executable'], b'inspect', infra_container_id]) + 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 pod. + + Arguments: + action {str} -- action to perform - start, create, stop, pause + unpause, delete, restart, kill + """ + b_command = PodmanPodModuleParams(action, + self.module_params, + self.version, + self.module, + ).construct_command_from_params() + full_cmd = " ".join([self.module_params['executable'], 'pod'] + + [to_native(i) for i in b_command]) + self.module.log("PODMAN-POD-DEBUG: %s" % full_cmd) + self.actions.append(full_cmd) + if not self.module.check_mode: + rc, out, err = self.module.run_command( + [self.module_params['executable'], b'pod'] + b_command, + expand_user_and_vars=False) + self.stdout = out + self.stderr = err + if rc != 0: + self.module.fail_json( + msg="Can't %s pod %s" % (action, self.name), + stdout=out, stderr=err) + + def delete(self): + """Delete the pod.""" + self._perform_action('delete') + + def stop(self): + """Stop the pod.""" + self._perform_action('stop') + + def start(self): + """Start the pod.""" + self._perform_action('start') + + def create(self): + """Create the pod.""" + self._perform_action('create') + + def recreate(self): + """Recreate the pod.""" + self.delete() + self.create() + + def restart(self): + """Restart the pod.""" + self._perform_action('restart') + + def kill(self): + """Kill the pod.""" + self._perform_action('kill') + + def pause(self): + """Pause the pod.""" + self._perform_action('pause') + + def unpause(self): + """Unpause the pod.""" + self._perform_action('unpause') + + +class PodmanPodManager: + """Module manager class. + + Defines according to parameters what actions should be applied to pod + """ + + def __init__(self, module, params): + """Initialize PodmanManager class. + + Arguments: + module {obj} -- ansible module object + """ + + self.module = module + self.module_params = params + self.results = { + 'changed': False, + 'actions': [], + 'pod': {}, + } + self.name = self.module_params['name'] + self.executable = \ + self.module.get_bin_path(self.module_params['executable'], + required=True) + self.state = self.module_params['state'] + self.recreate = self.module_params['recreate'] + self.pod = PodmanPod(self.module, self.name, self.module_params) + + def update_pod_result(self, changed=True): + """Inspect the current pod, update results with last info, exit. + + Keyword Arguments: + changed {bool} -- whether any action was performed + (default: {True}) + """ + facts = self.pod.get_info() if changed else self.pod.info + out, err = self.pod.stdout, self.pod.stderr + self.results.update({'changed': changed, 'pod': facts, + 'podman_actions': self.pod.actions}, + stdout=out, stderr=err) + if self.pod.diff: + self.results.update({'diff': self.pod.diff}) + if self.module.params['debug'] or self.module_params['debug']: + self.results.update({'podman_version': self.pod.version}) + self.results.update( + {'podman_systemd': generate_systemd(self.module, + self.module_params, + self.name, + self.pod.version)}) + + def execute(self): + """Execute the desired action according to map of actions & states.""" + states_map = { + 'created': self.make_created, + 'started': self.make_started, + 'stopped': self.make_stopped, + 'restarted': self.make_restarted, + 'absent': self.make_absent, + 'killed': self.make_killed, + 'paused': self.make_paused, + 'unpaused': self.make_unpaused, + + } + process_action = states_map[self.state] + process_action() + return self.results + + def _create_or_recreate_pod(self): + """Ensure pod exists and is exactly as it should be by input params.""" + changed = False + if self.pod.exists: + if self.pod.different or self.recreate: + self.pod.recreate() + self.results['actions'].append('recreated %s' % self.pod.name) + changed = True + elif not self.pod.exists: + self.pod.create() + self.results['actions'].append('created %s' % self.pod.name) + changed = True + return changed + + def make_created(self): + """Run actions if desired state is 'created'.""" + if self.pod.exists and not self.pod.different: + self.update_pod_result(changed=False) + return + self._create_or_recreate_pod() + self.update_pod_result() + + def make_killed(self): + """Run actions if desired state is 'killed'.""" + self._create_or_recreate_pod() + self.pod.kill() + self.results['actions'].append('killed %s' % self.pod.name) + self.update_pod_result() + + def make_paused(self): + """Run actions if desired state is 'paused'.""" + changed = self._create_or_recreate_pod() + if self.pod.paused: + self.update_pod_result(changed=changed) + return + self.pod.pause() + self.results['actions'].append('paused %s' % self.pod.name) + self.update_pod_result() + + def make_unpaused(self): + """Run actions if desired state is 'unpaused'.""" + changed = self._create_or_recreate_pod() + if not self.pod.paused: + self.update_pod_result(changed=changed) + return + self.pod.unpause() + self.results['actions'].append('unpaused %s' % self.pod.name) + self.update_pod_result() + + def make_started(self): + """Run actions if desired state is 'started'.""" + changed = self._create_or_recreate_pod() + if not changed and self.pod.running: + self.update_pod_result(changed=changed) + return + + # self.pod.unpause() TODO(sshnaidm): to unpause if state == started? + self.pod.start() + self.results['actions'].append('started %s' % self.pod.name) + self.update_pod_result() + + def make_stopped(self): + """Run actions if desired state is 'stopped'.""" + if not self.pod.exists: + self.module.fail_json("Pod %s doesn't exist!" % self.pod.name) + if self.pod.running: + self.pod.stop() + self.results['actions'].append('stopped %s' % self.pod.name) + self.update_pod_result() + elif self.pod.stopped: + self.update_pod_result(changed=False) + + def make_restarted(self): + """Run actions if desired state is 'restarted'.""" + if self.pod.exists: + self.pod.restart() + self.results['actions'].append('restarted %s' % self.pod.name) + self.results.update({'changed': True}) + self.update_pod_result() + else: + self.module.fail_json("Pod %s doesn't exist!" % self.pod.name) + + def make_absent(self): + """Run actions if desired state is 'absent'.""" + if not self.pod.exists: + self.results.update({'changed': False}) + elif self.pod.exists: + delete_systemd(self.module, + self.module_params, + self.name, + self.pod.version) + self.pod.delete() + self.results['actions'].append('deleted %s' % self.pod.name) + self.results.update({'changed': True}) + self.results.update({'pod': {}, + 'podman_actions': self.pod.actions}) |