diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-26 04:06:02 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-26 04:06:02 +0000 |
commit | e3eb94c23206603103f3c4faec6c227f59a1544c (patch) | |
tree | f2639459807ba88f55fc9c54d745bd7075d7f15c /ansible_collections/containers/podman/plugins | |
parent | Releasing progress-linux version 9.4.0+dfsg-1~progress7.99u1. (diff) | |
download | ansible-e3eb94c23206603103f3c4faec6c227f59a1544c.tar.xz ansible-e3eb94c23206603103f3c4faec6c227f59a1544c.zip |
Merging upstream version 9.5.1+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/containers/podman/plugins')
14 files changed, 1005 insertions, 39 deletions
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 ff4c18629..bf42ffdee 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 @@ -10,6 +10,9 @@ from ansible_collections.containers.podman.plugins.module_utils.podman.common im 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.quadlet import create_quadlet_state +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import ContainerQuadlet + __metaclass__ = type @@ -17,7 +20,7 @@ ARGUMENTS_SPEC_CONTAINER = dict( name=dict(required=True, type='str'), executable=dict(default='podman', type='str'), state=dict(type='str', default='started', choices=[ - 'absent', 'present', 'stopped', 'started', 'created']), + 'absent', 'present', 'stopped', 'started', 'created', 'quadlet']), image=dict(type='str'), annotation=dict(type='dict'), attach=dict(type='list', elements='str', choices=['stdout', 'stderr', 'stdin']), @@ -116,6 +119,9 @@ ARGUMENTS_SPEC_CONTAINER = dict( publish=dict(type='list', elements='str', aliases=[ 'ports', 'published', 'published_ports']), publish_all=dict(type='bool'), + quadlet_dir=dict(type='path'), + quadlet_filename=dict(type='str'), + quadlet_options=dict(type='list', elements='str'), read_only=dict(type='bool'), read_only_tmpfs=dict(type='bool'), recreate=dict(type='bool', default=False), @@ -743,6 +749,8 @@ class PodmanDefaults: 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')): @@ -1180,21 +1188,29 @@ class PodmanContainerDiff: 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']]: + 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: @@ -1236,6 +1252,8 @@ class PodmanContainerDiff: 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 @@ -1669,6 +1687,9 @@ class PodmanManager: else: self.results['diff']['before'] += sysd['diff']['before'] self.results['diff']['after'] += sysd['diff']['after'] + quadlet = ContainerQuadlet(self.module_params) + quadlet_content = quadlet.create_quadlet_content() + self.results.update({'podman_quadlet': quadlet_content}) def make_started(self): """Run actions if desired state is 'started'.""" @@ -1800,6 +1821,10 @@ class PodmanManager: self.results.update({'container': {}, 'podman_actions': self.container.actions}) + def make_quadlet(self): + results_update = create_quadlet_state(self.module, "container") + self.results.update(results_update) + def execute(self): """Execute the desired action according to map of actions & states.""" states_map = { @@ -1808,6 +1833,7 @@ class PodmanManager: 'absent': self.make_absent, 'stopped': self.make_stopped, 'created': self.make_created, + 'quadlet': self.make_quadlet, } process_action = states_map[self.state] process_action() 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 4106136e2..e0031351f 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 @@ -1,11 +1,12 @@ from __future__ import (absolute_import, division, print_function) -import json +import json # noqa: F402 from ansible.module_utils._text import to_bytes, to_native from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion from ansible_collections.containers.podman.plugins.module_utils.podman.common import lower_keys from ansible_collections.containers.podman.plugins.module_utils.podman.common import generate_systemd from ansible_collections.containers.podman.plugins.module_utils.podman.common import delete_systemd +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state, PodQuadlet __metaclass__ = type @@ -23,6 +24,7 @@ ARGUMENTS_SPEC_POD = dict( 'stopped', 'paused', 'unpaused', + 'quadlet' ]), recreate=dict(type='bool', default=False), add_host=dict(type='list', required=False, elements='str'), @@ -62,6 +64,9 @@ ARGUMENTS_SPEC_POD = dict( pod_id_file=dict(type='str', required=False), publish=dict(type='list', required=False, elements='str', aliases=['ports']), + quadlet_dir=dict(type='path'), + quadlet_filename=dict(type='str'), + quadlet_options=dict(type='list', elements='str'), share=dict(type='str', required=False), subgidname=dict(type='str', required=False), subuidname=dict(type='str', required=False), @@ -267,7 +272,7 @@ class PodmanPodModuleParams: return c def addparam_no_hosts(self, c): - return c + ["=".join('--no-hosts', self.params['no_hosts'])] + return c + ["=".join(['--no-hosts', self.params['no_hosts']])] def addparam_pid(self, c): return c + ['--pid', self.params['pid']] @@ -465,6 +470,7 @@ class PodmanPodDiff: 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'] @@ -472,16 +478,24 @@ class PodmanPodDiff: 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']]: + 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: @@ -492,6 +506,11 @@ class PodmanPodDiff: 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) @@ -507,6 +526,8 @@ class PodmanPodDiff: 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 @@ -658,7 +679,16 @@ class PodmanPod: # pylint: disable=unused-variable rc, out, err = self.module.run_command( [self.module_params['executable'], b'pod', b'inspect', self.name]) - return json.loads(out) if rc == 0 else {} + if rc == 0: + info = json.loads(out) + # from podman 5 onwards, this is a list of dicts, + # before it was just a single dict when querying + # a single pod + if isinstance(info, list): + return info[0] + else: + return info + return {} def get_ps(self): """Inspect pod process and gather info about it.""" @@ -791,6 +821,8 @@ class PodmanPodManager: (default: {True}) """ facts = self.pod.get_info() if changed else self.pod.info + if isinstance(facts, list): + facts = facts[0] out, err = self.pod.stdout, self.pod.stderr self.results.update({'changed': changed, 'pod': facts, 'podman_actions': self.pod.actions}, @@ -812,6 +844,9 @@ class PodmanPodManager: else: self.results['diff']['before'] += sysd['diff']['before'] self.results['diff']['after'] += sysd['diff']['after'] + quadlet = PodQuadlet(self.module_params) + quadlet_content = quadlet.create_quadlet_content() + self.results.update({'podman_quadlet': quadlet_content}) def execute(self): """Execute the desired action according to map of actions & states.""" @@ -824,7 +859,7 @@ class PodmanPodManager: 'killed': self.make_killed, 'paused': self.make_paused, 'unpaused': self.make_unpaused, - + 'quadlet': self.make_quadlet, } process_action = states_map[self.state] process_action() @@ -926,3 +961,7 @@ class PodmanPodManager: self.results.update({'changed': True}) self.results.update({'pod': {}, 'podman_actions': self.pod.actions}) + + def make_quadlet(self): + results_update = create_quadlet_state(self.module, "pod") + self.results.update(results_update) diff --git a/ansible_collections/containers/podman/plugins/module_utils/podman/quadlet.py b/ansible_collections/containers/podman/plugins/module_utils/podman/quadlet.py new file mode 100644 index 000000000..17764b60d --- /dev/null +++ b/ansible_collections/containers/podman/plugins/module_utils/podman/quadlet.py @@ -0,0 +1,636 @@ +# Copyright (c) 2024 Sagi Shnaidman (@sshnaidm) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os + +from ansible_collections.containers.podman.plugins.module_utils.podman.common import compare_systemd_file_content + +QUADLET_ROOT_PATH = "/etc/containers/systemd/" +QUADLET_NON_ROOT_PATH = "~/.config/containers/systemd/" + + +class Quadlet: + param_map = {} + + def __init__(self, section: str, params: dict): + self.section = section + self.custom_params = self.custom_prepare_params(params) + self.dict_params = self.prepare_params() + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for specific Quadlet types. + """ + # This should be implemented in child classes if needed. + return params + + def prepare_params(self) -> dict: + """ + Convert parameter values as per param_map. + """ + processed_params = [] + for param_key, quadlet_key in self.param_map.items(): + value = self.custom_params.get(param_key) + if value is not None: + if isinstance(value, list): + # Add an entry for each item in the list + for item in value: + processed_params.append([quadlet_key, item]) + else: + if isinstance(value, bool): + value = str(value).lower() + # Add a single entry for the key + processed_params.append([quadlet_key, value]) + return processed_params + + def create_quadlet_content(self) -> str: + """ + Construct the quadlet content as a string. + """ + custom_user_options = self.custom_params.get("quadlet_options") + custom_text = "\n" + "\n".join(custom_user_options) if custom_user_options else "" + return f"[{self.section}]\n" + "\n".join( + f"{key}={value}" for key, value in self.dict_params + ) + custom_text + "\n" + + def write_to_file(self, path: str): + """ + Write the quadlet content to a file at the specified path. + """ + content = self.create_quadlet_content() + with open(path, 'w') as file: + file.write(content) + + +class ContainerQuadlet(Quadlet): + param_map = { + 'cap_add': 'AddCapability', + 'device': 'AddDevice', + 'annotation': 'Annotation', + 'name': 'ContainerName', + # the following are not implemented yet in Podman module + 'AutoUpdate': 'AutoUpdate', + 'ContainersConfModule': 'ContainersConfModule', + # end of not implemented yet + 'dns': 'DNS', + 'dns_option': 'DNSOption', + 'dns_search': 'DNSSearch', + 'cap_drop': 'DropCapability', + 'entrypoint': 'Entrypoint', + 'env': 'Environment', + 'env_file': 'EnvironmentFile', + 'env_host': 'EnvironmentHost', + 'command': 'Exec', + 'expose': 'ExposeHostPort', + 'gidmap': 'GIDMap', + 'global_args': 'GlobalArgs', + 'group': 'Group', # Does not exist in module parameters + 'healthcheck': 'HealthCheckCmd', + '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 + 'hostname': 'HostName', + 'image': 'Image', + 'ip': 'IP', + # the following are not implemented yet in Podman module + 'IP6': 'IP6', + # end of not implemented yet + 'label': 'Label', + 'log_driver': 'LogDriver', + "Mask": "Mask", # add it in security_opt + 'mount': 'Mount', + 'network': 'Network', + 'no_new_privileges': 'NoNewPrivileges', + 'sdnotify': 'Notify', + 'pids_limit': 'PidsLimit', + 'pod': 'Pod', + 'publish': 'PublishPort', + # the following are not implemented yet in Podman module + "Pull": "Pull", + # end of not implemented yet + 'read_only': 'ReadOnly', + 'read_only_tmpfs': 'ReadOnlyTmpfs', + 'rootfs': 'Rootfs', + 'init': 'RunInit', + 'SeccompProfile': 'SeccompProfile', + 'secrets': 'Secret', + # All these are in security_opt + 'SecurityLabelDisable': 'SecurityLabelDisable', + 'SecurityLabelFileType': 'SecurityLabelFileType', + 'SecurityLabelLevel': 'SecurityLabelLevel', + 'SecurityLabelNested': 'SecurityLabelNested', + 'SecurityLabelType': 'SecurityLabelType', + 'shm_size': 'ShmSize', + 'stop_timeout': 'StopTimeout', + 'subgidname': 'SubGIDMap', + 'subuidname': 'SubUIDMap', + 'sysctl': 'Sysctl', + 'timezone': 'Timezone', + 'tmpfs': 'Tmpfs', + 'uidmap': 'UIDMap', + 'ulimit': 'Ulimit', + 'Unmask': 'Unmask', # --security-opt unmask=ALL + 'user': 'User', + 'userns': 'UserNS', + 'volume': 'Volume', + 'workdir': 'WorkingDir', + 'podman_args': 'PodmanArgs', + } + + def __init__(self, params: dict): + super().__init__("Container", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for container-specific parameters. + """ + # Work on params in params_map and convert them to a right form + if params["annotation"]: + params['annotation'] = ["%s=%s" % + (k, v) for k, v in params['annotation'].items()] + if params["cap_add"]: + params["cap_add"] = " ".join(params["cap_add"]) + if params["cap_drop"]: + params["cap_drop"] = " ".join(params["cap_drop"]) + if params["command"]: + params["command"] = (" ".join(params["command"]) + if isinstance(params["command"], list) + else params["command"]) + if params["label"]: + params["label"] = ["%s=%s" % (k, v) for k, v in params["label"].items()] + if params["env"]: + params["env"] = ["%s=%s" % (k, v) for k, v in params["env"].items()] + if params["sysctl"]: + params["sysctl"] = ["%s=%s" % (k, v) for k, v in params["sysctl"].items()] + if params["tmpfs"]: + params["tmpfs"] = ["%s:%s" % (k, v) if v else k for k, v in params["tmpfs"].items()] + + # Work on params which are not in the param_map but can be calculated + params["global_args"] = [] + if params["user"] and len(str(params["user"]).split(":")) > 1: + user, group = params["user"].split(":") + params["user"] = user + params["group"] = group + if params["security_opt"]: + if "no-new-privileges" in params["security_opt"]: + params["no_new_privileges"] = True + params["security_opt"].remove("no-new-privileges") + if params["log_level"]: + params["global_args"].append(f"--log-level {params['log_level']}") + if params["debug"]: + params["global_args"].append("--log-level debug") + + # Work on params which are not in the param_map and add them to PodmanArgs + params["podman_args"] = [] + if params["authfile"]: + params["podman_args"].append(f"--authfile {params['authfile']}") + if params["attach"]: + for attach in params["attach"]: + params["podman_args"].append(f"--attach {attach}") + if params["blkio_weight"]: + params["podman_args"].append(f"--blkio-weight {params['blkio_weight']}") + if params["blkio_weight_device"]: + params["podman_args"].append(" ".join([ + 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_parent"]: + params["podman_args"].append(f"--cgroup-parent {params['cgroup_parent']}") + if params["cidfile"]: + params["podman_args"].append(f"--cidfile {params['cidfile']}") + if params["conmon_pidfile"]: + params["podman_args"].append(f"--conmon-pidfile {params['conmon_pidfile']}") + if params["cpuset_cpus"]: + params["podman_args"].append(f"--cpuset-cpus {params['cpuset_cpus']}") + if params["cpuset_mems"]: + params["podman_args"].append(f"--cpuset-mems {params['cpuset_mems']}") + if params["cpu_period"]: + params["podman_args"].append(f"--cpu-period {params['cpu_period']}") + if params["cpu_quota"]: + params["podman_args"].append(f"--cpu-quota {params['cpu_quota']}") + if params["cpu_rt_period"]: + params["podman_args"].append(f"--cpu-rt-period {params['cpu_rt_period']}") + if params["cpu_rt_runtime"]: + 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["device_read_bps"]: + for i in params["device_read_bps"]: + params["podman_args"].append(f"--device-read-bps {i}") + if params["device_read_iops"]: + for i in params["device_read_iops"]: + params["podman_args"].append(f"--device-read-iops {i}") + if params["device_write_bps"]: + for i in params["device_write_bps"]: + params["podman_args"].append(f"--device-write-bps {i}") + if params["device_write_iops"]: + for i in params["device_write_iops"]: + params["podman_args"].append(f"--device-write-iops {i}") + if params["etc_hosts"]: + for host_ip in params['etc_hosts'].items(): + params["podman_args"].append(f"--add-host {':'.join(host_ip)}") + if params["hooks_dir"]: + for hook in params["hooks_dir"]: + params["podman_args"].append(f"--hooks-dir {hook}") + if params["http_proxy"]: + 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_path"]: + params["podman_args"].append(f"--init-path {params['init_path']}") + if params["interactive"]: + params["podman_args"].append("--interactive") + if params["ipc"]: + params["podman_args"].append(f"--ipc {params['ipc']}") + if params["kernel_memory"]: + params["podman_args"].append(f"--kernel-memory {params['kernel_memory']}") + if params["label_file"]: + params["podman_args"].append(f"--label-file {params['label_file']}") + if params["log_opt"]: + for k, v in params['log_opt'].items(): + params["podman_args"].append(f"--log-opt {k.replace('max_size', 'max-size')}={v}") + if params["mac_address"]: + params["podman_args"].append(f"--mac-address {params['mac_address']}") + if params["memory"]: + params["podman_args"].append(f"--memory {params['memory']}") + if params["memory_reservation"]: + params["podman_args"].append(f"--memory-reservation {params['memory_reservation']}") + if params["memory_swap"]: + params["podman_args"].append(f"--memory-swap {params['memory_swap']}") + if params["memory_swappiness"]: + params["podman_args"].append(f"--memory-swappiness {params['memory_swappiness']}") + if params["network_aliases"]: + for alias in params["network_aliases"]: + params["podman_args"].append(f"--network-alias {alias}") + 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["pid"]: + params["podman_args"].append(f"--pid {params['pid']}") + if params["privileged"]: + params["podman_args"].append("--privileged") + if params["publish_all"]: + params["podman_args"].append("--publish-all") + 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["rm"]: + params["podman_args"].append("--rm") + if params["security_opt"]: + for security_opt in params["security_opt"]: + params["podman_args"].append(f"--security-opt {security_opt}") + 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["tty"]: + params["podman_args"].append("--tty") + 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["cmd_args"]: + params["podman_args"].append(params["cmd_args"]) + + # Return params with custom processing applied + return params + + +class NetworkQuadlet(Quadlet): + param_map = { + 'name': 'NetworkName', + 'internal': 'Internal', + 'driver': 'Driver', + 'gateway': 'Gateway', + 'disable_dns': 'DisableDNS', + 'subnet': 'Subnet', + 'ip_range': 'IPRange', + 'ipv6': 'IPv6', + "opt": "Options", + # Add more parameter mappings specific to networks + 'ContainersConfModule': 'ContainersConfModule', + "DNS": "DNS", + "IPAMDriver": "IPAMDriver", + "Label": "Label", + "global_args": "GlobalArgs", + "podman_args": "PodmanArgs", + } + + def __init__(self, params: dict): + super().__init__("Network", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for network-specific parameters. + """ + # Work on params in params_map and convert them to a right form + if params["debug"]: + params["global_args"].append("--log-level debug") + if params["opt"]: + new_opt = [] + for k, v in params["opt"].items(): + if v is not None: + new_opt.append(f"{k}={v}") + params["opt"] = new_opt + return params + + +# This is a inherited class that represents a Quadlet file for the Podman pod +class PodQuadlet(Quadlet): + param_map = { + 'name': 'PodName', + "network": "Network", + "publish": "PublishPort", + "volume": "Volume", + 'ContainersConfModule': 'ContainersConfModule', + "global_args": "GlobalArgs", + "podman_args": "PodmanArgs", + } + + def __init__(self, params: dict): + super().__init__("Pod", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for pod-specific parameters. + """ + # Work on params in params_map and convert them to a right form + params["global_args"] = [] + params["podman_args"] = [] + + if params["add_host"]: + for host in params['add_host']: + params["podman_args"].append(f"--add-host {host}") + if params["cgroup_parent"]: + params["podman_args"].append(f"--cgroup-parent {params['cgroup_parent']}") + if params["blkio_weight"]: + params["podman_args"].append(f"--blkio-weight {params['blkio_weight']}") + if params["blkio_weight_device"]: + params["podman_args"].append(" ".join([ + f"--blkio-weight-device {':'.join(blkio)}" for blkio in params["blkio_weight_device"].items()])) + if params["cpuset_cpus"]: + params["podman_args"].append(f"--cpuset-cpus {params['cpuset_cpus']}") + if params["cpuset_mems"]: + params["podman_args"].append(f"--cpuset-mems {params['cpuset_mems']}") + if params["cpu_shares"]: + params["podman_args"].append(f"--cpu-shares {params['cpu_shares']}") + if params["cpus"]: + params["podman_args"].append(f"--cpus {params['cpus']}") + if params["device"]: + for device in params["device"]: + params["podman_args"].append(f"--device {device}") + if params["device_read_bps"]: + for i in params["device_read_bps"]: + params["podman_args"].append(f"--device-read-bps {i}") + if params["device_write_bps"]: + for i in params["device_write_bps"]: + params["podman_args"].append(f"--device-write-bps {i}") + if params["dns"]: + for dns in params["dns"]: + params["podman_args"].append(f"--dns {dns}") + if params["dns_opt"]: + for dns_option in params["dns_opt"]: + params["podman_args"].append(f"--dns-option {dns_option}") + if params["dns_search"]: + for dns_search in params["dns_search"]: + params["podman_args"].append(f"--dns-search {dns_search}") + if params["gidmap"]: + for gidmap in params["gidmap"]: + params["podman_args"].append(f"--gidmap {gidmap}") + if params["hostname"]: + params["podman_args"].append(f"--hostname {params['hostname']}") + if params["infra"]: + params["podman_args"].append(f"--infra {params['infra']}") + if params["infra_command"]: + params["podman_args"].append(f"--infra-command {params['infra_command']}") + if params["infra_conmon_pidfile"]: + params["podman_args"].append(f"--infra-conmon-pidfile {params['infra_conmon_pidfile']}") + if params["infra_image"]: + params["podman_args"].append(f"--infra-image {params['infra_image']}") + if params["infra_name"]: + params["podman_args"].append(f"--infra-name {params['infra_name']}") + if params["ip"]: + params["podman_args"].append(f"--ip {params['ip']}") + if params["label"]: + for label, label_v in params["label"].items(): + params["podman_args"].append(f"--label {label}={label_v}") + if params["label_file"]: + params["podman_args"].append(f"--label-file {params['label_file']}") + if params["mac_address"]: + params["podman_args"].append(f"--mac-address {params['mac_address']}") + if params["memory"]: + params["podman_args"].append(f"--memory {params['memory']}") + if params["memory_swap"]: + params["podman_args"].append(f"--memory-swap {params['memory_swap']}") + if params["no_hosts"]: + params["podman_args"].append(f"--no-hosts {params['no_hosts']}") + if params["pid"]: + 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["share"]: + params["podman_args"].append(f"--share {params['share']}") + if params["subgidname"]: + params["podman_args"].append(f"--subgidname {params['subgidname']}") + if params["subuidname"]: + params["podman_args"].append(f"--subuidname {params['subuidname']}") + 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["debug"]: + params["global_args"].append("--log-level debug") + + return params + + +# This is a inherited class that represents a Quadlet file for the Podman volume +class VolumeQuadlet(Quadlet): + param_map = { + 'name': 'VolumeName', + 'driver': 'Driver', + 'label': 'Label', + # 'opt': 'Options', + 'ContainersConfModule': 'ContainersConfModule', + 'global_args': 'GlobalArgs', + 'podman_args': 'PodmanArgs', + } + + def __init__(self, params: dict): + super().__init__("Volume", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for volume-specific parameters. + """ + # Work on params in params_map and convert them to a right form + params["global_args"] = [] + params["podman_args"] = [] + + if params["debug"]: + params["global_args"].append("--log-level debug") + if params["label"]: + params["label"] = ["%s=%s" % (k, v) for k, v in params["label"].items()] + if params["options"]: + for opt in params["options"]: + params["podman_args"].append(f"--opt {opt}") + + return params + + +# This is a inherited class that represents a Quadlet file for the Podman kube +class KubeQuadlet(Quadlet): + param_map = { + 'configmap': 'ConfigMap', + 'log_driver': 'LogDriver', + 'network': 'Network', + 'kube_file': 'Yaml', + 'userns': 'UserNS', + 'AutoUpdate': 'AutoUpdate', + 'ExitCodePropagation': 'ExitCodePropagation', + 'KubeDownForce': 'KubeDownForce', + 'PublishPort': 'PublishPort', + 'SetWorkingDirectory': 'SetWorkingDirectory', + 'ContainersConfModule': 'ContainersConfModule', + 'global_args': 'GlobalArgs', + 'podman_args': 'PodmanArgs', + } + + def __init__(self, params: dict): + super().__init__("Kube", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for kube-specific parameters. + """ + # Work on params in params_map and convert them to a right form + params["global_args"] = [] + params["podman_args"] = [] + + if params["debug"]: + params["global_args"].append("--log-level debug") + + return params + + +# This is a inherited class that represents a Quadlet file for the Podman image +class ImageQuadlet(Quadlet): + param_map = { + 'AllTags': 'AllTags', + 'arch': 'Arch', + 'authfile': 'AuthFile', + 'ca_cert_dir': 'CertDir', + 'creds': 'Creds', + 'DecryptionKey': 'DecryptionKey', + 'name': 'Image', + 'ImageTag': 'ImageTag', + 'OS': 'OS', + 'validate_certs': 'TLSVerify', + 'Variant': 'Variant', + 'ContainersConfModule': 'ContainersConfModule', + 'global_args': 'GlobalArgs', + 'podman_args': 'PodmanArgs', + } + + def __init__(self, params: dict): + super().__init__("Image", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for image-specific parameters. + """ + # Work on params in params_map and convert them to a right form + params["global_args"] = [] + params["podman_args"] = [] + + if params["username"] and params["password"]: + params["creds"] = f"{params['username']}:{params['password']}" + # if params['validate_certs'] is not None: + # params['validate_certs'] = str(params['validate_certs']).lower() + + return params + + +def check_quadlet_directory(module, quadlet_dir): + '''Check if the directory exists and is writable. If not, fail the module.''' + if not os.path.exists(quadlet_dir): + try: + os.makedirs(quadlet_dir) + except Exception as e: + module.fail_json( + msg="Directory for quadlet_file can't be created: %s" % e) + if not os.access(quadlet_dir, os.W_OK): + module.fail_json( + msg="Directory for quadlet_file is not writable: %s" % quadlet_dir) + + +def create_quadlet_state(module, issuer): + '''Create a quadlet file for the specified issuer.''' + class_map = { + "container": ContainerQuadlet, + "network": NetworkQuadlet, + "pod": PodQuadlet, + "volume": VolumeQuadlet, + "kube": KubeQuadlet, + "image": ImageQuadlet, + } + # Let's detect which user is running + user = "root" if os.geteuid() == 0 else "user" + quadlet_dir = module.params.get('quadlet_dir') + if not quadlet_dir: + if user == "root": + quadlet_dir = QUADLET_ROOT_PATH + else: + quadlet_dir = os.path.expanduser(QUADLET_NON_ROOT_PATH) + # Create a filename based on the issuer + if not module.params.get('name') and not module.params.get('quadlet_filename'): + module.fail_json(msg=f"Filename for {issuer} is required for creating a quadlet file.") + if issuer == "image": + name = module.params['name'].split("/")[-1].split(":")[0] + else: + name = module.params.get('name') + quad_file_name = module.params['quadlet_filename'] + if quad_file_name and not quad_file_name.endswith(f".{issuer}"): + quad_file_name = f"{quad_file_name}.{issuer}" + filename = quad_file_name or f"{name}.{issuer}" + quadlet_file_path = os.path.join(quadlet_dir, filename) + # Check if the directory exists and is writable + check_quadlet_directory(module, quadlet_dir) + # Check if file already exists and if it's different + quadlet = class_map[issuer](module.params) + quadlet_content = quadlet.create_quadlet_content() + file_diff = compare_systemd_file_content(quadlet_file_path, quadlet_content) + if bool(file_diff): + quadlet.write_to_file(quadlet_file_path) + results_update = { + 'changed': True, + "diff": { + "before": "\n".join(file_diff[0]) if isinstance(file_diff[0], list) else file_diff[0] + "\n", + "after": "\n".join(file_diff[1]) if isinstance(file_diff[1], list) else file_diff[1] + "\n", + }} + else: + results_update = {} + return results_update + +# Check with following command: +# QUADLET_UNIT_DIRS=<Directory> /usr/lib/systemd/system-generators/podman-system-generator {--user} --dryrun diff --git a/ansible_collections/containers/podman/plugins/modules/podman_container.py b/ansible_collections/containers/podman/plugins/modules/podman_container.py index 51cb57a53..75349f14e 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_container.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_container.py @@ -55,6 +55,8 @@ options: If container doesn't exist, the module creates it and leaves it in 'created' state. If configuration doesn't match or 'recreate' option is set, the container will be recreated + - I(quadlet) - Write a quadlet file with the specified configuration. + Requires the C(quadlet_dir) option to be set. type: str default: started choices: @@ -63,6 +65,7 @@ options: - stopped - started - created + - quadlet image: description: - Repository path (or image name) and tag used to create the container. @@ -721,6 +724,22 @@ options: - Publish all exposed ports to random ports on the host interfaces. The default is false. type: bool + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes C(name) value. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual container args + options as a list of lines to add. + type: list + elements: str read_only: description: - Mount the container's root filesystem as read only. Default is false @@ -994,6 +1013,23 @@ EXAMPLES = r""" - --deploy-hook - "echo 1 > /var/lib/letsencrypt/complete" +- name: Create a Quadlet file + containers.podman.podman_container: + name: quadlet-container + image: nginx + state: quadlet + quadlet_filename: custome-container + device: "/dev/sda:/dev/xvda:rwm" + ports: + - "8080:80" + volumes: + - "/var/www:/usr/share/nginx/html" + quadlet_options: + - "AutoUpdate=registry" + - "Pull=true" + - | + [Install] + WantedBy=default.target """ RETURN = r""" diff --git a/ansible_collections/containers/podman/plugins/modules/podman_container_exec.py b/ansible_collections/containers/podman/plugins/modules/podman_container_exec.py index d30e85cdb..1827b0ce7 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_container_exec.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_container_exec.py @@ -40,6 +40,11 @@ options: description: - Set environment variables. type: dict + executable: + description: + - The path to the podman executable. + type: str + default: podman privileged: description: - Give extended privileges to the container. @@ -141,6 +146,7 @@ def run_container_exec(module: AnsibleModule) -> dict: tty = module.params['tty'] user = module.params['user'] workdir = module.params['workdir'] + executable = module.params['executable'] if command is not None: argv = shlex.split(command) @@ -156,7 +162,7 @@ def run_container_exec(module: AnsibleModule) -> dict: to_text(value, errors='surrogate_or_strict') exec_options += ['--env', - '%s="%s"' % (key, value)] + '%s=%s' % (key, value)] if privileged: exec_options.append('--privileged') @@ -178,7 +184,7 @@ def run_container_exec(module: AnsibleModule) -> dict: exec_with_args.extend(exec_options) rc, stdout, stderr = run_podman_command( - module=module, executable='podman', args=exec_with_args) + module=module, executable=executable, args=exec_with_args, ignore_errors=True) result = { 'changed': changed, @@ -211,6 +217,10 @@ def main(): 'type': 'bool', 'default': False, }, + 'executable': { + 'type': 'str', + 'default': 'podman', + }, 'env': { 'type': 'dict', }, diff --git a/ansible_collections/containers/podman/plugins/modules/podman_generate_systemd.py b/ansible_collections/containers/podman/plugins/modules/podman_generate_systemd.py index 486a18a86..b27c16c6e 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_generate_systemd.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_generate_systemd.py @@ -183,6 +183,7 @@ EXAMPLES = ''' - name: Postgres container must be started and enabled on systemd ansible.builtin.systemd: name: container-postgres_local + scope: user daemon_reload: true state: started enabled: true diff --git a/ansible_collections/containers/podman/plugins/modules/podman_image.py b/ansible_collections/containers/podman/plugins/modules/podman_image.py index 6305a5d5b..7fcb0041a 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_image.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_image.py @@ -64,6 +64,7 @@ DOCUMENTATION = r''' - present - absent - build + - quadlet validate_certs: description: - Require HTTPS and validate certificates when pulling or pushing. Also used during build if a pull or push is necessary. @@ -175,6 +176,24 @@ DOCUMENTATION = r''' - docker-daemon - oci-archive - ostree + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + required: false + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes image name without prefixes and tags. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual network args + options as a list of lines to add. + type: list + elements: str + required: false ''' EXAMPLES = r""" @@ -280,6 +299,18 @@ EXAMPLES = r""" containers.podman.podman_image: name: nginx arch: amd64 + +- name: Create a quadlet file for an image + containers.podman.podman_image: + name: docker.io/library/alpine:latest + state: quadlet + quadlet_dir: /etc/containers/systemd + quadlet_filename: alpine-latest + quadlet_options: + - Variant=arm/v7 + - | + [Install] + WantedBy=default.target """ RETURN = r""" @@ -410,6 +441,7 @@ import shlex from ansible.module_utils._text import to_native from ansible.module_utils.basic import AnsibleModule from ansible_collections.containers.podman.plugins.module_utils.podman.common import run_podman_command +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state class PodmanImageManager(object): @@ -451,6 +483,9 @@ class PodmanImageManager(object): if self.state in ['absent']: self.absent() + if self.state == 'quadlet': + self.make_quadlet() + def _run(self, args, expected_rc=0, ignore_errors=False): cmd = " ".join([self.executable] + [to_native(i) for i in args]) @@ -537,6 +572,11 @@ class PodmanImageManager(object): if not self.module.check_mode: self.remove_image_id() + def make_quadlet(self): + results_update = create_quadlet_state(self.module, "image") + self.results.update(results_update) + self.module.exit_json(**self.results) + def find_image(self, image_name=None): if image_name is None: image_name = self.image_name @@ -810,13 +850,16 @@ def main(): push=dict(type='bool', default=False), path=dict(type='str'), force=dict(type='bool', default=False), - state=dict(type='str', default='present', choices=['absent', 'present', 'build']), + state=dict(type='str', default='present', choices=['absent', 'present', 'build', 'quadlet']), validate_certs=dict(type='bool', aliases=['tlsverify', 'tls_verify']), executable=dict(type='str', default='podman'), auth_file=dict(type='path', aliases=['authfile']), username=dict(type='str'), password=dict(type='str', no_log=True), ca_cert_dir=dict(type='path'), + quadlet_dir=dict(type='path', required=False), + quadlet_filename=dict(type='str'), + quadlet_options=dict(type='list', elements='str', required=False), build=dict( type='dict', aliases=['build_args', 'buildargs'], diff --git a/ansible_collections/containers/podman/plugins/modules/podman_image_info.py b/ansible_collections/containers/podman/plugins/modules/podman_image_info.py index d8af08814..02b0f9ed1 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_image_info.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_image_info.py @@ -48,7 +48,7 @@ RETURN = r""" images: description: info from all or specified images returned: always - type: dict + type: list sample: [ { "Annotations": {}, diff --git a/ansible_collections/containers/podman/plugins/modules/podman_login.py b/ansible_collections/containers/podman/plugins/modules/podman_login.py index 8ae8418a9..25bdb8d99 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_login.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_login.py @@ -154,10 +154,7 @@ def main(): supports_check_mode=True, required_together=( ['username', 'password'], - ), - mutually_exclusive=( - ['certdir', 'tlsverify'], - ), + ) ) registry = module.params['registry'] diff --git a/ansible_collections/containers/podman/plugins/modules/podman_network.py b/ansible_collections/containers/podman/plugins/modules/podman_network.py index 3f52af4ce..37bfefede 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_network.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_network.py @@ -127,11 +127,30 @@ options: choices: - present - absent + - quadlet recreate: description: - Recreate network even if exists. type: bool default: false + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + required: false + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes I(name) value. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual network args + options as a list of lines to add. + type: list + elements: str + required: false """ EXAMPLES = r""" @@ -148,6 +167,14 @@ EXAMPLES = r""" subnet: 192.168.22.0/24 gateway: 192.168.22.1 become: true + +- name: Create Quadlet file for podman network + containers.podman.podman_network: + name: podman_network + state: quadlet + quadlet_options: + - IPv6=true + - Label="ipv6 network" """ RETURN = r""" @@ -197,7 +224,7 @@ network: ] """ -import json # noqa: F402 +import json try: import ipaddress HAS_IP_ADDRESS_MODULE = True @@ -208,6 +235,7 @@ from ansible.module_utils.basic import AnsibleModule # noqa: F402 from ansible.module_utils._text import to_bytes, to_native # noqa: F402 from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion from ansible_collections.containers.podman.plugins.module_utils.podman.common import lower_keys +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state class PodmanNetworkModuleParams: @@ -620,6 +648,7 @@ class PodmanNetworkManager: states_map = { 'present': self.make_present, 'absent': self.make_absent, + 'quadlet': self.make_quadlet, } process_action = states_map[self.state] process_action() @@ -652,12 +681,17 @@ class PodmanNetworkManager: 'podman_actions': self.network.actions}) self.module.exit_json(**self.results) + def make_quadlet(self): + results_update = create_quadlet_state(self.module, "network") + self.results.update(results_update) + self.module.exit_json(**self.results) + def main(): module = AnsibleModule( argument_spec=dict( state=dict(type='str', default="present", - choices=['present', 'absent']), + choices=['present', 'absent', 'quadlet']), name=dict(type='str', required=True), disable_dns=dict(type='bool', required=False), driver=dict(type='str', required=False), @@ -681,6 +715,9 @@ def main(): executable=dict(type='str', required=False, default='podman'), debug=dict(type='bool', default=False), recreate=dict(type='bool', default=False), + quadlet_dir=dict(type='path', required=False), + quadlet_filename=dict(type='str', required=False), + quadlet_options=dict(type='list', elements='str', required=False), ), required_by=dict( # for IP range and GW to set 'subnet' is required ip_range=('subnet'), diff --git a/ansible_collections/containers/podman/plugins/modules/podman_play.py b/ansible_collections/containers/podman/plugins/modules/podman_play.py index 10a9a06fa..66138efc0 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_play.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_play.py @@ -103,7 +103,8 @@ options: required: false tag: description: - - specify a custom log tag for the container. This option is currently supported only by the journald log driver in Podman. + - Specify a custom log tag for the container. + This option is currently supported only by the journald log driver in Podman. type: str required: false log_level: @@ -131,6 +132,7 @@ options: - created - started - absent + - quadlet required: True tls_verify: description: @@ -158,6 +160,24 @@ options: An empty value ("") means user namespaces are disabled. required: false type: str + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + required: false + quadlet_filename: + description: + - Name of quadlet file to write. Must be specified if state is quadlet. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual network args + options as a list of lines to add. + type: list + elements: str + required: false ''' EXAMPLES = ''' @@ -178,6 +198,19 @@ EXAMPLES = ''' log_opt: path: /tmp/my-container.log max_size: 10mb + +- name: Create a Quadlet file + containers.podman.podman_play: + kube_file: ~/kube.yaml + state: quadlet + annotations: + greeting: hello + greet_to: world + userns: host + quadlet_filename: kube-pod + quadlet_options: + - "SetWorkingDirectory=yaml" + - "ExitCodePropagation=any" ''' import re # noqa: F402 try: @@ -187,6 +220,8 @@ except ImportError: HAS_YAML = False from ansible.module_utils.basic import AnsibleModule # noqa: F402 +from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion, get_podman_version +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state # noqa: F402 class PodmanKubeManagement: @@ -196,11 +231,12 @@ class PodmanKubeManagement: self.actions = [] self.executable = executable self.command = [self.executable, 'play', 'kube'] + self.version = get_podman_version(module) creds = [] # pod_name = extract_pod_name(module.params['kube_file']) if self.module.params['annotation']: for k, v in self.module.params['annotation'].items(): - self.command.extend(['--annotation', '"{k}={v}"'.format(k=k, v=v)]) + self.command.extend(['--annotation', '{k}={v}'.format(k=k, v=v)]) if self.module.params['username']: creds += [self.module.params['username']] if self.module.params['password']: @@ -244,17 +280,31 @@ class PodmanKubeManagement: self.module.log('PODMAN-PLAY-KUBE rc: %s' % rc) return rc, out, err + def tear_down_pods(self): + ''' + Tear down the pod and contaiers by using --down option in kube play + which is supported since Podman 3.4.0 + ''' + changed = False + kube_file = self.module.params['kube_file'] + + rc, out, err = self._command_run([self.executable, "kube", "play", "--down", kube_file]) + if rc != 0: + self.module.fail_json(msg="Failed to delete Pod with %s" % (kube_file)) + else: + changed = True + + return changed, out, err + def discover_pods(self): pod_name = '' if self.module.params['kube_file']: if HAS_YAML: with open(self.module.params['kube_file']) as f: - pod = yaml.safe_load(f) - if 'metadata' in pod: - pod_name = pod['metadata'].get('name') - else: - self.module.fail_json( - "No metadata in Kube file!\n%s" % pod) + pods = list(yaml.safe_load_all(f)) + for pod in pods: + if 'metadata' in pod and pod['kind'] in ['Deployment', 'Pod']: + pod_name = pod['metadata'].get('name') else: with open(self.module.params['kube_file']) as text: # the following formats are matched for a kube name: @@ -266,7 +316,7 @@ class PodmanKubeManagement: if re_pod: pod_name = re_pod.group(1) if not pod_name: - self.module.fail_json("Deployment doesn't have a name!") + self.module.fail_json("This Kube file doesn't have Pod or Deployment!") # Find all pods all_pods = '' # In case of one pod or replicasets @@ -294,8 +344,12 @@ class PodmanKubeManagement: return changed, out_all, err_all def pod_recreate(self): - pods = self.discover_pods() - self.remove_associated_pods(pods) + if self.version is not None and LooseVersion(self.version) >= LooseVersion('3.4.0'): + self.tear_down_pods() + else: + pods = self.discover_pods() + self.remove_associated_pods(pods) + # Create a pod rc, out, err = self._command_run(self.command) if rc != 0: @@ -318,6 +372,12 @@ class PodmanKubeManagement: changed = True return changed, out, err + def make_quadlet(self): + results = {"changed": False} + results_update = create_quadlet_state(self.module, "kube") + results.update(results_update) + self.module.exit_json(**results) + def main(): module = AnsibleModule( @@ -341,7 +401,7 @@ def main(): network=dict(type='list', elements='str'), state=dict( type='str', - choices=['started', 'created', 'absent'], + choices=['started', 'created', 'absent', 'quadlet'], required=True), tls_verify=dict(type='bool'), debug=dict(type='bool'), @@ -351,16 +411,28 @@ def main(): log_level=dict( type='str', choices=["debug", "info", "warn", "error", "fatal", "panic"]), + quadlet_dir=dict(type='path', required=False), + quadlet_filename=dict(type='str', required=False), + quadlet_options=dict(type='list', elements='str', required=False), ), supports_check_mode=True, + required_if=[ + ('state', 'quadlet', ['quadlet_filename']), + ], ) executable = module.get_bin_path( module.params['executable'], required=True) manage = PodmanKubeManagement(module, executable) if module.params['state'] == 'absent': - pods = manage.discover_pods() - changed, out, err = manage.remove_associated_pods(pods) + if manage.version is not None and LooseVersion(manage.version) > LooseVersion('3.4.0'): + manage.module.log(msg="version: %s, kube file %s" % (manage.version, manage.module.params['kube_file'])) + changed, out, err = manage.tear_down_pods() + else: + pods = manage.discover_pods() + changed, out, err = manage.remove_associated_pods(pods) + elif module.params['state'] == 'quadlet': + manage.make_quadlet() else: changed, out, err = manage.play() results = { diff --git a/ansible_collections/containers/podman/plugins/modules/podman_pod.py b/ansible_collections/containers/podman/plugins/modules/podman_pod.py index 7b57fd302..a975921ea 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_pod.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_pod.py @@ -30,6 +30,7 @@ options: - stopped - paused - unpaused + - quadlet recreate: description: - Use with present and started states to force the re-creation of an @@ -340,6 +341,22 @@ options: required: false aliases: - ports + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes I(name) value. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual container args + options as a list of lines to add. + type: list + elements: str share: description: - A comma delimited list of kernel namespaces to share. If none or "" is specified, @@ -435,7 +452,7 @@ pod: EXAMPLES = ''' # What modules does for example -- podman_pod: +- containers.podman.podman_pod: name: pod1 state: started ports: @@ -447,6 +464,16 @@ EXAMPLES = ''' name: pod2 state: started publish: "127.0.0.1::80" + +# Create a Quadlet file for a pod +- containers.podman.podman_pod: + name: qpod + state: quadlet + ports: + - "4444:5555" + volume: + - /var/run/docker.sock:/var/run/docker.sock + quadlet_dir: /custom/dir ''' from ansible.module_utils.basic import AnsibleModule # noqa: F402 from ..module_utils.podman.podman_pod_lib import PodmanPodManager # noqa: F402 @@ -454,9 +481,7 @@ from ..module_utils.podman.podman_pod_lib import ARGUMENTS_SPEC_POD # noqa: F40 def main(): - module = AnsibleModule( - argument_spec=ARGUMENTS_SPEC_POD - ) + module = AnsibleModule(argument_spec=ARGUMENTS_SPEC_POD) results = PodmanPodManager(module, module.params).execute() module.exit_json(**results) diff --git a/ansible_collections/containers/podman/plugins/modules/podman_pod_info.py b/ansible_collections/containers/podman/plugins/modules/podman_pod_info.py index 8b2a4bf06..8597ae98d 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_pod_info.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_pod_info.py @@ -109,9 +109,12 @@ def get_pod_info(module, executable, name): rc, out, err = module.run_command(command + [pod]) errs.append(err.strip()) rcs += [rc] - if not out or json.loads(out) is None or not json.loads(out): + data = json.loads(out) if out else None + if isinstance(data, list) and data: + data = data[0] + if not out or data is None or not data: continue - result.append(json.loads(out)) + result.append(data) return result, errs, rcs diff --git a/ansible_collections/containers/podman/plugins/modules/podman_volume.py b/ansible_collections/containers/podman/plugins/modules/podman_volume.py index b4d5062fa..0b990354a 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_volume.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_volume.py @@ -24,6 +24,7 @@ options: choices: - present - absent + - quadlet recreate: description: - Recreate volume even if exists. @@ -62,6 +63,24 @@ options: - Return additional information which can be helpful for investigations. type: bool default: False + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + required: false + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes I(name) value. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual network args + options as a list of lines to add. + type: list + elements: str + required: false requirements: - "podman" @@ -88,7 +107,8 @@ volume: EXAMPLES = ''' # What modules does for example -- podman_volume: +- name: Create a volume + containers.podman.podman_volume: state: present name: volume1 label: @@ -97,6 +117,17 @@ EXAMPLES = ''' options: - "device=/dev/loop1" - "type=ext4" + +- name: Create a Quadlet file for a volume + containers.podman.podman_volume: + state: quadlet + name: quadlet_volume + quadlet_filename: custom-name + quadlet_options: + - Group=192 + - Copy=true + - Image=quay.io/centos/centos:latest + ''' # noqa: F402 import json # noqa: F402 @@ -105,6 +136,7 @@ from ansible.module_utils.basic import AnsibleModule # noqa: F402 from ansible.module_utils._text import to_bytes, to_native # noqa: F402 from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion from ansible_collections.containers.podman.plugins.module_utils.podman.common import lower_keys +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state class PodmanVolumeModuleParams: @@ -436,6 +468,7 @@ class PodmanVolumeManager: states_map = { 'present': self.make_present, 'absent': self.make_absent, + 'quadlet': self.make_quadlet, } process_action = states_map[self.state] process_action() @@ -468,12 +501,17 @@ class PodmanVolumeManager: 'podman_actions': self.volume.actions}) self.module.exit_json(**self.results) + def make_quadlet(self): + results_update = create_quadlet_state(self.module, "volume") + self.results.update(results_update) + self.module.exit_json(**self.results) + def main(): module = AnsibleModule( argument_spec=dict( state=dict(type='str', default="present", - choices=['present', 'absent']), + choices=['present', 'absent', 'quadlet']), name=dict(type='str', required=True), label=dict(type='dict', required=False), driver=dict(type='str', required=False), @@ -481,6 +519,9 @@ def main(): recreate=dict(type='bool', default=False), executable=dict(type='str', required=False, default='podman'), debug=dict(type='bool', default=False), + quadlet_dir=dict(type='path', required=False), + quadlet_filename=dict(type='str', required=False), + quadlet_options=dict(type='list', elements='str', required=False), )) PodmanVolumeManager(module).execute() |