diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-26 06:22:20 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-26 06:22:20 +0000 |
commit | 18bd2207b6c1977e99a93673a7be099e23f0f547 (patch) | |
tree | 40fd9e5913462a88be6ba24be6953383c5b39874 /ansible_collections/containers/podman/plugins | |
parent | Releasing progress-linux version 10.0.1+dfsg-1~progress7.99u1. (diff) | |
download | ansible-18bd2207b6c1977e99a93673a7be099e23f0f547.tar.xz ansible-18bd2207b6c1977e99a93673a7be099e23f0f547.zip |
Merging upstream version 10.1.0+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/containers/podman/plugins')
11 files changed, 1789 insertions, 781 deletions
diff --git a/ansible_collections/containers/podman/plugins/module_utils/podman/common.py b/ansible_collections/containers/podman/plugins/module_utils/podman/common.py index cbb6b080e..aac7b60eb 100644 --- a/ansible_collections/containers/podman/plugins/module_utils/podman/common.py +++ b/ansible_collections/containers/podman/plugins/module_utils/podman/common.py @@ -337,3 +337,88 @@ def get_podman_version(module, fail=True): (executable, err)) return None return out.split("version")[1].strip() + + +def createcommand(argument, info_config, boolean_type=False): + """Returns list of values for given argument from CreateCommand + from Podman container inspect output. + + Args: + argument (str): argument name + info_config (dict): dictionary with container info + boolean_type (bool): if True, then argument is boolean type + + Returns: + + all_values: list of values for given argument from createcommand + """ + if "createcommand" not in info_config: + return [] + cr_com = info_config["createcommand"] + argument_values = ARGUMENTS_OPTS_DICT.get(argument, [argument]) + all_values = [] + for arg in argument_values: + for ind, cr_opt in enumerate(cr_com): + if arg == cr_opt: + if boolean_type: + # This is a boolean argument and doesn't have value + return [True] + if not cr_com[ind + 1].startswith("-"): + # This is a key=value argument + all_values.append(cr_com[ind + 1]) + else: + # This is also a false/true switching argument + return [True] + if cr_opt.startswith("%s=" % arg): + all_values.append(cr_opt.split("=", 1)[1]) + return all_values + + +def diff_generic(params, info_config, module_arg, cmd_arg, boolean_type=False): + """ + Generic diff function for module arguments from CreateCommand + in Podman inspection output. + + Args: + params (dict): module parameters + info_config (dict): dictionary with container info + module_arg (str): module argument name + cmd_arg (str): command line argument name + boolean_type (bool): if True, then argument is boolean type + + Returns: + bool: True if there is a difference, False otherwise + + """ + before = createcommand(cmd_arg, info_config, boolean_type=boolean_type) + if before == []: + before = None + after = params[module_arg] + if boolean_type and (before, after) in [(None, False), (False, None)]: + before, after = False, False + return before, after + if before is None and after is None: + return before, after + if after is not None: + if isinstance(after, list): + after = ",".join(sorted([str(i).lower() for i in after])) + if before: + before = ",".join(sorted([str(i).lower() for i in before])) + elif isinstance(after, dict): + after = ",".join(sorted( + [str(k).lower() + "=" + str(v).lower() for k, v in after.items() if v is not None])) + if before: + before = ",".join(sorted([j.lower() for j in before])) + elif isinstance(after, bool): + after = str(after).capitalize() + if before is not None: + before = str(before[0]).capitalize() + elif isinstance(after, int): + after = str(after) + if before is not None: + before = str(before[0]) + else: + before = before[0] if before else None + else: + before = ",".join(sorted(before)) if len(before) > 1 else before[0] + return before, after 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 index bf42ffdee..76458f144 100644 --- 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 @@ -8,8 +8,8 @@ from ansible_collections.containers.podman.plugins.module_utils.podman.common im 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 -from ansible_collections.containers.podman.plugins.module_utils.podman.common import ARGUMENTS_OPTS_DICT +from ansible_collections.containers.podman.plugins.module_utils.podman.common import diff_generic +from ansible_collections.containers.podman.plugins.module_utils.podman.common import createcommand from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import ContainerQuadlet @@ -23,15 +23,18 @@ ARGUMENTS_SPEC_CONTAINER = dict( 'absent', 'present', 'stopped', 'started', 'created', 'quadlet']), image=dict(type='str'), annotation=dict(type='dict'), + arch=dict(type='str'), attach=dict(type='list', elements='str', choices=['stdout', 'stderr', 'stdin']), 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_conf=dict(type='dict'), cgroup_parent=dict(type='path'), cgroupns=dict(type='str'), cgroups=dict(type='str'), + chrootdirs=dict(type='str'), cidfile=dict(type='path'), cmd_args=dict(type='list', elements='str'), conmon_pidfile=dict(type='path'), @@ -44,6 +47,7 @@ ARGUMENTS_SPEC_CONTAINER = dict( cpus=dict(type='str'), cpuset_cpus=dict(type='str'), cpuset_mems=dict(type='str'), + decryption_key=dict(type='str', no_log=False), delete_depend=dict(type='bool'), delete_time=dict(type='str'), delete_volumes=dict(type='bool'), @@ -51,6 +55,7 @@ ARGUMENTS_SPEC_CONTAINER = dict( debug=dict(type='bool', default=False), detach_keys=dict(type='str', no_log=False), device=dict(type='list', elements='str'), + device_cgroup_rule=dict(type='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'), @@ -62,6 +67,7 @@ ARGUMENTS_SPEC_CONTAINER = dict( env=dict(type='dict'), env_file=dict(type='list', elements='path', aliases=['env_files']), env_host=dict(type='bool'), + env_merge=dict(type='dict'), etc_hosts=dict(type='dict', aliases=['add_hosts']), expose=dict(type='list', elements='str', aliases=[ 'exposed', 'exposed_ports']), @@ -70,23 +76,33 @@ ARGUMENTS_SPEC_CONTAINER = dict( force_delete=dict(type='bool', default=True), generate_systemd=dict(type='dict', default={}), gidmap=dict(type='list', elements='str'), + gpus=dict(type='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'), + group_entry=dict(type='str'), + healthcheck=dict(type='str', aliases=['health_cmd']), + healthcheck_interval=dict(type='str', aliases=['health_interval']), + healthcheck_retries=dict(type='int', aliases=['health_retries']), + healthcheck_start_period=dict(type='str', aliases=['health_start_period']), + health_startup_cmd=dict(type='str'), + health_startup_interval=dict(type='str'), + health_startup_retries=dict(type='int'), + health_startup_success=dict(type='int'), + health_startup_timeout=dict(type='str'), + healthcheck_timeout=dict(type='str', aliases=['health_timeout']), healthcheck_failure_action=dict(type='str', choices=[ - 'none', 'kill', 'restart', 'stop']), + 'none', 'kill', 'restart', 'stop'], aliases=['health_on_failure']), hooks_dir=dict(type='list', elements='str'), hostname=dict(type='str'), + hostuser=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_ctr=dict(type='str', choices=['once', 'always']), init_path=dict(type='str'), interactive=dict(type='bool'), ip=dict(type='str'), + ip6=dict(type='str'), ipc=dict(type='str', aliases=['ipc_mode']), kernel_memory=dict(type='str'), label=dict(type='dict', aliases=['labels']), @@ -108,32 +124,49 @@ ARGUMENTS_SPEC_CONTAINER = dict( 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'), + network_aliases=dict(type='list', elements='str', aliases=['network_alias']), + no_healthcheck=dict(type='bool'), no_hosts=dict(type='bool'), oom_kill_disable=dict(type='bool'), oom_score_adj=dict(type='int'), + os=dict(type='str'), + passwd=dict(type='bool', no_log=False), + passwd_entry=dict(type='str', no_log=False), + personality=dict(type='str'), pid=dict(type='str', aliases=['pid_mode']), + pid_file=dict(type='path'), pids_limit=dict(type='str'), + platform=dict(type='str'), pod=dict(type='str'), + pod_id_file=dict(type='path'), + preserve_fd=dict(type='list', elements='str'), + preserve_fds=dict(type='str'), privileged=dict(type='bool'), publish=dict(type='list', elements='str', aliases=[ 'ports', 'published', 'published_ports']), publish_all=dict(type='bool'), + pull=dict(type='str', choices=['always', 'missing', 'never', 'newer']), quadlet_dir=dict(type='path'), quadlet_filename=dict(type='str'), quadlet_options=dict(type='list', elements='str'), + rdt_class=dict(type='str'), 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'), restart_time=dict(type='str'), + retry=dict(type='int'), + retry_delay=dict(type='str'), rm=dict(type='bool', aliases=['remove', 'auto_remove']), + rmi=dict(type='bool'), rootfs=dict(type='bool'), + seccomp_policy=dict(type='str'), 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'), + shm_size_systemd=dict(type='str'), sig_proxy=dict(type='bool'), stop_signal=dict(type='int'), stop_timeout=dict(type='int'), @@ -142,14 +175,20 @@ ARGUMENTS_SPEC_CONTAINER = dict( subuidname=dict(type='str'), sysctl=dict(type='dict'), systemd=dict(type='str'), + timeout=dict(type='int'), timezone=dict(type='str'), + tls_verify=dict(type='bool'), tmpfs=dict(type='dict'), tty=dict(type='bool'), uidmap=dict(type='list', elements='str'), ulimit=dict(type='list', elements='str', aliases=['ulimits']), + umask=dict(type='str'), + unsetenv=dict(type='list', elements='str'), + unsetenv_all=dict(type='bool'), user=dict(type='str'), userns=dict(type='str', aliases=['userns_mode']), uts=dict(type='str'), + variant=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']) @@ -292,6 +331,9 @@ class PodmanModuleParams: c += ['--annotation', '='.join(annotate)] return c + def addparam_arch(self, c): + return c + ['--arch=%s' % self.params['arch']] + def addparam_attach(self, c): for attach in self.params['attach']: c += ['--attach=%s' % attach] @@ -329,6 +371,14 @@ class PodmanModuleParams: def addparam_cgroup_parent(self, c): return c + ['--cgroup-parent', self.params['cgroup_parent']] + def addparam_cgroup_conf(self, c): + for cgroup in self.params['cgroup_conf'].items(): + c += ['--cgroup-conf=%s' % '='.join([str(i) for i in cgroup])] + return c + + def addparam_chrootdirs(self, c): + return c + ['--chrootdirs', self.params['chrootdirs']] + def addparam_cidfile(self, c): return c + ['--cidfile', self.params['cidfile']] @@ -359,6 +409,9 @@ class PodmanModuleParams: def addparam_cpuset_mems(self, c): return c + ['--cpuset-mems', self.params['cpuset_mems']] + def addparam_decryption_key(self, c): + return c + ['--decryption-key=%s' % self.params['decryption_key']] + def addparam_detach(self, c): # Remove detach from create command and don't set if attach is true if self.action == 'create' or self.params['attach']: @@ -373,6 +426,9 @@ class PodmanModuleParams: c += ['--device', dev] return c + def addparam_device_cgroup_rule(self, c): + return c + ['--device-cgroup-rule=%s' % self.params['device_cgroup_rule']] + def addparam_device_read_bps(self, c): for dev in self.params['device_read_bps']: c += ['--device-read-bps', dev] @@ -426,6 +482,13 @@ class PodmanModuleParams: c += ['--add-host', ':'.join(host_ip)] return c + def addparam_env_merge(self, c): + for env_merge in self.params['env_merge'].items(): + c += ['--env-merge', + b"=".join([to_bytes(k, errors='surrogate_or_strict') + for k in env_merge])] + return c + def addparam_expose(self, c): for exp in self.params['expose']: c += ['--expose', exp] @@ -436,11 +499,17 @@ class PodmanModuleParams: c += ['--gidmap', gidmap] return c + def addparam_gpus(self, c): + return c + ['--gpus', self.params['gpus']] + def addparam_group_add(self, c): for g in self.params['group_add']: c += ['--group-add', g] return c + def addparam_group_entry(self, c): + return c + ['--group-entry', self.params['group_entry']] + def addparam_healthcheck(self, c): return c + ['--healthcheck-command', self.params['healthcheck']] @@ -456,10 +525,25 @@ class PodmanModuleParams: return c + ['--healthcheck-start-period', self.params['healthcheck_start_period']] + def addparam_health_startup_cmd(self, c): + return c + ['--health-startup-command', self.params['health_startup_cmd']] + + def addparam_health_startup_interval(self, c): + return c + ['--health-startup-interval', self.params['health_startup_interval']] + def addparam_healthcheck_timeout(self, c): return c + ['--healthcheck-timeout', self.params['healthcheck_timeout']] + def addparam_health_startup_retries(self, c): + return c + ['--health-startup-retries', self.params['health_startup_retries']] + + def addparam_health_startup_success(self, c): + return c + ['--health-startup-success', self.params['health_startup_success']] + + def addparam_health_startup_timeout(self, c): + return c + ['--health-startup-timeout', self.params['health_startup_timeout']] + def addparam_healthcheck_failure_action(self, c): return c + ['--health-on-failure', self.params['healthcheck_failure_action']] @@ -472,6 +556,9 @@ class PodmanModuleParams: def addparam_hostname(self, c): return c + ['--hostname', self.params['hostname']] + def addparam_hostuser(self, c): + return c + ['--hostuser', self.params['hostuser']] + def addparam_http_proxy(self, c): return c + ['--http-proxy=%s' % self.params['http_proxy']] @@ -486,12 +573,18 @@ class PodmanModuleParams: def addparam_init_path(self, c): return c + ['--init-path', self.params['init_path']] + def addparam_init_ctr(self, c): + return c + ['--init-ctr', self.params['init_ctr']] + 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_ip6(self, c): + return c + ['--ip6', self.params['ip6']] + def addparam_ipc(self, c): return c + ['--ipc', self.params['ipc']] @@ -559,21 +652,57 @@ class PodmanModuleParams: def addparam_no_hosts(self, c): return c + ['--no-hosts=%s' % self.params['no_hosts']] + def addparam_no_healthcheck(self, c): + if self.params['no_healthcheck']: + c += ['--no-healthcheck'] + return c + 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_os(self, c): + return c + ['--os', self.params['os']] + + def addparam_passwd(self, c): + if self.params['passwd']: + c += ['--passwd'] + return c + + def addparam_passwd_entry(self, c): + return c + ['--passwd-entry', self.params['passwd_entry']] + + def addparam_personality(self, c): + return c + ['--personality', self.params['personality']] + def addparam_pid(self, c): return c + ['--pid', self.params['pid']] + def addparam_pid_file(self, c): + return c + ['--pid-file', self.params['pid_file']] + def addparam_pids_limit(self, c): return c + ['--pids-limit', self.params['pids_limit']] + def addparam_platform(self, c): + return c + ['--platform', self.params['platform']] + def addparam_pod(self, c): return c + ['--pod', self.params['pod']] + def addparam_pod_id_file(self, c): + return c + ['--pod-id-file', self.params['pod_id_file']] + + def addparam_preserve_fd(self, c): + for fd in self.params['preserve_fd']: + c += ['--preserve-fd', fd] + return c + + def addparam_preserve_fds(self, c): + return c + ['--preserve-fds', self.params['preserve_fds']] + def addparam_privileged(self, c): return c + ['--privileged=%s' % self.params['privileged']] @@ -585,6 +714,12 @@ class PodmanModuleParams: def addparam_publish_all(self, c): return c + ['--publish-all=%s' % self.params['publish_all']] + def addparam_pull(self, c): + return c + ['--pull=%s' % self.params['pull']] + + def addparam_rdt_class(self, c): + return c + ['--rdt-class', self.params['rdt_class']] + def addparam_read_only(self, c): return c + ['--read-only=%s' % self.params['read_only']] @@ -597,17 +732,31 @@ class PodmanModuleParams: def addparam_restart_policy(self, c): return c + ['--restart=%s' % self.params['restart_policy']] + def addparam_retry(self, c): + return c + ['--retry', self.params['retry']] + + def addparam_retry_delay(self, c): + return c + ['--retry-delay', self.params['retry_delay']] + def addparam_rm(self, c): if self.params['rm']: c += ['--rm'] return c + def addparam_rmi(self, c): + if self.params['rmi']: + c += ['--rmi'] + 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_seccomp_policy(self, c): + return c + ['--seccomp-policy', self.params['seccomp_policy']] + def addparam_secrets(self, c): for secret in self.params['secrets']: c += ['--secret', secret] @@ -621,6 +770,9 @@ class PodmanModuleParams: def addparam_shm_size(self, c): return c + ['--shm-size', self.params['shm_size']] + def addparam_shm_size_systemd(self, c): + return c + ['--shm-size-systemd', self.params['shm_size_systemd']] + def addparam_sig_proxy(self, c): return c + ['--sig-proxy=%s' % self.params['sig_proxy']] @@ -646,14 +798,20 @@ class PodmanModuleParams: def addparam_systemd(self, c): return c + ['--systemd=%s' % str(self.params['systemd']).lower()] + def addparam_timeout(self, c): + return c + ['--timeout', self.params['timeout']] + + def addparam_timezone(self, c): + return c + ['--tz=%s' % self.params['timezone']] + + def addparam_tls_verify(self, c): + return c + ['--tls-verify=%s' % self.params['tls_verify']] + 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']] @@ -667,6 +825,19 @@ class PodmanModuleParams: c += ['--ulimit', u] return c + def addparam_umask(self, c): + return c + ['--umask', self.params['umask']] + + def addparam_unsetenv(self, c): + for unsetenv in self.params['unsetenv']: + c += ['--unsetenv', unsetenv] + return c + + def addparam_unsetenv_all(self, c): + if self.params['unsetenv_all']: + c += ['--unsetenv-all'] + return c + def addparam_user(self, c): return c + ['--user', self.params['user']] @@ -676,6 +847,9 @@ class PodmanModuleParams: def addparam_uts(self, c): return c + ['--uts', self.params['uts']] + def addparam_variant(self, c): + return c + ['--variant', self.params['variant']] + def addparam_volume(self, c): for vol in self.params['volume']: if vol: @@ -700,42 +874,9 @@ class PodmanDefaults: 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): @@ -744,17 +885,8 @@ class PodmanDefaults: 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('5.0.0')): - self.defaults['network'] = ["pasta"] 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 @@ -781,35 +913,6 @@ class PodmanContainerDiff: params_with_defaults[p] = self.module_params[p] return params_with_defaults - def _createcommand(self, argument): - """Returns list of values for given argument from CreateCommand - from Podman container inspect output. - - Args: - argument (str): argument name - - Returns: - - all_values: list of values for given argument from createcommand - """ - if "createcommand" not in self.info["config"]: - return [] - cr_com = self.info["config"]["createcommand"] - argument_values = ARGUMENTS_OPTS_DICT.get(argument, [argument]) - all_values = [] - for arg in argument_values: - for ind, cr_opt in enumerate(cr_com): - if arg == cr_opt: - # This is a key=value argument - if not cr_com[ind + 1].startswith("-"): - all_values.append(cr_com[ind + 1]) - else: - # This is a false/true switching argument - return [True] - if cr_opt.startswith("%s=" % arg): - all_values.append(cr_opt.split("=", 1)[1]) - return all_values - def _diff_update_and_compare(self, param_name, before, after): if before != after: self.diff['before'].update({param_name: before}) @@ -817,6 +920,24 @@ class PodmanContainerDiff: return True return False + def _diff_generic(self, module_arg, cmd_arg, boolean_type=False): + """ + Generic diff function for module arguments from CreateCommand + in Podman inspection output. + + Args: + module_arg (str): module argument name + cmd_arg (str): command line argument name + boolean_type (bool): if True, then argument is boolean type + + Returns: + bool: True if there is a difference, False otherwise + + """ + info_config = self.info["config"] + before, after = diff_generic(self.params, info_config, module_arg, cmd_arg, boolean_type) + return self._diff_update_and_compare(module_arg, before, after) + def diffparam_annotation(self): before = self.info['config']['annotations'] or {} after = before.copy() @@ -824,24 +945,17 @@ class PodmanContainerDiff: 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 inspect, recreate it if not default - before = False - after = self.params['env_host'] - return self._diff_update_and_compare('env_host', before, after) + def diffparam_arch(self): + return self._diff_generic('arch', '--arch') + + def diffparam_authfile(self): + return self._diff_generic('authfile', '--authfile') 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) + return self._diff_generic('blkio_weight', '--blkio-weight') 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) + return self._diff_generic('blkio_weight_device', '--blkio-weight-device') def diffparam_cap_add(self): before = self.info['effectivecaps'] or [] @@ -869,30 +983,27 @@ class PodmanContainerDiff: before, after = sorted(list(set(before))), sorted(list(set(after))) return self._diff_update_and_compare('cap_drop', before, after) + def diffparam_cgroup_conf(self): + return self._diff_generic('cgroup_conf', '--cgroup-conf') + 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 + return self._diff_generic('cgroup_parent', '--cgroup-parent') - 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_cgroupns(self): + return self._diff_generic('cgroupns', '--cgroupns') + + # Disabling idemotency check for cgroups as it's added by systemd generator + # https://github.com/containers/ansible-podman-collections/issues/775 + # def diffparam_cgroups(self): + # return self._diff_generic('cgroups', '--cgroups') + + def diffparam_chrootdirs(self): + return self._diff_generic('chrootdirs', '--chrootdirs') + + # Disabling idemotency check for cidfile as it's added by systemd generator + # https://github.com/containers/ansible-podman-collections/issues/775 + # def diffparam_cidfile(self): + # return self._diff_generic('cidfile', '--cidfile') def diffparam_command(self): # TODO(sshnaidm): to inspect image to get the default command @@ -905,107 +1016,73 @@ class PodmanContainerDiff: 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) + return self._diff_generic('conmon_pidfile', '--conmon-pidfile') def diffparam_cpu_period(self): - before = self.info['hostconfig']['cpuperiod'] - # if cpu_period left to default keep settings - after = self.params['cpu_period'] or before - return self._diff_update_and_compare('cpu_period', before, after) + return self._diff_generic('cpu_period', '--cpu-period') def diffparam_cpu_quota(self): - before = self.info['hostconfig']['cpuquota'] - # if cpu_quota left to default keep settings - after = self.params['cpu_quota'] or before - return self._diff_update_and_compare('cpu_quota', before, after) + return self._diff_generic('cpu_quota', '--cpu-quota') 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) + return self._diff_generic('cpu_rt_period', '--cpu-rt-period') 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) + return self._diff_generic('cpu_rt_runtime', '--cpu-rt-runtime') 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) + return self._diff_generic('cpu_shares', '--cpu-shares') def diffparam_cpus(self): - before = self.info['hostconfig']['nanocpus'] / 1000000000 - # if cpus left to default keep settings - after = float(self.params['cpus'] or before) - return self._diff_update_and_compare('cpus', before, after) + return self._diff_generic('cpus', '--cpus') 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) + return self._diff_generic('cpuset_cpus', '--cpuset-cpus') 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) + return self._diff_generic('cpuset_mems', '--cpuset-mems') + + def diffparam_decryption_key(self): + return self._diff_generic('decryption_key', '--decryption-key') 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']: - before = [i.lower() for i in self._createcommand('--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] - before, after = [i.lower() for i in before], [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) + return self._diff_generic('device', '--device') + + def diffparam_device_cgroup_rule(self): + return self._diff_generic('device_cgroup_rule', '--device-cgroup-rule') 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) + return self._diff_generic('device_read_bps', '--device-read-bps') 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) + return self._diff_generic('device_read_iops', '--device-read-iops') 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) + return self._diff_generic('device_write_bps', '--device-write-bps') 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) + return self._diff_generic('device_write_iops', '--device-write-iops') + + def diffparam_dns(self): + return self._diff_generic('dns', '--dns') + + def diffparam_dns_option(self): + return self._diff_generic('dns_option', '--dns-option') + + def diffparam_dns_search(self): + return self._diff_generic('dns_search', '--dns-search') - # 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) + return self._diff_generic('env', '--env') + + def diffparam_env_file(self): + return self._diff_generic('env_file', '--env-file') + + def diffparam_env_merge(self): + return self._diff_generic('env_merge', '--env-merge') + + def diffparam_env_host(self): + return self._diff_generic('env_host', '--env-host') def diffparam_etc_hosts(self): if self.info['hostconfig']['extrahosts']: @@ -1013,13 +1090,23 @@ class PodmanContainerDiff: for i in self.info['hostconfig']['extrahosts']]) else: before = {} - after = self.params['etc_hosts'] + after = self.params['etc_hosts'] or {} return self._diff_update_and_compare('etc_hosts', before, after) + def diffparam_expose(self): + return self._diff_generic('expose', '--expose') + + def diffparam_gidmap(self): + return self._diff_generic('gidmap', '--gidmap') + + def diffparam_gpus(self): + return self._diff_generic('gpus', '--gpus') + 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) + return self._diff_generic('group_add', '--group-add') + + def diffparam_group_entry(self): + return self._diff_generic('group_entry', '--group-entry') # Healthcheck is only defined in container config if a healthcheck # was configured; otherwise the config key isn't part of the config. @@ -1041,11 +1128,44 @@ class PodmanContainerDiff: after = self.params['healthcheck_failure_action'] or before return self._diff_update_and_compare('healthcheckonfailureaction', before, after) - # Because of hostname is random generated, this parameter has partial idempotency only. + def diffparam_healthcheck_interval(self): + return self._diff_generic('healthcheck_interval', '--healthcheck-interval') + + def diffparam_healthcheck_retries(self): + return self._diff_generic('healthcheck_retries', '--healthcheck-retries') + + def diffparam_healthcheck_start_period(self): + return self._diff_generic('healthcheck_start_period', '--healthcheck-start-period') + + def diffparam_health_startup_cmd(self): + return self._diff_generic('health_startup_cmd', '--health-startup-cmd') + + def diffparam_health_startup_interval(self): + return self._diff_generic('health_startup_interval', '--health-startup-interval') + + def diffparam_health_startup_retries(self): + return self._diff_generic('health_startup_retries', '--health-startup-retries') + + def diffparam_health_startup_success(self): + return self._diff_generic('health_startup_success', '--health-startup-success') + + def diffparam_health_startup_timeout(self): + return self._diff_generic('health_startup_timeout', '--health-startup-timeout') + + def diffparam_healthcheck_timeout(self): + return self._diff_generic('healthcheck_timeout', '--healthcheck-timeout') + + def diffparam_hooks_dir(self): + return self._diff_generic('hooks_dir', '--hooks-dir') + def diffparam_hostname(self): - before = self.info['config']['hostname'] - after = self.params['hostname'] or before - return self._diff_update_and_compare('hostname', before, after) + return self._diff_generic('hostname', '--hostname') + + def diffparam_hostuser(self): + return self._diff_generic('hostuser', '--hostuser') + + def diffparam_http_proxy(self): + return self._diff_generic('http_proxy', '--http-proxy') def diffparam_image(self): before_id = self.info['image'] or self.info['rootfs'] @@ -1066,12 +1186,29 @@ class PodmanContainerDiff: return self._diff_update_and_compare('image', before_id, after_id) return self._diff_update_and_compare('image', before, after) + def diffparam_image_volume(self): + return self._diff_generic('image_volume', '--image-volume') + + def diffparam_init(self): + return self._diff_generic('init', '--init', boolean_type=True) + + def diffparam_init_ctr(self): + return self._diff_generic('init_ctr', '--init-ctr') + + def diffparam_init_path(self): + return self._diff_generic('init_path', '--init-path') + + def diffparam_interactive(self): + return self._diff_generic('interactive', '--interactive') + + def diffparam_ip(self): + return self._diff_generic('ip', '--ip') + + def diffparam_ip6(self): + return self._diff_generic('ip6', '--ip6') + 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) + return self._diff_generic('ipc', '--ipc') def diffparam_label(self): before = self.info['config']['labels'] or {} @@ -1088,282 +1225,208 @@ class PodmanContainerDiff: before.pop('podman_systemd_unit', None) return self._diff_update_and_compare('label', before, after) + def diffparam_label_file(self): + return self._diff_generic('label_file', '--label-file') + 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) + return self._diff_generic('log_driver', '--log-driver') - # 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) + return self._diff_generic('log_opt', '--log-opt') 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']: - before = [i.lower() for i in self._createcommand('--mac-address')] - before = before[0] if before else '' - 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) + return self._diff_generic('mac_address', '--mac-address') + + def diffparam_memory(self): + return self._diff_generic('memory', '--memory') + + def diffparam_memory_reservation(self): + return self._diff_generic('memory_reservation', '--memory-reservation') + + def diffparam_memory_swap(self): + return self._diff_generic('memory_swap', '--memory-swap') + + def diffparam_memory_swappiness(self): + return self._diff_generic('memory_swappiness', '--memory-swappiness') + + def diffparam_mount(self): + return self._diff_generic('mount', '--mount') 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_net = [i.lower() for i in self._createcommand('--network')] - for cr_net_opt in cr_net: - if 'slirp4netns:' in cr_net_opt: - before = [cr_net_opt] - if net_mode_before == 'pasta': - cr_net = [i.lower() for i in self._createcommand('--network')] - for cr_net_opt in cr_net: - if 'pasta:' in cr_net_opt: - before = [cr_net_opt] - after = self.params['network'] or [] - after = [i.lower() for i in after] - # 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'], ['pasta']]: - 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_after = net_mode_after.replace('pasta', 'default') - net_mode_before = net_mode_before.replace('bridge', 'default') - net_mode_before = net_mode_before.replace('slirp4netns', 'default') - net_mode_before = net_mode_before.replace('pasta', '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) + return self._diff_generic('network', '--network') + + def diffparam_network_aliases(self): + return self._diff_generic('network_aliases', '--network-alias') + + def diffparam_no_healthcheck(self): + return self._diff_generic('no_healthcheck', '--no-healthcheck', boolean_type=True) + + def diffparam_no_hosts(self): + return self._diff_generic('no_hosts', '--no-hosts') + + def diffparam_oom_kill_disable(self): + return self._diff_generic('oom_kill_disable', '--oom-kill-disable') 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) + return self._diff_generic('oom_score_adj', '--oom-score-adj') - def diffparam_privileged(self): - before = self.info['hostconfig']['privileged'] - after = self.params['privileged'] - return self._diff_update_and_compare('privileged', before, after) + def diffparam_os(self): + return self._diff_generic('os', '--os') + + def diffparam_passwd(self): + return self._diff_generic('passwd', '--passwd', boolean_type=True) + + def diffparam_passwd_entry(self): + return self._diff_generic('passwd_entry', '--passwd-entry') + + def diffparam_personality(self): + return self._diff_generic('personality', '--personality') def diffparam_pid(self): - def get_container_id_by_name(name): - rc, podman_inspect_info, err = self.module.run_command( - [self.module.params["executable"], "inspect", name, "-f", "{{.Id}}"]) - if rc != 0: - return None - return podman_inspect_info.strip() - - before = self.info['hostconfig']['pidmode'] - after = self.params['pid'] - if after is not None and "container:" in after and "container:" in before: - if after.split(":")[1] == before.split(":")[1]: - return self._diff_update_and_compare('pid', before, after) - after = "container:" + get_container_id_by_name(after.split(":")[1]) - return self._diff_update_and_compare('pid', before, after) - - # TODO(sshnaidm) Need to add port ranges support + return self._diff_generic('pid', '--pid') + + def diffparam_pid_file(self): + return self._diff_generic('pid_file', '--pid-file') + + def diffparam_pids_limit(self): + return self._diff_generic('pids_limit', '--pids-limit') + + def diffparam_platform(self): + return self._diff_generic('platform', '--platform') + + # def diffparam_pod(self): + # return self._diff_generic('pod', '--pod') + + def diffparam_pod_id_file(self): + return self._diff_generic('pod_id_file', '--pod-id-file') + + def diffparam_privileged(self): + return self._diff_generic('privileged', '--privileged') + def diffparam_publish(self): - def compose(p, h): - s = ":".join( - [str(h["hostport"]), p.replace('/tcp', '')] - ).strip(":") - if h['hostip'] == '0.0.0.0' and LooseVersion(self.version) >= LooseVersion('5.0.0'): - return s - 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("]", "").replace("0.0.0.0:", "") - 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) + return self._diff_generic('publish', '--publish') + + def diffparam_publish_all(self): + return self._diff_generic('publish_all', '--publish-all') + + def diffparam_pull(self): + return self._diff_generic('pull', '--pull') + + def diffparam_rdt_class(self): + return self._diff_generic('rdt_class', '--rdt-class') 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) + return self._diff_generic('read_only', '--read-only') + + def diffparam_read_only_tmpfs(self): + return self._diff_generic('read_only_tmpfs', '--read-only-tmpfs') + + def diffparam_requires(self): + return self._diff_generic('requires', '--requires') def diffparam_restart_policy(self): - before = self.info['hostconfig']['restartpolicy']['name'] - before_max_count = int(self.info['hostconfig']['restartpolicy'].get('maximumretrycount', 0)) - after = self.params['restart_policy'] or "" - if ':' in after: - after, after_max_count = after.rsplit(':', 1) - after_max_count = int(after_max_count) - else: - after_max_count = 0 - before = "%s:%i" % (before, before_max_count) - after = "%s:%i" % (after, after_max_count) - return self._diff_update_and_compare('restart_policy', before, after) + return self._diff_generic('restart_policy', '--restart') + + def diffparam_retry(self): + return self._diff_generic('retry', '--retry') + + def diffparam_retry_delay(self): + return self._diff_generic('retry_delay', '--retry-delay') + + def diffparam_rootfs(self): + return self._diff_generic('rootfs', '--rootfs') + + # Disabling idemotency check for sdnotify as it's added by systemd generator + # https://github.com/containers/ansible-podman-collections/issues/775 + # def diffparam_sdnotify(self): + # return self._diff_generic('sdnotify', '--sdnotify') def diffparam_rm(self): before = self.info['hostconfig']['autoremove'] after = self.params['rm'] + if after is None: + return self._diff_update_and_compare('rm', '', '') return self._diff_update_and_compare('rm', before, after) + def diffparam_rmi(self): + return self._diff_generic('rmi', '--rmi', boolean_type=True) + + def diffparam_seccomp_policy(self): + return self._diff_generic('seccomp_policy', '--seccomp-policy') + + def diffparam_secrets(self): + return self._diff_generic('secrets', '--secret') + def diffparam_security_opt(self): - unsorted_before = self.info['hostconfig']['securityopt'] - unsorted_after = self.params['security_opt'] - # In rootful containers with apparmor there is a profile, "container-default", - # which is already added by default - # Since SElinux labels are basically annotations, they are merged in a single list - # element by podman so we need to split them in a (sorted) list if we want to compare it - # to the list we provide to the module - before = sorted(item for element in unsorted_before for item in element.split(',') - if 'apparmor=container-default' not in item) - after = sorted(list(set(unsorted_after))) - return self._diff_update_and_compare('security_opt', before, after) + return self._diff_generic('security_opt', '--security-opt') + + def diffparam_shm_size(self): + return self._diff_generic('shm_size', '--shm-size') + + def diffparam_shm_size_systemd(self): + return self._diff_generic('shm_size_systemd', '--shm-size-systemd') 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) + return self._diff_generic('stop_signal', '--stop-signal') + + def diffparam_stop_timeout(self): + return self._diff_generic('stop_timeout', '--stop-timeout') + + def diffparam_subgidname(self): + return self._diff_generic('subgidname', '--subgidname') + + def diffparam_subuidname(self): + return self._diff_generic('subuidname', '--subuidname') + + def diffparam_sysctl(self): + return self._diff_generic('sysctl', '--sysctl') + + def diffparam_systemd(self): + return self._diff_generic('systemd', '--systemd') + + def diffparam_timeout(self): + return self._diff_generic('timeout', '--timeout') def diffparam_timezone(self): - before = self.info['config'].get('timezone') - after = self.params['timezone'] - return self._diff_update_and_compare('timezone', before, after) + return self._diff_generic('timezone', '--tz') + + def diffparam_tls_verify(self): + return self._diff_generic('tls_verify', '--tls-verify') 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_tmpfs(self): + return self._diff_generic('tmpfs', '--tmpfs') + + def diffparam_uidmap(self): + return self._diff_generic('uidmap', '--uidmap') def diffparam_ulimit(self): - after = self.params['ulimit'] or [] - # In case of latest podman - if 'createcommand' in self.info['config']: - before = self._createcommand('--ulimit') - 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', '', '') + return self._diff_generic('ulimit', '--ulimit') + + def diffparam_umask(self): + return self._diff_generic('umask', '--umask') + + def diffparam_unsetenv(self): + return self._diff_generic('unsetenv', '--unsetenv') + + def diffparam_unsetenv_all(self): + return self._diff_generic('unsetenv_all', '--unsetenv-all', boolean_type=True) + + def diffparam_user(self): + return self._diff_generic('user', '--user') + + def diffparam_userns(self): + return self._diff_generic('userns', '--userns') 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) + return self._diff_generic('uts', '--uts') + + def diffparam_variant(self): + return self._diff_generic('variant', '--variant') def diffparam_volume(self): def clean_volume(x): @@ -1372,44 +1435,29 @@ class PodmanContainerDiff: 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: + before = createcommand('--volume', self.info['config']) + if before == []: + before = None + after = self.params['volume'] + if after 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))) + [clean_volume(i) for i in v.split(":")[:2]]) for v in self.params['volume']] + if before is not None: + before = [":".join([clean_volume(i) for i in v.split(":")[:2]]) for v in before] + self.module.log("PODMAN Before: %s and After: %s" % (before, after)) + if before is None and after is None: + return self._diff_update_and_compare('volume', before, after) + if after is not None: + after = ",".join(sorted([str(i).lower() for i in after])) + if before: + before = ",".join(sorted([str(i).lower() for i in before])) 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) + return self._diff_generic('volumes_from', '--volumes-from') def diffparam_workdir(self): - before = self.info['config']['workingdir'] - after = self.params['workdir'] - return self._diff_update_and_compare('workdir', before, after) + return self._diff_generic('workdir', '--workdir') def is_different(self): diff_func_list = [func for func in dir(self) 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 index e0031351f..8f315a5cc 100644 --- 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 @@ -6,6 +6,8 @@ from ansible_collections.containers.podman.plugins.module_utils.podman.common im 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 diff_generic +from ansible_collections.containers.podman.plugins.module_utils.podman.common import createcommand from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state, PodQuadlet @@ -28,9 +30,9 @@ ARGUMENTS_SPEC_POD = dict( ]), recreate=dict(type='bool', default=False), add_host=dict(type='list', required=False, elements='str'), - cgroup_parent=dict(type='str', required=False), blkio_weight=dict(type='str', required=False), blkio_weight_device=dict(type='list', elements='str', required=False), + cgroup_parent=dict(type='str', required=False), cpus=dict(type='str', required=False), cpuset_cpus=dict(type='str', required=False), cpuset_mems=dict(type='str', required=False), @@ -39,10 +41,12 @@ ARGUMENTS_SPEC_POD = dict( device_read_bps=dict(type='list', elements='str', required=False), device_write_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_opt=dict(type='list', elements='str', aliases=['dns_option'], required=False), dns_search=dict(type='list', elements='str', required=False), + exit_policy=dict(type='str', required=False, choices=['continue', 'stop']), generate_systemd=dict(type='dict', default={}), gidmap=dict(type='list', elements='str', required=False), + gpus=dict(type='str', required=False), hostname=dict(type='str', required=False), infra=dict(type='bool', required=False), infra_conmon_pidfile=dict(type='str', required=False), @@ -50,6 +54,7 @@ ARGUMENTS_SPEC_POD = dict( infra_image=dict(type='str', required=False), infra_name=dict(type='str', required=False), ip=dict(type='str', required=False), + ip6=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), @@ -67,13 +72,21 @@ ARGUMENTS_SPEC_POD = dict( quadlet_dir=dict(type='path'), quadlet_filename=dict(type='str'), quadlet_options=dict(type='list', elements='str'), + restart_policy=dict(type='str', required=False), + security_opt=dict(type='list', elements='str', required=False), share=dict(type='str', required=False), + share_parent=dict(type='bool', required=False), + shm_size=dict(type='str', required=False), + shm_size_systemd=dict(type='str', required=False), subgidname=dict(type='str', required=False), subuidname=dict(type='str', required=False), + sysctl=dict(type='dict', required=False), uidmap=dict(type='list', elements='str', required=False), userns=dict(type='str', required=False), + uts=dict(type='str', required=False), volume=dict(type='list', elements='str', aliases=['volumes'], required=False), + volumes_from=dict(type='list', elements='str', required=False), executable=dict(type='str', required=False, default='podman'), debug=dict(type='bool', default=False), ) @@ -200,7 +213,7 @@ class PodmanPodModuleParams: def addparam_dns_opt(self, c): for g in self.params['dns_opt']: - c += ['--dns-opt', g] + c += ['--dns-option', g] return c def addparam_dns_search(self, c): @@ -208,11 +221,17 @@ class PodmanPodModuleParams: c += ['--dns-search', g] return c + def addparam_exit_policy(self, c): + return c + ['--exit-policy=%s' % self.params['exit_policy']] + def addparam_gidmap(self, c): for gidmap in self.params['gidmap']: c += ['--gidmap', gidmap] return c + def addparam_gpus(self, c): + return c + ['--gpus', self.params['gpus']] + def addparam_hostname(self, c): return c + ['--hostname', self.params['hostname']] @@ -236,6 +255,9 @@ class PodmanPodModuleParams: def addparam_ip(self, c): return c + ['--ip', self.params['ip']] + def addparam_ip6(self, c): + return c + ['--ip6', self.params['ip6']] + def addparam_label(self, c): for label in self.params['label'].items(): c += ['--label', b'='.join( @@ -285,15 +307,39 @@ class PodmanPodModuleParams: c += ['--publish', g] return c + def addparam_restart_policy(self, c): + return c + ['--restart=%s' % self.params['restart_policy']] + + def addparam_security_opt(self, c): + for g in self.params['security_opt']: + c += ['--security-opt', g] + return c + def addparam_share(self, c): return c + ['--share', self.params['share']] + def addparam_share_parent(self, c): + if self.params['share_parent'] is not None: + return c + ['--share-parent=%s' % self.params['share_parent']] + return c + + def addparam_shm_size(self, c): + return c + ['--shm-size=%s' % self.params['shm_size']] + + def addparam_shm_size_systemd(self, c): + return c + ['--shm-size-systemd=%s' % self.params['shm_size_systemd']] + 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 k, v in self.params['sysctl'].items(): + c += ['--sysctl', "%s=%s" % (k, v)] + return c + def addparam_uidmap(self, c): for uidmap in self.params['uidmap']: c += ['--uidmap', uidmap] @@ -302,22 +348,26 @@ class PodmanPodModuleParams: 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 + 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': {}, } @@ -361,50 +411,79 @@ class PodmanPodDiff: return True return False + def _diff_generic(self, module_arg, cmd_arg, boolean_type=False): + """ + Generic diff function for module arguments from CreateCommand + in Podman inspection output. + + Args: + module_arg (str): module argument name + cmd_arg (str): command line argument name + boolean_type (bool): if True, then argument is boolean type + + Returns: + bool: True if there is a difference, False otherwise + + """ + info_config = self.info + before, after = diff_generic(self.params, info_config, module_arg, cmd_arg, boolean_type) + return self._diff_update_and_compare(module_arg, before, after) + 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) + return self._diff_generic('add_host', '--add-host') + + def diffparam_blkio_weight(self): + return self._diff_generic('blkio_weight', '--blkio-weight') + + def diffparam_blkio_weight_device(self): + return self._diff_generic('blkio_weight_device', '--blkio-weight-device') def diffparam_cgroup_parent(self): - before = (self.info.get('cgroupparent', '') - or self.info.get('hostconfig', {}).get('cgroupparent', '')) - after = self.params['cgroup_parent'] or before - return self._diff_update_and_compare('cgroup_parent', before, after) + return self._diff_generic('cgroup_parent', '--cgroup-parent') + + def diffparam_cpu_shares(self): + return self._diff_generic('cpu_shares', '--cpu-shares') + + def diffparam_cpus(self): + return self._diff_generic('cpus', '--cpus') + + def diffparam_cpuset_cpus(self): + return self._diff_generic('cpuset_cpus', '--cpuset-cpus') + + def diffparam_cpuset_mems(self): + return self._diff_generic('cpuset_mems', '--cpuset-mems') + + def diffparam_device(self): + return self._diff_generic('device', '--device') + + def diffparam_device_read_bps(self): + return self._diff_generic('device_read_bps', '--device-read-bps') + + def diffparam_device_write_bps(self): + return self._diff_generic('device_write_bps', '--device-write-bps') 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) + return self._diff_generic('dns', '--dns') 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) + return self._diff_generic('dns_opt', '--dns-option') 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) + return self._diff_generic('dns_search', '--dns-search') + + # Disabling idemotency check for exit policy as it's added by systemd generator + # https://github.com/containers/ansible-podman-collections/issues/774 + # def diffparam_exit_policy(self): + # return self._diff_generic('exit_policy', '--exit-policy') + + def diffparam_gidmap(self): + return self._diff_generic('gidmap', '--gidmap') + + def diffparam_gpus(self): + return self._diff_generic('gpus', '--gpus') 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) + return self._diff_generic('hostname', '--hostname') # TODO(sshnaidm): https://github.com/containers/podman/issues/6968 def diffparam_infra(self): @@ -416,30 +495,25 @@ class PodmanPodDiff: 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_command(self): + return self._diff_generic('infra_command', '--infra-command') + + # Disabling idemotency check for infra_conmon_pidfile as it's added by systemd generator + # https://github.com/containers/ansible-podman-collections/issues/774 + # def diffparam_infra_conmon_pidfile(self): + # return self._diff_generic('infra_conmon_pidfile', '--infra-conmon-pidfile') 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) + return self._diff_generic('infra_image', '--infra-image') + + def diffparam_infra_name(self): + return self._diff_generic('infra_name', '--infra-name') + + def diffparam_ip(self): + return self._diff_generic('ip', '--ip') + + def diffparam_ip6(self): + return self._diff_generic('ip6', '--ip6') def diffparam_label(self): if 'config' in self.info and 'labels' in self.info['config']: @@ -454,129 +528,101 @@ class PodmanPodDiff: 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_label_file(self): + return self._diff_generic('label_file', '--label-file') + + def diffparam_mac_address(self): + return self._diff_generic('mac_address', '--mac-address') + + def diffparam_memory(self): + return self._diff_generic('memory', '--memory') + + def diffparam_memory_swap(self): + return self._diff_generic('memory_swap', '--memory-swap') 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 [] - after = [i.lower() for i in after] - # 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] - if net_mode_before == 'pasta' 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 'pasta:' in cr_net: - before = [cr_net] - # Currently supported only 'host' and 'none' network modes idempotency - if after in [['bridge'], ['host'], ['slirp4netns'], ['pasta']]: - 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_after = net_mode_after.replace('pasta', 'default') - net_mode_before = net_mode_before.replace('bridge', 'default') - net_mode_before = net_mode_before.replace('slirp4netns', 'default') - net_mode_before = net_mode_before.replace('pasta', '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'] - # For pasta networking for Podman v5 - if not net_mode_after and net_mode_before == 'pasta' and not after: - net_mode_after = 'pasta' - if before == ['pasta']: - after = ['pasta'] - 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 + return self._diff_generic('network', '--network') + + def diffparam_network_alias(self): + return self._diff_generic('network_alias', '--network-alias') + + def diffparam_no_hosts(self): + return self._diff_generic('no_hosts', '--no-hosts', boolean_type=True) + + def diffparam_pid(self): + return self._diff_generic('pid', '--pid') + + # Disabling idemotency check for pod id file as it's added by systemd generator + # https://github.com/containers/ansible-podman-collections/issues/774 + # def diffparam_pod_id_file(self): + # return self._diff_generic('pod_id_file', '--pod-id-file') + def diffparam_publish(self): - def compose(p, h): - s = ":".join( - [str(h["hostport"]), p.replace('/tcp', '')] - ).strip(":") - if h['hostip'] == '0.0.0.0' and LooseVersion(self.version) >= LooseVersion('5.0.0'): - return s - 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) + return self._diff_generic('publish', '--publish') + + def diffparam_restart_policy(self): + return self._diff_generic('restart_policy', '--restart') + + def diffparam_security_opt(self): + return self._diff_generic('security_opt', '--security-opt') 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"] or self.params["userns"]: - after.append('user') - - before, after = sorted(list(set(before))), sorted(list(set(after))) - return self._diff_update_and_compare('share', before, after) + return self._diff_generic('share', '--share') + + def diffparam_share_parent(self): + return self._diff_generic('share_parent', '--share-parent') + + def diffparam_shm_size(self): + return self._diff_generic('shm_size', '--shm-size') + + def diffparam_shm_size_systemd(self): + return self._diff_generic('shm_size_systemd', '--shm-size-systemd') + + def diffparam_subgidname(self): + return self._diff_generic('subgidname', '--subgidname') + + def diffparam_subuidname(self): + return self._diff_generic('subuidname', '--subuidname') + + def diffparam_sysctl(self): + return self._diff_generic('sysctl', '--sysctl') + + def diffparam_uidmap(self): + return self._diff_generic('uidmap', '--uidmap') + + def diffparam_userns(self): + return self._diff_generic('userns', '--userns') + + def diffparam_uts(self): + return self._diff_generic('uts', '--uts') + + 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 = createcommand('--volume', self.info) + if before == []: + before = None + after = self.params['volume'] + if after is not None: + after = [":".join( + [clean_volume(i) for i in v.split(":")[:2]]) for v in self.params['volume']] + if before is not None: + before = [":".join([clean_volume(i) for i in v.split(":")[:2]]) for v in before] + self.module.log("PODMAN Before: %s and After: %s" % (before, after)) + if before is None and after is None: + return self._diff_update_and_compare('volume', before, after) + if after is not None: + after = ",".join(sorted([str(i).lower() for i in after])) + if before: + before = ",".join(sorted([str(i).lower() for i in before])) + return self._diff_update_and_compare('volume', before, after) + + def diffparam_volumes_from(self): + return self._diff_generic('volumes_from', '--volumes-from') def is_different(self): diff_func_list = [func for func in dir(self) diff --git a/ansible_collections/containers/podman/plugins/module_utils/podman/quadlet.py b/ansible_collections/containers/podman/plugins/module_utils/podman/quadlet.py index 17764b60d..6296c7b81 100644 --- a/ansible_collections/containers/podman/plugins/module_utils/podman/quadlet.py +++ b/ansible_collections/containers/podman/plugins/module_utils/podman/quadlet.py @@ -88,25 +88,21 @@ class ContainerQuadlet(Quadlet): 'gidmap': 'GIDMap', 'global_args': 'GlobalArgs', 'group': 'Group', # Does not exist in module parameters - 'healthcheck': 'HealthCheckCmd', + 'healthcheck': 'HealthCmd', 'healthcheck_interval': 'HealthInterval', 'healthcheck_failure_action': 'HealthOnFailure', 'healthcheck_retries': 'HealthRetries', 'healthcheck_start_period': 'HealthStartPeriod', 'healthcheck_timeout': 'HealthTimeout', - # the following are not implemented yet in Podman module - 'HealthStartupCmd': 'HealthStartupCmd', - 'HealthStartupInterval': 'HealthStartupInterval', - 'HealthStartupRetries': 'HealthStartupRetries', - 'HealthStartupSuccess': 'HealthStartupSuccess', - 'HealthStartupTimeout': 'HealthStartupTimeout', - # end of not implemented yet + 'health_startup_cmd': 'HealthStartupCmd', + 'health_startup_interval': 'HealthStartupInterval', + 'health_startup_retries': 'HealthStartupRetries', + 'health_startup_success': 'HealthStartupSuccess', + 'health_startup_timeout': 'HealthStartupTimeout', 'hostname': 'HostName', 'image': 'Image', 'ip': 'IP', - # the following are not implemented yet in Podman module - 'IP6': 'IP6', - # end of not implemented yet + 'ip6': 'IP6', 'label': 'Label', 'log_driver': 'LogDriver', "Mask": "Mask", # add it in security_opt @@ -117,9 +113,7 @@ class ContainerQuadlet(Quadlet): 'pids_limit': 'PidsLimit', 'pod': 'Pod', 'publish': 'PublishPort', - # the following are not implemented yet in Podman module - "Pull": "Pull", - # end of not implemented yet + "pull": "Pull", 'read_only': 'ReadOnly', 'read_only_tmpfs': 'ReadOnlyTmpfs', 'rootfs': 'Rootfs', @@ -194,6 +188,8 @@ class ContainerQuadlet(Quadlet): # Work on params which are not in the param_map and add them to PodmanArgs params["podman_args"] = [] + if params["arch"]: + params["podman_args"].append(f"--arch {params['arch']}") if params["authfile"]: params["podman_args"].append(f"--authfile {params['authfile']}") if params["attach"]: @@ -206,8 +202,13 @@ class ContainerQuadlet(Quadlet): f"--blkio-weight-device {':'.join(blkio)}" for blkio in params["blkio_weight_device"].items()])) if params["cgroupns"]: params["podman_args"].append(f"--cgroupns {params['cgroupns']}") + if params["cgroup_conf"]: + for k, v in params["cgroup_conf"].items(): + params["podman_args"].append(f"--cgroup-conf {k}={v}") if params["cgroup_parent"]: params["podman_args"].append(f"--cgroup-parent {params['cgroup_parent']}") + if params["chrootdirs"]: + params["podman_args"].append(f"--chrootdirs {params['chrootdirs']}") if params["cidfile"]: params["podman_args"].append(f"--cidfile {params['cidfile']}") if params["conmon_pidfile"]: @@ -226,6 +227,10 @@ class ContainerQuadlet(Quadlet): params["podman_args"].append(f"--cpu-rt-runtime {params['cpu_rt_runtime']}") if params["cpu_shares"]: params["podman_args"].append(f"--cpu-shares {params['cpu_shares']}") + if params["decryption_key"]: + params["podman_args"].append(f"--decryption-key {params['decryption_key']}") + if params["device_cgroup_rule"]: + params["podman_args"].append(f"--device-cgroup-rule {params['device_cgroup_rule']}") if params["device_read_bps"]: for i in params["device_read_bps"]: params["podman_args"].append(f"--device-read-bps {i}") @@ -241,6 +246,15 @@ class ContainerQuadlet(Quadlet): if params["etc_hosts"]: for host_ip in params['etc_hosts'].items(): params["podman_args"].append(f"--add-host {':'.join(host_ip)}") + if params["env_merge"]: + for k, v in params["env_merge"].items(): + params["podman_args"].append(f"--env {k}={v}") + if params["gpus"]: + params["podman_args"].append(f"--gpus {params['gpus']}") + if params["group_entry"]: + params["podman_args"].append(f"--group-entry {params['group_entry']}") + if params["hostuser"]: + params["podman_args"].append(f"--hostuser {params['hostuser']}") if params["hooks_dir"]: for hook in params["hooks_dir"]: params["podman_args"].append(f"--hooks-dir {hook}") @@ -248,6 +262,8 @@ class ContainerQuadlet(Quadlet): params["podman_args"].append(f"--http-proxy {params['http_proxy']}") if params["image_volume"]: params["podman_args"].append(f"--image-volume {params['image_volume']}") + if params["init_ctr"]: + params["podman_args"].append(f"--init-ctr {params['init_ctr']}") if params["init_path"]: params["podman_args"].append(f"--init-path {params['init_path']}") if params["interactive"]: @@ -274,37 +290,79 @@ class ContainerQuadlet(Quadlet): if params["network_aliases"]: for alias in params["network_aliases"]: params["podman_args"].append(f"--network-alias {alias}") + if params["no_healthcheck"]: + params["podman_args"].append("--no-healthcheck") if params["no_hosts"] is not None: params["podman_args"].append(f"--no-hosts={params['no_hosts']}") if params["oom_kill_disable"]: params["podman_args"].append(f"--oom-kill-disable={params['oom_kill_disable']}") if params["oom_score_adj"]: params["podman_args"].append(f"--oom-score-adj {params['oom_score_adj']}") + if params["os"]: + params["podman_args"].append(f"--os {params['os']}") + if params["passwd"]: + params["podman_args"].append("--passwd") + if params["passwd_entry"]: + params["podman_args"].append(f"--passwd-entry {params['passwd_entry']}") + if params["personality"]: + params["podman_args"].append(f"--personality {params['personality']}") if params["pid"]: params["podman_args"].append(f"--pid {params['pid']}") + if params["pid_file"]: + params["podman_args"].append(f"--pid-file {params['pid_file']}") + if params["preserve_fd"]: + for pres in params["preserve_fd"]: + params["podman_args"].append(f"--preserve-fd {pres}") + if params["preserve_fds"]: + params["podman_args"].append(f"--preserve-fds {params['preserve_fds']}") if params["privileged"]: params["podman_args"].append("--privileged") if params["publish_all"]: params["podman_args"].append("--publish-all") + if params["rdt_class"]: + params["podman_args"].append(f"--rdt-class {params['rdt_class']}") if params["requires"]: params["podman_args"].append(f"--requires {','.join(params['requires'])}") if params["restart_policy"]: params["podman_args"].append(f"--restart-policy {params['restart_policy']}") + if params["retry"]: + params["podman_args"].append(f"--retry {params['retry']}") + if params["retry_delay"]: + params["podman_args"].append(f"--retry-delay {params['retry_delay']}") if params["rm"]: params["podman_args"].append("--rm") + if params["rmi"]: + params["podman_args"].append("--rmi") + if params["seccomp_policy"]: + params["podman_args"].append(f"--seccomp-policy {params['seccomp_policy']}") if params["security_opt"]: for security_opt in params["security_opt"]: params["podman_args"].append(f"--security-opt {security_opt}") + if params["shm_size_systemd"]: + params["podman_args"].append(f"--shm-size-systemd {params['shm_size_systemd']}") if params["sig_proxy"]: params["podman_args"].append(f"--sig-proxy {params['sig_proxy']}") if params["stop_signal"]: params["podman_args"].append(f"--stop-signal {params['stop_signal']}") if params["systemd"]: params["podman_args"].append(f"--systemd={str(params['systemd']).lower()}") + if params["timeout"]: + params["podman_args"].append(f"--timeout {params['timeout']}") + if params["tls_verify"]: + params["podman_args"].append(f"--tls-verify={str(params['tls_verify']).lower()}") if params["tty"]: params["podman_args"].append("--tty") + if params["umask"]: + params["podman_args"].append(f"--umask {params['umask']}") + if params["unsetenv"]: + for unset in params["unsetenv"]: + params["podman_args"].append(f"--unsetenv {unset}") + if params["unsetenv_all"]: + params["podman_args"].append("--unsetenv-all") if params["uts"]: params["podman_args"].append(f"--uts {params['uts']}") + if params["variant"]: + params["podman_args"].append(f"--variant {params['variant']}") if params["volumes_from"]: for volume in params["volumes_from"]: params["podman_args"].append(f"--volumes-from {volume}") @@ -416,6 +474,10 @@ class PodQuadlet(Quadlet): if params["gidmap"]: for gidmap in params["gidmap"]: params["podman_args"].append(f"--gidmap {gidmap}") + if params["exit_policy"]: + params["podman_args"].append(f"--exit-policy={params['gpus']}") + if params["gpus"]: + params["podman_args"].append(f"--gpus {params['gpus']}") if params["hostname"]: params["podman_args"].append(f"--hostname {params['hostname']}") if params["infra"]: @@ -430,6 +492,8 @@ class PodQuadlet(Quadlet): params["podman_args"].append(f"--infra-name {params['infra_name']}") if params["ip"]: params["podman_args"].append(f"--ip {params['ip']}") + if params["ip6"]: + params["podman_args"].append(f"--ip6 {params['ip6']}") if params["label"]: for label, label_v in params["label"].items(): params["podman_args"].append(f"--label {label}={label_v}") @@ -447,17 +511,36 @@ class PodQuadlet(Quadlet): params["podman_args"].append(f"--pid {params['pid']}") if params["pod_id_file"]: params["podman_args"].append(f"--pod-id-file {params['pod_id_file']}") + if params["restart_policy"]: + params["podman_args"].append(f"--restart={params['restart_policy']}") + if params["security_opt"]: + for security_opt in params["security_opt"]: + params["podman_args"].append(f"--security-opt {security_opt}") if params["share"]: params["podman_args"].append(f"--share {params['share']}") + if params["share_parent"] is not None: + params["podman_args"].append(f"--share-parent={str(params['share_parent']).lower()}") + if params["shm_size"]: + params["podman_args"].append(f"--shm-size {params['shm_size']}") + if params["shm_size_systemd"]: + params["podman_args"].append(f"--shm-size-systemd {params['shm_size_systemd']}") if params["subgidname"]: params["podman_args"].append(f"--subgidname {params['subgidname']}") if params["subuidname"]: params["podman_args"].append(f"--subuidname {params['subuidname']}") + if params["sysctl"]: + for k, v in params["sysctl"].items(): + params["podman_args"].append(f"--sysctl {k}={v}") if params["uidmap"]: for uidmap in params["uidmap"]: params["podman_args"].append(f"--uidmap {uidmap}") if params["userns"]: params["podman_args"].append(f"--userns {params['userns']}") + if params["uts"]: + params["podman_args"].append(f"--uts {params['uts']}") + if params["volumes_from"]: + for volume in params["volumes_from"]: + params["podman_args"].append(f"--volumes-from {volume}") if params["debug"]: params["global_args"].append("--log-level debug") diff --git a/ansible_collections/containers/podman/plugins/modules/podman_container.py b/ansible_collections/containers/podman/plugins/modules/podman_container.py index 75349f14e..b06c9ae9e 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_container.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_container.py @@ -79,6 +79,11 @@ options: - Add an annotation to the container. The format is key value, multiple times. type: dict + arch: + description: + - Set the architecture for the container. + Override the architecture, defaults to hosts, of the image to be pulled. For example, arm. + type: str attach: description: - Attach to STDIN, STDOUT or STDERR. The default in Podman is false. @@ -125,6 +130,10 @@ options: the cgroups path of the init process. Cgroups will be created if they do not already exist. type: path + cgroup_conf: + description: + - When running on cgroup v2, specify the cgroup file to write to and its value. + type: dict cgroupns: description: - Path to cgroups under which the cgroup for the container will be @@ -137,6 +146,10 @@ options: The disabled option will force the container to not create CGroups, and thus conflicts with CGroup options cgroupns and cgroup-parent. type: str + chrootdirs: + description: + - Path to a directory inside the container that is treated as a chroot directory. + type: str cidfile: description: - Write the container ID to the file @@ -196,6 +209,10 @@ options: - Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. type: str + decryption_key: + description: + - The "key-passphrase" to be used for decryption of images. Key can point to keys and/or certificates. + type: str delete_depend: description: - Remove selected container and recursively remove all containers that depend on it. @@ -234,6 +251,12 @@ options: (e.g. device /dev/sdc:/dev/xvdc:rwm) type: list elements: str + device_cgroup_rule: + description: + - Add a rule to the cgroup allowed devices list. + The rule is expected to be in the format specified in the Linux kernel + documentation admin-guide/cgroup-v1/devices. + type: str device_read_bps: description: - Limit read rate (bytes per second) from a device @@ -307,6 +330,10 @@ options: - Use all current host environment variables in container. Defaults to false. type: bool + env_merge: + description: + - Preprocess default environment variables for the containers + type: dict etc_hosts: description: - Dict of host-to-IP mappings, where each host name is a key in the @@ -436,6 +463,10 @@ options: - Run the container in a new user namespace using the supplied mapping. type: list elements: str + gpus: + description: + - GPU devices to add to the container. + type: str group_add: description: - Add additional groups to run as @@ -443,33 +474,70 @@ options: elements: str aliases: - groups + group_entry: + description: + - Customize the entry that is written to the /etc/group file within the container when --user is used. + type: str healthcheck: description: - Set or alter a healthcheck command for a container. type: str + aliases: + - health_cmd healthcheck_interval: description: - Set an interval for the healthchecks (a value of disable results in no automatic timer setup) (default "30s") type: str + aliases: + - health_interval healthcheck_retries: description: - The number of retries allowed before a healthcheck is considered to be unhealthy. The default value is 3. type: int + aliases: + - health_retries healthcheck_start_period: description: - The initialization time needed for a container to bootstrap. The value can be expressed in time format like 2m3s. The default value is 0s type: str + aliases: + - health_start_period + health_startup_cmd: + description: + - Set a startup healthcheck command for a container. + type: str + health_startup_interval: + description: + - Set an interval for the startup healthcheck. + type: str + health_startup_retries: + description: + - The number of attempts allowed before the startup healthcheck restarts the container. + If set to 0, the container is never restarted. The default is 0. + type: int + health_startup_success: + description: + - The number of successful runs required before the startup healthcheck succeeds + and the regular healthcheck begins. A value of 0 means that any success begins the regular healthcheck. + The default is 0. + type: int + health_startup_timeout: + description: + - The maximum time a startup healthcheck command has to complete before it is marked as failed. + type: str healthcheck_timeout: description: - The maximum time allowed to complete the healthcheck before an interval is considered failed. Like start-period, the value can be expressed in a time format such as 1m22s. The default value is 30s type: str + aliases: + - health_timeout healthcheck_failure_action: description: - The action to be taken when the container is considered unhealthy. The action must be one of @@ -481,6 +549,8 @@ options: - 'kill' - 'restart' - 'stop' + aliases: + - health_on_failure hooks_dir: description: - Each .json file in the path configures a hook for Podman containers. @@ -493,6 +563,11 @@ options: - Container host name. Sets the container host name that is available inside the container. type: str + hostuser: + description: + - Add a user account to /etc/passwd from the host to the container. + The Username or UID must exist on the host system. + type: str http_proxy: description: - By default proxy environment variables are passed into the container if @@ -522,6 +597,14 @@ options: - Run an init inside the container that forwards signals and reaps processes. The default is false. type: bool + init_ctr: + description: + - (Pods only). When using pods, create an init style container, + which is run after the infra container is started but before regular pod containers are started. + type: str + choices: + - 'once' + - 'always' init_path: description: - Path to the container-init binary. @@ -542,6 +625,10 @@ options: The address must be within the default CNI network's pool (default 10.88.0.0/16). type: str + ip6: + description: + - Specify a static IPv6 address for the container + type: str ipc: description: - Default is to create a private IPC namespace (POSIX SysV IPC) for the @@ -671,6 +758,12 @@ options: This is a limitation that will be removed in a later release. type: list elements: str + aliases: + - network_alias + no_healthcheck: + description: + - Disable any defined healthchecks for container. + type: bool no_hosts: description: - Do not create /etc/hosts for the container @@ -685,23 +778,64 @@ options: description: - Tune the host's OOM preferences for containers (accepts -1000 to 1000) type: int + os: + description: + - Override the OS, defaults to hosts, of the image to be pulled. For example, windows. + type: str + passwd: + description: + - Allow Podman to add entries to /etc/passwd and /etc/group when used in conjunction with the --user option. + This is used to override the Podman provided user setup in favor of entrypoint configurations + such as libnss-extrausers. + type: bool + passwd_entry: + description: + - Customize the entry that is written to the /etc/passwd file within the container when --passwd is used. + type: str + personality: + description: + - Personality sets the execution domain via Linux personality(2). + type: str pid: description: - Set the PID mode for the container type: str aliases: - pid_mode + pid_file: + description: + - When the pidfile location is specified, the container process' PID is written to the pidfile. + type: path pids_limit: description: - Tune the container's PIDs limit. Set -1 to have unlimited PIDs for the container. type: str + platform: + description: + - Specify the platform for selecting the image. + type: str pod: description: - Run container in an existing pod. If you want podman to make the pod for you, prefix the pod name with "new:" type: str + pod_id_file: + description: + - Run container in an existing pod and read the pod's ID from the specified file. + When a container is run within a pod which has an infra-container, + the infra-container starts first. + type: path + preserve_fd: + description: + - Pass down to the process the additional file descriptors specified in the comma separated list. + type: list + elements: str + preserve_fds: + description: + - Pass down to the process N additional file descriptors (in addition to 0, 1, 2). The total FDs are 3\+N. + type: str privileged: description: - Give extended privileges to this container. The default is false. @@ -724,6 +858,15 @@ options: - Publish all exposed ports to random ports on the host interfaces. The default is false. type: bool + pull: + description: + - Pull image policy. The default is 'missing'. + type: str + choices: + - 'missing' + - 'always' + - 'never' + - 'newer' quadlet_dir: description: - Path to the directory to write quadlet file in. @@ -740,6 +883,10 @@ options: options as a list of lines to add. type: list elements: str + rdt_class: + description: + - Rdt-class sets the class of service (CLOS or COS) for the container to run in. Requires root. + type: str read_only: description: - Mount the container's root filesystem as read only. Default is false @@ -779,6 +926,15 @@ options: - Seconds to wait before forcibly stopping the container when restarting. Use -1 for infinite wait. Applies to "restarted" status. type: str + retry: + description: + - Number of times to retry pulling or pushing images between the registry and local storage in case of failure. + Default is 3. + type: int + retry_delay: + description: + - Duration of delay between retry attempts when pulling or pushing images between the registry and local storage in case of failure. + type: str rm: description: - Automatically remove the container when it exits. The default is false. @@ -786,6 +942,11 @@ options: aliases: - remove - auto_remove + rmi: + description: + - After exit of the container, remove the image unless another container is using it. + Implies --rm on the new container. The default is false. + type: bool rootfs: description: - If true, the first argument refers to an exploded container on the file @@ -803,6 +964,10 @@ options: L(documentation,https://docs.podman.io/en/latest/markdown/podman-run.1.html#secret-secret-opt-opt) for more details. type: list elements: str + seccomp_policy: + description: + - Specify the policy to select the seccomp profile. + type: str security_opt: description: - Security Options. For example security_opt "seccomp=unconfined" @@ -817,6 +982,10 @@ options: If you omit the unit, the system uses bytes. If you omit the size entirely, the system uses 64m type: str + shm_size_systemd: + description: + - Size of systemd-specific tmpfs mounts such as /run, /run/lock, /var/log/journal and /tmp. + type: str sig_proxy: description: - Proxy signals sent to the podman run command to the container process. @@ -853,6 +1022,11 @@ options: description: - Run container in systemd mode. The default is true. type: str + timeout: + description: + - Maximum time (in seconds) a container is allowed to run before conmon sends it the kill signal. + By default containers run until they exit or are stopped by "podman stop". + type: int timezone: description: - Set timezone in container. This flag takes area-based timezones, @@ -861,6 +1035,10 @@ options: See /usr/share/zoneinfo/ for valid timezones. Remote connections use local containers.conf for defaults. type: str + tls_verify: + description: + - Require HTTPS and verify certificates when pulling images. + type: bool tmpfs: description: - Create a tmpfs mount. For example tmpfs @@ -882,6 +1060,20 @@ options: elements: str aliases: - ulimits + umask: + description: + - Set the umask inside the container. Defaults to 0022. + Remote connections use local containers.conf for defaults. + type: str + unsetenv: + description: + - Unset default environment variables for the container. + type: list + elements: str + unsetenv_all: + description: + - Unset all default environment variables for the container. + type: bool user: description: - Sets the username or UID used and optionally the groupname or GID for @@ -899,6 +1091,10 @@ options: description: - Set the UTS mode for the container type: str + variant: + description: + - Use VARIANT instead of the default architecture variant of the container image. + type: str volume: description: - Create a bind mount. If you specify, volume /HOST-DIR:/CONTAINER-DIR, diff --git a/ansible_collections/containers/podman/plugins/modules/podman_image.py b/ansible_collections/containers/podman/plugins/modules/podman_image.py index 7fcb0041a..a46a6c3c5 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_image.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_image.py @@ -42,6 +42,10 @@ DOCUMENTATION = r''' description: Whether or not to pull the image. default: True type: bool + pull_extra_args: + description: + - Extra arguments to pass to the pull command. + type: str push: description: Whether or not to push an image. default: False @@ -67,7 +71,8 @@ DOCUMENTATION = r''' - quadlet validate_certs: description: - - Require HTTPS and validate certificates when pulling or pushing. Also used during build if a pull or push is necessary. + - Require HTTPS and validate certificates when pulling or pushing. + Also used during build if a pull or push is necessary. type: bool aliases: - tlsverify @@ -94,9 +99,15 @@ DOCUMENTATION = r''' - build_args - buildargs suboptions: + container_file: + description: + - Content of the Containerfile to use for building the image. + Mutually exclusive with the C(file) option which is path to the existing Containerfile. + type: str file: description: - Path to the Containerfile if it is not in the build context directory. + Mutually exclusive with the C(container_file) option. type: path volume: description: @@ -105,7 +116,8 @@ DOCUMENTATION = r''' elements: str annotation: description: - - Dictionary of key=value pairs to add to the image. Only works with OCI images. Ignored for Docker containers. + - Dictionary of key=value pairs to add to the image. Only works with OCI images. + Ignored for Docker containers. type: dict force_rm: description: @@ -148,7 +160,7 @@ DOCUMENTATION = r''' type: bool format: description: - - Manifest type to use when pushing an image using the 'dir' transport (default is manifest type of source). + - Manifest type to use when pushing an image using the 'dir' transport (default is manifest type of source) type: str choices: - oci @@ -168,14 +180,19 @@ DOCUMENTATION = r''' - destination transport: description: - - Transport to use when pushing in image. If no transport is set, will attempt to push to a remote registry. + - Transport to use when pushing in image. If no transport is set, will attempt to push to a remote registry type: str choices: - dir + - docker - docker-archive - docker-daemon - oci-archive - ostree + extra_args: + description: + - Extra args to pass to push, if executed. Does not idempotently check for new push args. + type: str quadlet_dir: description: - Path to the directory to write quadlet file in. @@ -300,6 +317,15 @@ EXAMPLES = r""" name: nginx arch: amd64 +- name: Build a container from file inline + containers.podman.podman_image: + name: mycustom_image + state: build + build: + container_file: |- + FROM alpine:latest + CMD echo "Hello, World!" + - name: Create a quadlet file for an image containers.podman.podman_image: name: docker.io/library/alpine:latest @@ -333,7 +359,7 @@ RETURN = r""" "/app-entrypoint.sh" ], "Env": [ - "PATH=/opt/bitnami/java/bin:/opt/bitnami/wildfly/bin:/opt/bitnami/nami/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "PATH=/opt/bitnami/java/bin:/opt/bitnami/wildfly/bin:/opt/bitnami/nami/bin:...", "IMAGE_OS=debian-9", "NAMI_VERSION=1.0.0-1", "GPG_KEY_SERVERS_LIST=ha.pool.sks-keyservers.net", @@ -373,10 +399,10 @@ RETURN = r""" "Digest": "sha256:5a8ab28e314c2222de3feaf6dac94a0436a37fc08979d2722c99d2bef2619a9b", "GraphDriver": { "Data": { - "LowerDir": "/var/lib/containers/storage/overlay/142c1beadf1bb09fbd929465ec98c9dca3256638220450efb4214727d0d0680e/diff:/var/lib/containers/s", - "MergedDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99723ed787878dbfea/merged", - "UpperDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99723ed787878dbfea/diff", - "WorkDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99723ed787878dbfea/work" + "LowerDir": "/var/lib/containers/storage/overlay/142c1beadf1bb09fbd929465e..../diff:/var/lib/containers/s", + "MergedDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99/merged", + "UpperDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99/diff", + "WorkDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99/work" }, "Name": "overlay" }, @@ -434,9 +460,12 @@ RETURN = r""" ] """ -import json -import re -import shlex +import json # noqa: E402 +import os # noqa: E402 +import re # noqa: E402 +import shlex # noqa: E402 +import tempfile # noqa: E402 +import time # noqa: E402 from ansible.module_utils._text import to_native from ansible.module_utils.basic import AnsibleModule @@ -456,6 +485,7 @@ class PodmanImageManager(object): self.executable = self.module.get_bin_path(module.params.get('executable'), required=True) self.tag = self.module.params.get('tag') self.pull = self.module.params.get('pull') + self.pull_extra_args = self.module.params.get('pull_extra_args') self.push = self.module.params.get('push') self.path = self.module.params.get('path') self.force = self.module.params.get('force') @@ -509,7 +539,7 @@ class PodmanImageManager(object): if not layer_ids: layer_ids = lines.splitlines() - return (layer_ids[-1]) + return layer_ids[-1] def present(self): image = self.find_image() @@ -520,9 +550,18 @@ class PodmanImageManager(object): digest_before = None if not image or self.force: - if self.path: + if self.state == 'build' or self.path: # Build the image - self.results['actions'].append('Built image {image_name} from {path}'.format(image_name=self.image_name, path=self.path)) + build_file = self.build.get('file') if self.build else None + container_file_txt = self.build.get('container_file') if self.build else None + if build_file and container_file_txt: + self.module.fail_json(msg='Cannot specify both build file and container file content!') + if not self.path and build_file: + self.path = os.path.dirname(build_file) + elif not self.path and not build_file and not container_file_txt: + self.module.fail_json(msg='Path to build context or file is required when building an image') + self.results['actions'].append('Built image {image_name} from {path}'.format( + image_name=self.image_name, path=self.path or 'default context')) if not self.module.check_mode: self.results['image'], self.results['stdout'] = self.build_image() image = self.results['image'] @@ -541,16 +580,8 @@ class PodmanImageManager(object): self.results['changed'] = True if self.push: - # Push the image - if '/' in self.image_name: - push_format_string = 'Pushed image {image_name}' - else: - push_format_string = 'Pushed image {image_name} to {dest}' - self.results['actions'].append(push_format_string.format(image_name=self.image_name, dest=self.push_args['dest'])) - self.results['changed'] = True - if not self.module.check_mode: - self.results['image'], output = self.push_image() - self.results['stdout'] += "\n" + output + self.results['image'], output = self.push_image() + self.results['stdout'] += "\n" + output if image and not self.results.get('image'): self.results['image'] = image @@ -654,13 +685,18 @@ class PodmanImageManager(object): if self.ca_cert_dir: args.extend(['--cert-dir', self.ca_cert_dir]) + if self.pull_extra_args: + args.extend(shlex.split(self.pull_extra_args)) + rc, out, err = self._run(args, ignore_errors=True) if rc != 0: if not self.pull: - self.module.fail_json(msg='Failed to find image {image_name} locally, image pull set to {pull_bool}'.format( - pull_bool=self.pull, image_name=image_name)) + self.module.fail_json( + msg='Failed to find image {image_name} locally, image pull set to {pull_bool}'.format( + pull_bool=self.pull, image_name=image_name)) else: - self.module.fail_json(msg='Failed to pull image {image_name}'.format(image_name=image_name)) + self.module.fail_json( + msg='Failed to pull image {image_name}'.format(image_name=image_name)) return self.inspect_image(out.strip()) def build_image(self): @@ -697,6 +733,17 @@ class PodmanImageManager(object): containerfile = self.build.get('file') if containerfile: args.extend(['--file', containerfile]) + container_file_txt = self.build.get('container_file') + if container_file_txt: + # create a temporarly file with the content of the Containerfile + if self.path: + container_file_path = os.path.join(self.path, 'Containerfile.generated_by_ansible_%s' % time.time()) + else: + container_file_path = os.path.join( + tempfile.gettempdir(), 'Containerfile.generated_by_ansible_%s' % time.time()) + with open(container_file_path, 'w') as f: + f.write(container_file_txt) + args.extend(['--file', container_file_path]) volume = self.build.get('volume') if volume: @@ -717,13 +764,16 @@ class PodmanImageManager(object): target = self.build.get('target') if target: args.extend(['--target', target]) - - args.append(self.path) + if self.path: + args.append(self.path) rc, out, err = self._run(args, ignore_errors=True) if rc != 0: - self.module.fail_json(msg="Failed to build image {image}: {out} {err}".format(image=self.image_name, out=out, err=err)) - + self.module.fail_json(msg="Failed to build image {image}: {out} {err}".format( + image=self.image_name, out=out, err=err)) + # remove the temporary file if it was created + if container_file_txt: + os.remove(container_file_path) last_id = self._get_id_from_output(out, startswith='-->') return self.inspect_image(last_id), out + err @@ -760,49 +810,55 @@ class PodmanImageManager(object): if sign_by_key: args.extend(['--sign-by', sign_by_key]) + push_extra_args = self.push_args.get('extra_args') + if push_extra_args: + args.extend(shlex.split(push_extra_args)) + args.append(self.image_name) # Build the destination argument dest = self.push_args.get('dest') - dest_format_string = '{dest}/{image_name}' - regexp = re.compile(r'/{name}(:{tag})?'.format(name=self.name, tag=self.tag)) - if not dest: - if '/' not in self.name: - self.module.fail_json(msg="'push_args['dest']' is required when pushing images that do not have the remote registry in the image name") - - # If the push destination contains the image name and/or the tag - # remove it and warn since it's not needed. - elif regexp.search(dest): - dest = regexp.sub('', dest) - self.module.warn("Image name and tag are automatically added to push_args['dest']. Destination changed to {dest}".format(dest=dest)) + transport = self.push_args.get('transport') - if dest and dest.endswith('/'): - dest = dest[:-1] + if dest is None: + dest = self.image_name - transport = self.push_args.get('transport') if transport: - if not dest: - self.module.fail_json("'push_args['transport'] requires 'push_args['dest'] but it was not provided.") if transport == 'docker': dest_format_string = '{transport}://{dest}' elif transport == 'ostree': dest_format_string = '{transport}:{name}@{dest}' else: dest_format_string = '{transport}:{dest}' - - dest_string = dest_format_string.format(transport=transport, name=self.name, dest=dest, image_name=self.image_name,) - - # Only append the destination argument if the image name is not a URL - if '/' not in self.name: - args.append(dest_string) - - rc, out, err = self._run(args, ignore_errors=True) - if rc != 0: - self.module.fail_json(msg="Failed to push image {image_name}: {err}".format(image_name=self.image_name, err=err)) - last_id = self._get_id_from_output( - out + err, contains=':', split_on=':') - - return self.inspect_image(last_id), out + err + if transport == 'docker-daemon' and ":" not in dest: + dest_format_string = '{transport}:{dest}:latest' + dest_string = dest_format_string.format(transport=transport, name=self.name, dest=dest) + else: + dest_string = dest + # In case of dest as a repository with org name only, append image name to it + if ":" not in dest and "@" not in dest and len(dest.rstrip("/").split("/")) == 2: + dest_string = dest.rstrip("/") + "/" + self.image_name + + if "/" not in dest_string and "@" not in dest_string and "docker-daemon" not in dest_string: + self.module.fail_json(msg="Destination must be a full URL or path to a directory.") + + args.append(dest_string) + self.module.log("PODMAN-IMAGE-DEBUG: Pushing image {image_name} to {dest_string}".format( + image_name=self.image_name, dest_string=dest_string)) + self.results['actions'].append(" ".join(args)) + self.results['podman_actions'].append(" ".join([self.executable] + args)) + self.results['changed'] = True + out, err = '', '' + if not self.module.check_mode: + rc, out, err = self._run(args, ignore_errors=True) + if rc != 0: + self.module.fail_json(msg="Failed to push image {image_name}".format( + image_name=self.image_name), + stdout=out, stderr=err, + actions=self.results['actions'], + podman_actions=self.results['podman_actions']) + + return self.inspect_image(self.image_name), out + err def remove_image(self, image_name=None): if image_name is None: @@ -813,7 +869,8 @@ class PodmanImageManager(object): args.append('--force') rc, out, err = self._run(args, ignore_errors=True) if rc != 0: - self.module.fail_json(msg='Failed to remove image {image_name}. {err}'.format(image_name=image_name, err=err)) + self.module.fail_json(msg='Failed to remove image {image_name}. {err}'.format( + image_name=image_name, err=err)) return out def remove_image_id(self, image_id=None): @@ -847,6 +904,7 @@ def main(): arch=dict(type='str'), tag=dict(type='str', default='latest'), pull=dict(type='bool', default=True), + pull_extra_args=dict(type='str'), push=dict(type='bool', default=False), path=dict(type='str'), force=dict(type='bool', default=False), @@ -868,6 +926,7 @@ def main(): annotation=dict(type='dict'), force_rm=dict(type='bool', default=False), file=dict(type='path'), + container_file=dict(type='str'), format=dict( type='str', choices=['oci', 'docker'], @@ -889,6 +948,7 @@ def main(): remove_signatures=dict(type='bool'), sign_by=dict(type='str'), dest=dict(type='str', aliases=['destination'],), + extra_args=dict(type='str'), transport=dict( type='str', choices=[ @@ -897,6 +957,7 @@ def main(): 'docker-daemon', 'oci-archive', 'ostree', + 'docker' ] ), ), diff --git a/ansible_collections/containers/podman/plugins/modules/podman_network.py b/ansible_collections/containers/podman/plugins/modules/podman_network.py index 37bfefede..7623fffc1 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_network.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_network.py @@ -33,6 +33,12 @@ options: description: - disable dns plugin (default "false") type: bool + dns: + description: + - Set network-scoped DNS resolver/nameserver for containers in this network. + If not set, the host servers from /etc/resolv.conf is used. + type: list + elements: str driver: description: - Driver to manage the network (default "bridge") @@ -61,11 +67,27 @@ options: description: - Allocate container IP from range type: str + ipam_driver: + description: + - Set the ipam driver (IP Address Management Driver) for the network. + When unset podman chooses an ipam driver automatically based on the network driver + type: str + choices: + - host-local + - dhcp + - none ipv6: description: - Enable IPv6 (Dual Stack) networking. You must pass a IPv6 subnet. The subnet option must be used with the ipv6 option. + Idempotency is not supported because it generates subnets randomly. type: bool + route: + description: + - A static route in the format <destination in CIDR notation>,<gateway>,<route metric (optional)>. + This route will be added to every container in this network. + type: list + elements: str subnet: description: - Subnet in CIDR format @@ -74,6 +96,29 @@ options: description: - Create a Macvlan connection based on this device type: str + net_config: + description: + - List of dictionaries with network configuration. + Each dictionary should contain 'subnet' and 'gateway' keys. + 'ip_range' is optional. + type: list + elements: dict + suboptions: + subnet: + description: + - Subnet in CIDR format + type: str + required: true + gateway: + description: + - Gateway for the subnet + type: str + required: true + ip_range: + description: + - Allocate container IP from range + type: str + required: false opt: description: - Add network options. Currently 'vlan' and 'mtu' are supported. @@ -297,6 +342,11 @@ class PodmanNetworkModuleParams: def addparam_gateway(self, c): return c + ['--gateway', self.params['gateway']] + def addparam_dns(self, c): + for dns in self.params['dns']: + c += ['--dns', dns] + return c + def addparam_driver(self, c): return c + ['--driver', self.params['driver']] @@ -312,6 +362,13 @@ class PodmanNetworkModuleParams: def addparam_macvlan(self, c): return c + ['--macvlan', self.params['macvlan']] + def addparam_net_config(self, c): + for net in self.params['net_config']: + for kw in ('subnet', 'gateway', 'ip_range'): + if kw in net and net[kw]: + c += ['--%s=%s' % (kw.replace('_', '-'), net[kw])] + return c + def addparam_interface_name(self, c): return c + ['--interface-name', self.params['interface_name']] @@ -326,6 +383,14 @@ class PodmanNetworkModuleParams: for k in opt])] return c + def addparam_route(self, c): + for route in self.params['route']: + c += ['--route', route] + return c + + def addparam_ipam_driver(self, c): + return c + ['--ipam-driver=%s' % self.params['ipam_driver']] + def addparam_disable_dns(self, c): return c + ['--disable-dns=%s' % self.params['disable_dns']] @@ -337,7 +402,6 @@ class PodmanNetworkDefaults: self.defaults = { 'driver': 'bridge', 'internal': False, - 'ipv6': False } def default_dict(self): @@ -385,32 +449,45 @@ class PodmanNetworkDiff: before = after = self.params['disable_dns'] return self._diff_update_and_compare('disable_dns', before, after) + def diffparam_dns(self): + before = self.info.get('network_dns_servers', []) + after = self.params['dns'] or [] + return self._diff_update_and_compare('dns', sorted(before), sorted(after)) + def diffparam_driver(self): # Currently only bridge is supported before = after = 'bridge' return self._diff_update_and_compare('driver', before, after) def diffparam_ipv6(self): - if LooseVersion(self.version) >= LooseVersion('4.0.0'): - before = self.info.get('ipv6_enabled', False) - after = self.params['ipv6'] - return self._diff_update_and_compare('ipv6', before, after) - before = after = '' - return self._diff_update_and_compare('ipv6', before, after) + # We don't support dual stack because it generates subnets randomly + return self._diff_update_and_compare('ipv6', '', '') def diffparam_gateway(self): # Disable idempotency of subnet for v4, subnets are added automatically # TODO(sshnaidm): check if it's still the issue in v5 - if LooseVersion(self.version) >= LooseVersion('4.0.0'): - return self._diff_update_and_compare('gateway', '', '') - try: - before = self.info['plugins'][0]['ipam']['ranges'][0][0]['gateway'] - except (IndexError, KeyError): - before = '' - after = before - if self.params['gateway'] is not None: + if LooseVersion(self.version) < LooseVersion('4.0.0'): + try: + before = self.info['plugins'][0]['ipam']['ranges'][0][0]['gateway'] + except (IndexError, KeyError): + before = '' + after = before + if self.params['gateway'] is not None: + after = self.params['gateway'] + return self._diff_update_and_compare('gateway', before, after) + else: + before_subs = self.info.get('subnets') after = self.params['gateway'] - return self._diff_update_and_compare('gateway', before, after) + if not before_subs: + before = None + if before_subs: + if len(before_subs) > 1 and after: + return self._diff_update_and_compare( + 'gateway', ",".join([i['gateway'] for i in before_subs]), after) + before = [i.get('gateway') for i in before_subs][0] + if not after: + after = before + return self._diff_update_and_compare('gateway', before, after) def diffparam_internal(self): if LooseVersion(self.version) >= LooseVersion('4.0.0'): @@ -429,21 +506,62 @@ class PodmanNetworkDiff: before = after = '' return self._diff_update_and_compare('ip_range', before, after) - def diffparam_subnet(self): - # Disable idempotency of subnet for v4, subnets are added automatically - # TODO(sshnaidm): check if it's still the issue in v5 - if LooseVersion(self.version) >= LooseVersion('4.0.0'): - return self._diff_update_and_compare('subnet', '', '') - try: - before = self.info['plugins'][0]['ipam']['ranges'][0][0]['subnet'] - except (IndexError, KeyError): + def diffparam_ipam_driver(self): + before = self.info.get("ipam_options", {}).get("driver", "") + after = self.params['ipam_driver'] + if not after: + after = before + return self._diff_update_and_compare('ipam_driver', before, after) + + def diffparam_net_config(self): + after = self.params['net_config'] + if not after: + return self._diff_update_and_compare('net_config', '', '') + before_subs = self.info.get('subnets', []) + if before_subs: + before = ":".join(sorted([",".join([i['subnet'], i['gateway']]).rstrip(",") for i in before_subs])) + else: before = '' - after = before - if self.params['subnet'] is not None: + after = ":".join(sorted([",".join([i['subnet'], i['gateway']]).rstrip(",") for i in after])) + return self._diff_update_and_compare('net_config', before, after) + + def diffparam_route(self): + routes = self.info.get('routes', []) + if routes: + before = [",".join([ + r['destination'], r['gateway'], str(r.get('metric', ''))]).rstrip(",") for r in routes] + else: + before = [] + after = self.params['route'] or [] + return self._diff_update_and_compare('route', sorted(before), sorted(after)) + + def diffparam_subnet(self): + # Disable idempotency of subnet for v3 and below + if LooseVersion(self.version) < LooseVersion('4.0.0'): + try: + before = self.info['plugins'][0]['ipam']['ranges'][0][0]['subnet'] + except (IndexError, KeyError): + before = '' + after = before + if self.params['subnet'] is not None: + after = self.params['subnet'] + if HAS_IP_ADDRESS_MODULE: + after = ipaddress.ip_network(after).compressed + return self._diff_update_and_compare('subnet', before, after) + else: + if self.params['ipv6'] is not None: + # We can't support dual stack, it generates subnets randomly + return self._diff_update_and_compare('subnet', '', '') after = self.params['subnet'] - if HAS_IP_ADDRESS_MODULE: - after = ipaddress.ip_network(after).compressed - return self._diff_update_and_compare('subnet', before, after) + if after is None: + # We can't guess what subnet was used before by default + return self._diff_update_and_compare('subnet', '', '') + before = self.info.get('subnets') + if before: + if len(before) > 1 and after: + return self._diff_update_and_compare('subnet', ",".join([i['subnet'] for i in before]), after) + before = [i['subnet'] for i in before][0] + return self._diff_update_and_compare('subnet', before, after) def diffparam_macvlan(self): before = after = '' @@ -694,12 +812,15 @@ def main(): choices=['present', 'absent', 'quadlet']), name=dict(type='str', required=True), disable_dns=dict(type='bool', required=False), + dns=dict(type='list', elements='str', required=False), driver=dict(type='str', required=False), force=dict(type='bool', default=False), gateway=dict(type='str', required=False), interface_name=dict(type='str', required=False), internal=dict(type='bool', required=False), ip_range=dict(type='str', required=False), + ipam_driver=dict(type='str', required=False, + choices=['host-local', 'dhcp', 'none']), ipv6=dict(type='bool', required=False), subnet=dict(type='str', required=False), macvlan=dict(type='str', required=False), @@ -715,14 +836,23 @@ def main(): executable=dict(type='str', required=False, default='podman'), debug=dict(type='bool', default=False), recreate=dict(type='bool', default=False), + route=dict(type='list', elements='str', required=False), quadlet_dir=dict(type='path', required=False), quadlet_filename=dict(type='str', required=False), quadlet_options=dict(type='list', elements='str', required=False), + net_config=dict(type='list', required=False, elements='dict', + options=dict( + subnet=dict(type='str', required=True), + gateway=dict(type='str', required=True), + ip_range=dict(type='str', required=False), + )), ), required_by=dict( # for IP range and GW to set 'subnet' is required ip_range=('subnet'), gateway=('subnet'), - )) + ), + # define or subnet or net config + mutually_exclusive=[['subnet', 'net_config']]) PodmanNetworkManager(module).execute() diff --git a/ansible_collections/containers/podman/plugins/modules/podman_pod.py b/ansible_collections/containers/podman/plugins/modules/podman_pod.py index a975921ea..cdf728243 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_pod.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_pod.py @@ -117,6 +117,8 @@ options: all containers in the pod. type: list elements: str + aliases: + - dns_option required: false dns_search: description: @@ -125,6 +127,14 @@ options: type: list elements: str required: false + exit_policy: + description: + - Set the exit policy of the pod when the last container exits. Supported policies are stop and continue + choices: + - stop + - continue + type: str + required: false generate_systemd: description: - Generate systemd unit file for container. @@ -227,6 +237,11 @@ options: elements: str required: false type: list + gpus: + description: + - GPU devices to add to the container ('all' to pass all GPUs). + type: str + required: false hostname: description: - Set a hostname to the pod @@ -266,6 +281,11 @@ options: - Set a static IP for the pod's shared network. type: str required: false + ip6: + description: + - Set a static IPv6 for the pod's shared network. + type: str + required: false label: description: - Add metadata to a pod, pass dictionary of label keys and values. @@ -357,6 +377,16 @@ options: options as a list of lines to add. type: list elements: str + restart_policy: + description: + - Restart policy to follow when containers exit. + type: str + security_opt: + description: + - Security options for the pod. + type: list + elements: str + required: false share: description: - A comma delimited list of kernel namespaces to share. If none or "" is specified, @@ -364,6 +394,30 @@ options: user, uts. type: str required: false + share_parent: + description: + - This boolean determines whether or not all containers entering the pod use the pod as their cgroup parent. + The default value of this option in Podman is true. + type: bool + required: false + shm_size: + description: + - Set the size of the /dev/shm shared memory space. + A unit can be b (bytes), k (kibibytes), m (mebibytes), or g (gibibytes). + If the unit is omitted, the system uses bytes. + If the size is omitted, the default is 64m. + When size is 0, there is no limit on the amount of memory used for IPC by the pod. + type: str + required: false + shm_size_systemd: + description: + - Size of systemd-specific tmpfs mounts such as /run, /run/lock, /var/log/journal and /tmp. + A unit can be b (bytes), k (kibibytes), m (mebibytes), or g (gibibytes). + If the unit is omitted, the system uses bytes. + If the size is omitted, the default is 64m. + When size is 0, the usage is limited to 50 percents of the host's available memory. + type: str + required: false subgidname: description: - Name for GID map from the /etc/subgid file. Using this flag will run the container @@ -377,6 +431,11 @@ options: This flag conflicts with `userns` and `uidmap`. required: false type: str + sysctl: + description: + - Set kernel parameters for the pod. + type: dict + required: false uidmap: description: - Run the container in a new user namespace using the supplied mapping. @@ -393,6 +452,11 @@ options: An empty value ("") means user namespaces are disabled. required: false type: str + uts: + description: + - Set the UTS namespace mode for the pod. + required: false + type: str volume: description: - Create a bind mount. @@ -401,6 +465,12 @@ options: elements: str required: false type: list + volumes_from: + description: + - Mount volumes from the specified container. + elements: str + required: false + type: list executable: description: - Path to C(podman) executable if it is not in the C($PATH) on the @@ -450,7 +520,7 @@ pod: ''' -EXAMPLES = ''' +EXAMPLES = r''' # What modules does for example - containers.podman.podman_pod: name: pod1 @@ -465,6 +535,62 @@ EXAMPLES = ''' state: started publish: "127.0.0.1::80" +# Full workflow example with pod and containers +- name: Create a pod with parameters + containers.podman.podman_pod: + name: mypod + state: created + network: host + share: net + userns: auto + security_opt: + - seccomp=unconfined + - apparmor=unconfined + hostname: mypod + dns: + - 1.1.1.1 + volumes: + - /tmp:/tmp/:ro + label: + key: cval + otherkey: kddkdk + somekey: someval + add_host: + - "google:5.5.5.5" + +- name: Create containers attached to the pod + containers.podman.podman_container: + name: "{{ item }}" + state: created + pod: mypod + image: alpine + command: sleep 1h + loop: + - "container1" + - "container2" + +- name: Start pod + containers.podman.podman_pod: + name: mypod + state: started + network: host + share: net + userns: auto + security_opt: + - seccomp=unconfined + - apparmor=unconfined + hostname: mypod + dns: + - 1.1.1.1 + volumes: + - /tmp:/tmp/:ro + label: + key: cval + otherkey: kddkdk + somekey: someval + add_host: + - "google:5.5.5.5" + # Create a Quadlet file for a pod - containers.podman.podman_pod: name: qpod diff --git a/ansible_collections/containers/podman/plugins/modules/podman_search.py b/ansible_collections/containers/podman/plugins/modules/podman_search.py new file mode 100644 index 000000000..128e3ce03 --- /dev/null +++ b/ansible_collections/containers/podman/plugins/modules/podman_search.py @@ -0,0 +1,131 @@ +#!/usr/bin/python +# Copyright (c) 2024 Ansible Project +# 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 + + +DOCUMENTATION = r''' +module: podman_search +author: + - Derek Waters (@derekwaters) +short_description: Search for remote images using podman +notes: + - Podman may required elevated privileges in order to run properly. +description: + - Search for remote images using C(podman) +options: + executable: + description: + - Path to C(podman) executable if it is not in the C($PATH) on the machine running C(podman) + default: 'podman' + type: str + term: + description: + - The search term to look for. Will search all default registries unless a registry is defined in the search term. + type: str + required: True + limit: + description: + - Limit the number of image results returned from the search (per image registry) + required: False + default: 25 + type: int + list_tags: + description: + - Whether or not to return the list of tags associated with each image + required: False + default: False + type: bool + +''' + +EXAMPLES = r""" +- name: Search for any rhel images + containers.podman.podman_search: + term: "rhel" + limit: 3 + +- name: Gather info on a specific remote image + containers.podman.podman_search: + term: "myimageregistry.com/ansible-automation-platform/ee-minimal-rhel8" + +- name: Gather tag info on a known remote image + containers.podman.podman_search: + term: "myimageregistry.com/ansible-automation-platform/ee-minimal-rhel8" + list_tags: True +""" + +RETURN = r""" +images: + description: info from all or specified images + returned: always + type: list + sample: [ + { + "Automated": "", + "Description": "Red Hat Enterprise Linux Atomic Image is a minimal, fully supported base image.", + "Index": "registry.access.redhat.com", + "Name": "registry.access.redhat.com/rhel7-atomic", + "Official": "", + "Stars": 0, + "Tags": ["1.0", "1.1", "1.1.1-devel"] + } + ] +""" + +import json + +from ansible.module_utils.basic import AnsibleModule + + +def search_images(module, executable, term, limit, list_tags): + command = [executable, 'search', term, '--format', 'json'] + command.extend(['--limit', "{0}".format(limit)]) + if list_tags: + command.extend(['--list-tags']) + + rc, out, err = module.run_command(command) + + if rc != 0: + module.fail_json(msg="Unable to gather info for '{0}': {1}".format(term, err)) + return out + + +def main(): + module = AnsibleModule( + argument_spec=dict( + executable=dict(type='str', default='podman'), + term=dict(type='str', required=True), + limit=dict(type='int', required=False, default=25), + list_tags=dict(type='bool', required=False, default=False) + ), + supports_check_mode=True, + ) + + executable = module.params['executable'] + term = module.params.get('term') + limit = module.params.get('limit') + list_tags = module.params.get('list_tags') + executable = module.get_bin_path(executable, required=True) + + result_str = search_images(module, executable, term, limit, list_tags) + if result_str == "": + results = [] + else: + try: + results = json.loads(result_str) + except json.decoder.JSONDecodeError: + module.fail_json(msg='Failed to parse JSON output from podman search: {out}'.format(out=result_str)) + + results = dict( + changed=False, + images=results + ) + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/containers/podman/plugins/modules/podman_secret.py b/ansible_collections/containers/podman/plugins/modules/podman_secret.py index a31aae9dc..76b10ad39 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_secret.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_secret.py @@ -21,6 +21,7 @@ options: data: description: - The value of the secret. Required when C(state) is C(present). + Mutually exclusive with C(env) and C(path). type: str driver: description: @@ -31,6 +32,11 @@ options: description: - Driver-specific key-value options. type: dict + env: + description: + - The name of the environment variable that contains the secret. + Mutually exclusive with C(data) and C(path). + type: str executable: description: - Path to C(podman) executable if it is not in the C($PATH) on the @@ -53,6 +59,11 @@ options: - The name of the secret. required: True type: str + path: + description: + - Path to the file that contains the secret. + Mutually exclusive with C(data) and C(env). + type: path state: description: - Whether to create or remove the named secret. @@ -67,7 +78,7 @@ options: type: dict debug: description: - - Enable debug mode for module. + - Enable debug mode for module. It prints secrets diff. type: bool default: False ''' @@ -99,6 +110,8 @@ EXAMPLES = r""" name: mysecret """ +import os + from ansible.module_utils.basic import AnsibleModule from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion from ansible_collections.containers.podman.plugins.module_utils.podman.common import get_podman_version @@ -116,14 +129,15 @@ def podman_secret_exists(module, executable, name, version): return rc == 0 -def need_update(module, executable, name, data, driver, driver_opts, debug, labels): - +def need_update(module, executable, name, data, path, env, skip, driver, driver_opts, debug, labels): cmd = [executable, 'secret', 'inspect', '--showsecret', name] rc, out, err = module.run_command(cmd) if rc != 0: if debug: module.log("PODMAN-SECRET-DEBUG: Unable to get secret info: %s" % err) return True + if skip: + return False try: secret = module.from_json(out)[0] # We support only file driver for now @@ -131,10 +145,37 @@ def need_update(module, executable, name, data, driver, driver_opts, debug, labe if debug: module.log("PODMAN-SECRET-DEBUG: Idempotency of driver %s is not supported" % driver) return True - if secret['SecretData'] != data: - diff['after'] = "<different-secret>" - diff['before'] = "<secret>" - return True + if data: + if secret['SecretData'] != data: + if debug: + diff['after'] = data + diff['before'] = secret['SecretData'] + else: + diff['after'] = "<different-secret>" + diff['before'] = "<secret>" + return True + if path: + with open(path, 'rb') as f: + text = f.read().decode('utf-8') + if secret['SecretData'] != text: + if debug: + diff['after'] = text + diff['before'] = secret['SecretData'] + else: + diff['after'] = "<different-secret>" + diff['before'] = "<secret>" + return True + if env: + env_data = os.environ.get(env) + if secret['SecretData'] != env_data: + if debug: + diff['after'] = env_data + diff['before'] = secret['SecretData'] + else: + diff['after'] = "<different-secret>" + diff['before'] = "<secret>" + return True + if driver_opts: for k, v in driver_opts.items(): if secret['Spec']['Driver']['Options'].get(k) != v: @@ -154,13 +195,13 @@ def need_update(module, executable, name, data, driver, driver_opts, debug, labe return False -def podman_secret_create(module, executable, name, data, force, skip, +def podman_secret_create(module, executable, name, data, path, env, force, skip, driver, driver_opts, debug, labels): podman_version = get_podman_version(module, fail=False) if (podman_version is not None and LooseVersion(podman_version) >= LooseVersion('4.7.0') and (driver is None or driver == 'file')): - if not skip and need_update(module, executable, name, data, driver, driver_opts, debug, labels): + if need_update(module, executable, name, data, path, env, skip, driver, driver_opts, debug, labels): podman_secret_remove(module, executable, name) else: return {"changed": False} @@ -182,9 +223,20 @@ def podman_secret_create(module, executable, name, data, force, skip, cmd.append('--label') cmd.append("=".join([k, v])) cmd.append(name) - cmd.append('-') + if data: + cmd.append('-') + elif path: + cmd.append(path) + elif env: + if os.environ.get(env) is None: + module.fail_json(msg="Environment variable %s is not set" % env) + cmd.append("--env") + cmd.append(env) - rc, out, err = module.run_command(cmd, data=data, binary_data=True) + if data: + rc, out, err = module.run_command(cmd, data=data, binary_data=True) + else: + rc, out, err = module.run_command(cmd) if rc != 0: module.fail_json(msg="Unable to create secret: %s" % err) @@ -219,6 +271,8 @@ def main(): state=dict(type='str', default='present', choices=['absent', 'present']), name=dict(type='str', required=True), data=dict(type='str', no_log=True), + env=dict(type='str'), + path=dict(type='path'), force=dict(type='bool', default=False), skip_existing=dict(type='bool', default=False), driver=dict(type='str'), @@ -226,6 +280,8 @@ def main(): labels=dict(type='dict'), debug=dict(type='bool', default=False), ), + required_if=[('state', 'present', ['path', 'env', 'data'], True)], + mutually_exclusive=[['path', 'env', 'data']], ) state = module.params['state'] @@ -234,16 +290,16 @@ def main(): if state == 'present': data = module.params['data'] - if data is None: - raise Exception("'data' is required when 'state' is 'present'") force = module.params['force'] skip = module.params['skip_existing'] driver = module.params['driver'] driver_opts = module.params['driver_opts'] debug = module.params['debug'] labels = module.params['labels'] + path = module.params['path'] + env = module.params['env'] results = podman_secret_create(module, executable, - name, data, force, skip, + name, data, path, env, force, skip, driver, driver_opts, debug, labels) else: results = podman_secret_remove(module, executable, name) diff --git a/ansible_collections/containers/podman/plugins/modules/podman_volume.py b/ansible_collections/containers/podman/plugins/modules/podman_volume.py index 0b990354a..cb958cc50 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_volume.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_volume.py @@ -24,6 +24,8 @@ options: choices: - present - absent + - mounted + - unmounted - quadlet recreate: description: @@ -131,6 +133,7 @@ EXAMPLES = ''' ''' # noqa: F402 import json # noqa: F402 +import os # noqa: F402 from ansible.module_utils.basic import AnsibleModule # noqa: F402 from ansible.module_utils._text import to_bytes, to_native # noqa: F402 @@ -160,7 +163,7 @@ class PodmanVolumeModuleParams: Returns: list -- list of byte strings for Popen command """ - if self.action in ['delete']: + if self.action in ['delete', 'mount', 'unmount']: return self._simple_action() if self.action in ['create']: return self._create_action() @@ -169,6 +172,12 @@ class PodmanVolumeModuleParams: if self.action == 'delete': cmd = ['rm', '-f', self.params['name']] return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] + if self.action == 'mount': + cmd = ['mount', self.params['name']] + return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] + if self.action == 'unmount': + cmd = ['unmount', self.params['name']] + return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] def _create_action(self): cmd = [self.action, self.params['name']] @@ -326,6 +335,7 @@ class PodmanVolume: self.module = module self.name = name self.stdout, self.stderr = '', '' + self.mount_point = None self.info = self.get_info() self.version = self._get_podman_version() self.diff = {} @@ -380,7 +390,7 @@ class PodmanVolume: """Perform action with volume. Arguments: - action {str} -- action to perform - create, stop, delete + action {str} -- action to perform - create, delete, mount, unmout """ b_command = PodmanVolumeModuleParams(action, self.module.params, @@ -389,11 +399,14 @@ class PodmanVolume: ).construct_command_from_params() full_cmd = " ".join([self.module.params['executable'], 'volume'] + [to_native(i) for i in b_command]) + # check if running not from root + if os.getuid() != 0 and action == 'mount': + full_cmd = f"{self.module.params['executable']} unshare {full_cmd}" self.module.log("PODMAN-VOLUME-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'volume'] + b_command, + full_cmd, expand_user_and_vars=False) self.stdout = out self.stderr = err @@ -401,6 +414,9 @@ class PodmanVolume: self.module.fail_json( msg="Can't %s volume %s" % (action, self.name), stdout=out, stderr=err) + # in case of mount/unmount, return path to the volume from stdout + if action in ['mount']: + self.mount_point = out.strip() def delete(self): """Delete the volume.""" @@ -410,6 +426,14 @@ class PodmanVolume: """Create the volume.""" self._perform_action('create') + def mount(self): + """Delete the volume.""" + self._perform_action('mount') + + def unmount(self): + """Create the volume.""" + self._perform_action('unmount') + def recreate(self): """Recreate the volume.""" self.delete() @@ -468,6 +492,8 @@ class PodmanVolumeManager: states_map = { 'present': self.make_present, 'absent': self.make_absent, + 'mounted': self.make_mount, + 'unmounted': self.make_unmount, 'quadlet': self.make_quadlet, } process_action = states_map[self.state] @@ -501,6 +527,26 @@ class PodmanVolumeManager: 'podman_actions': self.volume.actions}) self.module.exit_json(**self.results) + def make_mount(self): + """Run actions if desired state is 'mounted'.""" + if not self.volume.exists: + self.volume.create() + self.results['actions'].append('created %s' % self.volume.name) + self.volume.mount() + self.results['actions'].append('mounted %s' % self.volume.name) + if self.volume.mount_point: + self.results.update({'mount_point': self.volume.mount_point}) + self.update_volume_result() + + def make_unmount(self): + """Run actions if desired state is 'unmounted'.""" + if self.volume.exists: + self.volume.unmount() + self.results['actions'].append('unmounted %s' % self.volume.name) + self.update_volume_result() + else: + self.module.fail_json(msg="Volume %s does not exist!" % self.name) + def make_quadlet(self): results_update = create_quadlet_state(self.module, "volume") self.results.update(results_update) @@ -511,7 +557,7 @@ def main(): module = AnsibleModule( argument_spec=dict( state=dict(type='str', default="present", - choices=['present', 'absent', 'quadlet']), + choices=['present', 'absent', 'mounted', 'unmounted', 'quadlet']), name=dict(type='str', required=True), label=dict(type='dict', required=False), driver=dict(type='str', required=False), |