from __future__ import (absolute_import, division, print_function) import json # noqa: F402 import shlex # noqa: F402 from distutils.version import LooseVersion # 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 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 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'), 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), 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), 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_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): 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_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_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, "rm": False, "security_opt": [], "stop_signal": self.image_info['config'].get('stopsignal', "15"), "tty": False, "user": self.image_info.get('user', ''), "workdir": self.image_info['config'].get('workingdir', '/'), "uts": "", } def default_dict(self): # make here any changes to self.defaults related to podman version # https://github.com/containers/libpod/pull/5669 if (LooseVersion(self.version) >= LooseVersion('1.8.0') and LooseVersion(self.version) < LooseVersion('1.9.0')): self.defaults['cpu_shares'] = 1024 if (LooseVersion(self.version) >= LooseVersion('2.0.0')): self.defaults['network'] = ["slirp4netns"] self.defaults['ipc'] = "private" self.defaults['uts'] = "private" self.defaults['pid'] = "private" if (LooseVersion(self.version) >= LooseVersion('3.0.0')): self.defaults['log_level'] = "warning" 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']] after = [":".join(i.split(":")[:2]) for i in self.params['device']] before, after = sorted(list(set(before))), sorted(list(set(after))) return self._diff_update_and_compare('devices', before, after) def diffparam_device_read_bps(self): before = self.info['hostconfig']['blkiodevicereadbps'] or [] before = ["%s:%s" % (i['path'], i['rate']) for i in before] after = self.params['device_read_bps'] or [] before, after = sorted(list(set(before))), sorted(list(set(after))) return self._diff_update_and_compare('device_read_bps', before, after) def diffparam_device_read_iops(self): before = self.info['hostconfig']['blkiodevicereadiops'] or [] before = ["%s:%s" % (i['path'], i['rate']) for i in before] after = self.params['device_read_iops'] or [] before, after = sorted(list(set(before))), sorted(list(set(after))) return self._diff_update_and_compare('device_read_iops', before, after) def diffparam_device_write_bps(self): before = self.info['hostconfig']['blkiodevicewritebps'] or [] before = ["%s:%s" % (i['path'], i['rate']) for i in before] after = self.params['device_write_bps'] or [] before, after = sorted(list(set(before))), sorted(list(set(after))) return self._diff_update_and_compare('device_write_bps', before, after) def diffparam_device_write_iops(self): before = self.info['hostconfig']['blkiodevicewriteiops'] or [] before = ["%s:%s" % (i['path'], i['rate']) for i in before] after = self.params['device_write_iops'] or [] before, after = sorted(list(set(before))), sorted(list(set(after))) return self._diff_update_and_compare('device_write_iops', before, after) # Limited idempotency, it can't guess default values def diffparam_env(self): env_before = self.info['config']['env'] or {} before = {i.split("=")[0]: "=".join(i.split("=")[1:]) for i in env_before} after = before.copy() if self.params['env']: after.update({k: 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(":") for i in self.info['hostconfig']['extrahosts']]) else: before = {} after = self.params['etc_hosts'] return self._diff_update_and_compare('etc_hosts', before, after) def diffparam_group_add(self): before = self.info['hostconfig']['groupadd'] after = self.params['group_add'] return self._diff_update_and_compare('group_add', before, after) # Healthcheck is only defined in container config if a healthcheck # was configured; otherwise the config key isn't part of the config. def diffparam_healthcheck(self): if 'healthcheck' in self.info['config']: # the "test" key is a list of 2 items where the first one is # "CMD-SHELL" and the second one is the actual healthcheck command. before = self.info['config']['healthcheck']['test'][1] else: before = '' after = self.params['healthcheck'] or before return self._diff_update_and_compare('healthcheck', before, after) # Because of hostname is random generated, this parameter has partial idempotency only. def diffparam_hostname(self): before = self.info['config']['hostname'] after = self.params['hostname'] or before return self._diff_update_and_compare('hostname', before, after) def diffparam_image(self): before_id = self.info['image'] after_id = self.image_info['id'] if before_id == after_id: return self._diff_update_and_compare('image', before_id, after_id) before = self.info['config']['image'] after = self.params['image'] mode = self.params['image_strict'] if mode is None or not mode: # In a idempotency 'lite mode' assume all images from different registries are the same before = before.replace(":latest", "") after = after.replace(":latest", "") before = before.split("/")[-1] after = after.split("/")[-1] else: return self._diff_update_and_compare('image', before_id, after_id) return self._diff_update_and_compare('image', before, after) def diffparam_ipc(self): before = self.info['hostconfig']['ipcmode'] after = self.params['ipc'] if self.params['pod'] and not self.module_params['ipc']: after = before return self._diff_update_and_compare('ipc', before, after) def diffparam_label(self): before = self.info['config']['labels'] or {} after = self.image_info.get('labels') or {} if self.params['label']: after.update({ str(k).lower(): str(v) 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) def diffparam_log_level(self): excom = self.info['exitcommand'] if '--log-level' in excom: before = excom[excom.index('--log-level') + 1].lower() else: before = self.params['log_level'] after = self.params['log_level'] return self._diff_update_and_compare('log_level', before, after) # Parameter has limited idempotency, unable to guess the default log_path def diffparam_log_opt(self): before, 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 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_memory(self): before = str(self.info['hostconfig']['memory']) after = self.params['memory'] return self._diff_update_and_compare('memory', before, after) def diffparam_memory_swap(self): # By default it's twice memory parameter before = str(self.info['hostconfig']['memoryswap']) after = self.params['memory_swap'] if (self.module_params['memory_swap'] is None and self.params['memory'] != 0 and self.params['memory'].isdigit()): after = str(int(self.params['memory']) * 2) return self._diff_update_and_compare('memory_swap', before, after) def diffparam_memory_reservation(self): before = str(self.info['hostconfig']['memoryreservation']) after = self.params['memory_reservation'] return self._diff_update_and_compare('memory_reservation', before, after) def diffparam_network(self): net_mode_before = self.info['hostconfig']['networkmode'] net_mode_after = '' before = list(self.info['networksettings'].get('networks', {})) # 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['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_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([m['source'], m['destination']]) elif m['type'] == 'volume': local_vols.append([m['name'], m['destination']]) before = [":".join(v) for v in volumes] before_local_vols = [":".join(v) for v in local_vols] if self.params['volume'] is not None: after = [":".join( [clean_volume(i) for i in v.split(":")[:2]] ) for v in self.params['volume']] else: after = [] if before_local_vols: after = list(set(after).difference(before_local_vols)) before, after = sorted(list(set(before))), sorted(list(set(after))) return self._diff_update_and_compare('volume', before, after) def diffparam_volumes_from(self): # Possibly volumesfrom is not in config before = self.info['hostconfig'].get('volumesfrom', []) or [] after = self.params['volumes_from'] or [] return self._diff_update_and_compare('volumes_from', before, after) def diffparam_workdir(self): before = self.info['config']['workingdir'] after = self.params['workdir'] return self._diff_update_and_compare('workdir', before, after) def is_different(self): diff_func_list = [func for func in dir(self) if callable(getattr(self, func)) and func.startswith( "diffparam")] fail_fast = not bool(self.module._diff) different = False for func_name in diff_func_list: dff_func = getattr(self, func_name) if dff_func(): if fail_fast: return True different = True # Check non idempotent parameters for p in self.non_idempotent: if self.module_params[p] is not None and self.module_params[p] not in [{}, [], '']: different = True return different def ensure_image_exists(module, image, module_params): """If image is passed, ensure it exists, if not - pull it or fail. Arguments: module {obj} -- ansible module object image {str} -- name of image Returns: list -- list of image actions - if it pulled or nothing was done """ image_actions = [] module_exec = module_params['executable'] if not image: return image_actions rc, out, err = module.run_command([module_exec, 'image', 'exists', image]) if rc == 0: return image_actions rc, out, err = module.run_command([module_exec, 'image', 'pull', image]) if rc != 0: module.fail_json(msg="Can't pull image %s" % image, stdout=out, stderr=err) image_actions.append("pulled image %s" % image) return image_actions class PodmanContainer: """Perform container tasks. Manages podman container, inspects it and checks its current state """ def __init__(self, module, name, module_params): """Initialize PodmanContainer class. Arguments: module {obj} -- ansible module object name {str} -- name of container """ self.module = module self.module_params = module_params self.name = name self.stdout, self.stderr = '', '' self.info = self.get_info() self.version = self._get_podman_version() self.diff = {} self.actions = [] @property def exists(self): """Check if container exists.""" return bool(self.info != {}) @property def different(self): """Check if container is different.""" diffcheck = PodmanContainerDiff( self.module, self.module_params, self.info, self.get_image_info(), self.version) is_different = diffcheck.is_different() diffs = diffcheck.diff if self.module._diff and is_different and diffs['before'] and diffs['after']: self.diff['before'] = "\n".join( ["%s - %s" % (k, v) for k, v in sorted( diffs['before'].items())]) + "\n" self.diff['after'] = "\n".join( ["%s - %s" % (k, v) for k, v in sorted( diffs['after'].items())]) + "\n" return is_different @property def running(self): """Return True if container is running now.""" return self.exists and self.info['State']['Running'] @property def stopped(self): """Return True if container exists and is not running now.""" return self.exists and not self.info['State']['Running'] def get_info(self): """Inspect container and gather info about it.""" # pylint: disable=unused-variable rc, out, err = self.module.run_command( [self.module_params['executable'], b'container', b'inspect', self.name]) return json.loads(out)[0] if rc == 0 else {} def get_image_info(self): """Inspect container image and gather info about it.""" # pylint: disable=unused-variable rc, out, err = self.module.run_command( [self.module_params['executable'], b'image', b'inspect', self.module_params['image']]) return json.loads(out)[0] if rc == 0 else {} def _get_podman_version(self): # pylint: disable=unused-variable rc, out, err = self.module.run_command( [self.module_params['executable'], b'--version']) if rc != 0 or not out or "version" not in out: self.module.fail_json(msg="%s run failed!" % self.module_params['executable']) return out.split("version")[1].strip() def _perform_action(self, action): """Perform action with container. Arguments: action {str} -- action to perform - start, create, stop, run, delete, 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() self.delete() self.create() def recreate_run(self): """Recreate and run the container.""" if self.running: self.stop() 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'] 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)}) 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: 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