summaryrefslogtreecommitdiffstats
path: root/ansible_collections/containers/podman/plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-26 04:06:02 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-26 04:06:02 +0000
commite3eb94c23206603103f3c4faec6c227f59a1544c (patch)
treef2639459807ba88f55fc9c54d745bd7075d7f15c /ansible_collections/containers/podman/plugins
parentReleasing progress-linux version 9.4.0+dfsg-1~progress7.99u1. (diff)
downloadansible-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')
-rw-r--r--ansible_collections/containers/podman/plugins/module_utils/podman/podman_container_lib.py30
-rw-r--r--ansible_collections/containers/podman/plugins/module_utils/podman/podman_pod_lib.py49
-rw-r--r--ansible_collections/containers/podman/plugins/module_utils/podman/quadlet.py636
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_container.py36
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_container_exec.py14
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_generate_systemd.py1
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_image.py45
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_image_info.py2
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_login.py5
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_network.py41
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_play.py100
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_pod.py33
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_pod_info.py7
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_volume.py45
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()