summaryrefslogtreecommitdiffstats
path: root/ansible_collections/containers/podman/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/containers/podman/plugins')
-rw-r--r--ansible_collections/containers/podman/plugins/become/podman_unshare.py144
-rw-r--r--ansible_collections/containers/podman/plugins/connection/__init__.py0
-rw-r--r--ansible_collections/containers/podman/plugins/connection/buildah.py203
-rw-r--r--ansible_collections/containers/podman/plugins/connection/podman.py231
-rw-r--r--ansible_collections/containers/podman/plugins/module_utils/__init__.py0
-rw-r--r--ansible_collections/containers/podman/plugins/module_utils/podman/__init__.py0
-rw-r--r--ansible_collections/containers/podman/plugins/module_utils/podman/common.py232
-rw-r--r--ansible_collections/containers/podman/plugins/module_utils/podman/podman_container_lib.py1696
-rw-r--r--ansible_collections/containers/podman/plugins/module_utils/podman/podman_pod_lib.py880
-rw-r--r--ansible_collections/containers/podman/plugins/modules/__init__.py0
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_container.py1063
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_container_info.py416
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_containers.py132
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_export.py106
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_generate_systemd.py604
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_image.py862
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_image_info.py236
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_import.py157
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_load.py199
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_login.py184
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_login_info.py116
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_logout.py153
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_network.py673
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_network_info.py138
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_play.py311
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_pod.py415
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_pod_info.py145
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_prune.py252
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_save.py145
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_secret.py178
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_tag.py91
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_volume.py484
-rw-r--r--ansible_collections/containers/podman/plugins/modules/podman_volume_info.py100
33 files changed, 10546 insertions, 0 deletions
diff --git a/ansible_collections/containers/podman/plugins/become/podman_unshare.py b/ansible_collections/containers/podman/plugins/become/podman_unshare.py
new file mode 100644
index 000000000..6453f2397
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/become/podman_unshare.py
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2022 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+# Written by Janos Gerzson (grzs@backendo.com)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = """
+ name: podman_unshare
+ short_description: Run tasks using podman unshare
+ description:
+ - "This become plugins allows your remote/login user
+ to execute commands in its container user namespace.
+ Official documentation: https://docs.podman.io/en/latest/markdown/podman-unshare.1.html"
+ author:
+ - Janos Gerzson (@grzs)
+ version_added: 1.9.0
+ options:
+ become_user:
+ description: User you 'become' to execute the task ('root' is not a valid value here).
+ ini:
+ - section: privilege_escalation
+ key: become_user
+ - section: sudo_become_plugin
+ key: user
+ vars:
+ - name: ansible_become_user
+ - name: ansible_sudo_user
+ env:
+ - name: ANSIBLE_BECOME_USER
+ - name: ANSIBLE_SUDO_USER
+ become_exe:
+ description: Sudo executable
+ default: sudo
+ ini:
+ - section: privilege_escalation
+ key: become_exe
+ - section: sudo_become_plugin
+ key: executable
+ vars:
+ - name: ansible_become_exe
+ - name: ansible_sudo_exe
+ env:
+ - name: ANSIBLE_BECOME_EXE
+ - name: ANSIBLE_SUDO_EXE
+ become_pass:
+ description: Password to pass to sudo
+ required: False
+ vars:
+ - name: ansible_become_password
+ - name: ansible_become_pass
+ - name: ansible_sudo_pass
+ env:
+ - name: ANSIBLE_BECOME_PASS
+ - name: ANSIBLE_SUDO_PASS
+ ini:
+ - section: sudo_become_plugin
+ key: password
+"""
+
+EXAMPLES = """
+- name: checking uid of file 'foo'
+ ansible.builtin.stat:
+ path: "{{ test_dir }}/foo"
+ register: foo
+- ansible.builtin.debug:
+ var: foo.stat.uid
+# The output shows that it's owned by the login user
+# ok: [test_host] => {
+# "foo.stat.uid": "1003"
+# }
+
+- name: mounting the file to an unprivileged container and modifying its owner
+ containers.podman.podman_container:
+ name: chmod_foo
+ image: alpine
+ rm: true
+ volume:
+ - "{{ test_dir }}:/opt/test:z"
+ command: chown 1000 /opt/test/foo
+
+# Now the file 'foo' is owned by the container uid 1000,
+# which is mapped to something completaly different on the host.
+# It creates a situation when the file is unaccessible to the host user (uid 1003)
+# Running stat again, debug output will be like this:
+# ok: [test_host] => {
+# "foo.stat.uid": "328679"
+# }
+
+- name: running stat in modified user namespace
+ become_method: containers.podman.podman_unshare
+ become: true
+ ansible.builtin.stat:
+ path: "{{ test_dir }}/foo"
+ register: foo
+# By gathering file stats with podman_ushare
+# we can see the uid set in the container:
+# ok: [test_host] => {
+# "foo.stat.uid": "1000"
+# }
+
+- name: resetting file ownership with podman unshare
+ become_method: containers.podman.podman_unshare
+ become: true
+ ansible.builtin.file:
+ state: file
+ path: "{{ test_dir }}/foo"
+ owner: 0 # in a modified user namespace host uid is mapped to 0
+# If we run stat and debug with 'become: false',
+# we can see that the file is ours again:
+# ok: [test_host] => {
+# "foo.stat.uid": "1003"
+# }
+"""
+
+
+from ansible.plugins.become import BecomeBase
+
+
+class BecomeModule(BecomeBase):
+
+ name = 'containers.podman.podman_unshare'
+
+ def build_become_command(self, cmd, shell):
+ super(BecomeModule, self).build_become_command(cmd, shell)
+
+ if not cmd:
+ return cmd
+
+ becomecmd = 'podman unshare'
+
+ user = self.get_option('become_user') or 'root'
+ if user != 'root':
+ cmdlist = [self.get_option('become_exe') or 'sudo']
+ # -i is required, because
+ # podman unshare should be executed in a login shell to avoid chdir permission errors
+ cmdlist.append('-iu %s' % user)
+ if self.get_option('become_pass'):
+ self.prompt = '[sudo podman unshare via ansible, key=%s] password:' % self._id
+ cmdlist.append('-p "%s"' % self.prompt)
+ cmdlist.append('-- %s' % becomecmd)
+ becomecmd = ' '.join(cmdlist)
+
+ return ' '.join([becomecmd, self._build_success_command(cmd, shell)])
diff --git a/ansible_collections/containers/podman/plugins/connection/__init__.py b/ansible_collections/containers/podman/plugins/connection/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/connection/__init__.py
diff --git a/ansible_collections/containers/podman/plugins/connection/buildah.py b/ansible_collections/containers/podman/plugins/connection/buildah.py
new file mode 100644
index 000000000..69fc63c47
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/connection/buildah.py
@@ -0,0 +1,203 @@
+# Based on the docker connection plugin
+# Copyright (c) 2017 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+# Connection plugin for building container images using buildah tool
+# https://github.com/projectatomic/buildah
+#
+# Written by: Tomas Tomecek (https://github.com/TomasTomecek)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+ short_description: Interact with an existing buildah container
+ description:
+ - Run commands or put/fetch files to an existing container using buildah tool.
+ author: Tomas Tomecek (@TomasTomecek)
+ name: buildah
+ options:
+ remote_addr:
+ description:
+ - The ID of the container you want to access.
+ default: inventory_hostname
+ vars:
+ - name: ansible_host
+ - name: inventory_hostname
+# keyword:
+# - name: hosts
+ remote_user:
+ description:
+ - User specified via name or ID which is used to execute commands inside the container.
+ ini:
+ - section: defaults
+ key: remote_user
+ env:
+ - name: ANSIBLE_REMOTE_USER
+ vars:
+ - name: ansible_user
+# keyword:
+# - name: remote_user
+'''
+
+import os
+import shlex
+import shutil
+import subprocess
+
+from ansible.errors import AnsibleError
+from ansible.module_utils._text import to_bytes, to_native, to_text
+from ansible.plugins.connection import ConnectionBase, ensure_connect
+from ansible.utils.display import Display
+
+display = Display()
+
+
+# this _has to be_ named Connection
+class Connection(ConnectionBase):
+ """
+ This is a connection plugin for buildah: it uses buildah binary to interact with the containers
+ """
+
+ # String used to identify this Connection class from other classes
+ transport = 'containers.podman.buildah'
+ has_pipelining = True
+
+ def __init__(self, play_context, new_stdin, *args, **kwargs):
+ super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+ self._container_id = self._play_context.remote_addr
+ self._connected = False
+ # container filesystem will be mounted here on host
+ self._mount_point = None
+ # `buildah inspect` doesn't contain info about what the default user is -- if it's not
+ # set, it's empty
+ self.user = self._play_context.remote_user
+ display.vvvv("Using buildah connection from collection")
+
+ def _set_user(self):
+ self._buildah(b"config", [b"--user=" + to_bytes(self.user, errors='surrogate_or_strict')])
+
+ def _buildah(self, cmd, cmd_args=None, in_data=None, outfile_stdout=None):
+ """
+ run buildah executable
+
+ :param cmd: buildah's command to execute (str)
+ :param cmd_args: list of arguments to pass to the command (list of str/bytes)
+ :param in_data: data passed to buildah's stdin
+ :param outfile_stdout: file for writing STDOUT to
+ :return: return code, stdout, stderr
+ """
+ buildah_exec = 'buildah'
+ local_cmd = [buildah_exec]
+
+ if isinstance(cmd, str):
+ local_cmd.append(cmd)
+ else:
+ local_cmd.extend(cmd)
+ if self.user and self.user != 'root':
+ if cmd == 'run':
+ local_cmd.extend(("--user", self.user))
+ elif cmd == 'copy':
+ local_cmd.extend(("--chown", self.user))
+ local_cmd.append(self._container_id)
+
+ if cmd_args:
+ if isinstance(cmd_args, str):
+ local_cmd.append(cmd_args)
+ else:
+ local_cmd.extend(cmd_args)
+
+ local_cmd = [to_bytes(i, errors='surrogate_or_strict')
+ for i in local_cmd]
+
+ display.vvv("RUN %s" % (local_cmd,), host=self._container_id)
+ if outfile_stdout:
+ stdout_fd = open(outfile_stdout, "wb")
+ else:
+ stdout_fd = subprocess.PIPE
+ p = subprocess.Popen(local_cmd, shell=False, stdin=subprocess.PIPE,
+ stdout=stdout_fd, stderr=subprocess.PIPE)
+
+ stdout, stderr = p.communicate(input=in_data)
+ display.vvvv("STDOUT %s" % to_text(stdout))
+ display.vvvv("STDERR %s" % to_text(stderr))
+ display.vvvv("RC CODE %s" % p.returncode)
+ stdout = to_bytes(stdout, errors='surrogate_or_strict')
+ stderr = to_bytes(stderr, errors='surrogate_or_strict')
+ return p.returncode, stdout, stderr
+
+ def _connect(self):
+ """
+ no persistent connection is being maintained, mount container's filesystem
+ so we can easily access it
+ """
+ super(Connection, self)._connect()
+ rc, self._mount_point, stderr = self._buildah("mount")
+ if rc != 0:
+ display.v("Failed to mount container %s: %s" % (self._container_id, stderr.strip()))
+ else:
+ self._mount_point = self._mount_point.strip() + to_bytes(os.path.sep, errors='surrogate_or_strict')
+ display.vvvv("MOUNTPOINT %s RC %s STDERR %r" % (self._mount_point, rc, stderr))
+ self._connected = True
+
+ @ensure_connect
+ def exec_command(self, cmd, in_data=None, sudoable=False):
+ """ run specified command in a running OCI container using buildah """
+ super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+ # shlex.split has a bug with text strings on Python-2.6 and can only handle text strings on Python-3
+ cmd_args_list = shlex.split(to_native(cmd, errors='surrogate_or_strict'))
+
+ rc, stdout, stderr = self._buildah("run", cmd_args_list, in_data)
+
+ display.vvvv("STDOUT %r\nSTDERR %r" % (stderr, stderr))
+ return rc, stdout, stderr
+
+ def put_file(self, in_path, out_path):
+ """ Place a local file located in 'in_path' inside container at 'out_path' """
+ super(Connection, self).put_file(in_path, out_path)
+ display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._container_id)
+ if not self._mount_point or self.user:
+ rc, stdout, stderr = self._buildah(
+ "copy", [in_path, out_path])
+ if rc != 0:
+ raise AnsibleError(
+ "Failed to copy file from %s to %s in container %s\n%s" % (
+ in_path, out_path, self._container_id, stderr)
+ )
+ else:
+ real_out_path = self._mount_point + to_bytes(out_path, errors='surrogate_or_strict')
+ shutil.copyfile(
+ to_bytes(in_path, errors='surrogate_or_strict'),
+ to_bytes(real_out_path, errors='surrogate_or_strict')
+ )
+
+ def fetch_file(self, in_path, out_path):
+ """ obtain file specified via 'in_path' from the container and place it at 'out_path' """
+ super(Connection, self).fetch_file(in_path, out_path)
+ display.vvv("FETCH %s TO %s" %
+ (in_path, out_path), host=self._container_id)
+ if not self._mount_point:
+ rc, stdout, stderr = self._buildah(
+ "run",
+ ["cat", to_bytes(in_path, errors='surrogate_or_strict')],
+ outfile_stdout=out_path)
+ if rc != 0:
+ raise AnsibleError("Failed to fetch file from %s to %s from container %s\n%s" % (
+ in_path, out_path, self._container_id, stderr))
+ else:
+ real_in_path = self._mount_point + \
+ to_bytes(in_path, errors='surrogate_or_strict')
+ shutil.copyfile(
+ to_bytes(real_in_path, errors='surrogate_or_strict'),
+ to_bytes(out_path, errors='surrogate_or_strict')
+ )
+
+ def close(self):
+ """ unmount container's filesystem """
+ super(Connection, self).close()
+ rc, stdout, stderr = self._buildah("umount")
+ display.vvvv("RC %s STDOUT %r STDERR %r" % (rc, stdout, stderr))
+ self._connected = False
diff --git a/ansible_collections/containers/podman/plugins/connection/podman.py b/ansible_collections/containers/podman/plugins/connection/podman.py
new file mode 100644
index 000000000..2ade9180d
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/connection/podman.py
@@ -0,0 +1,231 @@
+# Based on the buildah connection plugin
+# Copyright (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+# Connection plugin to interact with existing podman containers.
+# https://github.com/containers/libpod
+#
+# Written by: Tomas Tomecek (https://github.com/TomasTomecek)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = '''
+ author: Tomas Tomecek (@TomasTomecek)
+ name: podman
+ short_description: Interact with an existing podman container
+ description:
+ - Run commands or put/fetch files to an existing container using podman tool.
+ options:
+ remote_addr:
+ description:
+ - The ID of the container you want to access.
+ default: inventory_hostname
+ vars:
+ - name: ansible_host
+ - name: inventory_hostname
+ - name: ansible_podman_host
+ remote_user:
+ description:
+ - User specified via name or UID which is used to execute commands inside the container. If you
+ specify the user via UID, you must set C(ANSIBLE_REMOTE_TMP) to a path that exits
+ inside the container and is writable by Ansible.
+ ini:
+ - section: defaults
+ key: remote_user
+ env:
+ - name: ANSIBLE_REMOTE_USER
+ vars:
+ - name: ansible_user
+ podman_extra_args:
+ description:
+ - Extra arguments to pass to the podman command line.
+ default: ''
+ ini:
+ - section: defaults
+ key: podman_extra_args
+ vars:
+ - name: ansible_podman_extra_args
+ env:
+ - name: ANSIBLE_PODMAN_EXTRA_ARGS
+ podman_executable:
+ description:
+ - Executable for podman command.
+ default: podman
+ vars:
+ - name: ansible_podman_executable
+ env:
+ - name: ANSIBLE_PODMAN_EXECUTABLE
+'''
+
+import os
+import shlex
+import shutil
+import subprocess
+
+from ansible.module_utils.common.process import get_bin_path
+from ansible.errors import AnsibleError
+from ansible.module_utils._text import to_bytes, to_native
+from ansible.plugins.connection import ConnectionBase, ensure_connect
+from ansible.utils.display import Display
+
+display = Display()
+
+
+# this _has to be_ named Connection
+class Connection(ConnectionBase):
+ """
+ This is a connection plugin for podman. It uses podman binary to interact with the containers
+ """
+
+ # String used to identify this Connection class from other classes
+ transport = 'containers.podman.podman'
+ # We know that pipelining does not work with podman. Do not enable it, or
+ # users will start containers and fail to connect to them.
+ has_pipelining = False
+
+ def __init__(self, play_context, new_stdin, *args, **kwargs):
+ super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+ self._container_id = self._play_context.remote_addr
+ self._connected = False
+ # container filesystem will be mounted here on host
+ self._mount_point = None
+ self.user = self._play_context.remote_user
+ display.vvvv("Using podman connection from collection")
+
+ def _podman(self, cmd, cmd_args=None, in_data=None, use_container_id=True):
+ """
+ run podman executable
+
+ :param cmd: podman's command to execute (str or list)
+ :param cmd_args: list of arguments to pass to the command (list of str/bytes)
+ :param in_data: data passed to podman's stdin
+ :param use_container_id: whether to append the container ID to the command
+ :return: return code, stdout, stderr
+ """
+ podman_exec = self.get_option('podman_executable')
+ try:
+ podman_cmd = get_bin_path(podman_exec)
+ except ValueError:
+ raise AnsibleError("%s command not found in PATH" % podman_exec)
+ if not podman_cmd:
+ raise AnsibleError("%s command not found in PATH" % podman_exec)
+ local_cmd = [podman_cmd]
+ if self.get_option('podman_extra_args'):
+ local_cmd += shlex.split(
+ to_native(
+ self.get_option('podman_extra_args'),
+ errors='surrogate_or_strict'))
+ if isinstance(cmd, str):
+ local_cmd.append(cmd)
+ else:
+ local_cmd.extend(cmd)
+
+ if use_container_id:
+ local_cmd.append(self._container_id)
+ if cmd_args:
+ local_cmd += cmd_args
+ local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
+
+ display.vvv("RUN %s" % (local_cmd,), host=self._container_id)
+ p = subprocess.Popen(local_cmd, shell=False, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ stdout, stderr = p.communicate(input=in_data)
+ display.vvvvv("STDOUT %s" % stdout)
+ display.vvvvv("STDERR %s" % stderr)
+ display.vvvvv("RC CODE %s" % p.returncode)
+ stdout = to_bytes(stdout, errors='surrogate_or_strict')
+ stderr = to_bytes(stderr, errors='surrogate_or_strict')
+ return p.returncode, stdout, stderr
+
+ def _connect(self):
+ """
+ no persistent connection is being maintained, mount container's filesystem
+ so we can easily access it
+ """
+ super(Connection, self)._connect()
+ rc, self._mount_point, stderr = self._podman("mount")
+ if rc != 0:
+ display.vvvv("Failed to mount container %s: %s" % (self._container_id, stderr.strip()))
+ elif not os.listdir(self._mount_point.strip()):
+ display.vvvv("Failed to mount container with CGroups2: empty dir %s" % self._mount_point.strip())
+ self._mount_point = None
+ else:
+ self._mount_point = self._mount_point.strip()
+ display.vvvvv("MOUNTPOINT %s RC %s STDERR %r" % (self._mount_point, rc, stderr))
+ self._connected = True
+
+ @ensure_connect
+ def exec_command(self, cmd, in_data=None, sudoable=False):
+ """ run specified command in a running OCI container using podman """
+ super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+ # shlex.split has a bug with text strings on Python-2.6 and can only handle text strings on Python-3
+ cmd_args_list = shlex.split(to_native(cmd, errors='surrogate_or_strict'))
+ exec_args_list = ["exec"]
+ if self.user:
+ exec_args_list.extend(("--user", self.user))
+
+ rc, stdout, stderr = self._podman(exec_args_list, cmd_args_list, in_data)
+
+ display.vvvvv("STDOUT %r STDERR %r" % (stderr, stderr))
+ return rc, stdout, stderr
+
+ def put_file(self, in_path, out_path):
+ """ Place a local file located in 'in_path' inside container at 'out_path' """
+ super(Connection, self).put_file(in_path, out_path)
+ display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._container_id)
+ if not self._mount_point or self.user:
+ rc, stdout, stderr = self._podman(
+ "cp", [in_path, self._container_id + ":" + out_path], use_container_id=False
+ )
+ if rc != 0:
+ rc, stdout, stderr = self._podman(
+ "cp", ["--pause=false", in_path, self._container_id + ":" + out_path], use_container_id=False
+ )
+ if rc != 0:
+ raise AnsibleError(
+ "Failed to copy file from %s to %s in container %s\n%s" % (
+ in_path, out_path, self._container_id, stderr)
+ )
+ if self.user:
+ rc, stdout, stderr = self._podman(
+ "exec", ["chown", self.user, out_path])
+ if rc != 0:
+ raise AnsibleError(
+ "Failed to chown file %s for user %s in container %s\n%s" % (
+ out_path, self.user, self._container_id, stderr)
+ )
+ else:
+ real_out_path = self._mount_point + to_bytes(out_path, errors='surrogate_or_strict')
+ shutil.copyfile(
+ to_bytes(in_path, errors='surrogate_or_strict'),
+ to_bytes(real_out_path, errors='surrogate_or_strict')
+ )
+
+ def fetch_file(self, in_path, out_path):
+ """ obtain file specified via 'in_path' from the container and place it at 'out_path' """
+ super(Connection, self).fetch_file(in_path, out_path)
+ display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._container_id)
+ if not self._mount_point:
+ rc, stdout, stderr = self._podman(
+ "cp", [self._container_id + ":" + in_path, out_path], use_container_id=False)
+ if rc != 0:
+ raise AnsibleError("Failed to fetch file from %s to %s from container %s\n%s" % (
+ in_path, out_path, self._container_id, stderr))
+ else:
+ real_in_path = self._mount_point + to_bytes(in_path, errors='surrogate_or_strict')
+ shutil.copyfile(
+ to_bytes(real_in_path, errors='surrogate_or_strict'),
+ to_bytes(out_path, errors='surrogate_or_strict')
+ )
+
+ def close(self):
+ """ unmount container's filesystem """
+ super(Connection, self).close()
+ # we actually don't need to unmount since the container is mounted anyway
+ # rc, stdout, stderr = self._podman("umount")
+ # display.vvvvv("RC %s STDOUT %r STDERR %r" % (rc, stdout, stderr))
+ self._connected = False
diff --git a/ansible_collections/containers/podman/plugins/module_utils/__init__.py b/ansible_collections/containers/podman/plugins/module_utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/module_utils/__init__.py
diff --git a/ansible_collections/containers/podman/plugins/module_utils/podman/__init__.py b/ansible_collections/containers/podman/plugins/module_utils/podman/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/module_utils/podman/__init__.py
diff --git a/ansible_collections/containers/podman/plugins/module_utils/podman/common.py b/ansible_collections/containers/podman/plugins/module_utils/podman/common.py
new file mode 100644
index 000000000..dba3aff65
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/module_utils/podman/common.py
@@ -0,0 +1,232 @@
+# Copyright (c) 2020 Red Hat
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import json
+import os
+import shutil
+
+from ansible.module_utils.six import raise_from
+try:
+ from ansible.module_utils.compat.version import LooseVersion # noqa: F401
+except ImportError:
+ try:
+ from distutils.version import LooseVersion # noqa: F401
+ except ImportError as exc:
+ raise_from(ImportError('To use this plugin or module with ansible-core'
+ ' < 2.11, you need to use Python < 3.12 with '
+ 'distutils.version present'), exc)
+
+
+def run_podman_command(module, executable='podman', args=None, expected_rc=0, ignore_errors=False):
+ if not isinstance(executable, list):
+ command = [executable]
+ if args is not None:
+ command.extend(args)
+ rc, out, err = module.run_command(command)
+ if not ignore_errors and rc != expected_rc:
+ module.fail_json(
+ msg='Failed to run {command} {args}: {err}'.format(
+ command=command, args=args, err=err))
+ return rc, out, err
+
+
+def run_generate_systemd_command(module, module_params, name, version):
+ """Generate systemd unit file."""
+ command = [module_params['executable'], 'generate', 'systemd',
+ name, '--format', 'json']
+ sysconf = module_params['generate_systemd']
+ gt4ver = LooseVersion(version) >= LooseVersion('4.0.0')
+ if sysconf.get('restart_policy'):
+ if sysconf.get('restart_policy') not in [
+ "no", "on-success", "on-failure", "on-abnormal", "on-watchdog",
+ "on-abort", "always"]:
+ module.fail_json(
+ 'Restart policy for systemd unit file is "%s" and must be one of: '
+ '"no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", or "always"' %
+ sysconf.get('restart_policy'))
+ command.extend([
+ '--restart-policy',
+ sysconf['restart_policy']])
+ if sysconf.get('time'):
+ command.extend(['--time', str(sysconf['time'])])
+ if sysconf.get('no_header'):
+ command.extend(['--no-header'])
+ if sysconf.get('names', True):
+ command.extend(['--name'])
+ if sysconf.get("new"):
+ command.extend(["--new"])
+ if sysconf.get('container_prefix') is not None:
+ command.extend(['--container-prefix=%s' % sysconf['container_prefix']])
+ if sysconf.get('pod_prefix') is not None:
+ command.extend(['--pod-prefix=%s' % sysconf['pod_prefix']])
+ if sysconf.get('separator') is not None:
+ command.extend(['--separator=%s' % sysconf['separator']])
+ if sysconf.get('after') is not None:
+
+ sys_after = sysconf['after']
+ if isinstance(sys_after, str):
+ sys_after = [sys_after]
+ for after in sys_after:
+ command.extend(['--after=%s' % after])
+ if sysconf.get('wants') is not None:
+ sys_wants = sysconf['wants']
+ if isinstance(sys_wants, str):
+ sys_wants = [sys_wants]
+ for want in sys_wants:
+ command.extend(['--wants=%s' % want])
+ if sysconf.get('requires') is not None:
+ sys_req = sysconf['requires']
+ if isinstance(sys_req, str):
+ sys_req = [sys_req]
+ for require in sys_req:
+ command.extend(['--requires=%s' % require])
+ for param in ['after', 'wants', 'requires']:
+ if sysconf.get(param) is not None and not gt4ver:
+ module.fail_json(msg="Systemd parameter '%s' is supported from "
+ "podman version 4 only! Current version is %s" % (
+ param, version))
+
+ if module.params['debug'] or module_params['debug']:
+ module.log("PODMAN-CONTAINER-DEBUG: systemd command: %s" %
+ " ".join(command))
+ rc, systemd, err = module.run_command(command)
+ return rc, systemd, err
+
+
+def generate_systemd(module, module_params, name, version):
+ empty = {}
+ sysconf = module_params['generate_systemd']
+ rc, systemd, err = run_generate_systemd_command(module, module_params, name, version)
+ if rc != 0:
+ module.log(
+ "PODMAN-CONTAINER-DEBUG: Error generating systemd: %s" % err)
+ return empty
+ else:
+ try:
+ data = json.loads(systemd)
+ if sysconf.get('path'):
+ full_path = os.path.expanduser(sysconf['path'])
+ if not os.path.exists(full_path):
+ os.makedirs(full_path)
+ if not os.path.isdir(full_path):
+ module.fail_json("Path %s is not a directory! "
+ "Can not save systemd unit files there!"
+ % full_path)
+ for file_name, file_content in data.items():
+ file_name += ".service"
+ with open(os.path.join(full_path, file_name), 'w') as f:
+ f.write(file_content)
+ return data
+ except Exception as e:
+ module.log(
+ "PODMAN-CONTAINER-DEBUG: Error writing systemd: %s" % e)
+ return empty
+
+
+def delete_systemd(module, module_params, name, version):
+ sysconf = module_params['generate_systemd']
+ if not sysconf.get('path'):
+ # We don't know where systemd files are located, nothing to delete
+ module.log(
+ "PODMAN-CONTAINER-DEBUG: Not deleting systemd file - no path!")
+ return
+ rc, systemd, err = run_generate_systemd_command(module, module_params, name, version)
+ if rc != 0:
+ module.log(
+ "PODMAN-CONTAINER-DEBUG: Error generating systemd: %s" % err)
+ return
+ else:
+ try:
+ data = json.loads(systemd)
+ for file_name in data.keys():
+ file_name += ".service"
+ full_dir_path = os.path.expanduser(sysconf['path'])
+ file_path = os.path.join(full_dir_path, file_name)
+ if os.path.exists(file_path):
+ os.unlink(file_path)
+ return
+ except Exception as e:
+ module.log(
+ "PODMAN-CONTAINER-DEBUG: Error deleting systemd: %s" % e)
+ return
+
+
+def lower_keys(x):
+ if isinstance(x, list):
+ return [lower_keys(v) for v in x]
+ elif isinstance(x, dict):
+ return dict((k.lower(), lower_keys(v)) for k, v in x.items())
+ else:
+ return x
+
+
+def remove_file_or_dir(path):
+ if os.path.isfile(path):
+ os.unlink(path)
+ elif os.path.isdir(path):
+ shutil.rmtree(path)
+ else:
+ raise ValueError("file %s is not a file or dir." % path)
+
+
+# Generated from https://github.com/containers/podman/blob/main/pkg/signal/signal_linux.go
+# and https://github.com/containers/podman/blob/main/pkg/signal/signal_linux_mipsx.go
+_signal_map = {
+ "ABRT": 6,
+ "ALRM": 14,
+ "BUS": 7,
+ "CHLD": 17,
+ "CLD": 17,
+ "CONT": 18,
+ "EMT": 7,
+ "FPE": 8,
+ "HUP": 1,
+ "ILL": 4,
+ "INT": 2,
+ "IO": 29,
+ "IOT": 6,
+ "KILL": 9,
+ "PIPE": 13,
+ "POLL": 29,
+ "PROF": 27,
+ "PWR": 30,
+ "QUIT": 3,
+ "RTMAX": 64,
+ "RTMIN": 34,
+ "SEGV": 11,
+ "STKFLT": 16,
+ "STOP": 19,
+ "SYS": 31,
+ "TERM": 15,
+ "TRAP": 5,
+ "TSTP": 20,
+ "TTIN": 21,
+ "TTOU": 22,
+ "URG": 23,
+ "USR1": 10,
+ "USR2": 12,
+ "VTALRM": 26,
+ "WINCH": 28,
+ "XCPU": 24,
+ "XFSZ": 25
+}
+
+for i in range(1, _signal_map['RTMAX'] - _signal_map['RTMIN'] + 1):
+ _signal_map['RTMIN+{0}'.format(i)] = _signal_map['RTMIN'] + i
+ _signal_map['RTMAX-{0}'.format(i)] = _signal_map['RTMAX'] - i
+
+
+def normalize_signal(signal_name_or_number):
+ signal_name_or_number = str(signal_name_or_number)
+ if signal_name_or_number.isdigit():
+ return signal_name_or_number
+ else:
+ signal_name = signal_name_or_number.upper()
+ if signal_name.startswith('SIG'):
+ signal_name = signal_name[3:]
+ if signal_name not in _signal_map:
+ raise RuntimeError("Unknown signal '{0}'".format(signal_name_or_number))
+ return str(_signal_map[signal_name])
diff --git a/ansible_collections/containers/podman/plugins/module_utils/podman/podman_container_lib.py b/ansible_collections/containers/podman/plugins/module_utils/podman/podman_container_lib.py
new file mode 100644
index 000000000..1ba28f4c8
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/module_utils/podman/podman_container_lib.py
@@ -0,0 +1,1696 @@
+from __future__ import (absolute_import, division, print_function)
+import json # noqa: F402
+import os # noqa: F402
+import shlex # noqa: F402
+
+from ansible.module_utils._text import to_bytes, to_native # noqa: F402
+from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion
+from ansible_collections.containers.podman.plugins.module_utils.podman.common import lower_keys
+from ansible_collections.containers.podman.plugins.module_utils.podman.common import generate_systemd
+from ansible_collections.containers.podman.plugins.module_utils.podman.common import delete_systemd
+from ansible_collections.containers.podman.plugins.module_utils.podman.common import normalize_signal
+
+__metaclass__ = type
+
+ARGUMENTS_SPEC_CONTAINER = dict(
+ name=dict(required=True, type='str'),
+ executable=dict(default='podman', type='str'),
+ state=dict(type='str', default='started', choices=[
+ 'absent', 'present', 'stopped', 'started', 'created']),
+ image=dict(type='str'),
+ annotation=dict(type='dict'),
+ authfile=dict(type='path'),
+ blkio_weight=dict(type='int'),
+ blkio_weight_device=dict(type='dict'),
+ cap_add=dict(type='list', elements='str', aliases=['capabilities']),
+ cap_drop=dict(type='list', elements='str'),
+ cgroup_parent=dict(type='path'),
+ cgroupns=dict(type='str'),
+ cgroups=dict(type='str'),
+ cidfile=dict(type='path'),
+ cmd_args=dict(type='list', elements='str'),
+ conmon_pidfile=dict(type='path'),
+ command=dict(type='raw'),
+ cpu_period=dict(type='int'),
+ cpu_rt_period=dict(type='int'),
+ cpu_rt_runtime=dict(type='int'),
+ cpu_shares=dict(type='int'),
+ cpus=dict(type='str'),
+ cpuset_cpus=dict(type='str'),
+ cpuset_mems=dict(type='str'),
+ detach=dict(type='bool', default=True),
+ debug=dict(type='bool', default=False),
+ detach_keys=dict(type='str', no_log=False),
+ device=dict(type='list', elements='str'),
+ device_read_bps=dict(type='list', elements='str'),
+ device_read_iops=dict(type='list', elements='str'),
+ device_write_bps=dict(type='list', elements='str'),
+ device_write_iops=dict(type='list', elements='str'),
+ dns=dict(type='list', elements='str', aliases=['dns_servers']),
+ dns_option=dict(type='str', aliases=['dns_opts']),
+ dns_search=dict(type='str', aliases=['dns_search_domains']),
+ entrypoint=dict(type='str'),
+ env=dict(type='dict'),
+ env_file=dict(type='path'),
+ env_host=dict(type='bool'),
+ etc_hosts=dict(type='dict', aliases=['add_hosts']),
+ expose=dict(type='list', elements='str', aliases=[
+ 'exposed', 'exposed_ports']),
+ force_restart=dict(type='bool', default=False,
+ aliases=['restart']),
+ generate_systemd=dict(type='dict', default={}),
+ gidmap=dict(type='list', elements='str'),
+ group_add=dict(type='list', elements='str', aliases=['groups']),
+ healthcheck=dict(type='str'),
+ healthcheck_interval=dict(type='str'),
+ healthcheck_retries=dict(type='int'),
+ healthcheck_start_period=dict(type='str'),
+ healthcheck_timeout=dict(type='str'),
+ hooks_dir=dict(type='list', elements='str'),
+ hostname=dict(type='str'),
+ http_proxy=dict(type='bool'),
+ image_volume=dict(type='str', choices=['bind', 'tmpfs', 'ignore']),
+ image_strict=dict(type='bool', default=False),
+ init=dict(type='bool'),
+ init_path=dict(type='str'),
+ interactive=dict(type='bool'),
+ ip=dict(type='str'),
+ ipc=dict(type='str', aliases=['ipc_mode']),
+ kernel_memory=dict(type='str'),
+ label=dict(type='dict', aliases=['labels']),
+ label_file=dict(type='str'),
+ log_driver=dict(type='str', choices=[
+ 'k8s-file', 'journald', 'json-file']),
+ log_level=dict(
+ type='str',
+ choices=["debug", "info", "warn", "error", "fatal", "panic"]),
+ log_opt=dict(type='dict', aliases=['log_options'],
+ options=dict(
+ max_size=dict(type='str'),
+ path=dict(type='str'),
+ tag=dict(type='str'))),
+ mac_address=dict(type='str'),
+ memory=dict(type='str'),
+ memory_reservation=dict(type='str'),
+ memory_swap=dict(type='str'),
+ memory_swappiness=dict(type='int'),
+ mount=dict(type='list', elements='str', aliases=['mounts']),
+ network=dict(type='list', elements='str', aliases=['net', 'network_mode']),
+ network_aliases=dict(type='list', elements='str'),
+ no_hosts=dict(type='bool'),
+ oom_kill_disable=dict(type='bool'),
+ oom_score_adj=dict(type='int'),
+ pid=dict(type='str', aliases=['pid_mode']),
+ pids_limit=dict(type='str'),
+ pod=dict(type='str'),
+ privileged=dict(type='bool'),
+ publish=dict(type='list', elements='str', aliases=[
+ 'ports', 'published', 'published_ports']),
+ publish_all=dict(type='bool'),
+ read_only=dict(type='bool'),
+ read_only_tmpfs=dict(type='bool'),
+ recreate=dict(type='bool', default=False),
+ requires=dict(type='list', elements='str'),
+ restart_policy=dict(type='str'),
+ rm=dict(type='bool', aliases=['remove', 'auto_remove']),
+ rootfs=dict(type='bool'),
+ secrets=dict(type='list', elements='str', no_log=True),
+ sdnotify=dict(type='str'),
+ security_opt=dict(type='list', elements='str'),
+ shm_size=dict(type='str'),
+ sig_proxy=dict(type='bool'),
+ stop_signal=dict(type='int'),
+ stop_timeout=dict(type='int'),
+ subgidname=dict(type='str'),
+ subuidname=dict(type='str'),
+ sysctl=dict(type='dict'),
+ systemd=dict(type='str'),
+ timezone=dict(type='str'),
+ tmpfs=dict(type='dict'),
+ tty=dict(type='bool'),
+ uidmap=dict(type='list', elements='str'),
+ ulimit=dict(type='list', elements='str', aliases=['ulimits']),
+ user=dict(type='str'),
+ userns=dict(type='str', aliases=['userns_mode']),
+ uts=dict(type='str'),
+ volume=dict(type='list', elements='str', aliases=['volumes']),
+ volumes_from=dict(type='list', elements='str'),
+ workdir=dict(type='str', aliases=['working_dir'])
+)
+
+
+def init_options():
+ default = {}
+ opts = ARGUMENTS_SPEC_CONTAINER
+ for k, v in opts.items():
+ if 'default' in v:
+ default[k] = v['default']
+ else:
+ default[k] = None
+ return default
+
+
+def update_options(opts_dict, container):
+ def to_bool(x):
+ return str(x).lower() not in ['no', 'false']
+
+ aliases = {}
+ for k, v in ARGUMENTS_SPEC_CONTAINER.items():
+ if 'aliases' in v:
+ for alias in v['aliases']:
+ aliases[alias] = k
+ for k in list(container):
+ if k in aliases:
+ key = aliases[k]
+ container[key] = container.pop(k)
+ else:
+ key = k
+ if ARGUMENTS_SPEC_CONTAINER[key]['type'] == 'list' and not isinstance(container[key], list):
+ opts_dict[key] = [container[key]]
+ elif ARGUMENTS_SPEC_CONTAINER[key]['type'] == 'bool' and not isinstance(container[key], bool):
+ opts_dict[key] = to_bool(container[key])
+ elif ARGUMENTS_SPEC_CONTAINER[key]['type'] == 'int' and not isinstance(container[key], int):
+ opts_dict[key] = int(container[key])
+ else:
+ opts_dict[key] = container[key]
+
+ return opts_dict
+
+
+def set_container_opts(input_vars):
+ default_options_templ = init_options()
+ options_dict = update_options(default_options_templ, input_vars)
+ return options_dict
+
+
+class PodmanModuleParams:
+ """Creates list of arguments for podman CLI command.
+
+ Arguments:
+ action {str} -- action type from 'run', 'stop', 'create', 'delete',
+ 'start', 'restart'
+ params {dict} -- dictionary of module parameters
+
+ """
+
+ def __init__(self, action, params, podman_version, module):
+ self.params = params
+ self.action = action
+ self.podman_version = podman_version
+ self.module = module
+
+ def construct_command_from_params(self):
+ """Create a podman command from given module parameters.
+
+ Returns:
+ list -- list of byte strings for Popen command
+ """
+ if self.action in ['start', 'stop', 'delete', 'restart']:
+ return self.start_stop_delete()
+ if self.action in ['create', 'run']:
+ cmd = [self.action, '--name', self.params['name']]
+ all_param_methods = [func for func in dir(self)
+ if callable(getattr(self, func))
+ and func.startswith("addparam")]
+ params_set = (i for i in self.params if self.params[i] is not None)
+ for param in params_set:
+ func_name = "_".join(["addparam", param])
+ if func_name in all_param_methods:
+ cmd = getattr(self, func_name)(cmd)
+ cmd.append(self.params['image'])
+ if self.params['command']:
+ if isinstance(self.params['command'], list):
+ cmd += self.params['command']
+ else:
+ cmd += self.params['command'].split()
+ return [to_bytes(i, errors='surrogate_or_strict') for i in cmd]
+
+ def start_stop_delete(self):
+
+ if self.action in ['stop', 'start', 'restart']:
+ cmd = [self.action, self.params['name']]
+ return [to_bytes(i, errors='surrogate_or_strict') for i in cmd]
+
+ if self.action == 'delete':
+ cmd = ['rm', '-f', self.params['name']]
+ return [to_bytes(i, errors='surrogate_or_strict') for i in cmd]
+
+ def check_version(self, param, minv=None, maxv=None):
+ if minv and LooseVersion(minv) > LooseVersion(
+ self.podman_version):
+ self.module.fail_json(msg="Parameter %s is supported from podman "
+ "version %s only! Current version is %s" % (
+ param, minv, self.podman_version))
+ if maxv and LooseVersion(maxv) < LooseVersion(
+ self.podman_version):
+ self.module.fail_json(msg="Parameter %s is supported till podman "
+ "version %s only! Current version is %s" % (
+ param, minv, self.podman_version))
+
+ def addparam_annotation(self, c):
+ for annotate in self.params['annotation'].items():
+ c += ['--annotation', '='.join(annotate)]
+ return c
+
+ def addparam_authfile(self, c):
+ return c + ['--authfile', self.params['authfile']]
+
+ def addparam_blkio_weight(self, c):
+ return c + ['--blkio-weight', self.params['blkio_weight']]
+
+ def addparam_blkio_weight_device(self, c):
+ for blkio in self.params['blkio_weight_device'].items():
+ c += ['--blkio-weight-device', ':'.join(blkio)]
+ return c
+
+ def addparam_cap_add(self, c):
+ for cap_add in self.params['cap_add']:
+ c += ['--cap-add', cap_add]
+ return c
+
+ def addparam_cap_drop(self, c):
+ for cap_drop in self.params['cap_drop']:
+ c += ['--cap-drop', cap_drop]
+ return c
+
+ def addparam_cgroups(self, c):
+ self.check_version('--cgroups', minv='1.6.0')
+ return c + ['--cgroups=%s' % self.params['cgroups']]
+
+ def addparam_cgroupns(self, c):
+ self.check_version('--cgroupns', minv='1.6.2')
+ return c + ['--cgroupns=%s' % self.params['cgroupns']]
+
+ def addparam_cgroup_parent(self, c):
+ return c + ['--cgroup-parent', self.params['cgroup_parent']]
+
+ def addparam_cidfile(self, c):
+ return c + ['--cidfile', self.params['cidfile']]
+
+ def addparam_conmon_pidfile(self, c):
+ return c + ['--conmon-pidfile', self.params['conmon_pidfile']]
+
+ def addparam_cpu_period(self, c):
+ return c + ['--cpu-period', self.params['cpu_period']]
+
+ def addparam_cpu_rt_period(self, c):
+ return c + ['--cpu-rt-period', self.params['cpu_rt_period']]
+
+ def addparam_cpu_rt_runtime(self, c):
+ return c + ['--cpu-rt-runtime', self.params['cpu_rt_runtime']]
+
+ def addparam_cpu_shares(self, c):
+ return c + ['--cpu-shares', self.params['cpu_shares']]
+
+ def addparam_cpus(self, c):
+ return c + ['--cpus', self.params['cpus']]
+
+ def addparam_cpuset_cpus(self, c):
+ return c + ['--cpuset-cpus', self.params['cpuset_cpus']]
+
+ def addparam_cpuset_mems(self, c):
+ return c + ['--cpuset-mems', self.params['cpuset_mems']]
+
+ def addparam_detach(self, c):
+ return c + ['--detach=%s' % self.params['detach']]
+
+ def addparam_detach_keys(self, c):
+ return c + ['--detach-keys', self.params['detach_keys']]
+
+ def addparam_device(self, c):
+ for dev in self.params['device']:
+ c += ['--device', dev]
+ return c
+
+ def addparam_device_read_bps(self, c):
+ for dev in self.params['device_read_bps']:
+ c += ['--device-read-bps', dev]
+ return c
+
+ def addparam_device_read_iops(self, c):
+ for dev in self.params['device_read_iops']:
+ c += ['--device-read-iops', dev]
+ return c
+
+ def addparam_device_write_bps(self, c):
+ for dev in self.params['device_write_bps']:
+ c += ['--device-write-bps', dev]
+ return c
+
+ def addparam_device_write_iops(self, c):
+ for dev in self.params['device_write_iops']:
+ c += ['--device-write-iops', dev]
+ return c
+
+ def addparam_dns(self, c):
+ return c + ['--dns', ','.join(self.params['dns'])]
+
+ def addparam_dns_option(self, c):
+ return c + ['--dns-option', self.params['dns_option']]
+
+ def addparam_dns_search(self, c):
+ return c + ['--dns-search', self.params['dns_search']]
+
+ def addparam_entrypoint(self, c):
+ return c + ['--entrypoint', self.params['entrypoint']]
+
+ def addparam_env(self, c):
+ for env_value in self.params['env'].items():
+ c += ['--env',
+ b"=".join([to_bytes(k, errors='surrogate_or_strict')
+ for k in env_value])]
+ return c
+
+ def addparam_env_file(self, c):
+ return c + ['--env-file', self.params['env_file']]
+
+ def addparam_env_host(self, c):
+ self.check_version('--env-host', minv='1.5.0')
+ return c + ['--env-host=%s' % self.params['env_host']]
+
+ def addparam_etc_hosts(self, c):
+ for host_ip in self.params['etc_hosts'].items():
+ c += ['--add-host', ':'.join(host_ip)]
+ return c
+
+ def addparam_expose(self, c):
+ for exp in self.params['expose']:
+ c += ['--expose', exp]
+ return c
+
+ def addparam_gidmap(self, c):
+ for gidmap in self.params['gidmap']:
+ c += ['--gidmap', gidmap]
+ return c
+
+ def addparam_group_add(self, c):
+ for g in self.params['group_add']:
+ c += ['--group-add', g]
+ return c
+
+ def addparam_healthcheck(self, c):
+ return c + ['--healthcheck-command', self.params['healthcheck']]
+
+ def addparam_healthcheck_interval(self, c):
+ return c + ['--healthcheck-interval',
+ self.params['healthcheck_interval']]
+
+ def addparam_healthcheck_retries(self, c):
+ return c + ['--healthcheck-retries',
+ self.params['healthcheck_retries']]
+
+ def addparam_healthcheck_start_period(self, c):
+ return c + ['--healthcheck-start-period',
+ self.params['healthcheck_start_period']]
+
+ def addparam_healthcheck_timeout(self, c):
+ return c + ['--healthcheck-timeout',
+ self.params['healthcheck_timeout']]
+
+ def addparam_hooks_dir(self, c):
+ for hook_dir in self.params['hooks_dir']:
+ c += ['--hooks-dir=%s' % hook_dir]
+ return c
+
+ def addparam_hostname(self, c):
+ return c + ['--hostname', self.params['hostname']]
+
+ def addparam_http_proxy(self, c):
+ return c + ['--http-proxy=%s' % self.params['http_proxy']]
+
+ def addparam_image_volume(self, c):
+ return c + ['--image-volume', self.params['image_volume']]
+
+ def addparam_init(self, c):
+ if self.params['init']:
+ c += ['--init']
+ return c
+
+ def addparam_init_path(self, c):
+ return c + ['--init-path', self.params['init_path']]
+
+ def addparam_interactive(self, c):
+ return c + ['--interactive=%s' % self.params['interactive']]
+
+ def addparam_ip(self, c):
+ return c + ['--ip', self.params['ip']]
+
+ def addparam_ipc(self, c):
+ return c + ['--ipc', self.params['ipc']]
+
+ def addparam_kernel_memory(self, c):
+ return c + ['--kernel-memory', self.params['kernel_memory']]
+
+ def addparam_label(self, c):
+ for label in self.params['label'].items():
+ c += ['--label', b'='.join([to_bytes(la, errors='surrogate_or_strict')
+ for la in label])]
+ return c
+
+ def addparam_label_file(self, c):
+ return c + ['--label-file', self.params['label_file']]
+
+ def addparam_log_driver(self, c):
+ return c + ['--log-driver', self.params['log_driver']]
+
+ def addparam_log_opt(self, c):
+ for k, v in self.params['log_opt'].items():
+ if v is not None:
+ c += ['--log-opt',
+ b"=".join([to_bytes(k.replace('max_size', 'max-size'),
+ errors='surrogate_or_strict'),
+ to_bytes(v,
+ errors='surrogate_or_strict')])]
+ return c
+
+ def addparam_log_level(self, c):
+ return c + ['--log-level', self.params['log_level']]
+
+ def addparam_mac_address(self, c):
+ return c + ['--mac-address', self.params['mac_address']]
+
+ def addparam_memory(self, c):
+ return c + ['--memory', self.params['memory']]
+
+ def addparam_memory_reservation(self, c):
+ return c + ['--memory-reservation', self.params['memory_reservation']]
+
+ def addparam_memory_swap(self, c):
+ return c + ['--memory-swap', self.params['memory_swap']]
+
+ def addparam_memory_swappiness(self, c):
+ return c + ['--memory-swappiness', self.params['memory_swappiness']]
+
+ def addparam_mount(self, c):
+ for mnt in self.params['mount']:
+ if mnt:
+ c += ['--mount', mnt]
+ return c
+
+ def addparam_network(self, c):
+ if LooseVersion(self.podman_version) >= LooseVersion('4.0.0'):
+ for net in self.params['network']:
+ c += ['--network', net]
+ return c
+ return c + ['--network', ",".join(self.params['network'])]
+
+ def addparam_network_aliases(self, c):
+ for alias in self.params['network_aliases']:
+ c += ['--network-alias', alias]
+ return c
+
+ def addparam_no_hosts(self, c):
+ return c + ['--no-hosts=%s' % self.params['no_hosts']]
+
+ def addparam_oom_kill_disable(self, c):
+ return c + ['--oom-kill-disable=%s' % self.params['oom_kill_disable']]
+
+ def addparam_oom_score_adj(self, c):
+ return c + ['--oom-score-adj', self.params['oom_score_adj']]
+
+ def addparam_pid(self, c):
+ return c + ['--pid', self.params['pid']]
+
+ def addparam_pids_limit(self, c):
+ return c + ['--pids-limit', self.params['pids_limit']]
+
+ def addparam_pod(self, c):
+ return c + ['--pod', self.params['pod']]
+
+ def addparam_privileged(self, c):
+ return c + ['--privileged=%s' % self.params['privileged']]
+
+ def addparam_publish(self, c):
+ for pub in self.params['publish']:
+ c += ['--publish', pub]
+ return c
+
+ def addparam_publish_all(self, c):
+ return c + ['--publish-all=%s' % self.params['publish_all']]
+
+ def addparam_read_only(self, c):
+ return c + ['--read-only=%s' % self.params['read_only']]
+
+ def addparam_read_only_tmpfs(self, c):
+ return c + ['--read-only-tmpfs=%s' % self.params['read_only_tmpfs']]
+
+ def addparam_requires(self, c):
+ return c + ['--requires', ",".join(self.params['requires'])]
+
+ def addparam_restart_policy(self, c):
+ return c + ['--restart=%s' % self.params['restart_policy']]
+
+ def addparam_rm(self, c):
+ if self.params['rm']:
+ c += ['--rm']
+ return c
+
+ def addparam_rootfs(self, c):
+ return c + ['--rootfs=%s' % self.params['rootfs']]
+
+ def addparam_sdnotify(self, c):
+ return c + ['--sdnotify=%s' % self.params['sdnotify']]
+
+ def addparam_secrets(self, c):
+ for secret in self.params['secrets']:
+ c += ['--secret', secret]
+ return c
+
+ def addparam_security_opt(self, c):
+ for secopt in self.params['security_opt']:
+ c += ['--security-opt', secopt]
+ return c
+
+ def addparam_shm_size(self, c):
+ return c + ['--shm-size', self.params['shm_size']]
+
+ def addparam_sig_proxy(self, c):
+ return c + ['--sig-proxy=%s' % self.params['sig_proxy']]
+
+ def addparam_stop_signal(self, c):
+ return c + ['--stop-signal', self.params['stop_signal']]
+
+ def addparam_stop_timeout(self, c):
+ return c + ['--stop-timeout', self.params['stop_timeout']]
+
+ def addparam_subgidname(self, c):
+ return c + ['--subgidname', self.params['subgidname']]
+
+ def addparam_subuidname(self, c):
+ return c + ['--subuidname', self.params['subuidname']]
+
+ def addparam_sysctl(self, c):
+ for sysctl in self.params['sysctl'].items():
+ c += ['--sysctl',
+ b"=".join([to_bytes(k, errors='surrogate_or_strict')
+ for k in sysctl])]
+ return c
+
+ def addparam_systemd(self, c):
+ return c + ['--systemd=%s' % str(self.params['systemd']).lower()]
+
+ def addparam_tmpfs(self, c):
+ for tmpfs in self.params['tmpfs'].items():
+ c += ['--tmpfs', ':'.join(tmpfs)]
+ return c
+
+ def addparam_timezone(self, c):
+ return c + ['--tz=%s' % self.params['timezone']]
+
+ def addparam_tty(self, c):
+ return c + ['--tty=%s' % self.params['tty']]
+
+ def addparam_uidmap(self, c):
+ for uidmap in self.params['uidmap']:
+ c += ['--uidmap', uidmap]
+ return c
+
+ def addparam_ulimit(self, c):
+ for u in self.params['ulimit']:
+ c += ['--ulimit', u]
+ return c
+
+ def addparam_user(self, c):
+ return c + ['--user', self.params['user']]
+
+ def addparam_userns(self, c):
+ return c + ['--userns', self.params['userns']]
+
+ def addparam_uts(self, c):
+ return c + ['--uts', self.params['uts']]
+
+ def addparam_volume(self, c):
+ for vol in self.params['volume']:
+ if vol:
+ c += ['--volume', vol]
+ return c
+
+ def addparam_volumes_from(self, c):
+ for vol in self.params['volumes_from']:
+ c += ['--volumes-from', vol]
+ return c
+
+ def addparam_workdir(self, c):
+ return c + ['--workdir', self.params['workdir']]
+
+ # Add your own args for podman command
+ def addparam_cmd_args(self, c):
+ return c + self.params['cmd_args']
+
+
+class PodmanDefaults:
+ def __init__(self, image_info, podman_version):
+ self.version = podman_version
+ self.image_info = image_info
+ self.defaults = {
+ "blkio_weight": 0,
+ "cgroups": "default",
+ "cidfile": "",
+ "cpus": 0.0,
+ "cpu_shares": 0,
+ "cpu_quota": 0,
+ "cpu_period": 0,
+ "cpu_rt_runtime": 0,
+ "cpu_rt_period": 0,
+ "cpuset_cpus": "",
+ "cpuset_mems": "",
+ "detach": True,
+ "device": [],
+ "env_host": False,
+ "etc_hosts": {},
+ "group_add": [],
+ "ipc": "",
+ "kernelmemory": "0",
+ "log_level": "error",
+ "memory": "0",
+ "memory_swap": "0",
+ "memory_reservation": "0",
+ # "memory_swappiness": -1,
+ "no_hosts": False,
+ # libpod issue with networks in inspection
+ "oom_score_adj": 0,
+ "pid": "",
+ "privileged": False,
+ "read_only": False,
+ "rm": False,
+ "security_opt": [],
+ "stop_signal": self.image_info.get('config', {}).get('stopsignal', "15"),
+ "tty": False,
+ "user": self.image_info.get('user', ''),
+ "workdir": self.image_info.get('config', {}).get('workingdir', '/'),
+ "uts": "",
+ }
+
+ def default_dict(self):
+ # make here any changes to self.defaults related to podman version
+ # https://github.com/containers/libpod/pull/5669
+ if (LooseVersion(self.version) >= LooseVersion('1.8.0')
+ and LooseVersion(self.version) < LooseVersion('1.9.0')):
+ self.defaults['cpu_shares'] = 1024
+ if (LooseVersion(self.version) >= LooseVersion('2.0.0')):
+ self.defaults['network'] = ["slirp4netns"]
+ self.defaults['ipc'] = "private"
+ self.defaults['uts'] = "private"
+ self.defaults['pid'] = "private"
+ if (LooseVersion(self.version) >= LooseVersion('3.0.0')):
+ self.defaults['log_level'] = "warning"
+ if (LooseVersion(self.version) >= LooseVersion('4.1.0')):
+ self.defaults['ipc'] = "shareable"
+ return self.defaults
+
+
+class PodmanContainerDiff:
+ def __init__(self, module, module_params, info, image_info, podman_version):
+ self.module = module
+ self.module_params = module_params
+ self.version = podman_version
+ self.default_dict = None
+ self.info = lower_keys(info)
+ self.image_info = lower_keys(image_info)
+ self.params = self.defaultize()
+ self.diff = {'before': {}, 'after': {}}
+ self.non_idempotent = {}
+
+ def defaultize(self):
+ params_with_defaults = {}
+ self.default_dict = PodmanDefaults(
+ self.image_info, self.version).default_dict()
+ for p in self.module_params:
+ if self.module_params[p] is None and p in self.default_dict:
+ params_with_defaults[p] = self.default_dict[p]
+ else:
+ params_with_defaults[p] = self.module_params[p]
+ return params_with_defaults
+
+ def _diff_update_and_compare(self, param_name, before, after):
+ if before != after:
+ self.diff['before'].update({param_name: before})
+ self.diff['after'].update({param_name: after})
+ return True
+ return False
+
+ def diffparam_annotation(self):
+ before = self.info['config']['annotations'] or {}
+ after = before.copy()
+ if self.module_params['annotation'] is not None:
+ after.update(self.params['annotation'])
+ return self._diff_update_and_compare('annotation', before, after)
+
+ def diffparam_env_host(self):
+ # It's impossible to get from inspest, recreate it if not default
+ before = False
+ after = self.params['env_host']
+ return self._diff_update_and_compare('env_host', before, after)
+
+ def diffparam_blkio_weight(self):
+ before = self.info['hostconfig']['blkioweight']
+ after = self.params['blkio_weight']
+ return self._diff_update_and_compare('blkio_weight', before, after)
+
+ def diffparam_blkio_weight_device(self):
+ before = self.info['hostconfig']['blkioweightdevice']
+ if before == [] and self.module_params['blkio_weight_device'] is None:
+ after = []
+ else:
+ after = self.params['blkio_weight_device']
+ return self._diff_update_and_compare('blkio_weight_device', before, after)
+
+ def diffparam_cap_add(self):
+ before = self.info['effectivecaps'] or []
+ before = [i.lower() for i in before]
+ after = []
+ if self.module_params['cap_add'] is not None:
+ for cap in self.module_params['cap_add']:
+ cap = cap.lower()
+ cap = cap if cap.startswith('cap_') else 'cap_' + cap
+ after.append(cap)
+ after += before
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('cap_add', before, after)
+
+ def diffparam_cap_drop(self):
+ before = self.info['effectivecaps'] or []
+ before = [i.lower() for i in before]
+ after = before[:]
+ if self.module_params['cap_drop'] is not None:
+ for cap in self.module_params['cap_drop']:
+ cap = cap.lower()
+ cap = cap if cap.startswith('cap_') else 'cap_' + cap
+ if cap in after:
+ after.remove(cap)
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('cap_drop', before, after)
+
+ def diffparam_cgroup_parent(self):
+ before = self.info['hostconfig']['cgroupparent']
+ after = self.params['cgroup_parent']
+ if after is None:
+ after = before
+ return self._diff_update_and_compare('cgroup_parent', before, after)
+
+ def diffparam_cgroups(self):
+ # Cgroups output is not supported in all versions
+ if 'cgroups' in self.info['hostconfig']:
+ before = self.info['hostconfig']['cgroups']
+ after = self.params['cgroups']
+ return self._diff_update_and_compare('cgroups', before, after)
+ return False
+
+ def diffparam_cidfile(self):
+ before = self.info['hostconfig']['containeridfile']
+ after = self.params['cidfile']
+ labels = self.info['config']['labels'] or {}
+ # Ignore cidfile that is coming from systemd files
+ # https://github.com/containers/ansible-podman-collections/issues/276
+ if 'podman_systemd_unit' in labels:
+ after = before
+ return self._diff_update_and_compare('cidfile', before, after)
+
+ def diffparam_command(self):
+ # TODO(sshnaidm): to inspect image to get the default command
+ if self.module_params['command'] is not None:
+ before = self.info['config']['cmd']
+ after = self.params['command']
+ if isinstance(after, str):
+ after = shlex.split(after)
+ return self._diff_update_and_compare('command', before, after)
+ return False
+
+ def diffparam_conmon_pidfile(self):
+ before = self.info['conmonpidfile']
+ if self.module_params['conmon_pidfile'] is None:
+ after = before
+ else:
+ after = self.params['conmon_pidfile']
+ return self._diff_update_and_compare('conmon_pidfile', before, after)
+
+ def diffparam_cpu_period(self):
+ before = self.info['hostconfig']['cpuperiod']
+ after = self.params['cpu_period']
+ return self._diff_update_and_compare('cpu_period', before, after)
+
+ def diffparam_cpu_rt_period(self):
+ before = self.info['hostconfig']['cpurealtimeperiod']
+ after = self.params['cpu_rt_period']
+ return self._diff_update_and_compare('cpu_rt_period', before, after)
+
+ def diffparam_cpu_rt_runtime(self):
+ before = self.info['hostconfig']['cpurealtimeruntime']
+ after = self.params['cpu_rt_runtime']
+ return self._diff_update_and_compare('cpu_rt_runtime', before, after)
+
+ def diffparam_cpu_shares(self):
+ before = self.info['hostconfig']['cpushares']
+ after = self.params['cpu_shares']
+ return self._diff_update_and_compare('cpu_shares', before, after)
+
+ def diffparam_cpus(self):
+ before = int(self.info['hostconfig']['nanocpus']) / 1000000000
+ after = self.params['cpus']
+ return self._diff_update_and_compare('cpus', before, after)
+
+ def diffparam_cpuset_cpus(self):
+ before = self.info['hostconfig']['cpusetcpus']
+ after = self.params['cpuset_cpus']
+ return self._diff_update_and_compare('cpuset_cpus', before, after)
+
+ def diffparam_cpuset_mems(self):
+ before = self.info['hostconfig']['cpusetmems']
+ after = self.params['cpuset_mems']
+ return self._diff_update_and_compare('cpuset_mems', before, after)
+
+ def diffparam_device(self):
+ before = [":".join([i['pathonhost'], i['pathincontainer']])
+ for i in self.info['hostconfig']['devices']]
+ if not before and 'createcommand' in self.info['config']:
+ cr_com = self.info['config']['createcommand']
+ if '--device' in cr_com:
+ before = [cr_com[k + 1].lower()
+ for k, i in enumerate(cr_com) if i == '--device']
+ before = [":".join((i, i))
+ if len(i.split(":")) == 1 else i for i in before]
+ after = [":".join(i.split(":")[:2]) for i in self.params['device']]
+ after = [":".join((i, i))
+ if len(i.split(":")) == 1 else i for i in after]
+ after = [i.lower() for i in after]
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('devices', before, after)
+
+ def diffparam_device_read_bps(self):
+ before = self.info['hostconfig']['blkiodevicereadbps'] or []
+ before = ["%s:%s" % (i['path'], i['rate']) for i in before]
+ after = self.params['device_read_bps'] or []
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('device_read_bps', before, after)
+
+ def diffparam_device_read_iops(self):
+ before = self.info['hostconfig']['blkiodevicereadiops'] or []
+ before = ["%s:%s" % (i['path'], i['rate']) for i in before]
+ after = self.params['device_read_iops'] or []
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('device_read_iops', before, after)
+
+ def diffparam_device_write_bps(self):
+ before = self.info['hostconfig']['blkiodevicewritebps'] or []
+ before = ["%s:%s" % (i['path'], i['rate']) for i in before]
+ after = self.params['device_write_bps'] or []
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('device_write_bps', before, after)
+
+ def diffparam_device_write_iops(self):
+ before = self.info['hostconfig']['blkiodevicewriteiops'] or []
+ before = ["%s:%s" % (i['path'], i['rate']) for i in before]
+ after = self.params['device_write_iops'] or []
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('device_write_iops', before, after)
+
+ # Limited idempotency, it can't guess default values
+ def diffparam_env(self):
+ env_before = self.info['config']['env'] or {}
+ before = {i.split("=")[0]: "=".join(i.split("=")[1:])
+ for i in env_before}
+ after = before.copy()
+ if self.params['env']:
+ after.update({k: str(v) for k, v in self.params['env'].items()})
+ return self._diff_update_and_compare('env', before, after)
+
+ def diffparam_etc_hosts(self):
+ if self.info['hostconfig']['extrahosts']:
+ before = dict([i.split(":", 1)
+ for i in self.info['hostconfig']['extrahosts']])
+ else:
+ before = {}
+ after = self.params['etc_hosts']
+ return self._diff_update_and_compare('etc_hosts', before, after)
+
+ def diffparam_group_add(self):
+ before = self.info['hostconfig']['groupadd']
+ after = self.params['group_add']
+ return self._diff_update_and_compare('group_add', before, after)
+
+ # Healthcheck is only defined in container config if a healthcheck
+ # was configured; otherwise the config key isn't part of the config.
+ def diffparam_healthcheck(self):
+ if 'healthcheck' in self.info['config']:
+ # the "test" key is a list of 2 items where the first one is
+ # "CMD-SHELL" and the second one is the actual healthcheck command.
+ before = self.info['config']['healthcheck']['test'][1]
+ else:
+ before = ''
+ after = self.params['healthcheck'] or before
+ return self._diff_update_and_compare('healthcheck', before, after)
+
+ # Because of hostname is random generated, this parameter has partial idempotency only.
+ def diffparam_hostname(self):
+ before = self.info['config']['hostname']
+ after = self.params['hostname'] or before
+ return self._diff_update_and_compare('hostname', before, after)
+
+ def diffparam_image(self):
+ before_id = self.info['image'] or self.info['rootfs']
+ after_id = self.image_info['id']
+ if before_id == after_id:
+ return self._diff_update_and_compare('image', before_id, after_id)
+ is_rootfs = self.info['rootfs'] != '' or self.params['rootfs']
+ before = self.info['config']['image'] or before_id
+ after = self.params['image']
+ mode = self.params['image_strict'] or is_rootfs
+ if mode is None or not mode:
+ # In a idempotency 'lite mode' assume all images from different registries are the same
+ before = before.replace(":latest", "")
+ after = after.replace(":latest", "")
+ before = before.split("/")[-1]
+ after = after.split("/")[-1]
+ else:
+ return self._diff_update_and_compare('image', before_id, after_id)
+ return self._diff_update_and_compare('image', before, after)
+
+ def diffparam_ipc(self):
+ before = self.info['hostconfig']['ipcmode']
+ after = self.params['ipc']
+ if self.params['pod'] and not self.module_params['ipc']:
+ after = before
+ return self._diff_update_and_compare('ipc', before, after)
+
+ def diffparam_label(self):
+ before = self.info['config']['labels'] or {}
+ after = self.image_info.get('labels') or {}
+ if self.params['label']:
+ after.update({
+ str(k).lower(): str(v)
+ for k, v in self.params['label'].items()
+ })
+ # Strip out labels that are coming from systemd files
+ # https://github.com/containers/ansible-podman-collections/issues/276
+ if 'podman_systemd_unit' in before:
+ after.pop('podman_systemd_unit', None)
+ before.pop('podman_systemd_unit', None)
+ return self._diff_update_and_compare('label', before, after)
+
+ def diffparam_log_driver(self):
+ before = self.info['hostconfig']['logconfig']['type']
+ if self.module_params['log_driver'] is not None:
+ after = self.params['log_driver']
+ else:
+ after = before
+ return self._diff_update_and_compare('log_driver', before, after)
+
+ # Parameter has limited idempotency, unable to guess the default log_path
+ def diffparam_log_opt(self):
+ before, after = {}, {}
+
+ # Log path
+ path_before = None
+ if 'logpath' in self.info:
+ path_before = self.info['logpath']
+ # For Podman v3
+ if ('logconfig' in self.info['hostconfig'] and
+ 'path' in self.info['hostconfig']['logconfig']):
+ path_before = self.info['hostconfig']['logconfig']['path']
+ if path_before is not None:
+ if (self.module_params['log_opt'] and
+ 'path' in self.module_params['log_opt'] and
+ self.module_params['log_opt']['path'] is not None):
+ path_after = self.params['log_opt']['path']
+ else:
+ path_after = path_before
+ if path_before != path_after:
+ before.update({'log-path': path_before})
+ after.update({'log-path': path_after})
+
+ # Log tag
+ tag_before = None
+ if 'logtag' in self.info:
+ tag_before = self.info['logtag']
+ # For Podman v3
+ if ('logconfig' in self.info['hostconfig'] and
+ 'tag' in self.info['hostconfig']['logconfig']):
+ tag_before = self.info['hostconfig']['logconfig']['tag']
+ if tag_before is not None:
+ if (self.module_params['log_opt'] and
+ 'tag' in self.module_params['log_opt'] and
+ self.module_params['log_opt']['tag'] is not None):
+ tag_after = self.params['log_opt']['tag']
+ else:
+ tag_after = ''
+ if tag_before != tag_after:
+ before.update({'log-tag': tag_before})
+ after.update({'log-tag': tag_after})
+
+ # Log size
+ # For Podman v3
+ # size_before = '0B'
+ # TODO(sshnaidm): integrate B/KB/MB/GB calculation for sizes
+ # if ('logconfig' in self.info['hostconfig'] and
+ # 'size' in self.info['hostconfig']['logconfig']):
+ # size_before = self.info['hostconfig']['logconfig']['size']
+ # if size_before != '0B':
+ # if (self.module_params['log_opt'] and
+ # 'max_size' in self.module_params['log_opt'] and
+ # self.module_params['log_opt']['max_size'] is not None):
+ # size_after = self.params['log_opt']['max_size']
+ # else:
+ # size_after = ''
+ # if size_before != size_after:
+ # before.update({'log-size': size_before})
+ # after.update({'log-size': size_after})
+
+ return self._diff_update_and_compare('log_opt', before, after)
+
+ def diffparam_mac_address(self):
+ before = str(self.info['networksettings']['macaddress'])
+ if not before and self.info['networksettings'].get('networks'):
+ nets = self.info['networksettings']['networks']
+ macs = [
+ nets[i]["macaddress"] for i in nets if nets[i]["macaddress"]]
+ if macs:
+ before = macs[0]
+ if not before and 'createcommand' in self.info['config']:
+ cr_com = self.info['config']['createcommand']
+ if '--mac-address' in cr_com:
+ before = cr_com[cr_com.index('--mac-address') + 1].lower()
+ if self.module_params['mac_address'] is not None:
+ after = self.params['mac_address']
+ else:
+ after = before
+ return self._diff_update_and_compare('mac_address', before, after)
+
+ def diffparam_network(self):
+ net_mode_before = self.info['hostconfig']['networkmode']
+ net_mode_after = ''
+ before = list(self.info['networksettings'].get('networks', {}))
+ # Remove default 'podman' network in v3 for comparison
+ if before == ['podman']:
+ before = []
+ # Special case for options for slirp4netns rootless networking from v2
+ if net_mode_before == 'slirp4netns' and 'createcommand' in self.info['config']:
+ cr_com = self.info['config']['createcommand']
+ if '--network' in cr_com:
+ cr_net = cr_com[cr_com.index('--network') + 1].lower()
+ if 'slirp4netns:' in cr_net:
+ before = [cr_net]
+ after = self.params['network'] or []
+ # If container is in pod and no networks are provided
+ if not self.module_params['network'] and self.params['pod']:
+ after = before
+ return self._diff_update_and_compare('network', before, after)
+ # Check special network modes
+ if after in [['bridge'], ['host'], ['slirp4netns'], ['none']]:
+ net_mode_after = after[0]
+ # If changes are only for network mode and container has no networks
+ if net_mode_after and not before:
+ # Remove differences between v1 and v2
+ net_mode_after = net_mode_after.replace('bridge', 'default')
+ net_mode_after = net_mode_after.replace('slirp4netns', 'default')
+ net_mode_before = net_mode_before.replace('bridge', 'default')
+ net_mode_before = net_mode_before.replace('slirp4netns', 'default')
+ return self._diff_update_and_compare('network', net_mode_before, net_mode_after)
+ # If container is attached to network of a different container
+ if "container" in net_mode_before:
+ for netw in after:
+ if "container" in netw:
+ before = after = netw
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('network', before, after)
+
+ def diffparam_oom_score_adj(self):
+ before = self.info['hostconfig']['oomscoreadj']
+ after = self.params['oom_score_adj']
+ return self._diff_update_and_compare('oom_score_adj', before, after)
+
+ def diffparam_privileged(self):
+ before = self.info['hostconfig']['privileged']
+ after = self.params['privileged']
+ return self._diff_update_and_compare('privileged', before, after)
+
+ def diffparam_pid(self):
+ before = self.info['hostconfig']['pidmode']
+ after = self.params['pid']
+ return self._diff_update_and_compare('pid', before, after)
+
+ # TODO(sshnaidm) Need to add port ranges support
+ def diffparam_publish(self):
+ def compose(p, h):
+ s = ":".join(
+ [str(h["hostport"]), p.replace('/tcp', '')]
+ ).strip(":")
+ if h['hostip']:
+ return ":".join([h['hostip'], s])
+ return s
+
+ ports = self.info['hostconfig']['portbindings']
+ before = []
+ for port, hosts in ports.items():
+ if hosts:
+ for h in hosts:
+ before.append(compose(port, h))
+ after = self.params['publish'] or []
+ if self.params['publish_all']:
+ image_ports = self.image_info.get('config', {}).get('exposedports', {})
+ if image_ports:
+ after += list(image_ports.keys())
+ after = [
+ i.replace("/tcp", "").replace("[", "").replace("]", "")
+ for i in after]
+ # No support for port ranges yet
+ for ports in after:
+ if "-" in ports:
+ return self._diff_update_and_compare('publish', '', '')
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('publish', before, after)
+
+ def diffparam_read_only(self):
+ before = self.info['hostconfig']['readonlyrootfs']
+ after = self.params['read_only']
+ return self._diff_update_and_compare('read_only', before, after)
+
+ def diffparam_restart_policy(self):
+ before = self.info['hostconfig']['restartpolicy']['name']
+ after = self.params['restart_policy'] or ""
+ return self._diff_update_and_compare('restart_policy', before, after)
+
+ def diffparam_rm(self):
+ before = self.info['hostconfig']['autoremove']
+ after = self.params['rm']
+ return self._diff_update_and_compare('rm', before, after)
+
+ def diffparam_security_opt(self):
+ before = self.info['hostconfig']['securityopt']
+ # In rootful containers with apparmor there is a default security opt
+ before = [o for o in before if 'apparmor=containers-default' not in o]
+ after = self.params['security_opt']
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('security_opt', before, after)
+
+ def diffparam_stop_signal(self):
+ before = normalize_signal(self.info['config']['stopsignal'])
+ after = normalize_signal(self.params['stop_signal'])
+ return self._diff_update_and_compare('stop_signal', before, after)
+
+ def diffparam_timezone(self):
+ before = self.info['config'].get('timezone')
+ after = self.params['timezone']
+ return self._diff_update_and_compare('timezone', before, after)
+
+ def diffparam_tty(self):
+ before = self.info['config']['tty']
+ after = self.params['tty']
+ return self._diff_update_and_compare('tty', before, after)
+
+ def diffparam_user(self):
+ before = self.info['config']['user']
+ after = self.params['user']
+ return self._diff_update_and_compare('user', before, after)
+
+ def diffparam_ulimit(self):
+ after = self.params['ulimit'] or []
+ # In case of latest podman
+ if 'createcommand' in self.info['config']:
+ ulimits = []
+ for k, c in enumerate(self.info['config']['createcommand']):
+ if c == '--ulimit':
+ ulimits.append(self.info['config']['createcommand'][k + 1])
+ before = ulimits
+ before, after = sorted(before), sorted(after)
+ return self._diff_update_and_compare('ulimit', before, after)
+ if after:
+ ulimits = self.info['hostconfig']['ulimits']
+ before = {
+ u['name'].replace('rlimit_', ''): "%s:%s" % (u['soft'], u['hard']) for u in ulimits}
+ after = {i.split('=')[0]: i.split('=')[1]
+ for i in self.params['ulimit']}
+ new_before = []
+ new_after = []
+ for u in list(after.keys()):
+ # We don't support unlimited ulimits because it depends on platform
+ if u in before and "-1" not in after[u]:
+ new_before.append([u, before[u]])
+ new_after.append([u, after[u]])
+ return self._diff_update_and_compare('ulimit', new_before, new_after)
+ return self._diff_update_and_compare('ulimit', '', '')
+
+ def diffparam_uts(self):
+ before = self.info['hostconfig']['utsmode']
+ after = self.params['uts']
+ if self.params['pod'] and not self.module_params['uts']:
+ after = before
+ return self._diff_update_and_compare('uts', before, after)
+
+ def diffparam_volume(self):
+ def clean_volume(x):
+ '''Remove trailing and double slashes from volumes.'''
+ if not x.rstrip("/"):
+ return "/"
+ return x.replace("//", "/").rstrip("/")
+
+ before = self.info['mounts']
+ before_local_vols = []
+ if before:
+ volumes = []
+ local_vols = []
+ for m in before:
+ if m['type'] != 'volume':
+ volumes.append(
+ [
+ clean_volume(m['source']),
+ clean_volume(m['destination'])
+ ])
+ elif m['type'] == 'volume':
+ local_vols.append(
+ [m['name'], clean_volume(m['destination'])])
+ before = [":".join(v) for v in volumes]
+ before_local_vols = [":".join(v) for v in local_vols]
+ if self.params['volume'] is not None:
+ after = [":".join(
+ [clean_volume(i) for i in v.split(":")[:2]]
+ ) for v in self.params['volume']]
+ else:
+ after = []
+ if before_local_vols:
+ after = list(set(after).difference(before_local_vols))
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('volume', before, after)
+
+ def diffparam_volumes_from(self):
+ # Possibly volumesfrom is not in config
+ before = self.info['hostconfig'].get('volumesfrom', []) or []
+ after = self.params['volumes_from'] or []
+ return self._diff_update_and_compare('volumes_from', before, after)
+
+ def diffparam_workdir(self):
+ before = self.info['config']['workingdir']
+ after = self.params['workdir']
+ return self._diff_update_and_compare('workdir', before, after)
+
+ def is_different(self):
+ diff_func_list = [func for func in dir(self)
+ if callable(getattr(self, func)) and func.startswith(
+ "diffparam")]
+ fail_fast = not bool(self.module._diff)
+ different = False
+ for func_name in diff_func_list:
+ dff_func = getattr(self, func_name)
+ if dff_func():
+ if fail_fast:
+ return True
+ different = True
+ # Check non idempotent parameters
+ for p in self.non_idempotent:
+ if self.module_params[p] is not None and self.module_params[p] not in [{}, [], '']:
+ different = True
+ return different
+
+
+def ensure_image_exists(module, image, module_params):
+ """If image is passed, ensure it exists, if not - pull it or fail.
+
+ Arguments:
+ module {obj} -- ansible module object
+ image {str} -- name of image
+
+ Returns:
+ list -- list of image actions - if it pulled or nothing was done
+ """
+ image_actions = []
+ module_exec = module_params['executable']
+ is_rootfs = module_params['rootfs']
+
+ if is_rootfs:
+ if not os.path.exists(image) or not os.path.isdir(image):
+ module.fail_json(msg="Image rootfs doesn't exist %s" % image)
+ return image_actions
+ if not image:
+ return image_actions
+ rc, out, err = module.run_command([module_exec, 'image', 'exists', image])
+ if rc == 0:
+ return image_actions
+ rc, out, err = module.run_command([module_exec, 'image', 'pull', image])
+ if rc != 0:
+ module.fail_json(msg="Can't pull image %s" % image, stdout=out,
+ stderr=err)
+ image_actions.append("pulled image %s" % image)
+ return image_actions
+
+
+class PodmanContainer:
+ """Perform container tasks.
+
+ Manages podman container, inspects it and checks its current state
+ """
+
+ def __init__(self, module, name, module_params):
+ """Initialize PodmanContainer class.
+
+ Arguments:
+ module {obj} -- ansible module object
+ name {str} -- name of container
+ """
+
+ self.module = module
+ self.module_params = module_params
+ self.name = name
+ self.stdout, self.stderr = '', ''
+ self.info = self.get_info()
+ self.version = self._get_podman_version()
+ self.diff = {}
+ self.actions = []
+
+ @property
+ def exists(self):
+ """Check if container exists."""
+ return bool(self.info != {})
+
+ @property
+ def different(self):
+ """Check if container is different."""
+ diffcheck = PodmanContainerDiff(
+ self.module,
+ self.module_params,
+ self.info,
+ self.get_image_info(),
+ self.version)
+ is_different = diffcheck.is_different()
+ diffs = diffcheck.diff
+ if self.module._diff and is_different and diffs['before'] and diffs['after']:
+ self.diff['before'] = "\n".join(
+ ["%s - %s" % (k, v) for k, v in sorted(
+ diffs['before'].items())]) + "\n"
+ self.diff['after'] = "\n".join(
+ ["%s - %s" % (k, v) for k, v in sorted(
+ diffs['after'].items())]) + "\n"
+ return is_different
+
+ @property
+ def running(self):
+ """Return True if container is running now."""
+ return self.exists and self.info['State']['Running']
+
+ @property
+ def stopped(self):
+ """Return True if container exists and is not running now."""
+ return self.exists and not self.info['State']['Running']
+
+ def get_info(self):
+ """Inspect container and gather info about it."""
+ # pylint: disable=unused-variable
+ rc, out, err = self.module.run_command(
+ [self.module_params['executable'], b'container', b'inspect', self.name])
+ return json.loads(out)[0] if rc == 0 else {}
+
+ def get_image_info(self):
+ """Inspect container image and gather info about it."""
+ # pylint: disable=unused-variable
+ is_rootfs = self.module_params['rootfs']
+ if is_rootfs:
+ return {'Id': self.module_params['image']}
+ rc, out, err = self.module.run_command(
+ [self.module_params['executable'], b'image', b'inspect', self.module_params['image']])
+ return json.loads(out)[0] if rc == 0 else {}
+
+ def _get_podman_version(self):
+ # pylint: disable=unused-variable
+ rc, out, err = self.module.run_command(
+ [self.module_params['executable'], b'--version'])
+ if rc != 0 or not out or "version" not in out:
+ self.module.fail_json(msg="%s run failed!" %
+ self.module_params['executable'])
+ return out.split("version")[1].strip()
+
+ def _perform_action(self, action):
+ """Perform action with container.
+
+ Arguments:
+ action {str} -- action to perform - start, create, stop, run,
+ delete, restart
+ """
+ b_command = PodmanModuleParams(action,
+ self.module_params,
+ self.version,
+ self.module,
+ ).construct_command_from_params()
+ if action == 'create':
+ b_command.remove(b'--detach=True')
+ full_cmd = " ".join([self.module_params['executable']]
+ + [to_native(i) for i in b_command])
+ self.actions.append(full_cmd)
+ if self.module.check_mode:
+ self.module.log(
+ "PODMAN-CONTAINER-DEBUG (check_mode): %s" % full_cmd)
+ else:
+ rc, out, err = self.module.run_command(
+ [self.module_params['executable'], b'container'] + b_command,
+ expand_user_and_vars=False)
+ self.module.log("PODMAN-CONTAINER-DEBUG: %s" % full_cmd)
+ if self.module_params['debug']:
+ self.module.log("PODMAN-CONTAINER-DEBUG STDOUT: %s" % out)
+ self.module.log("PODMAN-CONTAINER-DEBUG STDERR: %s" % err)
+ self.module.log("PODMAN-CONTAINER-DEBUG RC: %s" % rc)
+ self.stdout = out
+ self.stderr = err
+ if rc != 0:
+ self.module.fail_json(
+ msg="Can't %s container %s" % (action, self.name),
+ stdout=out, stderr=err)
+
+ def run(self):
+ """Run the container."""
+ self._perform_action('run')
+
+ def delete(self):
+ """Delete the container."""
+ self._perform_action('delete')
+
+ def stop(self):
+ """Stop the container."""
+ self._perform_action('stop')
+
+ def start(self):
+ """Start the container."""
+ self._perform_action('start')
+
+ def restart(self):
+ """Restart the container."""
+ self._perform_action('restart')
+
+ def create(self):
+ """Create the container."""
+ self._perform_action('create')
+
+ def recreate(self):
+ """Recreate the container."""
+ if self.running:
+ self.stop()
+ if not self.info['HostConfig']['AutoRemove']:
+ self.delete()
+ self.create()
+
+ def recreate_run(self):
+ """Recreate and run the container."""
+ if self.running:
+ self.stop()
+ if not self.info['HostConfig']['AutoRemove']:
+ self.delete()
+ self.run()
+
+
+class PodmanManager:
+ """Module manager class.
+
+ Defines according to parameters what actions should be applied to container
+ """
+
+ def __init__(self, module, params):
+ """Initialize PodmanManager class.
+
+ Arguments:
+ module {obj} -- ansible module object
+ """
+
+ self.module = module
+ self.results = {
+ 'changed': False,
+ 'actions': [],
+ 'container': {},
+ }
+ self.module_params = params
+ self.name = self.module_params['name']
+ self.executable = \
+ self.module.get_bin_path(self.module_params['executable'],
+ required=True)
+ self.image = self.module_params['image']
+ image_actions = ensure_image_exists(
+ self.module, self.image, self.module_params)
+ self.results['actions'] += image_actions
+ self.state = self.module_params['state']
+ self.restart = self.module_params['force_restart']
+ self.recreate = self.module_params['recreate']
+
+ if self.module_params['generate_systemd'].get('new'):
+ self.module_params['rm'] = True
+
+ self.container = PodmanContainer(
+ self.module, self.name, self.module_params)
+
+ def update_container_result(self, changed=True):
+ """Inspect the current container, update results with last info, exit.
+
+ Keyword Arguments:
+ changed {bool} -- whether any action was performed
+ (default: {True})
+ """
+ facts = self.container.get_info() if changed else self.container.info
+ out, err = self.container.stdout, self.container.stderr
+ self.results.update({'changed': changed, 'container': facts,
+ 'podman_actions': self.container.actions},
+ stdout=out, stderr=err)
+ if self.container.diff:
+ self.results.update({'diff': self.container.diff})
+ if self.module.params['debug'] or self.module_params['debug']:
+ self.results.update({'podman_version': self.container.version})
+ self.results.update(
+ {'podman_systemd': generate_systemd(self.module,
+ self.module_params,
+ self.name,
+ self.container.version)})
+
+ def make_started(self):
+ """Run actions if desired state is 'started'."""
+ if not self.image:
+ if not self.container.exists:
+ self.module.fail_json(msg='Cannot start container when image'
+ ' is not specified!')
+ if self.restart:
+ self.container.restart()
+ self.results['actions'].append('restarted %s' %
+ self.container.name)
+ else:
+ self.container.start()
+ self.results['actions'].append('started %s' %
+ self.container.name)
+ self.update_container_result()
+ return
+ if self.container.exists and self.restart:
+ if self.container.running:
+ self.container.restart()
+ self.results['actions'].append('restarted %s' %
+ self.container.name)
+ else:
+ self.container.start()
+ self.results['actions'].append('started %s' %
+ self.container.name)
+ self.update_container_result()
+ return
+ if self.container.running and \
+ (self.container.different or self.recreate):
+ self.container.recreate_run()
+ self.results['actions'].append('recreated %s' %
+ self.container.name)
+ self.update_container_result()
+ return
+ elif self.container.running and not self.container.different:
+ if self.restart:
+ self.container.restart()
+ self.results['actions'].append('restarted %s' %
+ self.container.name)
+ self.update_container_result()
+ return
+ self.update_container_result(changed=False)
+ return
+ elif not self.container.exists:
+ self.container.run()
+ self.results['actions'].append('started %s' % self.container.name)
+ self.update_container_result()
+ return
+ elif self.container.stopped and self.container.different:
+ self.container.recreate_run()
+ self.results['actions'].append('recreated %s' %
+ self.container.name)
+ self.update_container_result()
+ return
+ elif self.container.stopped and not self.container.different:
+ self.container.start()
+ self.results['actions'].append('started %s' % self.container.name)
+ self.update_container_result()
+ return
+
+ def make_created(self):
+ """Run actions if desired state is 'created'."""
+ if not self.container.exists and not self.image:
+ self.module.fail_json(msg='Cannot create container when image'
+ ' is not specified!')
+ if not self.container.exists:
+ self.container.create()
+ self.results['actions'].append('created %s' % self.container.name)
+ self.update_container_result()
+ return
+ else:
+ if (self.container.different or self.recreate):
+ self.container.recreate()
+ self.results['actions'].append('recreated %s' %
+ self.container.name)
+ if self.container.running:
+ self.container.start()
+ self.results['actions'].append('started %s' %
+ self.container.name)
+ self.update_container_result()
+ return
+ elif self.restart:
+ if self.container.running:
+ self.container.restart()
+ self.results['actions'].append('restarted %s' %
+ self.container.name)
+ else:
+ self.container.start()
+ self.results['actions'].append('started %s' %
+ self.container.name)
+ self.update_container_result()
+ return
+ self.update_container_result(changed=False)
+ return
+
+ def make_stopped(self):
+ """Run actions if desired state is 'stopped'."""
+ if not self.container.exists and not self.image:
+ self.module.fail_json(msg='Cannot create container when image'
+ ' is not specified!')
+ if not self.container.exists:
+ self.container.create()
+ self.results['actions'].append('created %s' % self.container.name)
+ self.update_container_result()
+ return
+ if self.container.stopped:
+ self.update_container_result(changed=False)
+ return
+ elif self.container.running:
+ self.container.stop()
+ self.results['actions'].append('stopped %s' % self.container.name)
+ self.update_container_result()
+ return
+
+ def make_absent(self):
+ """Run actions if desired state is 'absent'."""
+ if not self.container.exists:
+ self.results.update({'changed': False})
+ elif self.container.exists:
+ delete_systemd(self.module,
+ self.module_params,
+ self.name,
+ self.container.version)
+ self.container.delete()
+ self.results['actions'].append('deleted %s' % self.container.name)
+ self.results.update({'changed': True})
+ self.results.update({'container': {},
+ 'podman_actions': self.container.actions})
+
+ def execute(self):
+ """Execute the desired action according to map of actions & states."""
+ states_map = {
+ 'present': self.make_created,
+ 'started': self.make_started,
+ 'absent': self.make_absent,
+ 'stopped': self.make_stopped,
+ 'created': self.make_created,
+ }
+ process_action = states_map[self.state]
+ process_action()
+ return self.results
diff --git a/ansible_collections/containers/podman/plugins/module_utils/podman/podman_pod_lib.py b/ansible_collections/containers/podman/plugins/module_utils/podman/podman_pod_lib.py
new file mode 100644
index 000000000..0b4afc0bc
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/module_utils/podman/podman_pod_lib.py
@@ -0,0 +1,880 @@
+from __future__ import (absolute_import, division, print_function)
+import json
+
+from ansible.module_utils._text import to_bytes, to_native
+from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion
+from ansible_collections.containers.podman.plugins.module_utils.podman.common import lower_keys
+from ansible_collections.containers.podman.plugins.module_utils.podman.common import generate_systemd
+from ansible_collections.containers.podman.plugins.module_utils.podman.common import delete_systemd
+
+
+__metaclass__ = type
+
+ARGUMENTS_SPEC_POD = dict(
+ state=dict(
+ type='str',
+ default="created",
+ choices=[
+ 'created',
+ 'killed',
+ 'restarted',
+ 'absent',
+ 'started',
+ 'stopped',
+ 'paused',
+ 'unpaused',
+ ]),
+ recreate=dict(type='bool', default=False),
+ add_host=dict(type='list', required=False, elements='str'),
+ cgroup_parent=dict(type='str', required=False),
+ cpus=dict(type='str', required=False),
+ cpuset_cpus=dict(type='str', required=False),
+ device=dict(type='list', elements='str', required=False),
+ device_read_bps=dict(type='list', elements='str', required=False),
+ dns=dict(type='list', elements='str', required=False),
+ dns_opt=dict(type='list', elements='str', required=False),
+ dns_search=dict(type='list', elements='str', required=False),
+ generate_systemd=dict(type='dict', default={}),
+ gidmap=dict(type='list', elements='str', required=False),
+ hostname=dict(type='str', required=False),
+ infra=dict(type='bool', required=False),
+ infra_conmon_pidfile=dict(type='str', required=False),
+ infra_command=dict(type='str', required=False),
+ infra_image=dict(type='str', required=False),
+ infra_name=dict(type='str', required=False),
+ ip=dict(type='str', required=False),
+ label=dict(type='dict', required=False),
+ label_file=dict(type='str', required=False),
+ mac_address=dict(type='str', required=False),
+ name=dict(type='str', required=True),
+ network=dict(type='list', elements='str', required=False),
+ network_alias=dict(type='list', elements='str', required=False,
+ aliases=['network_aliases']),
+ no_hosts=dict(type='bool', required=False),
+ pid=dict(type='str', required=False),
+ pod_id_file=dict(type='str', required=False),
+ publish=dict(type='list', required=False,
+ elements='str', aliases=['ports']),
+ share=dict(type='str', required=False),
+ subgidname=dict(type='str', required=False),
+ subuidname=dict(type='str', required=False),
+ uidmap=dict(type='list', elements='str', required=False),
+ userns=dict(type='str', required=False),
+ volume=dict(type='list', elements='str', aliases=['volumes'],
+ required=False),
+ executable=dict(type='str', required=False, default='podman'),
+ debug=dict(type='bool', default=False),
+)
+
+
+class PodmanPodModuleParams:
+ """Creates list of arguments for podman CLI command.
+
+ Arguments:
+ action {str} -- action type from 'run', 'stop', 'create', 'delete',
+ 'start'
+ params {dict} -- dictionary of module parameters
+
+ """
+
+ def __init__(self, action, params, podman_version, module):
+ self.params = params
+ self.action = action
+ self.podman_version = podman_version
+ self.module = module
+
+ def construct_command_from_params(self):
+ """Create a podman command from given module parameters.
+
+ Returns:
+ list -- list of byte strings for Popen command
+ """
+ if self.action in ['start', 'restart', 'stop', 'delete', 'pause',
+ 'unpause', 'kill']:
+ return self._simple_action()
+ if self.action in ['create']:
+ return self._create_action()
+ self.module.fail_json(msg="Unknown action %s" % self.action)
+
+ def _simple_action(self):
+ if self.action in ['start', 'restart', 'stop', 'pause', 'unpause', 'kill']:
+ cmd = [self.action, self.params['name']]
+ return [to_bytes(i, errors='surrogate_or_strict') for i in cmd]
+
+ if self.action == 'delete':
+ cmd = ['rm', '-f', self.params['name']]
+ return [to_bytes(i, errors='surrogate_or_strict') for i in cmd]
+ self.module.fail_json(msg="Unknown action %s" % self.action)
+
+ def _create_action(self):
+ cmd = [self.action]
+ all_param_methods = [func for func in dir(self)
+ if callable(getattr(self, func))
+ and func.startswith("addparam")]
+ params_set = (i for i in self.params if self.params[i] is not None)
+ for param in params_set:
+ func_name = "_".join(["addparam", param])
+ if func_name in all_param_methods:
+ cmd = getattr(self, func_name)(cmd)
+ return [to_bytes(i, errors='surrogate_or_strict') for i in cmd]
+
+ def check_version(self, param, minv=None, maxv=None):
+ if minv and LooseVersion(minv) > LooseVersion(
+ self.podman_version):
+ self.module.fail_json(msg="Parameter %s is supported from podman "
+ "version %s only! Current version is %s" % (
+ param, minv, self.podman_version))
+ if maxv and LooseVersion(maxv) < LooseVersion(
+ self.podman_version):
+ self.module.fail_json(msg="Parameter %s is supported till podman "
+ "version %s only! Current version is %s" % (
+ param, minv, self.podman_version))
+
+ def addparam_add_host(self, c):
+ for g in self.params['add_host']:
+ c += ['--add-host', g]
+ return c
+
+ def addparam_cgroup_parent(self, c):
+ return c + ['--cgroup-parent', self.params['cgroup_parent']]
+
+ def addparam_cpus(self, c):
+ return c + ['--cpus', self.params['cpus']]
+
+ def addparam_cpuset_cpus(self, c):
+ return c + ['--cpuset-cpus', self.params['cpuset_cpus']]
+
+ def addparam_device(self, c):
+ for dev in self.params['device']:
+ c += ['--device', dev]
+ return c
+
+ def addparam_device_read_bps(self, c):
+ for dev in self.params['device_read_bps']:
+ c += ['--device-read-bps', dev]
+ return c
+
+ def addparam_dns(self, c):
+ for g in self.params['dns']:
+ c += ['--dns', g]
+ return c
+
+ def addparam_dns_opt(self, c):
+ for g in self.params['dns_opt']:
+ c += ['--dns-opt', g]
+ return c
+
+ def addparam_dns_search(self, c):
+ for g in self.params['dns_search']:
+ c += ['--dns-search', g]
+ return c
+
+ def addparam_gidmap(self, c):
+ for gidmap in self.params['gidmap']:
+ c += ['--gidmap', gidmap]
+ return c
+
+ def addparam_hostname(self, c):
+ return c + ['--hostname', self.params['hostname']]
+
+ def addparam_infra(self, c):
+ return c + [b'='.join([b'--infra',
+ to_bytes(self.params['infra'],
+ errors='surrogate_or_strict')])]
+
+ def addparam_infra_conmon_pidfile(self, c):
+ return c + ['--infra-conmon-pidfile', self.params['infra_conmon_pidfile']]
+
+ def addparam_infra_command(self, c):
+ return c + ['--infra-command', self.params['infra_command']]
+
+ def addparam_infra_image(self, c):
+ return c + ['--infra-image', self.params['infra_image']]
+
+ def addparam_infra_name(self, c):
+ return c + ['--infra-name', self.params['infra_name']]
+
+ def addparam_ip(self, c):
+ return c + ['--ip', self.params['ip']]
+
+ def addparam_label(self, c):
+ for label in self.params['label'].items():
+ c += ['--label', b'='.join(
+ [to_bytes(i, errors='surrogate_or_strict') for i in label])]
+ return c
+
+ def addparam_label_file(self, c):
+ return c + ['--label-file', self.params['label_file']]
+
+ def addparam_mac_address(self, c):
+ return c + ['--mac-address', self.params['mac_address']]
+
+ def addparam_name(self, c):
+ return c + ['--name', self.params['name']]
+
+ def addparam_network(self, c):
+ if LooseVersion(self.podman_version) >= LooseVersion('4.0.0'):
+ for net in self.params['network']:
+ c += ['--network', net]
+ return c
+ return c + ['--network', ",".join(self.params['network'])]
+
+ def addparam_network_aliases(self, c):
+ for alias in self.params['network_aliases']:
+ c += ['--network-alias', alias]
+ return c
+
+ def addparam_no_hosts(self, c):
+ return c + ["=".join('--no-hosts', self.params['no_hosts'])]
+
+ def addparam_pid(self, c):
+ return c + ['--pid', self.params['pid']]
+
+ def addparam_pod_id_file(self, c):
+ return c + ['--pod-id-file', self.params['pod_id_file']]
+
+ def addparam_publish(self, c):
+ for g in self.params['publish']:
+ c += ['--publish', g]
+ return c
+
+ def addparam_share(self, c):
+ return c + ['--share', self.params['share']]
+
+ def addparam_subgidname(self, c):
+ return c + ['--subgidname', self.params['subgidname']]
+
+ def addparam_subuidname(self, c):
+ return c + ['--subuidname', self.params['subuidname']]
+
+ def addparam_uidmap(self, c):
+ for uidmap in self.params['uidmap']:
+ c += ['--uidmap', uidmap]
+ return c
+
+ def addparam_userns(self, c):
+ return c + ['--userns', self.params['userns']]
+
+ def addparam_volume(self, c):
+ for vol in self.params['volume']:
+ if vol:
+ c += ['--volume', vol]
+ return c
+
+
+class PodmanPodDefaults:
+ def __init__(self, module, podman_version):
+ self.module = module
+ self.version = podman_version
+ self.defaults = {
+ 'add_host': [],
+ 'dns': [],
+ 'dns_opt': [],
+ 'dns_search': [],
+ 'infra': True,
+ 'label': {},
+ }
+
+ def default_dict(self):
+ # make here any changes to self.defaults related to podman version
+ # https://github.com/containers/libpod/pull/5669
+ # if (LooseVersion(self.version) >= LooseVersion('1.8.0')
+ # and LooseVersion(self.version) < LooseVersion('1.9.0')):
+ # self.defaults['cpu_shares'] = 1024
+ return self.defaults
+
+
+class PodmanPodDiff:
+ def __init__(self, module, module_params, info, infra_info, podman_version):
+ self.module = module
+ self.module_params = module_params
+ self.version = podman_version
+ self.default_dict = None
+ self.info = lower_keys(info)
+ self.infra_info = lower_keys(infra_info)
+ self.params = self.defaultize()
+ self.diff = {'before': {}, 'after': {}}
+ self.non_idempotent = {}
+
+ def defaultize(self):
+ params_with_defaults = {}
+ self.default_dict = PodmanPodDefaults(
+ self.module, self.version).default_dict()
+ for p in self.module_params:
+ if self.module_params[p] is None and p in self.default_dict:
+ params_with_defaults[p] = self.default_dict[p]
+ else:
+ params_with_defaults[p] = self.module_params[p]
+ return params_with_defaults
+
+ def _diff_update_and_compare(self, param_name, before, after):
+ if before != after:
+ self.diff['before'].update({param_name: before})
+ self.diff['after'].update({param_name: after})
+ return True
+ return False
+
+ def diffparam_add_host(self):
+ if not self.infra_info:
+ return self._diff_update_and_compare('add_host', '', '')
+ before = self.infra_info['hostconfig']['extrahosts'] or []
+ after = self.params['add_host']
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('add_host', before, after)
+
+ def diffparam_cgroup_parent(self):
+ if 'cgroupparent' in self.info:
+ before = self.info['cgroupparent']
+ elif 'config' in self.info and self.info['config'].get('cgroupparent'):
+ before = self.info['config']['cgroupparent']
+ after = self.params['cgroup_parent'] or before
+ return self._diff_update_and_compare('cgroup_parent', before, after)
+
+ def diffparam_dns(self):
+ if not self.infra_info:
+ return self._diff_update_and_compare('dns', '', '')
+ before = self.infra_info['hostconfig']['dns'] or []
+ after = self.params['dns']
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('dns', before, after)
+
+ def diffparam_dns_opt(self):
+ if not self.infra_info:
+ return self._diff_update_and_compare('dns_opt', '', '')
+ before = self.infra_info['hostconfig']['dnsoptions'] or []
+ after = self.params['dns_opt']
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('dns_opt', before, after)
+
+ def diffparam_dns_search(self):
+ if not self.infra_info:
+ return self._diff_update_and_compare('dns_search', '', '')
+ before = self.infra_info['hostconfig']['dnssearch'] or []
+ after = self.params['dns_search']
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('dns_search', before, after)
+
+ def diffparam_hostname(self):
+ if not self.infra_info:
+ return self._diff_update_and_compare('hostname', '', '')
+ before = self.infra_info['config']['hostname']
+ after = self.params['hostname'] or before
+ return self._diff_update_and_compare('hostname', before, after)
+
+ # TODO(sshnaidm): https://github.com/containers/podman/issues/6968
+ def diffparam_infra(self):
+ if 'state' in self.info and 'infracontainerid' in self.info['state']:
+ before = self.info['state']['infracontainerid'] != ""
+ else:
+ # TODO(sshnaidm): https://github.com/containers/podman/issues/6968
+ before = 'infracontainerid' in self.info
+ after = self.params['infra']
+ return self._diff_update_and_compare('infra', before, after)
+
+ # TODO(sshnaidm): https://github.com/containers/podman/issues/6969
+ # def diffparam_infra_command(self):
+ # before = str(self.info['hostconfig']['infra_command'])
+ # after = self.params['infra_command']
+ # return self._diff_update_and_compare('infra_command', before, after)
+
+ def diffparam_infra_image(self):
+ if not self.infra_info:
+ return self._diff_update_and_compare('infra_image', '', '')
+ before = str(self.infra_info['imagename'])
+ after = before
+ if self.module_params['infra_image']:
+ after = self.params['infra_image']
+ before = before.replace(":latest", "")
+ after = after.replace(":latest", "")
+ before = before.split("/")[-1] # pylint: disable=W,C,R
+ after = after.split("/")[-1] # pylint: disable=W,C,R
+ return self._diff_update_and_compare('infra_image', before, after)
+
+ # TODO(sshnaidm): https://github.com/containers/podman/pull/6956
+ # def diffparam_ip(self):
+ # before = str(self.info['hostconfig']['ip'])
+ # after = self.params['ip']
+ # return self._diff_update_and_compare('ip', before, after)
+
+ def diffparam_label(self):
+ if 'config' in self.info and 'labels' in self.info['config']:
+ before = self.info['config'].get('labels') or {}
+ else:
+ before = self.info['labels'] if 'labels' in self.info else {}
+ after = self.params['label']
+ # Strip out labels that are coming from systemd files
+ # https://github.com/containers/ansible-podman-collections/issues/276
+ if 'podman_systemd_unit' in before:
+ after.pop('podman_systemd_unit', None)
+ before.pop('podman_systemd_unit', None)
+ return self._diff_update_and_compare('label', before, after)
+
+ # TODO(sshnaidm): https://github.com/containers/podman/pull/6956
+ # def diffparam_mac_address(self):
+ # before = str(self.info['hostconfig']['mac_address'])
+ # after = self.params['mac_address']
+ # return self._diff_update_and_compare('mac_address', before, after)
+
+ def diffparam_network(self):
+ if not self.infra_info:
+ return self._diff_update_and_compare('network', [], [])
+ net_mode_before = self.infra_info['hostconfig']['networkmode']
+ net_mode_after = ''
+ before = list(self.infra_info['networksettings'].get('networks', {}))
+ # Remove default 'podman' network in v3 for comparison
+ if before == ['podman']:
+ before = []
+ after = self.params['network'] or []
+ # Special case for options for slirp4netns rootless networking from v2
+ if net_mode_before == 'slirp4netns' and 'createcommand' in self.info:
+ cr_com = self.info['createcommand']
+ if '--network' in cr_com:
+ cr_net = cr_com[cr_com.index('--network') + 1].lower()
+ if 'slirp4netns:' in cr_net:
+ before = [cr_net]
+ # Currently supported only 'host' and 'none' network modes idempotency
+ if after in [['bridge'], ['host'], ['slirp4netns']]:
+ net_mode_after = after[0]
+
+ if net_mode_after and not before:
+ # Remove differences between v1 and v2
+ net_mode_after = net_mode_after.replace('bridge', 'default')
+ net_mode_after = net_mode_after.replace('slirp4netns', 'default')
+ net_mode_before = net_mode_before.replace('bridge', 'default')
+ net_mode_before = net_mode_before.replace('slirp4netns', 'default')
+ return self._diff_update_and_compare('network', net_mode_before, net_mode_after)
+ # For 4.4.0+ podman versions with no network specified
+ if not net_mode_after and net_mode_before == 'slirp4netns' and not after:
+ net_mode_after = 'slirp4netns'
+ if before == ['slirp4netns']:
+ after = ['slirp4netns']
+ if not net_mode_after and net_mode_before == 'bridge' and not after:
+ net_mode_after = 'bridge'
+ if before == ['bridge']:
+ after = ['bridge']
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('network', before, after)
+
+ # TODO(sshnaidm)
+ # def diffparam_no_hosts(self):
+ # before = str(self.info['hostconfig']['no_hosts'])
+ # after = self.params['no_hosts']
+ # return self._diff_update_and_compare('no_hosts', before, after)
+
+ # TODO(sshnaidm) Need to add port ranges support
+ def diffparam_publish(self):
+ def compose(p, h):
+ s = ":".join(
+ [str(h["hostport"]), p.replace('/tcp', '')]
+ ).strip(":")
+ if h['hostip']:
+ return ":".join([h['hostip'], s])
+ return s
+
+ if not self.infra_info:
+ return self._diff_update_and_compare('publish', '', '')
+
+ ports = self.infra_info['hostconfig']['portbindings']
+ before = []
+ for port, hosts in ports.items():
+ if hosts:
+ for h in hosts:
+ before.append(compose(port, h))
+ after = self.params['publish'] or []
+ after = [
+ i.replace("/tcp", "").replace("[", "").replace("]", "")
+ for i in after]
+ # No support for port ranges yet
+ for ports in after:
+ if "-" in ports:
+ return self._diff_update_and_compare('publish', '', '')
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('publish', before, after)
+
+ def diffparam_share(self):
+ if not self.infra_info:
+ return self._diff_update_and_compare('share', '', '')
+ if 'sharednamespaces' in self.info:
+ before = self.info['sharednamespaces']
+ elif 'config' in self.info:
+ before = [
+ i.split('shares')[1].lower()
+ for i in self.info['config'] if 'shares' in i]
+ # TODO(sshnaidm): to discover why in podman v1 'cgroup' appears
+ before.remove('cgroup')
+ else:
+ before = []
+ if self.params['share'] is not None:
+ after = self.params['share'].split(",")
+ else:
+ after = ['uts', 'ipc', 'net']
+ # TODO: find out why on Ubuntu the 'net' is not present
+ if 'net' not in before:
+ after.remove('net')
+ if self.params["uidmap"] or self.params["gidmap"]:
+ after.append('user')
+
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('share', before, after)
+
+ def is_different(self):
+ diff_func_list = [func for func in dir(self)
+ if callable(getattr(self, func)) and func.startswith(
+ "diffparam")]
+ fail_fast = not bool(self.module._diff)
+ different = False
+ for func_name in diff_func_list:
+ dff_func = getattr(self, func_name)
+ if dff_func():
+ if fail_fast:
+ return True
+ different = True
+ # Check non idempotent parameters
+ for p in self.non_idempotent:
+ if self.module_params[p] is not None and self.module_params[p] not in [{}, [], '']:
+ different = True
+ return different
+
+
+class PodmanPod:
+ """Perform pod tasks.
+
+ Manages podman pod, inspects it and checks its current state
+ """
+
+ def __init__(self, module, name, module_params):
+ """Initialize PodmanPod class.
+
+ Arguments:
+ module {obj} -- ansible module object
+ name {str} -- name of pod
+ """
+
+ self.module = module
+ self.module_params = module_params
+ self.name = name
+ self.stdout, self.stderr = '', ''
+ self.info = self.get_info()
+ self.infra_info = self.get_infra_info()
+ self.version = self._get_podman_version()
+ self.diff = {}
+ self.actions = []
+
+ @property
+ def exists(self):
+ """Check if pod exists."""
+ return bool(self.info != {})
+
+ @property
+ def different(self):
+ """Check if pod is different."""
+ diffcheck = PodmanPodDiff(
+ self.module,
+ self.module_params,
+ self.info,
+ self.infra_info,
+ self.version)
+ is_different = diffcheck.is_different()
+ diffs = diffcheck.diff
+ if self.module._diff and is_different and diffs['before'] and diffs['after']:
+ self.diff['before'] = "\n".join(
+ ["%s - %s" % (k, v) for k, v in sorted(
+ diffs['before'].items())]) + "\n"
+ self.diff['after'] = "\n".join(
+ ["%s - %s" % (k, v) for k, v in sorted(
+ diffs['after'].items())]) + "\n"
+ return is_different
+
+ @property
+ def running(self):
+ """Return True if pod is running now."""
+ if 'status' in self.info['State']:
+ return self.info['State']['status'] == 'Running'
+ # older podman versions (1.6.x) don't have status in 'podman pod inspect'
+ # if other methods fail, use 'podman pod ps'
+ ps_info = self.get_ps()
+ if 'status' in ps_info:
+ return ps_info['status'] == 'Running'
+ return self.info['State'] == 'Running'
+
+ @property
+ def paused(self):
+ """Return True if pod is paused now."""
+ if 'status' in self.info['State']:
+ return self.info['State']['status'] == 'Paused'
+ return self.info['State'] == 'Paused'
+
+ @property
+ def stopped(self):
+ """Return True if pod exists and is not running now."""
+ if not self.exists:
+ return False
+ if 'status' in self.info['State']:
+ return not (self.info['State']['status'] == 'Running')
+ return not (self.info['State'] == 'Running')
+
+ def get_info(self):
+ """Inspect pod and gather info about it."""
+ # pylint: disable=unused-variable
+ rc, out, err = self.module.run_command(
+ [self.module_params['executable'], b'pod', b'inspect', self.name])
+ return json.loads(out) if rc == 0 else {}
+
+ def get_ps(self):
+ """Inspect pod process and gather info about it."""
+ # pylint: disable=unused-variable
+ rc, out, err = self.module.run_command(
+ [self.module_params['executable'], b'pod', b'ps', b'--format', b'json', b'--filter', 'name=' + self.name])
+ return json.loads(out)[0] if rc == 0 else {}
+
+ def get_infra_info(self):
+ """Inspect pod and gather info about it."""
+ if not self.info:
+ return {}
+ if 'InfraContainerID' in self.info:
+ infra_container_id = self.info['InfraContainerID']
+ elif 'State' in self.info and 'infraContainerID' in self.info['State']:
+ infra_container_id = self.info['State']['infraContainerID']
+ else:
+ return {}
+ # pylint: disable=unused-variable
+ rc, out, err = self.module.run_command(
+ [self.module_params['executable'], b'inspect', infra_container_id])
+ return json.loads(out)[0] if rc == 0 else {}
+
+ def _get_podman_version(self):
+ # pylint: disable=unused-variable
+ rc, out, err = self.module.run_command(
+ [self.module_params['executable'], b'--version'])
+ if rc != 0 or not out or "version" not in out:
+ self.module.fail_json(msg="%s run failed!" % self.module_params['executable'])
+ return out.split("version")[1].strip()
+
+ def _perform_action(self, action):
+ """Perform action with pod.
+
+ Arguments:
+ action {str} -- action to perform - start, create, stop, pause
+ unpause, delete, restart, kill
+ """
+ b_command = PodmanPodModuleParams(action,
+ self.module_params,
+ self.version,
+ self.module,
+ ).construct_command_from_params()
+ full_cmd = " ".join([self.module_params['executable'], 'pod']
+ + [to_native(i) for i in b_command])
+ self.module.log("PODMAN-POD-DEBUG: %s" % full_cmd)
+ self.actions.append(full_cmd)
+ if not self.module.check_mode:
+ rc, out, err = self.module.run_command(
+ [self.module_params['executable'], b'pod'] + b_command,
+ expand_user_and_vars=False)
+ self.stdout = out
+ self.stderr = err
+ if rc != 0:
+ self.module.fail_json(
+ msg="Can't %s pod %s" % (action, self.name),
+ stdout=out, stderr=err)
+
+ def delete(self):
+ """Delete the pod."""
+ self._perform_action('delete')
+
+ def stop(self):
+ """Stop the pod."""
+ self._perform_action('stop')
+
+ def start(self):
+ """Start the pod."""
+ self._perform_action('start')
+
+ def create(self):
+ """Create the pod."""
+ self._perform_action('create')
+
+ def recreate(self):
+ """Recreate the pod."""
+ self.delete()
+ self.create()
+
+ def restart(self):
+ """Restart the pod."""
+ self._perform_action('restart')
+
+ def kill(self):
+ """Kill the pod."""
+ self._perform_action('kill')
+
+ def pause(self):
+ """Pause the pod."""
+ self._perform_action('pause')
+
+ def unpause(self):
+ """Unpause the pod."""
+ self._perform_action('unpause')
+
+
+class PodmanPodManager:
+ """Module manager class.
+
+ Defines according to parameters what actions should be applied to pod
+ """
+
+ def __init__(self, module, params):
+ """Initialize PodmanManager class.
+
+ Arguments:
+ module {obj} -- ansible module object
+ """
+
+ self.module = module
+ self.module_params = params
+ self.results = {
+ 'changed': False,
+ 'actions': [],
+ 'pod': {},
+ }
+ self.name = self.module_params['name']
+ self.executable = \
+ self.module.get_bin_path(self.module_params['executable'],
+ required=True)
+ self.state = self.module_params['state']
+ self.recreate = self.module_params['recreate']
+ self.pod = PodmanPod(self.module, self.name, self.module_params)
+
+ def update_pod_result(self, changed=True):
+ """Inspect the current pod, update results with last info, exit.
+
+ Keyword Arguments:
+ changed {bool} -- whether any action was performed
+ (default: {True})
+ """
+ facts = self.pod.get_info() if changed else self.pod.info
+ out, err = self.pod.stdout, self.pod.stderr
+ self.results.update({'changed': changed, 'pod': facts,
+ 'podman_actions': self.pod.actions},
+ stdout=out, stderr=err)
+ if self.pod.diff:
+ self.results.update({'diff': self.pod.diff})
+ if self.module.params['debug'] or self.module_params['debug']:
+ self.results.update({'podman_version': self.pod.version})
+ self.results.update(
+ {'podman_systemd': generate_systemd(self.module,
+ self.module_params,
+ self.name,
+ self.pod.version)})
+
+ def execute(self):
+ """Execute the desired action according to map of actions & states."""
+ states_map = {
+ 'created': self.make_created,
+ 'started': self.make_started,
+ 'stopped': self.make_stopped,
+ 'restarted': self.make_restarted,
+ 'absent': self.make_absent,
+ 'killed': self.make_killed,
+ 'paused': self.make_paused,
+ 'unpaused': self.make_unpaused,
+
+ }
+ process_action = states_map[self.state]
+ process_action()
+ return self.results
+
+ def _create_or_recreate_pod(self):
+ """Ensure pod exists and is exactly as it should be by input params."""
+ changed = False
+ if self.pod.exists:
+ if self.pod.different or self.recreate:
+ self.pod.recreate()
+ self.results['actions'].append('recreated %s' % self.pod.name)
+ changed = True
+ elif not self.pod.exists:
+ self.pod.create()
+ self.results['actions'].append('created %s' % self.pod.name)
+ changed = True
+ return changed
+
+ def make_created(self):
+ """Run actions if desired state is 'created'."""
+ if self.pod.exists and not self.pod.different:
+ self.update_pod_result(changed=False)
+ return
+ self._create_or_recreate_pod()
+ self.update_pod_result()
+
+ def make_killed(self):
+ """Run actions if desired state is 'killed'."""
+ self._create_or_recreate_pod()
+ self.pod.kill()
+ self.results['actions'].append('killed %s' % self.pod.name)
+ self.update_pod_result()
+
+ def make_paused(self):
+ """Run actions if desired state is 'paused'."""
+ changed = self._create_or_recreate_pod()
+ if self.pod.paused:
+ self.update_pod_result(changed=changed)
+ return
+ self.pod.pause()
+ self.results['actions'].append('paused %s' % self.pod.name)
+ self.update_pod_result()
+
+ def make_unpaused(self):
+ """Run actions if desired state is 'unpaused'."""
+ changed = self._create_or_recreate_pod()
+ if not self.pod.paused:
+ self.update_pod_result(changed=changed)
+ return
+ self.pod.unpause()
+ self.results['actions'].append('unpaused %s' % self.pod.name)
+ self.update_pod_result()
+
+ def make_started(self):
+ """Run actions if desired state is 'started'."""
+ changed = self._create_or_recreate_pod()
+ if not changed and self.pod.running:
+ self.update_pod_result(changed=changed)
+ return
+
+ # self.pod.unpause() TODO(sshnaidm): to unpause if state == started?
+ self.pod.start()
+ self.results['actions'].append('started %s' % self.pod.name)
+ self.update_pod_result()
+
+ def make_stopped(self):
+ """Run actions if desired state is 'stopped'."""
+ if not self.pod.exists:
+ self.module.fail_json("Pod %s doesn't exist!" % self.pod.name)
+ if self.pod.running:
+ self.pod.stop()
+ self.results['actions'].append('stopped %s' % self.pod.name)
+ self.update_pod_result()
+ elif self.pod.stopped:
+ self.update_pod_result(changed=False)
+
+ def make_restarted(self):
+ """Run actions if desired state is 'restarted'."""
+ if self.pod.exists:
+ self.pod.restart()
+ self.results['actions'].append('restarted %s' % self.pod.name)
+ self.results.update({'changed': True})
+ self.update_pod_result()
+ else:
+ self.module.fail_json("Pod %s doesn't exist!" % self.pod.name)
+
+ def make_absent(self):
+ """Run actions if desired state is 'absent'."""
+ if not self.pod.exists:
+ self.results.update({'changed': False})
+ elif self.pod.exists:
+ delete_systemd(self.module,
+ self.module_params,
+ self.name,
+ self.pod.version)
+ self.pod.delete()
+ self.results['actions'].append('deleted %s' % self.pod.name)
+ self.results.update({'changed': True})
+ self.results.update({'pod': {},
+ 'podman_actions': self.pod.actions})
diff --git a/ansible_collections/containers/podman/plugins/modules/__init__.py b/ansible_collections/containers/podman/plugins/modules/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/__init__.py
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_container.py b/ansible_collections/containers/podman/plugins/modules/podman_container.py
new file mode 100644
index 000000000..7878352da
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_container.py
@@ -0,0 +1,1063 @@
+#!/usr/bin/python
+# Copyright (c) 2020 Red Hat
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# flake8: noqa: E501
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+DOCUMENTATION = r"""
+module: podman_container
+author:
+ - "Sagi Shnaidman (@sshnaidm)"
+version_added: '1.0.0'
+short_description: Manage podman containers
+notes: []
+description:
+ - Start, stop, restart and manage Podman containers
+requirements:
+ - podman
+options:
+ name:
+ description:
+ - Name of the container
+ required: True
+ type: str
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+ state:
+ description:
+ - I(absent) - A container matching the specified name will be stopped and
+ removed.
+ - I(present) - Asserts the existence of a container matching the name and
+ any provided configuration parameters. If no container matches the
+ name, a container will be created. If a container matches the name but
+ the provided configuration does not match, the container will be
+ updated, if it can be. If it cannot be updated, it will be removed and
+ re-created with the requested config. Image version will be taken into
+ account when comparing configuration. Use the recreate option to force
+ the re-creation of the matching container.
+ - I(started) - Asserts there is a running container matching the name and
+ any provided configuration. If no container matches the name, a
+ container will be created and started. Use recreate to always re-create
+ a matching container, even if it is running. Use force_restart to force
+ a matching container to be stopped and restarted.
+ - I(stopped) - Asserts that the container is first I(present), and then
+ if the container is running moves it to a stopped state.
+ - I(created) - Asserts that the container exists with given configuration.
+ 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
+ type: str
+ default: started
+ choices:
+ - absent
+ - present
+ - stopped
+ - started
+ - created
+ image:
+ description:
+ - Repository path (or image name) and tag used to create the container.
+ If an image is not found, the image will be pulled from the registry.
+ If no tag is included, C(latest) will be used.
+ - Can also be an image ID. If this is the case, the image is assumed to
+ be available locally.
+ type: str
+ annotation:
+ description:
+ - Add an annotation to the container. The format is key value, multiple
+ times.
+ type: dict
+ authfile:
+ description:
+ - Path of the authentication file. Default is
+ ``${XDG_RUNTIME_DIR}/containers/auth.json``
+ (Not available for remote commands) You can also override the default
+ path of the authentication file by setting the ``REGISTRY_AUTH_FILE``
+ environment variable. ``export REGISTRY_AUTH_FILE=path``
+ type: path
+ blkio_weight:
+ description:
+ - Block IO weight (relative weight) accepts a weight value between 10 and
+ 1000
+ type: int
+ blkio_weight_device:
+ description:
+ - Block IO weight (relative device weight, format DEVICE_NAME[:]WEIGHT).
+ type: dict
+ cap_add:
+ description:
+ - List of capabilities to add to the container.
+ type: list
+ elements: str
+ aliases:
+ - capabilities
+ cap_drop:
+ description:
+ - List of capabilities to drop from the container.
+ type: list
+ elements: str
+ cgroup_parent:
+ description:
+ - Path to cgroups under which the cgroup for the container will be
+ created.
+ If the path is not absolute, the path is considered to be relative to
+ the cgroups path of the init process. Cgroups will be created if they
+ do not already exist.
+ type: path
+ cgroupns:
+ description:
+ - Path to cgroups under which the cgroup for the container will be
+ created.
+ type: str
+ cgroups:
+ description:
+ - Determines whether the container will create CGroups.
+ Valid values are enabled and disabled, which the default being enabled.
+ The disabled option will force the container to not create CGroups,
+ and thus conflicts with CGroup options cgroupns and cgroup-parent.
+ type: str
+ cidfile:
+ description:
+ - Write the container ID to the file
+ type: path
+ cmd_args:
+ description:
+ - Any additional command options you want to pass to podman command itself,
+ for example C(--log-level=debug) or C(--syslog). This is NOT command to
+ run in container, but rather options for podman itself.
+ For container command please use I(command) option.
+ type: list
+ elements: str
+ conmon_pidfile:
+ description:
+ - Write the pid of the conmon process to a file.
+ conmon runs in a separate process than Podman,
+ so this is necessary when using systemd to restart Podman containers.
+ type: path
+ command:
+ description:
+ - Override command of container. Can be a string or a list.
+ type: raw
+ cpu_period:
+ description:
+ - Limit the CPU real-time period in microseconds
+ type: int
+ cpu_rt_period:
+ description:
+ - Limit the CPU real-time period in microseconds.
+ Limit the container's Real Time CPU usage. This flag tell the kernel to
+ restrict the container's Real Time CPU usage to the period you specify.
+ type: int
+ cpu_rt_runtime:
+ description:
+ - Limit the CPU real-time runtime in microseconds.
+ This flag tells the kernel to limit the amount of time in a given CPU
+ period Real Time tasks may consume.
+ type: int
+ cpu_shares:
+ description:
+ - CPU shares (relative weight)
+ type: int
+ cpus:
+ description:
+ - Number of CPUs. The default is 0.0 which means no limit.
+ type: str
+ cpuset_cpus:
+ description:
+ - CPUs in which to allow execution (0-3, 0,1)
+ type: str
+ cpuset_mems:
+ description:
+ - Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only
+ effective on NUMA systems.
+ type: str
+ detach:
+ description:
+ - Run container in detach mode
+ type: bool
+ default: True
+ debug:
+ description:
+ - Return additional information which can be helpful for investigations.
+ type: bool
+ default: False
+ detach_keys:
+ description:
+ - Override the key sequence for detaching a container. Format is a single
+ character or ctrl-value
+ type: str
+ device:
+ description:
+ - Add a host device to the container.
+ The format is <device-on-host>[:<device-on-container>][:<permissions>]
+ (e.g. device /dev/sdc:/dev/xvdc:rwm)
+ type: list
+ elements: str
+ device_read_bps:
+ description:
+ - Limit read rate (bytes per second) from a device
+ (e.g. device-read-bps /dev/sda:1mb)
+ type: list
+ elements: str
+ device_read_iops:
+ description:
+ - Limit read rate (IO per second) from a device
+ (e.g. device-read-iops /dev/sda:1000)
+ type: list
+ elements: str
+ device_write_bps:
+ description:
+ - Limit write rate (bytes per second) to a device
+ (e.g. device-write-bps /dev/sda:1mb)
+ type: list
+ elements: str
+ device_write_iops:
+ description:
+ - Limit write rate (IO per second) to a device
+ (e.g. device-write-iops /dev/sda:1000)
+ type: list
+ elements: str
+ dns:
+ description:
+ - Set custom DNS servers
+ type: list
+ elements: str
+ aliases:
+ - dns_servers
+ dns_option:
+ description:
+ - Set custom DNS options
+ type: str
+ aliases:
+ - dns_opts
+ dns_search:
+ description:
+ - Set custom DNS search domains (Use dns_search with '' if you don't wish
+ to set the search domain)
+ type: str
+ aliases:
+ - dns_search_domains
+ entrypoint:
+ description:
+ - Overwrite the default ENTRYPOINT of the image
+ type: str
+ env:
+ description:
+ - Set environment variables.
+ This option allows you to specify arbitrary environment variables that
+ are available for the process that will be launched inside of the
+ container.
+ type: dict
+ env_file:
+ description:
+ - Read in a line delimited file of environment variables. Doesn't support
+ idempotency. If users changes the file with environment variables it's
+ on them to recreate the container.
+ type: path
+ env_host:
+ description:
+ - Use all current host environment variables in container.
+ Defaults to false.
+ type: bool
+ etc_hosts:
+ description:
+ - Dict of host-to-IP mappings, where each host name is a key in the
+ dictionary. Each host name will be added to the container's
+ ``/etc/hosts`` file.
+ type: dict
+ aliases:
+ - add_hosts
+ expose:
+ description:
+ - Expose a port, or a range of ports (e.g. expose "3300-3310") to set up
+ port redirection on the host system.
+ type: list
+ elements: str
+ aliases:
+ - exposed
+ - exposed_ports
+ force_restart:
+ description:
+ - Force restart of container.
+ type: bool
+ default: False
+ aliases:
+ - restart
+ generate_systemd:
+ description:
+ - Generate systemd unit file for container.
+ type: dict
+ default: {}
+ suboptions:
+ path:
+ description:
+ - Specify a path to the directory where unit files will be generated.
+ Required for this option. If it doesn't exist, the directory will be created.
+ type: str
+ required: false
+ restart_policy:
+ description:
+ - Specify a restart policy for the service. The restart-policy must be one of
+ "no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", or "always".
+ The default policy is "on-failure".
+ type: str
+ required: false
+ choices:
+ - 'no'
+ - 'on-success'
+ - 'on-failure'
+ - 'on-abnormal'
+ - 'on-watchdog'
+ - 'on-abort'
+ - 'always'
+ time:
+ description:
+ - Override the default stop timeout for the container with the given value.
+ type: int
+ required: false
+ no_header:
+ description:
+ - Do not generate the header including meta data such as the Podman version and the timestamp.
+ From podman version 3.1.0.
+ type: bool
+ default: false
+ names:
+ description:
+ - Use names of the containers for the start, stop, and description in the unit file.
+ Default is true.
+ type: bool
+ default: true
+ container_prefix:
+ description:
+ - Set the systemd unit name prefix for containers. The default is "container".
+ type: str
+ required: false
+ pod_prefix:
+ description:
+ - Set the systemd unit name prefix for pods. The default is "pod".
+ type: str
+ required: false
+ separator:
+ description:
+ - Set the systemd unit name separator between the name/id of a
+ container/pod and the prefix. The default is "-" (dash).
+ type: str
+ required: false
+ new:
+ description:
+ - Create containers and pods when the unit is started instead of
+ expecting them to exist. The default is "false".
+ Refer to podman-generate-systemd(1) for more information.
+ type: bool
+ default: false
+ after:
+ type: list
+ elements: str
+ required: false
+ description:
+ - Add the systemd unit after (After=) option, that ordering dependencies between the list of dependencies and this service.
+ wants:
+ type: list
+ elements: str
+ required: false
+ description:
+ - Add the systemd unit wants (Wants=) option, that this service is (weak) dependent on.
+ requires:
+ type: list
+ elements: str
+ required: false
+ description:
+ - Set the systemd unit requires (Requires=) option. Similar to wants, but declares a stronger requirement dependency.
+ gidmap:
+ description:
+ - Run the container in a new user namespace using the supplied mapping.
+ type: list
+ elements: str
+ group_add:
+ description:
+ - Add additional groups to run as
+ type: list
+ elements: str
+ aliases:
+ - groups
+ healthcheck:
+ description:
+ - Set or alter a healthcheck command for a container.
+ type: str
+ healthcheck_interval:
+ description:
+ - Set an interval for the healthchecks
+ (a value of disable results in no automatic timer setup)
+ (default "30s")
+ type: str
+ healthcheck_retries:
+ description:
+ - The number of retries allowed before a healthcheck is considered to be
+ unhealthy. The default value is 3.
+ type: int
+ healthcheck_start_period:
+ description:
+ - The initialization time needed for a container to bootstrap.
+ The value can be expressed in time format like 2m3s. The default value
+ is 0s
+ type: str
+ healthcheck_timeout:
+ description:
+ - The maximum time allowed to complete the healthcheck before an interval
+ is considered failed. Like start-period, the value can be expressed in
+ a time format such as 1m22s. The default value is 30s
+ type: str
+ hooks_dir:
+ description:
+ - Each .json file in the path configures a hook for Podman containers.
+ For more details on the syntax of the JSON files and the semantics of
+ hook injection, see oci-hooks(5). Can be set multiple times.
+ type: list
+ elements: str
+ hostname:
+ description:
+ - Container host name. Sets the container host name that is available
+ inside the container.
+ type: str
+ http_proxy:
+ description:
+ - By default proxy environment variables are passed into the container if
+ set for the podman process. This can be disabled by setting the
+ http_proxy option to false. The environment variables passed in
+ include http_proxy, https_proxy, ftp_proxy, no_proxy, and also the
+ upper case versions of those.
+ Defaults to true
+ type: bool
+ image_volume:
+ description:
+ - Tells podman how to handle the builtin image volumes.
+ The options are bind, tmpfs, or ignore (default bind)
+ type: str
+ choices:
+ - 'bind'
+ - 'tmpfs'
+ - 'ignore'
+ image_strict:
+ description:
+ - Whether to compare images in idempotency by taking into account a full
+ name with registry and namespaces.
+ type: bool
+ default: False
+ init:
+ description:
+ - Run an init inside the container that forwards signals and reaps
+ processes. The default is false.
+ type: bool
+ init_path:
+ description:
+ - Path to the container-init binary.
+ type: str
+ interactive:
+ description:
+ - Keep STDIN open even if not attached. The default is false.
+ When set to true, keep stdin open even if not attached.
+ The default is false.
+ type: bool
+ ip:
+ description:
+ - Specify a static IP address for the container, for example
+ '10.88.64.128'.
+ Can only be used if no additional CNI networks to join were specified
+ via 'network:', and if the container is not joining another container's
+ network namespace via 'network container:<name|id>'.
+ The address must be within the default CNI network's pool
+ (default 10.88.0.0/16).
+ type: str
+ ipc:
+ description:
+ - Default is to create a private IPC namespace (POSIX SysV IPC) for the
+ container
+ type: str
+ aliases:
+ - ipc_mode
+ kernel_memory:
+ description:
+ - Kernel memory limit
+ (format <number>[<unit>], where unit = b, k, m or g)
+ Note - idempotency is supported for integers only.
+ type: str
+ label:
+ description:
+ - Add metadata to a container, pass dictionary of label names and values
+ aliases:
+ - labels
+ type: dict
+ label_file:
+ description:
+ - Read in a line delimited file of labels
+ type: str
+ log_driver:
+ description:
+ - Logging driver. Used to set the log driver for the container.
+ For example log_driver "k8s-file".
+ type: str
+ choices:
+ - k8s-file
+ - journald
+ - json-file
+ log_level:
+ description:
+ - Logging level for Podman. Log messages above specified level
+ ("debug"|"info"|"warn"|"error"|"fatal"|"panic") (default "error")
+ type: str
+ choices:
+ - debug
+ - info
+ - warn
+ - error
+ - fatal
+ - panic
+ log_opt:
+ description:
+ - Logging driver specific options. Used to set the path to the container
+ log file.
+ type: dict
+ aliases:
+ - log_options
+ suboptions:
+ path:
+ description:
+ - Specify a path to the log file (e.g. /var/log/container/mycontainer.json).
+ type: str
+ required: false
+ max_size:
+ description:
+ - Specify a max size of the log file (e.g 10mb).
+ type: str
+ required: false
+ tag:
+ description:
+ - Specify a custom log tag for the container.
+ type: str
+ required: false
+
+ mac_address:
+ description:
+ - Specify a MAC address for the container, for example
+ '92:d0:c6:0a:29:33'.
+ Don't forget that it must be unique within one Ethernet network.
+ type: str
+ memory:
+ description:
+ - Memory limit (format 10k, where unit = b, k, m or g)
+ Note - idempotency is supported for integers only.
+ type: str
+ memory_reservation:
+ description:
+ - Memory soft limit (format 100m, where unit = b, k, m or g)
+ Note - idempotency is supported for integers only.
+ type: str
+ memory_swap:
+ description:
+ - A limit value equal to memory plus swap. Must be used with the -m
+ (--memory) flag.
+ The swap LIMIT should always be larger than -m (--memory) value.
+ By default, the swap LIMIT will be set to double the value of --memory
+ Note - idempotency is supported for integers only.
+ type: str
+ memory_swappiness:
+ description:
+ - Tune a container's memory swappiness behavior. Accepts an integer
+ between 0 and 100.
+ type: int
+ mount:
+ description:
+ - Attach a filesystem mount to the container. bind or tmpfs
+ For example mount
+ "type=bind,source=/path/on/host,destination=/path/in/container"
+ type: list
+ elements: str
+ aliases:
+ - mounts
+ network:
+ description:
+ - Set the Network mode for the container
+ * bridge create a network stack on the default bridge
+ * none no networking
+ * container:<name|id> reuse another container's network stack
+ * host use the podman host network stack.
+ * <network-name>|<network-id> connect to a user-defined network
+ * ns:<path> path to a network namespace to join
+ * slirp4netns use slirp4netns to create a user network stack.
+ This is the default for rootless containers
+ type: list
+ elements: str
+ aliases:
+ - net
+ - network_mode
+ network_aliases:
+ description:
+ - Add network-scoped alias for the container.
+ A container will only have access to aliases on the first network that it joins.
+ This is a limitation that will be removed in a later release.
+ type: list
+ elements: str
+ no_hosts:
+ description:
+ - Do not create /etc/hosts for the container
+ Default is false.
+ type: bool
+ oom_kill_disable:
+ description:
+ - Whether to disable OOM Killer for the container or not.
+ Default is false.
+ type: bool
+ oom_score_adj:
+ description:
+ - Tune the host's OOM preferences for containers (accepts -1000 to 1000)
+ type: int
+ pid:
+ description:
+ - Set the PID mode for the container
+ type: str
+ aliases:
+ - pid_mode
+ pids_limit:
+ description:
+ - Tune the container's PIDs limit. Set -1 to have unlimited PIDs for the
+ container.
+ type: str
+ pod:
+ description:
+ - Run container in an existing pod.
+ If you want podman to make the pod for you, prefix the pod name
+ with "new:"
+ type: str
+ privileged:
+ description:
+ - Give extended privileges to this container. The default is false.
+ type: bool
+ publish:
+ description:
+ - Publish a container's port, or range of ports, to the host.
+ Format - ip:hostPort:containerPort | ip::containerPort |
+ hostPort:containerPort | containerPort
+ In case of only containerPort is set, the hostPort will chosen
+ randomly by Podman.
+ type: list
+ elements: str
+ aliases:
+ - ports
+ - published
+ - published_ports
+ publish_all:
+ description:
+ - Publish all exposed ports to random ports on the host interfaces. The
+ default is false.
+ type: bool
+ read_only:
+ description:
+ - Mount the container's root filesystem as read only. Default is false
+ type: bool
+ read_only_tmpfs:
+ description:
+ - If container is running in --read-only mode, then mount a read-write
+ tmpfs on /run, /tmp, and /var/tmp. The default is true
+ type: bool
+ recreate:
+ description:
+ - Use with present and started states to force the re-creation of an
+ existing container.
+ type: bool
+ default: False
+ requires:
+ description:
+ - Specify one or more requirements. A requirement is a dependency
+ container that will be started before this container.
+ Containers can be specified by name or ID.
+ type: list
+ elements: str
+ restart_policy:
+ description:
+ - Restart policy to follow when containers exit.
+ Restart policy will not take effect if a container is stopped via the
+ podman kill or podman stop commands. Valid values are
+ * no - Do not restart containers on exit
+ * on-failure[:max_retries] - Restart containers when they exit with a
+ non-0 exit code, retrying indefinitely
+ or until the optional max_retries count is hit
+ * always - Restart containers when they exit, regardless of status,
+ retrying indefinitely
+ type: str
+ rm:
+ description:
+ - Automatically remove the container when it exits. The default is false.
+ type: bool
+ aliases:
+ - remove
+ - auto_remove
+ rootfs:
+ description:
+ - If true, the first argument refers to an exploded container on the file
+ system. The default is false.
+ type: bool
+ sdnotify:
+ description:
+ - Determines how to use the NOTIFY_SOCKET, as passed with systemd and Type=notify.
+ Can be container, conmon, ignore.
+ type: str
+ secrets:
+ description:
+ - Add the named secrets into the container.
+ The format is C(secret[,opt=opt...]), see
+ L(documentation,https://docs.podman.io/en/latest/markdown/podman-run.1.html#secret-secret-opt-opt) for more details.
+ type: list
+ elements: str
+ security_opt:
+ description:
+ - Security Options. For example security_opt "seccomp=unconfined"
+ type: list
+ elements: str
+ shm_size:
+ description:
+ - Size of /dev/shm. The format is <number><unit>. number must be greater
+ than 0.
+ Unit is optional and can be b (bytes), k (kilobytes), m(megabytes), or
+ g (gigabytes).
+ If you omit the unit, the system uses bytes. If you omit the size
+ entirely, the system uses 64m
+ type: str
+ sig_proxy:
+ description:
+ - Proxy signals sent to the podman run command to the container process.
+ SIGCHLD, SIGSTOP, and SIGKILL are not proxied. The default is true.
+ type: bool
+ stop_signal:
+ description:
+ - Signal to stop a container. Default is SIGTERM.
+ type: int
+ stop_timeout:
+ description:
+ - Timeout (in seconds) to stop a container. Default is 10.
+ type: int
+ subgidname:
+ description:
+ - Run the container in a new user namespace using the map with 'name' in
+ the /etc/subgid file.
+ type: str
+ subuidname:
+ description:
+ - Run the container in a new user namespace using the map with 'name' in
+ the /etc/subuid file.
+ type: str
+ sysctl:
+ description:
+ - Configure namespaced kernel parameters at runtime
+ type: dict
+ systemd:
+ description:
+ - Run container in systemd mode. The default is true.
+ type: str
+ timezone:
+ description:
+ - Set timezone in container. This flag takes area-based timezones,
+ GMT time, as well as local, which sets the timezone in the container to
+ match the host machine.
+ See /usr/share/zoneinfo/ for valid timezones.
+ Remote connections use local containers.conf for defaults.
+ type: str
+ tmpfs:
+ description:
+ - Create a tmpfs mount. For example tmpfs
+ "/tmp" "rw,size=787448k,mode=1777"
+ type: dict
+ tty:
+ description:
+ - Allocate a pseudo-TTY. The default is false.
+ type: bool
+ uidmap:
+ description:
+ - Run the container in a new user namespace using the supplied mapping.
+ type: list
+ elements: str
+ ulimit:
+ description:
+ - Ulimit options
+ type: list
+ elements: str
+ aliases:
+ - ulimits
+ user:
+ description:
+ - Sets the username or UID used and optionally the groupname or GID for
+ the specified command.
+ type: str
+ userns:
+ description:
+ - Set the user namespace mode for the container.
+ It defaults to the PODMAN_USERNS environment variable.
+ An empty value means user namespaces are disabled.
+ type: str
+ aliases:
+ - userns_mode
+ uts:
+ description:
+ - Set the UTS mode for the container
+ type: str
+ volume:
+ description:
+ - Create a bind mount. If you specify, volume /HOST-DIR:/CONTAINER-DIR,
+ podman bind mounts /HOST-DIR in the host to /CONTAINER-DIR in the
+ podman container.
+ type: list
+ elements: str
+ aliases:
+ - volumes
+ volumes_from:
+ description:
+ - Mount volumes from the specified container(s).
+ type: list
+ elements: str
+ workdir:
+ description:
+ - Working directory inside the container.
+ The default working directory for running binaries within a container
+ is the root directory (/).
+ type: str
+ aliases:
+ - working_dir
+"""
+
+EXAMPLES = r"""
+- name: Run container
+ containers.podman.podman_container:
+ name: container
+ image: quay.io/bitnami/wildfly
+ state: started
+
+- name: Create a data container
+ containers.podman.podman_container:
+ name: mydata
+ image: busybox
+ volume:
+ - /tmp/data
+
+- name: Re-create a redis container with systemd service file generated in /tmp/
+ containers.podman.podman_container:
+ name: myredis
+ image: redis
+ command: redis-server --appendonly yes
+ state: present
+ recreate: true
+ expose:
+ - 6379
+ volumes_from:
+ - mydata
+ generate_systemd:
+ path: /tmp/
+ restart_policy: always
+ time: 120
+ names: true
+ container_prefix: ainer
+
+- name: Restart a container
+ containers.podman.podman_container:
+ name: myapplication
+ image: redis
+ state: started
+ restart: true
+ etc_hosts:
+ other: "127.0.0.1"
+ restart_policy: "no"
+ device: "/dev/sda:/dev/xvda:rwm"
+ ports:
+ - "8080:9000"
+ - "127.0.0.1:8081:9001/udp"
+ env:
+ SECRET_KEY: "ssssh"
+ BOOLEAN_KEY: "yes"
+
+- name: Container present
+ containers.podman.podman_container:
+ name: mycontainer
+ state: present
+ image: ubuntu:14.04
+ command: "sleep 1d"
+
+- name: Stop a container
+ containers.podman.podman_container:
+ name: mycontainer
+ state: stopped
+
+- name: Start 4 load-balanced containers
+ containers.podman.podman_container:
+ name: "container{{ item }}"
+ recreate: true
+ image: someuser/anotherappimage
+ command: sleep 1d
+ with_sequence: count=4
+
+- name: remove container
+ containers.podman.podman_container:
+ name: ohno
+ state: absent
+
+- name: Writing output
+ containers.podman.podman_container:
+ name: myservice
+ image: busybox
+ log_options: path=/var/log/container/mycontainer.json
+ log_driver: k8s-file
+"""
+
+RETURN = r"""
+container:
+ description:
+ - Facts representing the current state of the container. Matches the
+ podman inspection output.
+ - Note that facts are part of the registered vars since Ansible 2.8. For
+ compatibility reasons, the facts
+ are also accessible directly as C(podman_container). Note that the
+ returned fact will be removed in Ansible 2.12.
+ - Empty if C(state) is I(absent).
+ returned: always
+ type: dict
+ sample: '{
+ "AppArmorProfile": "",
+ "Args": [
+ "sh"
+ ],
+ "BoundingCaps": [
+ "CAP_CHOWN",
+ ...
+ ],
+ "Config": {
+ "Annotations": {
+ "io.kubernetes.cri-o.ContainerType": "sandbox",
+ "io.kubernetes.cri-o.TTY": "false"
+ },
+ "AttachStderr": false,
+ "AttachStdin": false,
+ "AttachStdout": false,
+ "Cmd": [
+ "sh"
+ ],
+ "Domainname": "",
+ "Entrypoint": "",
+ "Env": [
+ "PATH=/usr/sbin:/usr/bin:/sbin:/bin",
+ "TERM=xterm",
+ "HOSTNAME=",
+ "container=podman"
+ ],
+ "Hostname": "",
+ "Image": "docker.io/library/busybox:latest",
+ "Labels": null,
+ "OpenStdin": false,
+ "StdinOnce": false,
+ "StopSignal": 15,
+ "Tty": false,
+ "User": {
+ "gid": 0,
+ "uid": 0
+ },
+ "Volumes": null,
+ "WorkingDir": "/"
+ },
+ "ConmonPidFile": "...",
+ "Created": "2019-06-17T19:13:09.873858307+03:00",
+ "Dependencies": [],
+ "Driver": "overlay",
+ "EffectiveCaps": [
+ "CAP_CHOWN",
+ ...
+ ],
+ "ExecIDs": [],
+ "ExitCommand": [
+ "/usr/bin/podman",
+ "--root",
+ ...
+ ],
+ "GraphDriver": {
+ ...
+ },
+ "HostConfig": {
+ ...
+ },
+ "HostnamePath": "...",
+ "HostsPath": "...",
+ "ID": "...",
+ "Image": "...",
+ "ImageName": "docker.io/library/busybox:latest",
+ "IsInfra": false,
+ "LogPath": "/tmp/container/mycontainer.json",
+ "MountLabel": "system_u:object_r:container_file_t:s0:c282,c782",
+ "Mounts": [
+ ...
+ ],
+ "Name": "myservice",
+ "Namespace": "",
+ "NetworkSettings": {
+ "Bridge": "",
+ ...
+ },
+ "Path": "sh",
+ "ProcessLabel": "system_u:system_r:container_t:s0:c282,c782",
+ "ResolvConfPath": "...",
+ "RestartCount": 0,
+ "Rootfs": "",
+ "State": {
+ "Dead": false,
+ "Error": "",
+ "ExitCode": 0,
+ "FinishedAt": "2019-06-17T19:13:10.157518963+03:00",
+ "Healthcheck": {
+ "FailingStreak": 0,
+ "Log": null,
+ "Status": ""
+ },
+ "OOMKilled": false,
+ "OciVersion": "1.0.1-dev",
+ "Paused": false,
+ "Pid": 4083,
+ "Restarting": false,
+ "Running": false,
+ "StartedAt": "2019-06-17T19:13:10.152479729+03:00",
+ "Status": "exited"
+ },
+ "StaticDir": "..."
+ ...
+ }'
+"""
+
+from ansible.module_utils.basic import AnsibleModule # noqa: F402
+from ..module_utils.podman.podman_container_lib import PodmanManager # noqa: F402
+from ..module_utils.podman.podman_container_lib import ARGUMENTS_SPEC_CONTAINER # noqa: F402
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=ARGUMENTS_SPEC_CONTAINER,
+ mutually_exclusive=(
+ ['no_hosts', 'etc_hosts'],
+ ),
+ supports_check_mode=True,
+ )
+
+ # work on input vars
+ if (module.params['state'] in ['present', 'created']
+ and not module.params['force_restart']
+ and not module.params['image']):
+ module.fail_json(msg="State '%s' required image to be configured!" %
+ module.params['state'])
+
+ results = PodmanManager(module, module.params).execute()
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_container_info.py b/ansible_collections/containers/podman/plugins/modules/podman_container_info.py
new file mode 100644
index 000000000..bbdd29fb9
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_container_info.py
@@ -0,0 +1,416 @@
+#!/usr/bin/python
+# Copyright (c) 2020 Red Hat
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+module: podman_container_info
+author:
+ - Sagi Shnaidman (@sshnaidm)
+ - Emilien Macchi (@EmilienM)
+short_description: Gather facts about containers using podman
+notes:
+ - Podman may require elevated privileges in order to run properly.
+description:
+ - Gather facts about containers using C(podman)
+requirements:
+ - "Podman installed on host"
+options:
+ name:
+ description:
+ - List of container names to gather facts about. If no name is given
+ return facts about all containers.
+ type: list
+ elements: str
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+'''
+
+EXAMPLES = r"""
+- name: Gather facts for all containers
+ containers.podman.podman_container_info:
+
+- name: Gather facts on a specific container
+ containers.podman.podman_container_info:
+ name: web1
+
+- name: Gather facts on several containers
+ containers.podman.podman_container_info:
+ name:
+ - redis
+ - web1
+"""
+
+RETURN = r"""
+containers:
+ description: Facts from all or specificed containers
+ returned: always
+ type: list
+ elements: dict
+ sample: [
+ {
+ "Id": "c5c39f9b80a6ea2ad665aa9946435934e478a0c5322da835f3883872f",
+ "Created": "2019-10-01T12:51:00.233106443Z",
+ "Path": "dumb-init",
+ "Args": [
+ "--single-child",
+ "--",
+ "kolla_start"
+ ],
+ "State": {
+ "OciVersion": "1.0.1-dev",
+ "Status": "configured",
+ "Running": false,
+ "Paused": false,
+ "Restarting": false,
+ "OOMKilled": false,
+ "Dead": false,
+ "Pid": 0,
+ "ExitCode": 0,
+ "Error": "",
+ "StartedAt": "0001-01-01T00:00:00Z",
+ "FinishedAt": "0001-01-01T00:00:00Z",
+ "Healthcheck": {
+ "Status": "",
+ "FailingStreak": 0,
+ "Log": null
+ }
+ },
+ "Image": "0e267acda67d0ebd643e900d820a91b961d859743039e620191ca1",
+ "ImageName": "docker.io/tripleomaster/centos-haproxy:latest",
+ "Rootfs": "",
+ "Pod": "",
+ "ResolvConfPath": "",
+ "HostnamePath": "",
+ "HostsPath": "",
+ "OCIRuntime": "runc",
+ "Name": "haproxy",
+ "RestartCount": 0,
+ "Driver": "overlay",
+ "MountLabel": "system_u:object_r:svirt_sandbox_file_t:s0:c78,c866",
+ "ProcessLabel": "system_u:system_r:svirt_lxc_net_t:s0:c785,c866",
+ "AppArmorProfile": "",
+ "EffectiveCaps": [
+ "CAP_CHOWN",
+ "CAP_DAC_OVERRIDE",
+ "CAP_FSETID",
+ "CAP_FOWNER",
+ "CAP_MKNOD",
+ "CAP_NET_RAW",
+ "CAP_SETGID",
+ "CAP_SETUID",
+ "CAP_SETFCAP",
+ "CAP_SETPCAP",
+ "CAP_NET_BIND_SERVICE",
+ "CAP_SYS_CHROOT",
+ "CAP_KILL",
+ "CAP_AUDIT_WRITE"
+ ],
+ "BoundingCaps": [
+ "CAP_CHOWN",
+ "CAP_DAC_OVERRIDE",
+ "CAP_FSETID",
+ "CAP_FOWNER",
+ "CAP_MKNOD",
+ "CAP_NET_RAW",
+ "CAP_SETGID",
+ "CAP_SETUID",
+ "CAP_SETFCAP",
+ "CAP_SETPCAP",
+ "CAP_NET_BIND_SERVICE",
+ "CAP_SYS_CHROOT",
+ "CAP_KILL",
+ "CAP_AUDIT_WRITE"
+ ],
+ "ExecIDs": [],
+ "GraphDriver": {
+ "Name": "overlay"
+ },
+ "Mounts": [],
+ "Dependencies": [],
+ "NetworkSettings": {
+ "Bridge": "",
+ "SandboxID": "",
+ "HairpinMode": false,
+ "LinkLocalIPv6Address": "",
+ "LinkLocalIPv6PrefixLen": 0,
+ "Ports": [],
+ "SandboxKey": "",
+ "SecondaryIPAddresses": null,
+ "SecondaryIPv6Addresses": null,
+ "EndpointID": "",
+ "Gateway": "",
+ "GlobalIPv6Address": "",
+ "GlobalIPv6PrefixLen": 0,
+ "IPAddress": "",
+ "IPPrefixLen": 0,
+ "IPv6Gateway": "",
+ "MacAddress": ""
+ },
+ "ExitCommand": [
+ "/usr/bin/podman",
+ "--root",
+ "/var/lib/containers/storage",
+ "--runroot",
+ "/var/run/containers/storage",
+ "--log-level",
+ "error",
+ "--cgroup-manager",
+ "systemd",
+ "--tmpdir",
+ "/var/run/libpod",
+ "--runtime",
+ "runc",
+ "--storage-driver",
+ "overlay",
+ "--events-backend",
+ "journald",
+ "container",
+ "cleanup",
+ "c9e813703f9b80a6ea2ad665aa9946435934e478a0c5322da835f3883872f"
+ ],
+ "Namespace": "",
+ "IsInfra": false,
+ "Config": {
+ "Hostname": "c5c39e813703",
+ "Domainname": "",
+ "User": "",
+ "AttachStdin": false,
+ "AttachStdout": false,
+ "AttachStderr": false,
+ "Tty": false,
+ "OpenStdin": false,
+ "StdinOnce": false,
+ "Env": [
+ "PATH=/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+ "TERM=xterm",
+ "HOSTNAME=",
+ "container=oci",
+ "KOLLA_INSTALL_METATYPE=rdo",
+ "KOLLA_BASE_DISTRO=centos",
+ "KOLLA_INSTALL_TYPE=binary",
+ "KOLLA_DISTRO_PYTHON_VERSION=2.7",
+ "KOLLA_BASE_ARCH=x86_64"
+ ],
+ "Cmd": [
+ "kolla_start"
+ ],
+ "Image": "docker.io/tripleomaster/centos-haproxy:latest",
+ "Volumes": null,
+ "WorkingDir": "/",
+ "Entrypoint": "dumb-init --single-child --",
+ "OnBuild": null,
+ "Labels": {
+ "build-date": "20190919",
+ "kolla_version": "8.1.0",
+ "name": "haproxy",
+ "org.label-schema.build-date": "20190801",
+ "org.label-schema.license": "GPLv2",
+ "org.label-schema.name": "CentOS Base Image",
+ "org.label-schema.schema-version": "1.0",
+ "org.label-schema.vendor": "CentOS"
+ },
+ "Annotations": {
+ "io.kubernetes.cri-o.ContainerType": "sandbox",
+ "io.kubernetes.cri-o.TTY": "false",
+ "io.podman.annotations.autoremove": "FALSE",
+ "io.podman.annotations.init": "FALSE",
+ "io.podman.annotations.privileged": "FALSE",
+ "io.podman.annotations.publish-all": "FALSE"
+ },
+ "StopSignal": 15
+ },
+ "HostConfig": {
+ "Binds": [],
+ "ContainerIDFile": "",
+ "LogConfig": {
+ "Type": "k8s-file",
+ "Config": null
+ },
+ "NetworkMode": "default",
+ "PortBindings": {},
+ "RestartPolicy": {
+ "Name": "",
+ "MaximumRetryCount": 0
+ },
+ "AutoRemove": false,
+ "VolumeDriver": "",
+ "VolumesFrom": null,
+ "CapAdd": [],
+ "CapDrop": [],
+ "Dns": [],
+ "DnsOptions": [],
+ "DnsSearch": [],
+ "ExtraHosts": [],
+ "GroupAdd": [],
+ "IpcMode": "",
+ "Cgroup": "",
+ "Links": null,
+ "OomScoreAdj": 0,
+ "PidMode": "",
+ "Privileged": false,
+ "PublishAllPorts": false,
+ "ReadonlyRootfs": false,
+ "SecurityOpt": [],
+ "Tmpfs": {},
+ "UTSMode": "",
+ "UsernsMode": "",
+ "ShmSize": 65536000,
+ "Runtime": "oci",
+ "ConsoleSize": [
+ 0,
+ 0
+ ],
+ "Isolation": "",
+ "CpuShares": 0,
+ "Memory": 0,
+ "NanoCpus": 0,
+ "CgroupParent": "",
+ "BlkioWeight": 0,
+ "BlkioWeightDevice": null,
+ "BlkioDeviceReadBps": null,
+ "BlkioDeviceWriteBps": null,
+ "BlkioDeviceReadIOps": null,
+ "BlkioDeviceWriteIOps": null,
+ "CpuPeriod": 0,
+ "CpuQuota": 0,
+ "CpuRealtimePeriod": 0,
+ "CpuRealtimeRuntime": 0,
+ "CpusetCpus": "",
+ "CpusetMems": "",
+ "Devices": [],
+ "DiskQuota": 0,
+ "KernelMemory": 0,
+ "MemoryReservation": 0,
+ "MemorySwap": 0,
+ "MemorySwappiness": -1,
+ "OomKillDisable": false,
+ "PidsLimit": 0,
+ "Ulimits": [
+ {
+ "Name": "RLIMIT_NOFILE",
+ "Soft": 1048576,
+ "Hard": 1048576
+ },
+ {
+ "Name": "RLIMIT_NPROC",
+ "Soft": 1048576,
+ "Hard": 1048576
+ }
+ ],
+ "CpuCount": 0,
+ "CpuPercent": 0,
+ "IOMaximumIOps": 0,
+ "IOMaximumBandwidth": 0
+ }
+ }
+ ]
+"""
+
+import json
+import time
+from ansible.module_utils.basic import AnsibleModule
+
+
+def get_containers_facts(module, executable, name):
+ """Collect containers facts for all containers or for specified in 'name'.
+
+ Arguments:
+ module {AnsibleModule} -- instance of AnsibleModule
+ executable {string} -- binary to execute when inspecting containers
+ name {list} -- list of names or None in case of all containers
+
+ Returns:
+ list of containers info, stdout, stderr
+ """
+ retry = 0
+ retry_limit = 4
+ if not name:
+ all_names = [executable, 'container', 'ls', '-q', '-a']
+ rc, out, err = module.run_command(all_names)
+ # This should not fail in regular circumstances, so retry again
+ # https://github.com/containers/podman/issues/10225
+ while rc != 0 and retry <= retry_limit:
+ module.log(msg="Unable to get list of containers: %s" % err)
+ time.sleep(1)
+ retry += 1
+ rc, out, err = module.run_command(all_names)
+ if rc != 0:
+ module.fail_json(msg="Unable to get list of containers during"
+ " %s retries" % retry_limit)
+ name = out.split()
+ if not name:
+ return [], out, err
+ command = [executable, 'container', 'inspect']
+ command.extend(name)
+ rc, out, err = module.run_command(command)
+ if rc == 0:
+ json_out = json.loads(out) if out else None
+ if json_out is None:
+ return [], out, err
+ return json_out, out, err
+ if rc != 0 and 'no such ' in err:
+ if len(name) < 2:
+ return [], out, err
+ return cycle_over(module, executable, name)
+ module.fail_json(msg="Unable to gather info for %s: %s" % (",".join(name), err))
+
+
+def cycle_over(module, executable, name):
+ """Inspect each container in a cycle in case some of them don't exist.
+
+ Arguments:
+ module {AnsibleModule} -- instance of AnsibleModule
+ executable {string} -- binary to execute when inspecting containers
+ name {list} -- list of containers names to inspect
+
+ Returns:
+ list of containers info, stdout as empty, stderr
+ """
+ inspection = []
+ stderrs = []
+ for container in name:
+ command = [executable, 'container', 'inspect', container]
+ rc, out, err = module.run_command(command)
+ if rc != 0 and 'no such ' not in err:
+ module.fail_json(msg="Unable to gather info for %s: %s" % (container, err))
+ if rc == 0 and out:
+ json_out = json.loads(out)
+ if json_out:
+ inspection += json_out
+ stderrs.append(err)
+ return inspection, "", "\n".join(stderrs)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec={
+ 'executable': {'type': 'str', 'default': 'podman'},
+ 'name': {'type': 'list', 'elements': 'str'},
+ },
+ supports_check_mode=True,
+ )
+
+ name = module.params['name']
+ executable = module.get_bin_path(module.params['executable'], required=True)
+ # pylint: disable=unused-variable
+ inspect_results, out, err = get_containers_facts(module, executable, name)
+
+ results = {
+ "changed": False,
+ "containers": inspect_results,
+ "stderr": err
+ }
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_containers.py b/ansible_collections/containers/podman/plugins/modules/podman_containers.py
new file mode 100644
index 000000000..c67aee344
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_containers.py
@@ -0,0 +1,132 @@
+#!/usr/bin/python
+# Copyright (c) 2020 Red Hat
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: podman_containers
+author:
+ - "Sagi Shnaidman (@sshnaidm)"
+version_added: '1.4.0'
+short_description: Manage podman containers in a batch
+description:
+ - Manage groups of podman containers
+requirements:
+ - "podman"
+options:
+ containers:
+ description:
+ - List of dictionaries with data for running containers for podman_container module.
+ required: True
+ type: list
+ elements: dict
+ debug:
+ description:
+ - Return additional information which can be helpful for investigations.
+ type: bool
+ default: False
+'''
+
+EXAMPLES = '''
+- name: Run three containers at once
+ podman_containers:
+ containers:
+ - name: alpine
+ image: alpine
+ command: sleep 1d
+ - name: web
+ image: nginx
+ - name: test
+ image: python:3-alpine
+ command: python -V
+'''
+
+from ansible.module_utils.basic import AnsibleModule # noqa: F402
+from ..module_utils.podman.podman_container_lib import PodmanManager # noqa: F402
+from ..module_utils.podman.podman_container_lib import set_container_opts # noqa: F402
+
+
+def combine(results):
+ changed = any(i.get('changed', False) for i in results)
+ failed = any(i.get('failed', False) for i in results)
+ actions = []
+ podman_actions = []
+ containers = []
+ podman_version = ''
+ diffs = {}
+ stderr = ''
+ stdout = ''
+ for i in results:
+ if 'actions' in i and i['actions']:
+ actions += i['actions']
+ if 'podman_actions' in i and i['podman_actions']:
+ podman_actions += i['podman_actions']
+ if 'container' in i and i['container']:
+ containers.append(i['container'])
+ if 'podman_version' in i:
+ podman_version = i['podman_version']
+ if 'diff' in i:
+ diffs[i['container']['Name']] = i['diff']
+ if 'stderr' in i:
+ stderr += i['stderr']
+ if 'stdout' in i:
+ stdout += i['stdout']
+
+ total = {
+ 'changed': changed,
+ 'failed': failed,
+ 'actions': actions,
+ 'podman_actions': podman_actions,
+ 'containers': containers,
+ 'stdout': stdout,
+ 'stderr': stderr,
+ }
+ if podman_version:
+ total['podman_version'] = podman_version
+ if diffs:
+ before = after = ''
+ for k, v in diffs.items():
+ before += "".join([str(k), ": ", str(v['before']), "\n"])
+ after += "".join([str(k), ": ", str(v['after']), "\n"])
+ total['diff'] = {
+ 'before': before,
+ 'after': after
+ }
+ return total
+
+
+def check_input_strict(container):
+ if container['state'] in ['started', 'present'] and not container['image']:
+ return "State '%s' required image to be configured!" % container['state']
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ containers=dict(type='list', elements='dict', required=True),
+ debug=dict(type='bool', default=False),
+ ),
+ supports_check_mode=True,
+ )
+ # work on input vars
+
+ results = []
+ for container in module.params['containers']:
+ options_dict = set_container_opts(container)
+ options_dict['debug'] = module.params['debug'] or options_dict['debug']
+ test_input = check_input_strict(options_dict)
+ if test_input:
+ module.fail_json(
+ msg="Failed to run container %s because: %s" % (options_dict['name'], test_input))
+ res = PodmanManager(module, options_dict).execute()
+ results.append(res)
+ total_results = combine(results)
+ module.exit_json(**total_results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_export.py b/ansible_collections/containers/podman/plugins/modules/podman_export.py
new file mode 100644
index 000000000..e2bb19614
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_export.py
@@ -0,0 +1,106 @@
+#!/usr/bin/python
+# coding: utf-8 -*-
+
+# Copyright (c) 2021, Sagi Shnaidman <sshnaidm@redhat.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+module: podman_export
+short_description: Export a podman container
+author: Sagi Shnaidman (@sshnaidm)
+description:
+ - podman export exports the filesystem of a container and saves it as a
+ tarball on the local machine
+options:
+ dest:
+ description:
+ - Path to export container to.
+ type: str
+ required: true
+ container:
+ description:
+ - Container to export.
+ type: str
+ required: true
+ force:
+ description:
+ - Force saving to file even if it exists.
+ type: bool
+ default: True
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+requirements:
+ - "Podman installed on host"
+'''
+
+RETURN = '''
+'''
+
+EXAMPLES = '''
+# What modules does for example
+- containers.podman.podman_export:
+ dest: /path/to/tar/file
+ container: container-name
+'''
+
+import os # noqa: E402
+from ansible.module_utils.basic import AnsibleModule # noqa: E402
+from ..module_utils.podman.common import remove_file_or_dir # noqa: E402
+
+
+def export(module, executable):
+ changed = False
+ command = [executable, 'export']
+ command += ['-o=%s' % module.params['dest'], module.params['container']]
+ if module.params['force']:
+ dest = module.params['dest']
+ if os.path.exists(dest):
+ changed = True
+ if module.check_mode:
+ return changed, '', ''
+ try:
+ remove_file_or_dir(dest)
+ except Exception as e:
+ module.fail_json(msg="Error deleting %s path: %s" % (dest, e))
+ else:
+ changed = not os.path.exists(module.params['dest'])
+ if module.check_mode:
+ return changed, '', ''
+ rc, out, err = module.run_command(command)
+ if rc != 0:
+ module.fail_json(msg="Error exporting container %s: %s" % (
+ module.params['container'], err))
+ return changed, out, err
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ dest=dict(type='str', required=True),
+ container=dict(type='str', required=True),
+ force=dict(type='bool', default=True),
+ executable=dict(type='str', default='podman')
+ ),
+ supports_check_mode=True,
+ )
+
+ executable = module.get_bin_path(module.params['executable'], required=True)
+ changed, out, err = export(module, executable)
+
+ results = {
+ "changed": changed,
+ "stdout": out,
+ "stderr": err,
+ }
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_generate_systemd.py b/ansible_collections/containers/podman/plugins/modules/podman_generate_systemd.py
new file mode 100644
index 000000000..9c9bc7b27
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_generate_systemd.py
@@ -0,0 +1,604 @@
+#!/usr/bin/python
+# coding: utf-8 -*-
+
+# 2022, Sébastien Gendre <seb@k-7.ch>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+module: podman_generate_systemd
+author:
+ - Sébastien Gendre (@CyberFox001)
+short_description: Generate systemd unit from a pod or a container
+description:
+ - Generate systemd .service unit file(s) from a pod or a container
+ - Support Ansible check mode
+options:
+ name:
+ description:
+ - Name of the pod or container to export
+ type: str
+ required: true
+ dest:
+ description:
+ - Destination of the generated systemd unit file(s).
+ - Use C(/etc/systemd/system) for the system-wide systemd instance.
+ - Use C(/etc/systemd/user) or C(~/.config/systemd/user) for use with per-user instances of systemd.
+ type: path
+ new:
+ description:
+ - Generate unit files that create containers and pods, not only start them.
+ - Refer to podman-generate-systemd(1) man page for more information.
+ type: bool
+ default: false
+ restart_policy:
+ description:
+ - Restart policy of the service
+ type: str
+ choices:
+ - no-restart
+ - on-success
+ - on-failure
+ - on-abnormal
+ - on-watchdog
+ - on-abort
+ - always
+ restart_sec:
+ description:
+ - Configures the time to sleep before restarting a service (as configured with restart-policy).
+ - Takes a value in seconds.
+ - Only with Podman 4.0.0 and above
+ type: int
+ start_timeout:
+ description:
+ - Override the default start timeout for the container with the given value in seconds.
+ - Only with Podman 4.0.0 and above
+ type: int
+ stop_timeout:
+ description:
+ - Override the default stop timeout for the container with the given value in seconds.
+ type: int
+ env:
+ description:
+ - Set environment variables to the systemd unit files.
+ - Keys are the environment variable names, and values are the environment variable values
+ - Only with Podman 4.3.0 and above
+ type: dict
+ use_names:
+ description:
+ - Use name of the containers for the start, stop, and description in the unit file.
+ type: bool
+ default: true
+ container_prefix:
+ description:
+ - Set the systemd unit name prefix for containers.
+ - If not set, use the default defined by podman, C(container).
+ - Refer to podman-generate-systemd(1) man page for more information.
+ type: str
+ pod_prefix:
+ description:
+ - Set the systemd unit name prefix for pods.
+ - If not set, use the default defined by podman, C(pod).
+ - Refer to podman-generate-systemd(1) man page for more information.
+ type: str
+ separator:
+ description:
+ - Systemd unit name separator between the name/id of a container/pod and the prefix.
+ - If not set, use the default defined by podman, C(-).
+ - Refer to podman-generate-systemd(1) man page for more information.
+ type: str
+ no_header:
+ description:
+ - Do not generate the header including meta data such as the Podman version and the timestamp.
+ type: bool
+ default: false
+ after:
+ description:
+ - Add the systemd unit after (C(After=)) option, that ordering dependencies between the list of dependencies and this service.
+ - This option may be specified more than once.
+ - User-defined dependencies will be appended to the generated unit file
+ - But any existing options such as needed or defined by default (e.g. C(online.target)) will not be removed or overridden.
+ - Only with Podman 4.0.0 and above
+ type: list
+ elements: str
+ wants:
+ description:
+ - Add the systemd unit wants (C(Wants=)) option, that this service is (weak) dependent on.
+ - This option may be specified more than once.
+ - This option does not influence the order in which services are started or stopped.
+ - User-defined dependencies will be appended to the generated unit file
+ - But any existing options such as needed or defined by default (e.g. C(online.target)) will not be removed or overridden.
+ - Only with Podman 4.0.0 and above
+ type: list
+ elements: str
+ requires:
+ description:
+ - Set the systemd unit requires (Requires=) option.
+ - Similar to wants, but declares a stronger requirement dependency.
+ - Only with Podman 4.0.0 and above
+ type: list
+ elements: str
+ executable:
+ description:
+ - C(Podman) executable name or full path
+ type: str
+ default: podman
+requirements:
+ - Podman installed on target host
+notes:
+ - If you indicate a pod, the systemd units for it and all its containers will be generated
+ - Create all your pods, containers and their dependencies before generating the systemd files
+ - If a container or pod is already started before you do a C(systemctl daemon-reload),
+ systemd will not see the container or pod as started
+ - Stop your container or pod before you do a C(systemctl daemon-reload),
+ then you can start them with C(systemctl start my_container.service)
+'''
+
+EXAMPLES = '''
+# Example of creating a container and systemd unit file.
+# When using podman_generate_systemd with new:true then
+# the container needs rm:true for idempotence.
+- name: Create postgres container
+ containers.podman.podman_container:
+ name: postgres
+ image: docker.io/library/postgres:latest
+ rm: true
+ state: created
+
+- name: Generate systemd unit file for postgres container
+ containers.podman.podman_generate_systemd:
+ name: postgres
+ new: true
+ no_header: true
+ dest: /etc/systemd/system
+
+- name: Ensure postgres container is started and enabled
+ ansible.builtin.systemd:
+ name: container-postgres
+ daemon_reload: true
+ state: started
+ enabled: true
+
+
+# Example of creating a container and integrate it into systemd
+- name: A postgres container must exist, stopped
+ containers.podman.podman_container:
+ name: postgres_local
+ image: docker.io/library/postgres:latest
+ state: stopped
+
+- name: Systemd unit files for postgres container must exist
+ containers.podman.podman_generate_systemd:
+ name: postgres_local
+ dest: ~/.config/systemd/user/
+
+- name: Postgres container must be started and enabled on systemd
+ ansible.builtin.systemd:
+ name: container-postgres_local
+ daemon_reload: true
+ state: started
+ enabled: true
+
+
+# Generate the unit files, but store them on an Ansible variable
+# instead of writing them on target host
+- name: Systemd unit files for postgres container must be generated
+ containers.podman.podman_generate_systemd:
+ name: postgres_local
+ register: postgres_local_systemd_unit
+
+# Generate the unit files with environment variables sets
+- name: Systemd unit files for postgres container must be generated
+ containers.podman.podman_generate_systemd:
+ name: postgres_local
+ env:
+ POSTGRES_USER: my_app
+ POSTGRES_PASSWORD: example
+ register: postgres_local_systemd_unit
+'''
+
+RETURN = '''
+systemd_units:
+ description: A copy of the generated systemd .service unit(s)
+ returned: always
+ type: dict
+ sample: {
+ "container-postgres_local": " #Content of the systemd .servec unit for postgres_local container",
+ "pod-my_webapp": " #Content of the systemd .servec unit for my_webapp pod",
+ }
+podman_command:
+ description: A copy of the podman command used to generate the systemd unit(s)
+ returned: always
+ type: str
+ sample: "podman generate systemd my_webapp"
+'''
+
+
+import os
+from ansible.module_utils.basic import AnsibleModule
+import json
+
+
+RESTART_POLICY_CHOICES = [
+ 'no-restart',
+ 'on-success',
+ 'on-failure',
+ 'on-abnormal',
+ 'on-watchdog',
+ 'on-abort',
+ 'always',
+]
+
+
+def generate_systemd(module):
+ '''Generate systemd .service unit file from a pod or container.
+
+ Parameter:
+ - module (AnsibleModule): An AnsibleModule object
+
+ Returns (tuple[bool, list[str], str]):
+ - A boolean which indicate whether the targeted systemd state is modified
+ - A copy of the generated systemd .service units content
+ - A copy of the command, as a string
+ '''
+ # Flag which indicate whether the targeted system state is modified
+ changed = False
+
+ # Build the podman command, based on the module parameters
+ command_options = []
+
+ # New option
+ if module.params['new']:
+ command_options.append('--new')
+
+ # Restart policy option
+ restart_policy = module.params['restart_policy']
+ if restart_policy:
+ # add the restart policy to options
+ if restart_policy == 'no-restart':
+ restart_policy = 'no'
+ command_options.append(
+ '--restart-policy={restart_policy}'.format(
+ restart_policy=restart_policy,
+ ),
+ )
+
+ # Restart-sec option (only for Podman 4.0.0 and above)
+ restart_sec = module.params['restart_sec']
+ if restart_sec:
+ command_options.append(
+ '--restart-sec={restart_sec}'.format(
+ restart_sec=restart_sec,
+ ),
+ )
+
+ # Start-timeout option (only for Podman 4.0.0 and above)
+ start_timeout = module.params['start_timeout']
+ if start_timeout:
+ command_options.append(
+ '--start-timeout={start_timeout}'.format(
+ start_timeout=start_timeout,
+ ),
+ )
+
+ # Stop-timeout option
+ stop_timeout = module.params['stop_timeout']
+ if stop_timeout:
+ command_options.append(
+ '--stop-timeout={stop_timeout}'.format(
+ stop_timeout=stop_timeout,
+ ),
+ )
+
+ # Use container name(s) option
+ if module.params['use_names']:
+ command_options.append('--name')
+
+ # Container-prefix option
+ container_prefix = module.params['container_prefix']
+ if container_prefix is not None:
+ command_options.append(
+ '--container-prefix={container_prefix}'.format(
+ container_prefix=container_prefix,
+ ),
+ )
+
+ # Pod-prefix option
+ pod_prefix = module.params['pod_prefix']
+ if pod_prefix is not None:
+ command_options.append(
+ '--pod-prefix={pod_prefix}'.format(
+ pod_prefix=pod_prefix,
+ ),
+ )
+
+ # Separator option
+ separator = module.params['separator']
+ if separator is not None:
+ command_options.append(
+ '--separator={separator}'.format(
+ separator=separator,
+ ),
+ )
+
+ # No-header option
+ if module.params['no_header']:
+ command_options.append('--no-header')
+
+ # After option (only for Podman 4.0.0 and above)
+ after = module.params['after']
+ if after:
+ for item in after:
+ command_options.append(
+ '--after={item}'.format(
+ item=item,
+ ),
+ )
+
+ # Wants option (only for Podman 4.0.0 and above)
+ wants = module.params['wants']
+ if wants:
+ for item in wants:
+ command_options.append(
+ '--wants={item}'.format(
+ item=item,
+ )
+ )
+
+ # Requires option (only for Podman 4.0.0 and above)
+ requires = module.params['requires']
+ if requires:
+ for item in requires:
+ command_options.append(
+ '--requires={item}'.format(
+ item=item,
+ ),
+ )
+
+ # Environment variables (only for Podman 4.3.0 and above)
+ environment_variables = module.params['env']
+ if environment_variables:
+ for env_var_name, env_var_value in environment_variables.items():
+ command_options.append(
+ "-e='{env_var_name}={env_var_value}'".format(
+ env_var_name=env_var_name,
+ env_var_value=env_var_value,
+ ),
+ )
+
+ # Set output format, of podman command, to json
+ command_options.extend(['--format', 'json'])
+
+ # Full command build, with option included
+ # Base of the command
+ command = [
+ module.params['executable'], 'generate', 'systemd',
+ ]
+ # Add the options to the commande
+ command.extend(command_options)
+ # Add pod or container name to the command
+ command.append(module.params['name'])
+ # Build the string version of the command, only for module return
+ command_str = ' '.join(command)
+
+ # Run the podman command to generated systemd .service unit(s) content
+ return_code, stdout, stderr = module.run_command(command)
+
+ # In case of error in running the command
+ if return_code != 0:
+ # Print informations about the error and return and empty dictionary
+ message = 'Error generating systemd .service unit(s).'
+ message += ' Command executed: {command_str}'
+ message += ' Command returned with code: {return_code}.'
+ message += ' Error message: {stderr}.'
+ module.fail_json(
+ msg=message.format(
+ command_str=command_str,
+ return_code=return_code,
+ stderr=stderr,
+ ),
+ changed=changed,
+ systemd_units={},
+ podman_command=command_str,
+ )
+
+ # In case of command execution success, its stdout is a json
+ # dictionary. This dictionary is all the generated systemd units.
+ # Each key value pair is one systemd unit. The key is the unit name
+ # and the value is the unit content.
+
+ # Load the returned json dictionary as a python dictionary
+ systemd_units = json.loads(stdout)
+
+ # Write the systemd .service unit(s) content to file(s), if
+ # requested
+ if module.params['dest']:
+ try:
+ systemd_units_dest = module.params['dest']
+ # If destination don't exist
+ if not os.path.exists(systemd_units_dest):
+ # If not in check mode, make it
+ if not module.check_mode:
+ os.makedirs(systemd_units_dest)
+ changed = True
+ # If destination exist but not a directory
+ if not os.path.isdir(systemd_units_dest):
+ # Stop and tell user that the destination is not a directry
+ message = "Destination {systemd_units_dest} is not a directory."
+ message += " Can't save systemd unit files in."
+ module.fail_json(
+ msg=message.format(
+ systemd_units_dest=systemd_units_dest,
+ ),
+ changed=changed,
+ systemd_units=systemd_units,
+ podman_command=command_str,
+ )
+
+ # Write each systemd unit, if needed
+ for unit_name, unit_content in systemd_units.items():
+ # Build full path to unit file
+ unit_file_name = unit_name + '.service'
+ unit_file_full_path = os.path.join(
+ systemd_units_dest,
+ unit_file_name,
+ )
+
+ # See if we need to write the unit file, default yes
+ need_to_write_file = True
+ # If the unit file already exist, compare it with the
+ # generated content
+ if os.path.exists(unit_file_full_path):
+ # Read the file
+ with open(unit_file_full_path, 'r') as unit_file:
+ current_unit_file_content = unit_file.read()
+ # If current unit file content is the same as the
+ # generated content
+ # Remove comments from files, before comparing
+ current_unit_file_content_nocmnt = "\n".join([
+ line for line in current_unit_file_content.splitlines()
+ if not line.startswith('#')])
+ unit_content_nocmnt = "\n".join([
+ line for line in unit_content.splitlines()
+ if not line.startswith('#')])
+ if current_unit_file_content_nocmnt == unit_content_nocmnt:
+ # We don't need to write it
+ need_to_write_file = False
+
+ # Write the file, if needed
+ if need_to_write_file:
+ with open(unit_file_full_path, 'w') as unit_file:
+ # If not in check mode, write the file
+ if not module.check_mode:
+ unit_file.write(unit_content)
+ changed = True
+
+ except Exception as exception:
+ # When exception occurs while trying to write units file
+ message = 'PODMAN-GENERATE-SYSTEMD-DEBUG: '
+ message += 'Error writing systemd units files: '
+ message += '{exception}'
+ module.log(
+ message.format(
+ exception=exception
+ ),
+ )
+ # Return the systemd .service unit(s) content
+ return changed, systemd_units, command_str
+
+
+def run_module():
+ '''Run the module on the target'''
+ # Build the list of parameters user can use
+ module_parameters = {
+ 'name': {
+ 'type': 'str',
+ 'required': True,
+ },
+ 'dest': {
+ 'type': 'path',
+ 'required': False,
+ },
+ 'new': {
+ 'type': 'bool',
+ 'required': False,
+ 'default': False,
+ },
+ 'restart_policy': {
+ 'type': 'str',
+ 'required': False,
+ 'choices': RESTART_POLICY_CHOICES,
+ },
+ 'restart_sec': {
+ 'type': 'int',
+ 'required': False,
+ },
+ 'start_timeout': {
+ 'type': 'int',
+ 'required': False,
+ },
+ 'stop_timeout': {
+ 'type': 'int',
+ 'required': False,
+ },
+ 'env': {
+ 'type': 'dict',
+ 'required': False,
+ },
+ 'use_names': {
+ 'type': 'bool',
+ 'required': False,
+ 'default': True,
+ },
+ 'container_prefix': {
+ 'type': 'str',
+ 'required': False,
+ },
+ 'pod_prefix': {
+ 'type': 'str',
+ 'required': False,
+ },
+ 'separator': {
+ 'type': 'str',
+ 'required': False,
+ },
+ 'no_header': {
+ 'type': 'bool',
+ 'required': False,
+ 'default': False,
+ },
+ 'after': {
+ 'type': 'list',
+ 'elements': 'str',
+ 'required': False,
+ },
+ 'wants': {
+ 'type': 'list',
+ 'elements': 'str',
+ 'required': False,
+ },
+ 'requires': {
+ 'type': 'list',
+ 'elements': 'str',
+ 'required': False,
+ },
+ 'executable': {
+ 'type': 'str',
+ 'required': False,
+ 'default': 'podman',
+ },
+ }
+
+ # Build result dictionary
+ result = {
+ 'changed': False,
+ 'systemd_units': {},
+ 'podman_command': '',
+ }
+
+ # Build the Ansible Module
+ module = AnsibleModule(
+ argument_spec=module_parameters,
+ supports_check_mode=True
+ )
+
+ # Generate the systemd units
+ state_changed, systemd_units, podman_command = generate_systemd(module)
+
+ result['changed'] = state_changed
+ result['systemd_units'] = systemd_units
+ result['podman_command'] = podman_command
+
+ # Return the result
+ module.exit_json(**result)
+
+
+def main():
+ '''Main function of this script.'''
+ run_module()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_image.py b/ansible_collections/containers/podman/plugins/modules/podman_image.py
new file mode 100644
index 000000000..d66ff5d49
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_image.py
@@ -0,0 +1,862 @@
+#!/usr/bin/python
+# Copyright (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+ module: podman_image
+ author:
+ - Sam Doran (@samdoran)
+ short_description: Pull images for use by podman
+ notes: []
+ description:
+ - Build, pull, or push images using Podman.
+ options:
+ arch:
+ description:
+ - CPU architecutre for the container image
+ type: str
+ name:
+ description:
+ - Name of the image to pull, push, or delete. It may contain a tag using the format C(image:tag).
+ required: True
+ type: str
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the machine running C(podman).
+ default: 'podman'
+ type: str
+ ca_cert_dir:
+ description:
+ - Path to directory containing TLS certificates and keys to use.
+ type: 'path'
+ tag:
+ description:
+ - Tag of the image to pull, push, or delete.
+ default: "latest"
+ type: str
+ pull:
+ description: Whether or not to pull the image.
+ default: True
+ type: bool
+ push:
+ description: Whether or not to push an image.
+ default: False
+ type: bool
+ path:
+ description: Path to the build context directory.
+ type: str
+ force:
+ description:
+ - Whether or not to force push or pull an image.
+ - When building, force the build even if the image already exists.
+ type: bool
+ default: False
+ state:
+ description:
+ - Whether an image should be present, absent, or built.
+ default: "present"
+ type: str
+ choices:
+ - present
+ - absent
+ - build
+ validate_certs:
+ description:
+ - Require HTTPS and validate certificates when pulling or pushing. Also used during build if a pull or push is necessary.
+ type: bool
+ aliases:
+ - tlsverify
+ - tls_verify
+ password:
+ description:
+ - Password to use when authenticating to remote registries.
+ type: str
+ username:
+ description:
+ - username to use when authenticating to remote registries.
+ type: str
+ auth_file:
+ description:
+ - Path to file containing authorization credentials to the remote registry.
+ aliases:
+ - authfile
+ type: path
+ build:
+ description: Arguments that control image build.
+ type: dict
+ default: {}
+ aliases:
+ - build_args
+ - buildargs
+ suboptions:
+ file:
+ description:
+ - Path to the Containerfile if it is not in the build context directory.
+ type: path
+ volume:
+ description:
+ - Specify multiple volume / mount options to mount one or more mounts to a container.
+ type: list
+ elements: str
+ annotation:
+ description:
+ - Dictionary of key=value pairs to add to the image. Only works with OCI images. Ignored for Docker containers.
+ type: dict
+ force_rm:
+ description:
+ - Always remove intermediate containers after a build, even if the build is unsuccessful.
+ type: bool
+ default: False
+ format:
+ description:
+ - Format of the built image.
+ type: str
+ choices:
+ - docker
+ - oci
+ default: "oci"
+ cache:
+ description:
+ - Whether or not to use cached layers when building an image
+ type: bool
+ default: True
+ rm:
+ description: Remove intermediate containers after a successful build
+ type: bool
+ default: True
+ extra_args:
+ description:
+ - Extra args to pass to build, if executed. Does not idempotently check for new build args.
+ type: str
+ push_args:
+ description: Arguments that control pushing images.
+ type: dict
+ default: {}
+ suboptions:
+ compress:
+ description:
+ - Compress tarball image layers when pushing to a directory using the 'dir' transport.
+ type: bool
+ format:
+ description:
+ - Manifest type to use when pushing an image using the 'dir' transport (default is manifest type of source).
+ type: str
+ choices:
+ - oci
+ - v2s1
+ - v2s2
+ remove_signatures:
+ description: Discard any pre-existing signatures in the image
+ type: bool
+ sign_by:
+ description:
+ - Path to a key file to use to sign the image.
+ type: str
+ dest:
+ description: Path or URL where image will be pushed.
+ type: str
+ aliases:
+ - destination
+ transport:
+ description:
+ - Transport to use when pushing in image. If no transport is set, will attempt to push to a remote registry.
+ type: str
+ choices:
+ - dir
+ - docker-archive
+ - docker-daemon
+ - oci-archive
+ - ostree
+'''
+
+EXAMPLES = r"""
+- name: Pull an image
+ containers.podman.podman_image:
+ name: quay.io/bitnami/wildfly
+
+- name: Remove an image
+ containers.podman.podman_image:
+ name: quay.io/bitnami/wildfly
+ state: absent
+
+- name: Remove an image with image id
+ containers.podman.podman_image:
+ name: 0e901e68141f
+ state: absent
+
+- name: Pull a specific version of an image
+ containers.podman.podman_image:
+ name: redis
+ tag: 4
+
+- name: Build a basic OCI image
+ containers.podman.podman_image:
+ name: nginx
+ path: /path/to/build/dir
+
+- name: Build a basic OCI image with advanced parameters
+ containers.podman.podman_image:
+ name: nginx
+ path: /path/to/build/dir
+ build:
+ cache: no
+ force_rm: true
+ format: oci
+ annotation:
+ app: nginx
+ function: proxy
+ info: Load balancer for my cool app
+ extra_args: "--build-arg KEY=value"
+
+- name: Build a Docker formatted image
+ containers.podman.podman_image:
+ name: nginx
+ path: /path/to/build/dir
+ build:
+ format: docker
+
+- name: Build and push an image using existing credentials
+ containers.podman.podman_image:
+ name: nginx
+ path: /path/to/build/dir
+ push: true
+ push_args:
+ dest: quay.io/acme
+
+- name: Build and push an image using an auth file
+ containers.podman.podman_image:
+ name: nginx
+ push: true
+ auth_file: /etc/containers/auth.json
+ push_args:
+ dest: quay.io/acme
+
+- name: Build and push an image using username and password
+ containers.podman.podman_image:
+ name: nginx
+ push: true
+ username: bugs
+ password: "{{ vault_registry_password }}"
+ push_args:
+ dest: quay.io/acme
+
+- name: Build and push an image to multiple registries
+ containers.podman.podman_image:
+ name: "{{ item }}"
+ path: /path/to/build/dir
+ push: true
+ auth_file: /etc/containers/auth.json
+ loop:
+ - quay.io/acme/nginx
+ - docker.io/acme/nginx
+
+- name: Build and push an image to multiple registries with separate parameters
+ containers.podman.podman_image:
+ name: "{{ item.name }}"
+ tag: "{{ item.tag }}"
+ path: /path/to/build/dir
+ push: true
+ auth_file: /etc/containers/auth.json
+ push_args:
+ dest: "{{ item.dest }}"
+ loop:
+ - name: nginx
+ tag: 4
+ dest: docker.io/acme
+
+ - name: nginx
+ tag: 3
+ dest: docker.io/acme
+
+- name: Pull an image for a specific CPU architecture
+ containers.podman.podman_image:
+ name: nginx
+ arch: amd64
+"""
+
+RETURN = r"""
+ image:
+ description:
+ - Image inspection results for the image that was pulled, pushed, or built.
+ returned: success
+ type: dict
+ sample: [
+ {
+ "Annotations": {},
+ "Architecture": "amd64",
+ "Author": "",
+ "Comment": "from Bitnami with love",
+ "ContainerConfig": {
+ "Cmd": [
+ "/run.sh"
+ ],
+ "Entrypoint": [
+ "/app-entrypoint.sh"
+ ],
+ "Env": [
+ "PATH=/opt/bitnami/java/bin:/opt/bitnami/wildfly/bin:/opt/bitnami/nami/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+ "IMAGE_OS=debian-9",
+ "NAMI_VERSION=1.0.0-1",
+ "GPG_KEY_SERVERS_LIST=ha.pool.sks-keyservers.net",
+ "TINI_VERSION=v0.13.2",
+ "TINI_GPG_KEY=595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7",
+ "GOSU_VERSION=1.10",
+ "GOSU_GPG_KEY=B42F6819007F00F88E364FD4036A9C25BF357DD4",
+ "BITNAMI_IMAGE_VERSION=16.0.0-debian-9-r27",
+ "BITNAMI_PKG_CHMOD=-R g+rwX",
+ "BITNAMI_PKG_EXTRA_DIRS=/home/wildfly",
+ "HOME=/",
+ "BITNAMI_APP_NAME=wildfly",
+ "NAMI_PREFIX=/.nami",
+ "WILDFLY_HOME=/home/wildfly",
+ "WILDFLY_JAVA_HOME=",
+ "WILDFLY_JAVA_OPTS=",
+ "WILDFLY_MANAGEMENT_HTTP_PORT_NUMBER=9990",
+ "WILDFLY_PASSWORD=bitnami",
+ "WILDFLY_PUBLIC_CONSOLE=true",
+ "WILDFLY_SERVER_AJP_PORT_NUMBER=8009",
+ "WILDFLY_SERVER_HTTP_PORT_NUMBER=8080",
+ "WILDFLY_SERVER_INTERFACE=0.0.0.0",
+ "WILDFLY_USERNAME=user",
+ "WILDFLY_WILDFLY_HOME=/home/wildfly",
+ "WILDFLY_WILDFLY_OPTS=-Dwildfly.as.deployment.ondemand=false"
+ ],
+ "ExposedPorts": {
+ "8080/tcp": {},
+ "9990/tcp": {}
+ },
+ "Labels": {
+ "maintainer": "Bitnami <containers@bitnami.com>"
+ },
+ "User": "1001"
+ },
+ "Created": "2019-04-10T05:48:03.553887623Z",
+ "Digest": "sha256:5a8ab28e314c2222de3feaf6dac94a0436a37fc08979d2722c99d2bef2619a9b",
+ "GraphDriver": {
+ "Data": {
+ "LowerDir": "/var/lib/containers/storage/overlay/142c1beadf1bb09fbd929465ec98c9dca3256638220450efb4214727d0d0680e/diff:/var/lib/containers/s",
+ "MergedDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99723ed787878dbfea/merged",
+ "UpperDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99723ed787878dbfea/diff",
+ "WorkDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99723ed787878dbfea/work"
+ },
+ "Name": "overlay"
+ },
+ "History": [
+ {
+ "comment": "from Bitnami with love",
+ "created": "2019-04-09T22:27:40.659377677Z"
+ },
+ {
+ "created": "2019-04-09T22:38:53.86336555Z",
+ "created_by": "/bin/sh -c #(nop) LABEL maintainer=Bitnami <containers@bitnami.com>",
+ "empty_layer": true
+ },
+ {
+ "created": "2019-04-09T22:38:54.022778765Z",
+ "created_by": "/bin/sh -c #(nop) ENV IMAGE_OS=debian-9",
+ "empty_layer": true
+ },
+ ],
+ "Id": "ace34da54e4af2145e1ad277005adb235a214e4dfe1114c2db9ab460b840f785",
+ "Labels": {
+ "maintainer": "Bitnami <containers@bitnami.com>"
+ },
+ "ManifestType": "application/vnd.docker.distribution.manifest.v1+prettyjws",
+ "Os": "linux",
+ "Parent": "",
+ "RepoDigests": [
+ "quay.io/bitnami/wildfly@sha256:5a8ab28e314c2222de3feaf6dac94a0436a37fc08979d2722c99d2bef2619a9b"
+ ],
+ "RepoTags": [
+ "quay.io/bitnami/wildfly:latest"
+ ],
+ "RootFS": {
+ "Layers": [
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ ],
+ "Type": "layers"
+ },
+ "Size": 466180019,
+ "User": "1001",
+ "Version": "18.09.3",
+ "VirtualSize": 466180019
+ }
+ ]
+"""
+
+import json
+import re
+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
+
+
+class PodmanImageManager(object):
+
+ def __init__(self, module, results):
+
+ super(PodmanImageManager, self).__init__()
+
+ self.module = module
+ self.results = results
+ self.name = self.module.params.get('name')
+ self.executable = self.module.get_bin_path(module.params.get('executable'), required=True)
+ self.tag = self.module.params.get('tag')
+ self.pull = self.module.params.get('pull')
+ self.push = self.module.params.get('push')
+ self.path = self.module.params.get('path')
+ self.force = self.module.params.get('force')
+ self.state = self.module.params.get('state')
+ self.validate_certs = self.module.params.get('validate_certs')
+ self.auth_file = self.module.params.get('auth_file')
+ self.username = self.module.params.get('username')
+ self.password = self.module.params.get('password')
+ self.ca_cert_dir = self.module.params.get('ca_cert_dir')
+ self.build = self.module.params.get('build')
+ self.push_args = self.module.params.get('push_args')
+ self.arch = self.module.params.get('arch')
+
+ repo, repo_tag = parse_repository_tag(self.name)
+ if repo_tag:
+ self.name = repo
+ self.tag = repo_tag
+
+ delimiter = ':' if "sha256" not in self.tag else '@'
+ self.image_name = '{name}{d}{tag}'.format(name=self.name, d=delimiter, tag=self.tag)
+
+ if self.state in ['present', 'build']:
+ self.present()
+
+ if self.state in ['absent']:
+ self.absent()
+
+ def _run(self, args, expected_rc=0, ignore_errors=False):
+ cmd = " ".join([self.executable]
+ + [to_native(i) for i in args])
+ self.module.log("PODMAN-IMAGE-DEBUG: %s" % cmd)
+ self.results['podman_actions'].append(cmd)
+ return run_podman_command(
+ module=self.module,
+ executable=self.executable,
+ args=args,
+ expected_rc=expected_rc,
+ ignore_errors=ignore_errors)
+
+ def _get_id_from_output(self, lines, startswith=None, contains=None, split_on=' ', maxsplit=1):
+ layer_ids = []
+ for line in lines.splitlines():
+ if startswith and line.startswith(startswith) or contains and contains in line:
+ splitline = line.rsplit(split_on, maxsplit)
+ layer_ids.append(splitline[1])
+
+ # Podman 1.4 changed the output to only include the layer id when run in quiet mode
+ if not layer_ids:
+ layer_ids = lines.splitlines()
+
+ return (layer_ids[-1])
+
+ def present(self):
+ image = self.find_image()
+
+ if image:
+ digest_before = image[0].get('Digest', image[0].get('digest'))
+ else:
+ digest_before = None
+
+ if not image or self.force:
+ if self.path:
+ # Build the image
+ self.results['actions'].append('Built image {image_name} from {path}'.format(image_name=self.image_name, path=self.path))
+ if not self.module.check_mode:
+ self.results['image'], self.results['stdout'] = self.build_image()
+ image = self.results['image']
+ else:
+ # Pull the image
+ self.results['actions'].append('Pulled image {image_name}'.format(image_name=self.image_name))
+ if not self.module.check_mode:
+ image = self.results['image'] = self.pull_image()
+
+ if not image:
+ image = self.find_image()
+ if not self.module.check_mode:
+ digest_after = image[0].get('Digest', image[0].get('digest'))
+ self.results['changed'] = digest_before != digest_after
+ else:
+ self.results['changed'] = True
+
+ if self.push:
+ # Push the image
+ if '/' in self.image_name:
+ push_format_string = 'Pushed image {image_name}'
+ else:
+ push_format_string = 'Pushed image {image_name} to {dest}'
+ self.results['actions'].append(push_format_string.format(image_name=self.image_name, dest=self.push_args['dest']))
+ self.results['changed'] = True
+ if not self.module.check_mode:
+ self.results['image'], output = self.push_image()
+ self.results['stdout'] += "\n" + output
+
+ def absent(self):
+ image = self.find_image()
+ image_id = self.find_image_id()
+
+ if image:
+ self.results['actions'].append('Removed image {name}'.format(name=self.name))
+ self.results['changed'] = True
+ self.results['image']['state'] = 'Deleted'
+ if not self.module.check_mode:
+ self.remove_image()
+ elif image_id:
+ self.results['actions'].append(
+ 'Removed image with id {id}'.format(id=self.image_name))
+ self.results['changed'] = True
+ self.results['image']['state'] = 'Deleted'
+ if not self.module.check_mode:
+ self.remove_image_id()
+
+ def find_image(self, image_name=None):
+ if image_name is None:
+ image_name = self.image_name
+ args = ['image', 'ls', image_name, '--format', 'json']
+ rc, images, err = self._run(args, ignore_errors=True)
+ images = json.loads(images)
+ if len(images) > 0:
+ inspect_json = self.inspect_image(image_name)
+ if self._is_target_arch(inspect_json, self.arch) or not self.arch:
+ return images
+
+ return None
+
+ def _is_target_arch(self, inspect_json=None, arch=None):
+ return arch and inspect_json[0]['Architecture'] == arch
+
+ def find_image_id(self, image_id=None):
+ if image_id is None:
+ # If image id is set as image_name, remove tag
+ image_id = re.sub(':.*$', '', self.image_name)
+ args = ['image', 'ls', '--quiet', '--no-trunc']
+ rc, candidates, err = self._run(args, ignore_errors=True)
+ candidates = [re.sub('^sha256:', '', c)
+ for c in str.splitlines(candidates)]
+ for c in candidates:
+ if c.startswith(image_id):
+ return image_id
+ return None
+
+ def inspect_image(self, image_name=None):
+ if image_name is None:
+ image_name = self.image_name
+ args = ['inspect', image_name, '--format', 'json']
+ rc, image_data, err = self._run(args)
+ image_data = json.loads(image_data)
+ if len(image_data) > 0:
+ return image_data
+ else:
+ return None
+
+ def pull_image(self, image_name=None):
+ if image_name is None:
+ image_name = self.image_name
+
+ args = ['pull', image_name, '-q']
+
+ if self.arch:
+ args.extend(['--arch', self.arch])
+
+ if self.auth_file:
+ args.extend(['--authfile', self.auth_file])
+
+ if self.username and self.password:
+ cred_string = '{user}:{password}'.format(user=self.username, password=self.password)
+ args.extend(['--creds', cred_string])
+
+ if self.validate_certs is not None:
+ if self.validate_certs:
+ args.append('--tls-verify')
+ else:
+ args.append('--tls-verify=false')
+
+ if self.ca_cert_dir:
+ args.extend(['--cert-dir', self.ca_cert_dir])
+
+ rc, out, err = self._run(args, ignore_errors=True)
+ if rc != 0:
+ if not self.pull:
+ self.module.fail_json(msg='Failed to find image {image_name} locally, image pull set to {pull_bool}'.format(
+ pull_bool=self.pull, image_name=image_name))
+ else:
+ self.module.fail_json(msg='Failed to pull image {image_name}'.format(image_name=image_name))
+ return self.inspect_image(out.strip())
+
+ def build_image(self):
+ args = ['build']
+ args.extend(['-t', self.image_name])
+
+ if self.validate_certs is not None:
+ if self.validate_certs:
+ args.append('--tls-verify')
+ else:
+ args.append('--tls-verify=false')
+
+ annotation = self.build.get('annotation')
+ if annotation:
+ for k, v in annotation.items():
+ args.extend(['--annotation', '{k}={v}'.format(k=k, v=v)])
+
+ if self.ca_cert_dir:
+ args.extend(['--cert-dir', self.ca_cert_dir])
+
+ if self.build.get('force_rm'):
+ args.append('--force-rm')
+
+ image_format = self.build.get('format')
+ if image_format:
+ args.extend(['--format', image_format])
+
+ if not self.build.get('cache'):
+ args.append('--no-cache')
+
+ if self.build.get('rm'):
+ args.append('--rm')
+
+ containerfile = self.build.get('file')
+ if containerfile:
+ args.extend(['--file', containerfile])
+
+ volume = self.build.get('volume')
+ if volume:
+ for v in volume:
+ args.extend(['--volume', v])
+
+ if self.auth_file:
+ args.extend(['--authfile', self.auth_file])
+
+ if self.username and self.password:
+ cred_string = '{user}:{password}'.format(user=self.username, password=self.password)
+ args.extend(['--creds', cred_string])
+
+ extra_args = self.build.get('extra_args')
+ if extra_args:
+ args.extend(shlex.split(extra_args))
+
+ args.append(self.path)
+
+ rc, out, err = self._run(args, ignore_errors=True)
+ if rc != 0:
+ self.module.fail_json(msg="Failed to build image {image}: {out} {err}".format(image=self.image_name, out=out, err=err))
+
+ last_id = self._get_id_from_output(out, startswith='-->')
+ return self.inspect_image(last_id), out + err
+
+ def push_image(self):
+ args = ['push']
+
+ if self.validate_certs is not None:
+ if self.validate_certs:
+ args.append('--tls-verify')
+ else:
+ args.append('--tls-verify=false')
+
+ if self.ca_cert_dir:
+ args.extend(['--cert-dir', self.ca_cert_dir])
+
+ if self.username and self.password:
+ cred_string = '{user}:{password}'.format(user=self.username, password=self.password)
+ args.extend(['--creds', cred_string])
+
+ if self.auth_file:
+ args.extend(['--authfile', self.auth_file])
+
+ if self.push_args.get('compress'):
+ args.append('--compress')
+
+ push_format = self.push_args.get('format')
+ if push_format:
+ args.extend(['--format', push_format])
+
+ if self.push_args.get('remove_signatures'):
+ args.append('--remove-signatures')
+
+ sign_by_key = self.push_args.get('sign_by')
+ if sign_by_key:
+ args.extend(['--sign-by', sign_by_key])
+
+ args.append(self.image_name)
+
+ # Build the destination argument
+ dest = self.push_args.get('dest')
+ dest_format_string = '{dest}/{image_name}'
+ regexp = re.compile(r'/{name}(:{tag})?'.format(name=self.name, tag=self.tag))
+ if not dest:
+ if '/' not in self.name:
+ self.module.fail_json(msg="'push_args['dest']' is required when pushing images that do not have the remote registry in the image name")
+
+ # If the push destination contains the image name and/or the tag
+ # remove it and warn since it's not needed.
+ elif regexp.search(dest):
+ dest = regexp.sub('', dest)
+ self.module.warn("Image name and tag are automatically added to push_args['dest']. Destination changed to {dest}".format(dest=dest))
+
+ if dest and dest.endswith('/'):
+ dest = dest[:-1]
+
+ transport = self.push_args.get('transport')
+ if transport:
+ if not dest:
+ self.module.fail_json("'push_args['transport'] requires 'push_args['dest'] but it was not provided.")
+ if transport == 'docker':
+ dest_format_string = '{transport}://{dest}'
+ elif transport == 'ostree':
+ dest_format_string = '{transport}:{name}@{dest}'
+ else:
+ dest_format_string = '{transport}:{dest}'
+
+ dest_string = dest_format_string.format(transport=transport, name=self.name, dest=dest, image_name=self.image_name,)
+
+ # Only append the destination argument if the image name is not a URL
+ if '/' not in self.name:
+ args.append(dest_string)
+
+ rc, out, err = self._run(args, ignore_errors=True)
+ if rc != 0:
+ self.module.fail_json(msg="Failed to push image {image_name}: {err}".format(image_name=self.image_name, err=err))
+ last_id = self._get_id_from_output(
+ out + err, contains=':', split_on=':')
+
+ return self.inspect_image(last_id), out + err
+
+ def remove_image(self, image_name=None):
+ if image_name is None:
+ image_name = self.image_name
+
+ args = ['rmi', image_name]
+ if self.force:
+ args.append('--force')
+ rc, out, err = self._run(args, ignore_errors=True)
+ if rc != 0:
+ self.module.fail_json(msg='Failed to remove image {image_name}. {err}'.format(image_name=image_name, err=err))
+ return out
+
+ def remove_image_id(self, image_id=None):
+ if image_id is None:
+ image_id = re.sub(':.*$', '', self.image_name)
+
+ args = ['rmi', image_id]
+ if self.force:
+ args.append('--force')
+ rc, out, err = self._run(args, ignore_errors=True)
+ if rc != 0:
+ self.module.fail_json(msg='Failed to remove image with id {image_id}. {err}'.format(
+ image_id=image_id, err=err))
+ return out
+
+
+def parse_repository_tag(repo_name):
+ parts = repo_name.rsplit('@', 1)
+ if len(parts) == 2:
+ return tuple(parts)
+ parts = repo_name.rsplit(':', 1)
+ if len(parts) == 2 and '/' not in parts[1]:
+ return tuple(parts)
+ return repo_name, None
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ name=dict(type='str', required=True),
+ arch=dict(type='str'),
+ tag=dict(type='str', default='latest'),
+ pull=dict(type='bool', default=True),
+ 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']),
+ 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'),
+ build=dict(
+ type='dict',
+ aliases=['build_args', 'buildargs'],
+ default={},
+ options=dict(
+ annotation=dict(type='dict'),
+ force_rm=dict(type='bool', default=False),
+ file=dict(type='path'),
+ format=dict(
+ type='str',
+ choices=['oci', 'docker'],
+ default='oci'
+ ),
+ cache=dict(type='bool', default=True),
+ rm=dict(type='bool', default=True),
+ volume=dict(type='list', elements='str'),
+ extra_args=dict(type='str'),
+ ),
+ ),
+ push_args=dict(
+ type='dict',
+ default={},
+ options=dict(
+ compress=dict(type='bool'),
+ format=dict(type='str', choices=['oci', 'v2s1', 'v2s2']),
+ remove_signatures=dict(type='bool'),
+ sign_by=dict(type='str'),
+ dest=dict(type='str', aliases=['destination'],),
+ transport=dict(
+ type='str',
+ choices=[
+ 'dir',
+ 'docker-archive',
+ 'docker-daemon',
+ 'oci-archive',
+ 'ostree',
+ ]
+ ),
+ ),
+ ),
+ ),
+ supports_check_mode=True,
+ required_together=(
+ ['username', 'password'],
+ ),
+ mutually_exclusive=(
+ ['auth_file', 'username'],
+ ['auth_file', 'password'],
+ ),
+ )
+
+ results = dict(
+ changed=False,
+ actions=[],
+ podman_actions=[],
+ image={},
+ stdout='',
+ )
+
+ PodmanImageManager(module, results)
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_image_info.py b/ansible_collections/containers/podman/plugins/modules/podman_image_info.py
new file mode 100644
index 000000000..d8af08814
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_image_info.py
@@ -0,0 +1,236 @@
+#!/usr/bin/python
+# Copyright (c) 2019 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+module: podman_image_info
+author:
+ - Sam Doran (@samdoran)
+short_description: Gather info about images using podman
+notes:
+ - Podman may required elevated privileges in order to run properly.
+description:
+ - Gather info about images using C(podman)
+options:
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the machine running C(podman)
+ default: 'podman'
+ type: str
+ name:
+ description:
+ - List of tags or UID to gather info about. If no name is given return info about all images.
+ type: list
+ elements: str
+
+'''
+
+EXAMPLES = r"""
+- name: Gather info for all images
+ containers.podman.podman_image_info:
+
+- name: Gather info on a specific image
+ containers.podman.podman_image_info:
+ name: nginx
+
+- name: Gather info on several images
+ containers.podman.podman_image_info:
+ name:
+ - redis
+ - quay.io/bitnami/wildfly
+"""
+
+RETURN = r"""
+images:
+ description: info from all or specified images
+ returned: always
+ type: dict
+ sample: [
+ {
+ "Annotations": {},
+ "Architecture": "amd64",
+ "Author": "",
+ "Comment": "from Bitnami with love",
+ "ContainerConfig": {
+ "Cmd": [
+ "nami",
+ "start",
+ "--foreground",
+ "wildfly"
+ ],
+ "Entrypoint": [
+ "/app-entrypoint.sh"
+ ],
+ "Env": [
+ "PATH=/opt/bitnami/java/bin:/opt/bitnami/wildfly/bin:/opt/bitnami/nami/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+ "IMAGE_OS=debian-9",
+ "NAMI_VERSION=0.0.9-0",
+ "GPG_KEY_SERVERS_LIST=ha.pool.sks-keyservers.net \
+hkp://p80.pool.sks-keyservers.net:80 keyserver.ubuntu.com hkp://keyserver.ubuntu.com:80 pgp.mit.edu",
+ "TINI_VERSION=v0.13.2",
+ "TINI_GPG_KEY=595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7",
+ "GOSU_VERSION=1.10",
+ "GOSU_GPG_KEY=B42F6819007F00F88E364FD4036A9C25BF357DD4",
+ "BITNAMI_IMAGE_VERSION=14.0.1-debian-9-r12",
+ "BITNAMI_APP_NAME=wildfly",
+ "WILDFLY_JAVA_HOME=",
+ "WILDFLY_JAVA_OPTS=",
+ "WILDFLY_MANAGEMENT_HTTP_PORT_NUMBER=9990",
+ "WILDFLY_PASSWORD=bitnami",
+ "WILDFLY_PUBLIC_CONSOLE=true",
+ "WILDFLY_SERVER_AJP_PORT_NUMBER=8009",
+ "WILDFLY_SERVER_HTTP_PORT_NUMBER=8080",
+ "WILDFLY_SERVER_INTERFACE=0.0.0.0",
+ "WILDFLY_USERNAME=user",
+ "WILDFLY_WILDFLY_HOME=/home/wildfly",
+ "WILDFLY_WILDFLY_OPTS=-Dwildfly.as.deployment.ondemand=false"
+ ],
+ "ExposedPorts": {
+ "8080/tcp": {},
+ "9990/tcp": {}
+ },
+ "Labels": {
+ "maintainer": "Bitnami <containers@bitnami.com>"
+ }
+ },
+ "Created": "2018-09-25T04:07:45.934395523Z",
+ "Digest": "sha256:5c7d8e2dd66dcf4a152a4032a1d3c5a33458c67e1c1335edd8d18d738892356b",
+ "GraphDriver": {
+ "Data": {
+ "LowerDir": "/var/lib/containers/storage/overlay/a9dbf5616cc16919a8ac0dfc60aff87a72b5be52994c4649fcc91a089a12931\
+f/diff:/var/lib/containers/storage/overlay/67129bd46022122a7d8b7acb490092af6c7ce244ce4fbd7d9e2d2b7f5979e090/diff:/var/lib/containers/storage/overlay/7c51242c\
+4c5db5c74afda76d7fdbeab6965d8b21804bb3fc597dee09c770b0ca/diff:/var/lib/containers/storage/overlay/f97315dc58a9c002ba0cabccb9933d4b0d2113733d204188c88d72f75569b57b/diff:/var/lib/containers/storage/overlay/1dbde2dd497ddde2b467727125b900958a051a72561e58d29abe3d660dcaa9a7/diff:/var/lib/containers/storage/overlay/4aad9d80f30c3f0608f58173558b7554d84dee4dc4479672926eca29f75e6e33/diff:/var/lib/containers/storage/overlay/6751fc9b6868254870c062d75a511543fc8cfda2ce6262f4945f107449219632/diff:/var/lib/containers/storage/overlay/a27034d79081347421dd24d7e9e776c18271cd9a6e51053cb39af4d3d9c400e8/diff:/var/lib/containers/storage/overlay/537cf0045ed9cd7989f7944e7393019c81b16c1799a2198d8348cd182665397f/diff:/var/lib/containers/storage/overlay/27578615c5ae352af4e8449862d61aaf5c11b105a7d5905af55bd01b0c656d6e/diff:/var/lib/containers/storage/overlay/566542742840fe3034b3596f7cb9e62a6274c95a69f368f9e713746f8712c0b6/diff",
+ "MergedDir": "/var/lib/containers/storage/overlay/72bb96d6\
+c53ad57a0b1e44cab226a6251598accbead40b23fac89c19ad8c25ca/merged",
+ "UpperDir": "/var/lib/containers/storage/overlay/72bb96d6c53ad57a0b1e44cab226a6251598accbead40b23fac89c19ad8c25ca/diff",
+ "WorkDir": "/var/lib/containers/storage/overlay/72bb96d6c53ad57a0b1e44cab226a6251598accbead40b23fac89c19ad8c25ca/work"
+ },
+ "Name": "overlay"
+ },
+ "Id": "bcacbdf7a119c0fa934661ca8af839e625ce6540d9ceb6827cdd389f823d49e0",
+ "Labels": {
+ "maintainer": "Bitnami <containers@bitnami.com>"
+ },
+ "ManifestType": "application/vnd.docker.distribution.manifest.v1+prettyjws",
+ "Os": "linux",
+ "Parent": "",
+ "RepoDigests": [
+ "quay.io/bitnami/wildfly@sha256:5c7d8e2dd66dcf4a152a4032a1d3c5a33458c67e1c1335edd8d18d738892356b"
+ ],
+ "RepoTags": [
+ "quay.io/bitnami/wildfly:latest"
+ ],
+ "RootFS": {
+ "Layers": [
+ "sha256:75391df2c87e076b0c2f72d20c95c57dc8be7ee684cc07273416cce622b43367",
+ "sha256:7dd303f041039bfe8f0833092673ac35f93137d10e0fbc4302021ea65ad57731",
+ "sha256:720d9edf0cd2a9bb56b88b80be9070dbfaad359514c70094c65066963fed485d",
+ "sha256:6a567ecbf97725501a634fcb486271999aa4591b633b4ae9932a46b40f5aaf47",
+ "sha256:59e9a6db8f178f3da868614564faabb2820cdfb69be32e63a4405d6f7772f68c",
+ "sha256:310a82ccb092cd650215ab375da8943d235a263af9a029b8ac26a281446c04db",
+ "sha256:36cb91cf4513543a8f0953fed785747ea18b675bc2677f3839889cfca0aac79e"
+ ],
+ "Type": "layers"
+ },
+ "Size": 569919342,
+ "User": "",
+ "Version": "17.06.0-ce",
+ "VirtualSize": 569919342
+ }
+ ]
+"""
+
+import json
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def image_exists(module, executable, name):
+ command = [executable, 'image', 'exists', name]
+ rc, out, err = module.run_command(command)
+ if rc == 1:
+ return False
+ elif 'Command "exists" not found' in err:
+ # The 'exists' test is available in podman >= 0.12.1
+ command = [executable, 'image', 'ls', '-q', name]
+ rc2, out2, err2 = module.run_command(command)
+ if rc2 != 0:
+ return False
+ return True
+
+
+def filter_invalid_names(module, executable, name):
+ valid_names = []
+ names = name
+ if not isinstance(name, list):
+ names = [name]
+
+ for name in names:
+ if image_exists(module, executable, name):
+ valid_names.append(name)
+
+ return valid_names
+
+
+def get_image_info(module, executable, name):
+ names = name
+ if not isinstance(name, list):
+ names = [name]
+
+ if len(names) > 0:
+ command = [executable, 'image', 'inspect']
+ command.extend(names)
+ rc, out, err = module.run_command(command)
+
+ if rc != 0:
+ module.fail_json(msg="Unable to gather info for '{0}': {1}".format(', '.join(names), err))
+ return out
+
+ else:
+ return json.dumps([])
+
+
+def get_all_image_info(module, executable):
+ command = [executable, 'image', 'ls', '-q']
+ rc, out, err = module.run_command(command)
+ out = out.strip()
+ if out:
+ name = out.split('\n')
+ res = get_image_info(module, executable, name)
+ return res
+ return json.dumps([])
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ executable=dict(type='str', default='podman'),
+ name=dict(type='list', elements='str')
+ ),
+ supports_check_mode=True,
+ )
+
+ executable = module.params['executable']
+ name = module.params.get('name')
+ executable = module.get_bin_path(executable, required=True)
+
+ if name:
+ valid_names = filter_invalid_names(module, executable, name)
+ results = json.loads(get_image_info(module, executable, valid_names))
+ else:
+ results = json.loads(get_all_image_info(module, executable))
+
+ results = dict(
+ changed=False,
+ images=results
+ )
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_import.py b/ansible_collections/containers/podman/plugins/modules/podman_import.py
new file mode 100644
index 000000000..5090b177c
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_import.py
@@ -0,0 +1,157 @@
+#!/usr/bin/python
+# coding: utf-8 -*-
+
+# Copyright (c) 2021, Sagi Shnaidman <sshnaidm@redhat.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+module: podman_import
+short_description: Import Podman container from a tar file.
+author: Sagi Shnaidman (@sshnaidm)
+description:
+ - podman import imports a tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz)
+ and saves it as a filesystem image.
+options:
+ src:
+ description:
+ - Path to image file to load.
+ type: str
+ required: true
+ commit_message:
+ description:
+ - Set commit message for imported image
+ type: str
+ change:
+ description:
+ - Set changes as list of key-value pairs, see example.
+ type: list
+ elements: dict
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+requirements:
+ - "Podman installed on host"
+'''
+
+RETURN = '''
+image:
+ description: info from loaded image
+ returned: always
+ type: dict
+ sample: {
+ "Id": "cbc6d73c4d232db6e8441df96af81855f62c74157b5db80a1d5...",
+ "Digest": "sha256:8730c75be86a718929a658db4663d487e562d66762....",
+ "RepoTags": [],
+ "RepoDigests": [],
+ "Parent": "",
+ "Comment": "imported from tarball",
+ "Created": "2021-09-07T04:45:38.749977105+03:00",
+ "Config": {},
+ "Version": "",
+ "Author": "",
+ "Architecture": "amd64",
+ "Os": "linux",
+ "Size": 5882449,
+ "VirtualSize": 5882449,
+ "GraphDriver": {
+ "Name": "overlay",
+ "Data": {
+ "UpperDir": "/home/...34/diff",
+ "WorkDir": "/home/.../work"
+ }
+ },
+ "RootFS": {
+ "Type": "layers",
+ "Layers": [
+ "sha256:...."
+ ]
+ },
+ "Labels": null,
+ "Annotations": {},
+ "ManifestType": "application/vnd.oci.image.manifest.v1+json",
+ "User": "",
+ "History": [
+ {
+ "created": "2021-09-07T04:45:38.749977105+03:00",
+ "created_by": "/bin/sh -c #(nop) ADD file:091... in /",
+ "comment": "imported from tarball"
+ }
+ ],
+ "NamesHistory": null
+ }
+'''
+
+EXAMPLES = '''
+# What modules does for example
+- containers.podman.podman_import:
+ src: /path/to/tar/file
+ change:
+ - "CMD": /bin/bash
+ - "User": root
+ commit_message: "Importing image"
+'''
+
+import json # noqa: E402
+from ansible.module_utils.basic import AnsibleModule # noqa: E402
+
+
+def load(module, executable):
+ changed = False
+ command = [executable, 'import']
+ if module.params['commit_message']:
+ command.extend(['--message', module.params['commit_message']])
+ if module.params['change']:
+ for change in module.params['change']:
+ command += ['--change', "=".join(list(change.items())[0])]
+ command += [module.params['src']]
+ changed = True
+ if module.check_mode:
+ return changed, '', '', '', command
+ rc, out, err = module.run_command(command)
+ if rc != 0:
+ module.fail_json(msg="Image loading failed: %s" % (err))
+ image_name_line = [i for i in out.splitlines() if 'sha256' in i][0]
+ image_name = image_name_line.split(":", maxsplit=1)[1].strip()
+ rc, out2, err2 = module.run_command([executable, 'image', 'inspect', image_name])
+ if rc != 0:
+ module.fail_json(msg="Image %s inspection failed: %s" % (image_name, err2))
+ try:
+ info = json.loads(out2)[0]
+ except Exception as e:
+ module.fail_json(msg="Could not parse JSON from image %s: %s" % (image_name, e))
+ return changed, out, err, info, command
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ src=dict(type='str', required=True),
+ commit_message=dict(type='str'),
+ change=dict(type='list', elements='dict'),
+ executable=dict(type='str', default='podman')
+ ),
+ supports_check_mode=True,
+ )
+
+ executable = module.get_bin_path(module.params['executable'], required=True)
+ changed, out, err, image_info, command = load(module, executable)
+
+ results = {
+ "changed": changed,
+ "stdout": out,
+ "stderr": err,
+ "image": image_info,
+ "podman_command": " ".join(command)
+ }
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_load.py b/ansible_collections/containers/podman/plugins/modules/podman_load.py
new file mode 100644
index 000000000..4fa7bde01
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_load.py
@@ -0,0 +1,199 @@
+#!/usr/bin/python
+# coding: utf-8 -*-
+
+# Copyright (c) 2020, Sagi Shnaidman <sshnaidm@redhat.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+module: podman_load
+short_description: Load image from a tar file.
+author: Sagi Shnaidman (@sshnaidm)
+description:
+ - podman load loads an image from either an oci-archive or a docker-archive stored
+ on the local machine into container storage.
+ podman load is used for loading from the archive generated by podman save,
+ that includes the image parent layers.
+options:
+ input:
+ description:
+ - Path to image file to load.
+ type: str
+ required: true
+ aliases:
+ - path
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+requirements:
+ - "Podman installed on host"
+'''
+
+RETURN = '''
+image:
+ description: info from loaded image
+ returned: always
+ type: dict
+ sample: [
+ {
+ "Annotations": {},
+ "Architecture": "amd64",
+ "Author": "",
+ "Comment": "from Bitnami with love",
+ "ContainerConfig": {
+ "Cmd": [
+ "nami",
+ "start",
+ "--foreground",
+ "wildfly"
+ ],
+ "Entrypoint": [
+ "/app-entrypoint.sh"
+ ],
+ "Env": [
+ "PATH=/opt/bitnami/java/bin:/opt/bitnami/wildfly/bin:/opt/bitnami/nami/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+ "IMAGE_OS=debian-9",
+ "NAMI_VERSION=0.0.9-0",
+ "GPG_KEY_SERVERS_LIST=ha.pool.sks-keyservers.net \
+hkp://p80.pool.sks-keyservers.net:80 keyserver.ubuntu.com hkp://keyserver.ubuntu.com:80 pgp.mit.edu",
+ "TINI_VERSION=v0.13.2",
+ "TINI_GPG_KEY=595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7",
+ "GOSU_VERSION=1.10",
+ "GOSU_GPG_KEY=B42F6819007F00F88E364FD4036A9C25BF357DD4",
+ "BITNAMI_IMAGE_VERSION=14.0.1-debian-9-r12",
+ "BITNAMI_APP_NAME=wildfly",
+ "WILDFLY_JAVA_HOME=",
+ "WILDFLY_JAVA_OPTS=",
+ "WILDFLY_MANAGEMENT_HTTP_PORT_NUMBER=9990",
+ "WILDFLY_PASSWORD=bitnami",
+ "WILDFLY_PUBLIC_CONSOLE=true",
+ "WILDFLY_SERVER_AJP_PORT_NUMBER=8009",
+ "WILDFLY_SERVER_HTTP_PORT_NUMBER=8080",
+ "WILDFLY_SERVER_INTERFACE=0.0.0.0",
+ "WILDFLY_USERNAME=user",
+ "WILDFLY_WILDFLY_HOME=/home/wildfly",
+ "WILDFLY_WILDFLY_OPTS=-Dwildfly.as.deployment.ondemand=false"
+ ],
+ "ExposedPorts": {
+ "8080/tcp": {},
+ "9990/tcp": {}
+ },
+ "Labels": {
+ "maintainer": "Bitnami <containers@bitnami.com>"
+ }
+ },
+ "Created": "2018-09-25T04:07:45.934395523Z",
+ "Digest": "sha256:5c7d8e2dd66dcf4a152a4032a1d3c5a33458c67e1c1335edd8d18d738892356b",
+ "GraphDriver": {
+ "Data": {
+ "LowerDir": "/var/lib/containers/storage/overlay/a9dbf5616cc16919a8ac0dfc60aff87a72b5be52994c4649fcc91a089a12931\
+f/diff:/var/lib/containers/storage/overlay/67129bd46022122a7d8b7acb490092af6c7ce244ce4fbd7d9e2d2b7f5979e090/diff:/var/lib/containers/storage/overlay/7c51242c\
+4c5db5c74afda76d7fdbeab6965d8b21804bb3fc597dee09c770b0ca/diff:/var/lib/containers/storage/overlay/f97315dc58a9c002ba0cabccb9933d4b0d2113733d204188c88d72f75569b57b/diff:/var/lib/containers/storage/overlay/1dbde2dd497ddde2b467727125b900958a051a72561e58d29abe3d660dcaa9a7/diff:/var/lib/containers/storage/overlay/4aad9d80f30c3f0608f58173558b7554d84dee4dc4479672926eca29f75e6e33/diff:/var/lib/containers/storage/overlay/6751fc9b6868254870c062d75a511543fc8cfda2ce6262f4945f107449219632/diff:/var/lib/containers/storage/overlay/a27034d79081347421dd24d7e9e776c18271cd9a6e51053cb39af4d3d9c400e8/diff:/var/lib/containers/storage/overlay/537cf0045ed9cd7989f7944e7393019c81b16c1799a2198d8348cd182665397f/diff:/var/lib/containers/storage/overlay/27578615c5ae352af4e8449862d61aaf5c11b105a7d5905af55bd01b0c656d6e/diff:/var/lib/containers/storage/overlay/566542742840fe3034b3596f7cb9e62a6274c95a69f368f9e713746f8712c0b6/diff",
+ "MergedDir": "/var/lib/containers/storage/overlay/72bb96d6\
+c53ad57a0b1e44cab226a6251598accbead40b23fac89c19ad8c25ca/merged",
+ "UpperDir": "/var/lib/containers/storage/overlay/72bb96d6c53ad57a0b1e44cab226a6251598accbead40b23fac89c19ad8c25ca/diff",
+ "WorkDir": "/var/lib/containers/storage/overlay/72bb96d6c53ad57a0b1e44cab226a6251598accbead40b23fac89c19ad8c25ca/work"
+ },
+ "Name": "overlay"
+ },
+ "Id": "bcacbdf7a119c0fa934661ca8af839e625ce6540d9ceb6827cdd389f823d49e0",
+ "Labels": {
+ "maintainer": "Bitnami <containers@bitnami.com>"
+ },
+ "ManifestType": "application/vnd.docker.distribution.manifest.v1+prettyjws",
+ "Os": "linux",
+ "Parent": "",
+ "RepoDigests": [
+ "quay.io/bitnami/wildfly@sha256:5c7d8e2dd66dcf4a152a4032a1d3c5a33458c67e1c1335edd8d18d738892356b"
+ ],
+ "RepoTags": [
+ "quay.io/bitnami/wildfly:latest"
+ ],
+ "RootFS": {
+ "Layers": [
+ "sha256:75391df2c87e076b0c2f72d20c95c57dc8be7ee684cc07273416cce622b43367",
+ "sha256:7dd303f041039bfe8f0833092673ac35f93137d10e0fbc4302021ea65ad57731",
+ "sha256:720d9edf0cd2a9bb56b88b80be9070dbfaad359514c70094c65066963fed485d",
+ "sha256:6a567ecbf97725501a634fcb486271999aa4591b633b4ae9932a46b40f5aaf47",
+ "sha256:59e9a6db8f178f3da868614564faabb2820cdfb69be32e63a4405d6f7772f68c",
+ "sha256:310a82ccb092cd650215ab375da8943d235a263af9a029b8ac26a281446c04db",
+ "sha256:36cb91cf4513543a8f0953fed785747ea18b675bc2677f3839889cfca0aac79e"
+ ],
+ "Type": "layers"
+ },
+ "Size": 569919342,
+ "User": "",
+ "Version": "17.06.0-ce",
+ "VirtualSize": 569919342
+ }
+ ]
+'''
+
+EXAMPLES = '''
+# What modules does for example
+- containers.podman.podman_load:
+ input: /path/to/tar/file
+'''
+
+import json # noqa: E402
+from ansible.module_utils.basic import AnsibleModule # noqa: E402
+
+
+def load(module, executable):
+ changed = False
+ command = [executable, 'load', '--input']
+ command.append(module.params['input'])
+ changed = True
+ if module.check_mode:
+ return changed, '', '', ''
+ rc, out, err = module.run_command(command)
+ if rc != 0:
+ module.fail_json(msg="Image loading failed: %s" % (err))
+ image_name_line = [i for i in out.splitlines() if 'Loaded image' in i][0]
+ # For Podman < 4.x
+ if 'Loaded image(s):' in image_name_line:
+ image_name = image_name_line.split("Loaded image(s): ")[1].split(',')[0].strip()
+ # For Podman > 4.x
+ elif 'Loaded image:' in image_name_line:
+ image_name = image_name_line.split("Loaded image: ")[1].strip()
+ else:
+ module.fail_json(msg="Not found images in %s" % image_name_line)
+ rc, out2, err2 = module.run_command([executable, 'image', 'inspect', image_name])
+ if rc != 0:
+ module.fail_json(msg="Image %s inspection failed: %s" % (image_name, err2))
+ try:
+ info = json.loads(out2)[0]
+ except Exception as e:
+ module.fail_json(msg="Could not parse JSON from image %s: %s" % (image_name, e))
+ return changed, out, err, info
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ input=dict(type='str', required=True, aliases=['path']),
+ executable=dict(type='str', default='podman')
+ ),
+ supports_check_mode=True,
+ )
+
+ executable = module.get_bin_path(module.params['executable'], required=True)
+ changed, out, err, image_info = load(module, executable)
+
+ results = {
+ "changed": changed,
+ "stdout": out,
+ "stderr": err,
+ "image": image_info,
+ }
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_login.py b/ansible_collections/containers/podman/plugins/modules/podman_login.py
new file mode 100644
index 000000000..be417c761
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_login.py
@@ -0,0 +1,184 @@
+#!/usr/bin/python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+module: podman_login
+author:
+ - "Jason Hiatt (@jthiatt)"
+ - "Clemens Lange (@clelange)"
+ - "Michael Fox (@spmfox)"
+short_description: Login to a container registry using podman
+notes: []
+description:
+ - Login to a container registry server using the podman login command
+ If the registry is not specified, the first registry under
+ `[registries.search]` from `registries.conf `will be used. The path of
+ the authentication file can be overridden by the user by setting the
+ `authfile` flag. The default path used is
+ `${XDG_RUNTIME_DIR}/containers/auth.json`.
+requirements:
+ - "Podman installed on host"
+options:
+ authfile:
+ description:
+ - Path of the authentication file. Default is
+ ``${XDG_RUNTIME_DIR}/containers/auth.json``
+ You can also override the default path of the authentication
+ file by setting the ``REGISTRY_AUTH_FILE`` environment
+ variable. ``export REGISTRY_AUTH_FILE=path``
+ type: path
+ certdir:
+ description:
+ - Use certificates at path (*.crt, *.cert, *.key) to connect
+ to the registry. Default certificates directory
+ is /etc/containers/certs.d.
+ type: path
+ password:
+ description:
+ - Password for the registry server.
+ required: True
+ type: str
+ registry:
+ description:
+ - Registry server. If the registry is not specified,
+ the first registry under `[registries.search]` from
+ `registries.conf` will be used.
+ type: str
+ tlsverify:
+ description:
+ - Require HTTPS and verify certificates when
+ contacting registries. If explicitly set to true,
+ then TLS verification will be used. If set to false,
+ then TLS verification will not be used. If not specified,
+ TLS verification will be used unless the target registry
+ is listed as an insecure registry in registries.conf.
+ type: bool
+ username:
+ description:
+ - Username for the registry server.
+ required: True
+ type: str
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+'''
+
+EXAMPLES = r"""
+- name: Login to default registry and create ${XDG_RUNTIME_DIR}/containers/auth.json
+ containers.podman.podman_login:
+ username: user
+ password: 'p4ssw0rd'
+
+- name: Login to default registry and create ${XDG_RUNTIME_DIR}/containers/auth.json
+ containers.podman.podman_login:
+ username: user
+ password: 'p4ssw0rd'
+ registry: quay.io
+
+"""
+# noqa: F402
+
+import hashlib
+import os
+from ansible.module_utils.basic import AnsibleModule
+
+
+def login(module, executable, registry, authfile,
+ certdir, tlsverify, username, password):
+
+ command = [executable, 'login']
+ changed = False
+
+ if username:
+ command.extend(['--username', username])
+ if password:
+ command.extend(['--password', password])
+ if authfile:
+ command.extend(['--authfile', authfile])
+ authfile = os.path.expandvars(authfile)
+ else:
+ authfile = os.getenv('XDG_RUNTIME_DIR', '') + '/containers/auth.json'
+ if registry:
+ command.append(registry)
+ if certdir:
+ command.extend(['--cert-dir', certdir])
+ if tlsverify is not None:
+ if tlsverify:
+ command.append('--tls-verify')
+ else:
+ command.append('--tls-verify=False')
+ # Use a checksum to check if the auth JSON has changed
+ checksum = None
+ docker_authfile = os.path.expandvars('$HOME/.docker/config.json')
+ # podman falls back to ~/.docker/config.json if the default authfile doesn't exist
+ check_file = authfile if os.path.exists(authfile) else docker_authfile
+ if os.path.exists(check_file):
+ content = open(check_file, 'rb').read()
+ checksum = hashlib.sha256(content).hexdigest()
+ rc, out, err = module.run_command(command)
+ if rc != 0:
+ if 'Error: Not logged into' not in err:
+ module.fail_json(msg="Unable to gather info for %s: %s" % (registry, err))
+ else:
+ # If the command is successful, we managed to login
+ changed = True
+ if 'Existing credentials are valid' in out:
+ changed = False
+ # If we have managed to calculate a checksum before, check if it has changed
+ # due to the login
+ if checksum:
+ content = open(check_file, 'rb').read()
+ new_checksum = hashlib.sha256(content).hexdigest()
+ if new_checksum == checksum:
+ changed = False
+ return changed, out, err
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ executable=dict(type='str', default='podman'),
+ registry=dict(type='str'),
+ authfile=dict(type='path'),
+ username=dict(type='str', required=True),
+ password=dict(type='str', required=True, no_log=True),
+ certdir=dict(type='path'),
+ tlsverify=dict(type='bool'),
+ ),
+ supports_check_mode=True,
+ required_together=(
+ ['username', 'password'],
+ ),
+ mutually_exclusive=(
+ ['certdir', 'tlsverify'],
+ ),
+ )
+
+ registry = module.params['registry']
+ authfile = module.params['authfile']
+ username = module.params['username']
+ password = module.params['password']
+ certdir = module.params['certdir']
+ tlsverify = module.params['tlsverify']
+ executable = module.get_bin_path(module.params['executable'], required=True)
+
+ changed, out, err = login(module, executable, registry, authfile,
+ certdir, tlsverify, username, password)
+
+ results = {
+ "changed": changed,
+ "stdout": out,
+ "stderr": err,
+ }
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_login_info.py b/ansible_collections/containers/podman/plugins/modules/podman_login_info.py
new file mode 100644
index 000000000..739adb134
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_login_info.py
@@ -0,0 +1,116 @@
+#!/usr/bin/python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r"""
+module: podman_login_info
+author:
+ - "Clemens Lange (@clelange)"
+version_added: '1.0.0'
+short_description: Return the logged-in user if any for a given registry
+notes: []
+description:
+ - Return the logged-in user if any for a given registry.
+requirements:
+ - "Podman installed on host"
+options:
+ registry:
+ description:
+ - Registry server.
+ type: str
+ required: true
+ authfile:
+ description:
+ - Path of the authentication file. Default is
+ ``${XDG_RUNTIME_DIR}/containers/auth.json``
+ (Not available for remote commands) You can also override the default
+ path of the authentication file by setting the ``REGISTRY_AUTH_FILE``
+ environment variable. ``export REGISTRY_AUTH_FILE=path``
+ type: path
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+"""
+
+EXAMPLES = r"""
+- name: Return the logged-in user for docker hub registry
+ containers.podman.podman_login_info:
+ registry: docker.io
+
+- name: Return the logged-in user for quay.io registry
+ containers.podman.podman_login_info:
+ registry: quay.io
+"""
+
+RETURN = r"""
+login:
+ description: Logged in user for a registry
+ returned: always
+ type: dict
+ sample: {
+ "logged_in": true,
+ "registry": "docker.io",
+ "username": "clelange",
+ }
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def get_login_info(module, executable, authfile, registry):
+ command = [executable, 'login', '--get-login']
+ result = dict(
+ registry=registry,
+ username='',
+ logged_in=False,
+ )
+ if authfile:
+ command.extend(['--authfile', authfile])
+ if registry:
+ command.append(registry)
+ rc, out, err = module.run_command(command)
+ if rc != 0:
+ if 'Error: not logged into' in err:
+ # The error message is e.g. 'Error: not logged into docker.io'
+ # Therefore get last word to extract registry name
+ result["registry"] = err.split()[-1]
+ err = ''
+ return result
+ module.fail_json(msg="Unable to gather info for %s: %s" % (registry, err))
+ result["username"] = out.strip()
+ result["logged_in"] = True
+ return result
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ executable=dict(type='str', default='podman'),
+ authfile=dict(type='path'),
+ registry=dict(type='str', required=True)
+ ),
+ supports_check_mode=True,
+ )
+
+ registry = module.params['registry']
+ authfile = module.params['authfile']
+ executable = module.get_bin_path(module.params['executable'], required=True)
+
+ inspect_results = get_login_info(module, executable, authfile, registry)
+
+ results = {
+ "changed": False,
+ "login": inspect_results,
+ }
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_logout.py b/ansible_collections/containers/podman/plugins/modules/podman_logout.py
new file mode 100644
index 000000000..d5816a9c0
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_logout.py
@@ -0,0 +1,153 @@
+#!/usr/bin/python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+module: podman_logout
+author:
+ - "Clemens Lange (@clelange)"
+short_description: Log out of a container registry using podman
+notes: []
+description:
+ - Log out of a container registry server using the podman logout command
+ by deleting the cached credentials stored in the `auth.json` file.
+ If the registry is not specified, the first registry under
+ `[registries.search]` from `registries.conf `will be used. The path of
+ the authentication file can be overridden by the user by setting the
+ `authfile` flag. The default path used is
+ `${XDG_RUNTIME_DIR}/containers/auth.json`.
+ All the cached credentials can be removed by setting the `all` flag.
+ Warning - podman will use credentials in `${HOME}/.docker/config.json`
+ to authenticate in case they are not found in the default `authfile`.
+ However, the logout command will only removed credentials in the
+ `authfile` specified.
+requirements:
+ - "Podman installed on host"
+options:
+ registry:
+ description:
+ - Registry server. If the registry is not specified,
+ the first registry under `[registries.search]` from
+ `registries.conf` will be used.
+ type: str
+ authfile:
+ description:
+ - Path of the authentication file. Default is
+ ``${XDG_RUNTIME_DIR}/containers/auth.json``
+ You can also override the default path of the authentication
+ file by setting the ``REGISTRY_AUTH_FILE`` environment
+ variable. ``export REGISTRY_AUTH_FILE=path``
+ type: path
+ all:
+ description:
+ - Remove the cached credentials for all registries in the auth file.
+ type: bool
+ ignore_docker_credentials:
+ description:
+ - Credentials created using other tools such as `docker login` are not
+ removed unless the corresponding `authfile` is explicitly specified.
+ Since podman also uses existing credentials in these files by default
+ (for docker e.g. `${HOME}/.docker/config.json`), module execution will
+ fail if a docker login exists for the registry specified in any
+ `authfile` is used by podman. This can be ignored by setting
+ `ignore_docker_credentials` to `true` - the credentials will be kept and
+ `changed` will be false.
+ This option cannot be used together with `all` since in this case
+ podman will not check for existing `authfiles` created by other tools.
+ type: bool
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+'''
+
+EXAMPLES = r"""
+- name: Log out of default registry
+ podman_logout:
+
+- name: Log out of quay.io
+ podman_logout:
+ registry: quay.io
+
+- name: Log out of all registries in auth file
+ podman_logout:
+ all: true
+
+- name: Log out of all registries in specified auth file
+ podman_logout:
+ authfile: $HOME/.docker/config.json
+ all: true
+"""
+# noqa: F402
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def logout(module, executable, registry, authfile, all_registries, ignore_docker_credentials):
+ command = [executable, 'logout']
+ changed = False
+ if authfile:
+ command.extend(['--authfile', authfile])
+ if registry:
+ command.append(registry)
+ if all_registries:
+ command.append("--all")
+ rc, out, err = module.run_command(command)
+ if rc != 0:
+ if 'Error: Not logged into' not in err:
+ module.fail_json(msg="Unable to gather info for %s: %s" % (registry, err))
+ else:
+ # If the command is successful, we managed to log out
+ # Mind: This also applied if --all flag is used, while in this case
+ # there is no check whether one has been logged into any registry
+ changed = True
+ if 'Existing credentials were established via' in out:
+ # The command will return successfully but not log out the user if the
+ # credentials were initially created using docker. Catch this behaviour:
+ if not ignore_docker_credentials:
+ module.fail_json(msg="Unable to log out %s: %s" % (registry or '', out))
+ else:
+ changed = False
+ return changed, out, err
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ executable=dict(type='str', default='podman'),
+ registry=dict(type='str'),
+ authfile=dict(type='path'),
+ all=dict(type='bool'),
+ ignore_docker_credentials=dict(type='bool'),
+ ),
+ supports_check_mode=True,
+ mutually_exclusive=(
+ ['registry', 'all'],
+ ['ignore_docker_credentials', 'all'],
+ ),
+ )
+
+ registry = module.params['registry']
+ authfile = module.params['authfile']
+ all_registries = module.params['all']
+ ignore_docker_credentials = module.params['ignore_docker_credentials']
+ executable = module.get_bin_path(module.params['executable'], required=True)
+
+ changed, out, err = logout(module, executable, registry, authfile,
+ all_registries, ignore_docker_credentials)
+
+ results = {
+ "changed": changed,
+ "stdout": out,
+ "stderr": err,
+ }
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_network.py b/ansible_collections/containers/podman/plugins/modules/podman_network.py
new file mode 100644
index 000000000..846524b65
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_network.py
@@ -0,0 +1,673 @@
+#!/usr/bin/python
+# Copyright (c) 2020 Red Hat
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r"""
+module: podman_network
+author:
+ - "Sagi Shnaidman (@sshnaidm)"
+version_added: '1.0.0'
+short_description: Manage podman networks
+notes: []
+description:
+ - Manage podman networks with podman network command.
+requirements:
+ - podman
+options:
+ name:
+ description:
+ - Name of the network
+ type: str
+ required: True
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+ disable_dns:
+ description:
+ - disable dns plugin (default "false")
+ type: bool
+ driver:
+ description:
+ - Driver to manage the network (default "bridge")
+ type: str
+ gateway:
+ description:
+ - IPv4 or IPv6 gateway for the subnet
+ type: str
+ internal:
+ description:
+ - Restrict external access from this network (default "false")
+ type: bool
+ ip_range:
+ description:
+ - Allocate container IP from range
+ type: str
+ ipv6:
+ description:
+ - Enable IPv6 (Dual Stack) networking. You must pass a IPv6 subnet.
+ The subnet option must be used with the ipv6 option.
+ type: bool
+ subnet:
+ description:
+ - Subnet in CIDR format
+ type: str
+ macvlan:
+ description:
+ - Create a Macvlan connection based on this device
+ type: str
+ opt:
+ description:
+ - Add network options. Currently 'vlan' and 'mtu' are supported.
+ type: dict
+ suboptions:
+ isolate:
+ description:
+ - This option isolates networks by blocking traffic between those
+ that have this option enabled.
+ type: bool
+ required: false
+ metric:
+ description:
+ - Sets the Route Metric for the default route created in every
+ container joined to this network.
+ Can only be used with the Netavark network backend.
+ type: int
+ required: false
+ mode:
+ description:
+ - This option sets the specified ip/macvlan mode on the interface.
+ type: str
+ required: false
+ mtu:
+ description:
+ - MTU size for bridge network interface.
+ type: int
+ required: false
+ parent:
+ description:
+ - The host device which should be used for the macvlan interface.
+ Defaults to the default route interface.
+ type: str
+ required: false
+ vlan:
+ description:
+ - VLAN tag for bridge which enables vlan_filtering.
+ type: int
+ required: false
+ debug:
+ description:
+ - Return additional information which can be helpful for investigations.
+ type: bool
+ default: False
+ state:
+ description:
+ - State of network, default 'present'
+ type: str
+ default: present
+ choices:
+ - present
+ - absent
+ recreate:
+ description:
+ - Recreate network even if exists.
+ type: bool
+ default: false
+"""
+
+EXAMPLES = r"""
+- name: Create a podman network
+ containers.podman.podman_network:
+ name: podman_network
+ become: true
+
+- name: Create internal podman network
+ containers.podman.podman_network:
+ name: podman_internal
+ internal: true
+ ip_range: 192.168.22.128/25
+ subnet: 192.168.22.0/24
+ gateway: 192.168.22.1
+ become: true
+"""
+
+RETURN = r"""
+network:
+ description: Facts from created or updated networks
+ returned: always
+ type: list
+ sample: [
+ {
+ "cniVersion": "0.4.0",
+ "name": "podman",
+ "plugins": [
+ {
+ "bridge": "cni-podman0",
+ "ipMasq": true,
+ "ipam": {
+ "ranges": [
+ [
+ {
+ "gateway": "10.88.0.1",
+ "subnet": "10.88.0.0/16"
+ }
+ ]
+ ],
+ "routes": [
+ {
+ "dst": "0.0.0.0/0"
+ }
+ ],
+ "type": "host-local"
+ },
+ "isGateway": true,
+ "type": "bridge"
+ },
+ {
+ "capabilities": {
+ "portMappings": true
+ },
+ "type": "portmap"
+ },
+ {
+ "backend": "iptables",
+ "type": "firewall"
+ }
+ ]
+ }
+ ]
+"""
+
+import json # noqa: F402
+try:
+ import ipaddress
+ HAS_IP_ADDRESS_MODULE = True
+except ImportError:
+ HAS_IP_ADDRESS_MODULE = False
+
+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
+
+
+class PodmanNetworkModuleParams:
+ """Creates list of arguments for podman CLI command.
+
+ Arguments:
+ action {str} -- action type from 'create', 'delete'
+ params {dict} -- dictionary of module parameters
+
+ """
+
+ def __init__(self, action, params, podman_version, module):
+ self.params = params
+ self.action = action
+ self.podman_version = podman_version
+ self.module = module
+
+ def construct_command_from_params(self):
+ """Create a podman command from given module parameters.
+
+ Returns:
+ list -- list of byte strings for Popen command
+ """
+ if self.action in ['delete']:
+ return self._simple_action()
+ if self.action in ['create']:
+ return self._create_action()
+
+ def _simple_action(self):
+ if self.action == 'delete':
+ cmd = ['rm', '-f', self.params['name']]
+ return [to_bytes(i, errors='surrogate_or_strict') for i in cmd]
+
+ def _create_action(self):
+ cmd = [self.action, self.params['name']]
+ all_param_methods = [func for func in dir(self)
+ if callable(getattr(self, func))
+ and func.startswith("addparam")]
+ params_set = (i for i in self.params if self.params[i] is not None)
+ for param in params_set:
+ func_name = "_".join(["addparam", param])
+ if func_name in all_param_methods:
+ cmd = getattr(self, func_name)(cmd)
+ return [to_bytes(i, errors='surrogate_or_strict') for i in cmd]
+
+ def check_version(self, param, minv=None, maxv=None):
+ if minv and LooseVersion(minv) > LooseVersion(
+ self.podman_version):
+ self.module.fail_json(msg="Parameter %s is supported from podman "
+ "version %s only! Current version is %s" % (
+ param, minv, self.podman_version))
+ if maxv and LooseVersion(maxv) < LooseVersion(
+ self.podman_version):
+ self.module.fail_json(msg="Parameter %s is supported till podman "
+ "version %s only! Current version is %s" % (
+ param, minv, self.podman_version))
+
+ def addparam_gateway(self, c):
+ return c + ['--gateway', self.params['gateway']]
+
+ def addparam_driver(self, c):
+ return c + ['--driver', self.params['driver']]
+
+ def addparam_subnet(self, c):
+ return c + ['--subnet', self.params['subnet']]
+
+ def addparam_ip_range(self, c):
+ return c + ['--ip-range', self.params['ip_range']]
+
+ def addparam_ipv6(self, c):
+ return c + ['--ipv6=%s' % self.params['ipv6']]
+
+ def addparam_macvlan(self, c):
+ return c + ['--macvlan', self.params['macvlan']]
+
+ def addparam_internal(self, c):
+ return c + ['--internal=%s' % self.params['internal']]
+
+ def addparam_opt(self, c):
+ for opt in self.params['opt'].items():
+ if opt[1] is not None:
+ c += ['--opt',
+ b"=".join([to_bytes(k, errors='surrogate_or_strict')
+ for k in opt])]
+ return c
+
+ def addparam_disable_dns(self, c):
+ return c + ['--disable-dns=%s' % self.params['disable_dns']]
+
+
+class PodmanNetworkDefaults:
+ def __init__(self, module, podman_version):
+ self.module = module
+ self.version = podman_version
+ self.defaults = {
+ 'driver': 'bridge',
+ 'disable_dns': False,
+ 'internal': False,
+ 'ipv6': False
+ }
+
+ def default_dict(self):
+ # make here any changes to self.defaults related to podman version
+ return self.defaults
+
+
+class PodmanNetworkDiff:
+ def __init__(self, module, info, podman_version):
+ self.module = module
+ self.version = podman_version
+ self.default_dict = None
+ self.info = lower_keys(info)
+ self.params = self.defaultize()
+ self.diff = {'before': {}, 'after': {}}
+ self.non_idempotent = {}
+
+ def defaultize(self):
+ params_with_defaults = {}
+ self.default_dict = PodmanNetworkDefaults(
+ self.module, self.version).default_dict()
+ for p in self.module.params:
+ if self.module.params[p] is None and p in self.default_dict:
+ params_with_defaults[p] = self.default_dict[p]
+ else:
+ params_with_defaults[p] = self.module.params[p]
+ return params_with_defaults
+
+ def _diff_update_and_compare(self, param_name, before, after):
+ if before != after:
+ self.diff['before'].update({param_name: before})
+ self.diff['after'].update({param_name: after})
+ return True
+ return False
+
+ def diffparam_disable_dns(self):
+ # For v3 it's impossible to find out DNS settings.
+ if LooseVersion(self.version) >= LooseVersion('4.0.0'):
+ before = not self.info.get('dns_enabled', True)
+ after = self.params['disable_dns']
+ return self._diff_update_and_compare('disable_dns', before, after)
+ before = after = self.params['disable_dns']
+ return self._diff_update_and_compare('disable_dns', before, after)
+
+ def diffparam_driver(self):
+ # Currently only bridge is supported
+ before = after = 'bridge'
+ return self._diff_update_and_compare('driver', before, after)
+
+ def diffparam_ipv6(self):
+ if LooseVersion(self.version) >= LooseVersion('4.0.0'):
+ before = self.info.get('ipv6_enabled', False)
+ after = self.params['ipv6']
+ return self._diff_update_and_compare('ipv6', before, after)
+ before = after = ''
+ return self._diff_update_and_compare('ipv6', before, after)
+
+ def diffparam_gateway(self):
+ # Disable idempotency of subnet for v4, subnets are added automatically
+ # TODO(sshnaidm): check if it's still the issue in v5
+ if LooseVersion(self.version) >= LooseVersion('4.0.0'):
+ return self._diff_update_and_compare('gateway', '', '')
+ try:
+ before = self.info['plugins'][0]['ipam']['ranges'][0][0]['gateway']
+ except (IndexError, KeyError):
+ before = ''
+ after = before
+ if self.params['gateway'] is not None:
+ after = self.params['gateway']
+ return self._diff_update_and_compare('gateway', before, after)
+
+ def diffparam_internal(self):
+ if LooseVersion(self.version) >= LooseVersion('4.0.0'):
+ before = self.info.get('internal', False)
+ after = self.params['internal']
+ return self._diff_update_and_compare('internal', before, after)
+ try:
+ before = not self.info['plugins'][0]['isgateway']
+ except (IndexError, KeyError):
+ before = False
+ after = self.params['internal']
+ return self._diff_update_and_compare('internal', before, after)
+
+ def diffparam_ip_range(self):
+ # TODO(sshnaidm): implement IP to CIDR convert and vice versa
+ before = after = ''
+ return self._diff_update_and_compare('ip_range', before, after)
+
+ def diffparam_subnet(self):
+ # Disable idempotency of subnet for v4, subnets are added automatically
+ # TODO(sshnaidm): check if it's still the issue in v5
+ if LooseVersion(self.version) >= LooseVersion('4.0.0'):
+ return self._diff_update_and_compare('subnet', '', '')
+ try:
+ before = self.info['plugins'][0]['ipam']['ranges'][0][0]['subnet']
+ except (IndexError, KeyError):
+ before = ''
+ after = before
+ if self.params['subnet'] is not None:
+ after = self.params['subnet']
+ if HAS_IP_ADDRESS_MODULE:
+ after = ipaddress.ip_network(after).compressed
+ return self._diff_update_and_compare('subnet', before, after)
+
+ def diffparam_macvlan(self):
+ before = after = ''
+ return self._diff_update_and_compare('macvlan', before, after)
+
+ def diffparam_opt(self):
+ if LooseVersion(self.version) >= LooseVersion('4.0.0'):
+ vlan_before = self.info.get('options', {}).get('vlan')
+ else:
+ try:
+ vlan_before = self.info['plugins'][0].get('vlan')
+ except (IndexError, KeyError):
+ vlan_before = None
+ vlan_after = self.params['opt'].get('vlan') if self.params['opt'] else None
+ if vlan_before or vlan_after:
+ before, after = {'vlan': str(vlan_before)}, {'vlan': str(vlan_after)}
+ else:
+ before, after = {}, {}
+ if LooseVersion(self.version) >= LooseVersion('4.0.0'):
+ mtu_before = self.info.get('options', {}).get('mtu')
+ else:
+ try:
+ mtu_before = self.info['plugins'][0].get('mtu')
+ except (IndexError, KeyError):
+ mtu_before = None
+ mtu_after = self.params['opt'].get('mtu') if self.params['opt'] else None
+ if mtu_before or mtu_after:
+ before.update({'mtu': str(mtu_before)})
+ after.update({'mtu': str(mtu_after)})
+ return self._diff_update_and_compare('opt', before, after)
+
+ def is_different(self):
+ diff_func_list = [func for func in dir(self)
+ if callable(getattr(self, func)) and func.startswith(
+ "diffparam")]
+ fail_fast = not bool(self.module._diff)
+ different = False
+ for func_name in diff_func_list:
+ dff_func = getattr(self, func_name)
+ if dff_func():
+ if fail_fast:
+ return True
+ different = True
+ # Check non idempotent parameters
+ for p in self.non_idempotent:
+ if self.module.params[p] is not None and self.module.params[p] not in [{}, [], '']:
+ different = True
+ return different
+
+
+class PodmanNetwork:
+ """Perform network tasks.
+
+ Manages podman network, inspects it and checks its current state
+ """
+
+ def __init__(self, module, name):
+ """Initialize PodmanNetwork class.
+
+ Arguments:
+ module {obj} -- ansible module object
+ name {str} -- name of network
+ """
+
+ super(PodmanNetwork, self).__init__()
+ self.module = module
+ self.name = name
+ self.stdout, self.stderr = '', ''
+ self.info = self.get_info()
+ self.version = self._get_podman_version()
+ self.diff = {}
+ self.actions = []
+
+ @property
+ def exists(self):
+ """Check if network exists."""
+ return bool(self.info != {})
+
+ @property
+ def different(self):
+ """Check if network is different."""
+ diffcheck = PodmanNetworkDiff(
+ self.module,
+ self.info,
+ self.version)
+ is_different = diffcheck.is_different()
+ diffs = diffcheck.diff
+ if self.module._diff and is_different and diffs['before'] and diffs['after']:
+ self.diff['before'] = "\n".join(
+ ["%s - %s" % (k, v) for k, v in sorted(
+ diffs['before'].items())]) + "\n"
+ self.diff['after'] = "\n".join(
+ ["%s - %s" % (k, v) for k, v in sorted(
+ diffs['after'].items())]) + "\n"
+ return is_different
+
+ def get_info(self):
+ """Inspect network and gather info about it."""
+ # pylint: disable=unused-variable
+ rc, out, err = self.module.run_command(
+ [self.module.params['executable'], b'network', b'inspect', self.name])
+ return json.loads(out)[0] if rc == 0 else {}
+
+ def _get_podman_version(self):
+ # pylint: disable=unused-variable
+ rc, out, err = self.module.run_command(
+ [self.module.params['executable'], b'--version'])
+ if rc != 0 or not out or "version" not in out:
+ self.module.fail_json(msg="%s run failed!" %
+ self.module.params['executable'])
+ return out.split("version")[1].strip()
+
+ def _perform_action(self, action):
+ """Perform action with network.
+
+ Arguments:
+ action {str} -- action to perform - create, stop, delete
+ """
+ b_command = PodmanNetworkModuleParams(action,
+ self.module.params,
+ self.version,
+ self.module,
+ ).construct_command_from_params()
+ full_cmd = " ".join([self.module.params['executable'], 'network']
+ + [to_native(i) for i in b_command])
+ self.module.log("PODMAN-NETWORK-DEBUG: %s" % full_cmd)
+ self.actions.append(full_cmd)
+ if not self.module.check_mode:
+ rc, out, err = self.module.run_command(
+ [self.module.params['executable'], b'network'] + b_command,
+ expand_user_and_vars=False)
+ self.stdout = out
+ self.stderr = err
+ if rc != 0:
+ self.module.fail_json(
+ msg="Can't %s network %s" % (action, self.name),
+ stdout=out, stderr=err)
+
+ def delete(self):
+ """Delete the network."""
+ self._perform_action('delete')
+
+ def create(self):
+ """Create the network."""
+ self._perform_action('create')
+
+ def recreate(self):
+ """Recreate the network."""
+ self.delete()
+ self.create()
+
+
+class PodmanNetworkManager:
+ """Module manager class.
+
+ Defines according to parameters what actions should be applied to network
+ """
+
+ def __init__(self, module):
+ """Initialize PodmanManager class.
+
+ Arguments:
+ module {obj} -- ansible module object
+ """
+
+ super(PodmanNetworkManager, self).__init__()
+
+ self.module = module
+ self.results = {
+ 'changed': False,
+ 'actions': [],
+ 'network': {},
+ }
+ self.name = self.module.params['name']
+ self.executable = \
+ self.module.get_bin_path(self.module.params['executable'],
+ required=True)
+ self.state = self.module.params['state']
+ self.recreate = self.module.params['recreate']
+ self.network = PodmanNetwork(self.module, self.name)
+
+ def update_network_result(self, changed=True):
+ """Inspect the current network, update results with last info, exit.
+
+ Keyword Arguments:
+ changed {bool} -- whether any action was performed
+ (default: {True})
+ """
+ facts = self.network.get_info() if changed else self.network.info
+ out, err = self.network.stdout, self.network.stderr
+ self.results.update({'changed': changed, 'network': facts,
+ 'podman_actions': self.network.actions},
+ stdout=out, stderr=err)
+ if self.network.diff:
+ self.results.update({'diff': self.network.diff})
+ if self.module.params['debug']:
+ self.results.update({'podman_version': self.network.version})
+ self.module.exit_json(**self.results)
+
+ def execute(self):
+ """Execute the desired action according to map of actions & states."""
+ states_map = {
+ 'present': self.make_present,
+ 'absent': self.make_absent,
+ }
+ process_action = states_map[self.state]
+ process_action()
+ self.module.fail_json(msg="Unexpected logic error happened, "
+ "please contact maintainers ASAP!")
+
+ def make_present(self):
+ """Run actions if desired state is 'started'."""
+ if not self.network.exists:
+ self.network.create()
+ self.results['actions'].append('created %s' % self.network.name)
+ self.update_network_result()
+ elif self.recreate or self.network.different:
+ self.network.recreate()
+ self.results['actions'].append('recreated %s' %
+ self.network.name)
+ self.update_network_result()
+ else:
+ self.update_network_result(changed=False)
+
+ def make_absent(self):
+ """Run actions if desired state is 'absent'."""
+ if not self.network.exists:
+ self.results.update({'changed': False})
+ elif self.network.exists:
+ self.network.delete()
+ self.results['actions'].append('deleted %s' % self.network.name)
+ self.results.update({'changed': True})
+ self.results.update({'network': {},
+ 'podman_actions': self.network.actions})
+ self.module.exit_json(**self.results)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(type='str', default="present",
+ choices=['present', 'absent']),
+ name=dict(type='str', required=True),
+ disable_dns=dict(type='bool', required=False),
+ driver=dict(type='str', required=False),
+ gateway=dict(type='str', required=False),
+ internal=dict(type='bool', required=False),
+ ip_range=dict(type='str', required=False),
+ ipv6=dict(type='bool', required=False),
+ subnet=dict(type='str', required=False),
+ macvlan=dict(type='str', required=False),
+ opt=dict(type='dict', required=False,
+ options=dict(
+ isolate=dict(type='bool', required=False),
+ mtu=dict(type='int', required=False),
+ metric=dict(type='int', required=False),
+ mode=dict(type='str', required=False),
+ parent=dict(type='str', required=False),
+ vlan=dict(type='int', required=False),
+ )),
+ executable=dict(type='str', required=False, default='podman'),
+ debug=dict(type='bool', default=False),
+ recreate=dict(type='bool', default=False),
+ ),
+ required_by=dict( # for IP range and GW to set 'subnet' is required
+ ip_range=('subnet'),
+ gateway=('subnet'),
+ ))
+
+ PodmanNetworkManager(module).execute()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_network_info.py b/ansible_collections/containers/podman/plugins/modules/podman_network_info.py
new file mode 100644
index 000000000..a9e18cd4d
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_network_info.py
@@ -0,0 +1,138 @@
+#!/usr/bin/python
+# Copyright (c) 2020 Red Hat
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r"""
+module: podman_network_info
+author:
+ - "Sagi Shnaidman (@sshnaidm)"
+version_added: '1.0.0'
+short_description: Gather info about podman networks
+notes: []
+description:
+ - Gather info about podman networks with podman inspect command.
+requirements:
+ - "Podman installed on host"
+options:
+ name:
+ description:
+ - Name of the network
+ type: str
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+"""
+
+EXAMPLES = r"""
+- name: Gather info about all present networks
+ containers.podman.podman_network_info:
+
+- name: Gather info about specific network
+ containers.podman.podman_network_info:
+ name: podman
+"""
+
+RETURN = r"""
+networks:
+ description: Facts from all or specified networks
+ returned: always
+ type: list
+ sample: [
+ {
+ "cniVersion": "0.4.0",
+ "name": "podman",
+ "plugins": [
+ {
+ "bridge": "cni-podman0",
+ "ipMasq": true,
+ "ipam": {
+ "ranges": [
+ [
+ {
+ "gateway": "10.88.0.1",
+ "subnet": "10.88.0.0/16"
+ }
+ ]
+ ],
+ "routes": [
+ {
+ "dst": "0.0.0.0/0"
+ }
+ ],
+ "type": "host-local"
+ },
+ "isGateway": true,
+ "type": "bridge"
+ },
+ {
+ "capabilities": {
+ "portMappings": true
+ },
+ "type": "portmap"
+ },
+ {
+ "backend": "iptables",
+ "type": "firewall"
+ }
+ ]
+ }
+ ]
+"""
+
+import json
+from ansible.module_utils.basic import AnsibleModule
+
+
+def get_network_info(module, executable, name):
+ command = [executable, 'network', 'inspect']
+ if not name:
+ all_names = [executable, 'network', 'ls', '-q']
+ rc, out, err = module.run_command(all_names)
+ if rc != 0:
+ module.fail_json(msg="Unable to get list of networks: %s" % err)
+ name = out.split()
+ if not name:
+ return [], out, err
+ command += name
+ else:
+ command.append(name)
+ rc, out, err = module.run_command(command)
+ if rc != 0 or 'unable to find network configuration' in err:
+ module.fail_json(msg="Unable to gather info for %s: %s" % (name, err))
+ if not out or json.loads(out) is None:
+ return [], out, err
+ return json.loads(out), out, err
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ executable=dict(type='str', default='podman'),
+ name=dict(type='str')
+ ),
+ supports_check_mode=True,
+ )
+
+ name = module.params['name']
+ executable = module.get_bin_path(module.params['executable'], required=True)
+
+ inspect_results, out, err = get_network_info(module, executable, name)
+
+ results = {
+ "changed": False,
+ "networks": inspect_results,
+ "stderr": err
+ }
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_play.py b/ansible_collections/containers/podman/plugins/modules/podman_play.py
new file mode 100644
index 000000000..04a30441b
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_play.py
@@ -0,0 +1,311 @@
+#!/usr/bin/python
+# Copyright (c) 2020 Red Hat
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+module: podman_play
+author:
+ - "Sagi Shnaidman (@sshnaidm)"
+short_description: Play kubernetes YAML file using podman
+notes: []
+description:
+ - The module reads in a structured file of Kubernetes YAML.
+ It will then recreate the pod and containers described in the YAML.
+requirements:
+ - "Podman installed on host"
+options:
+ executable:
+ description:
+ - Name of executable to run, by default 'podman'
+ type: str
+ default: podman
+ kube_file:
+ description:
+ - Path to file with YAML configuration for a Pod.
+ type: path
+ required: True
+ authfile:
+ description:
+ - Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json,
+ which is set using podman login. If the authorization state is not found there,
+ $HOME/.docker/config.json is checked, which is set using docker login.
+ Note - You can also override the default path of the authentication file
+ by setting the REGISTRY_AUTH_FILE environment variable. export REGISTRY_AUTH_FILE=path
+ type: path
+ cert_dir:
+ description:
+ - Use certificates at path (*.crt, *.cert, *.key) to connect to the registry.
+ Default certificates directory is /etc/containers/certs.d.
+ (This option is not available with the remote Podman client)
+ type: path
+ configmap:
+ description:
+ - Use Kubernetes configmap YAML at path to provide a source for environment
+ variable values within the containers of the pod.
+ Note - The configmap option can be used multiple times to pass multiple
+ Kubernetes configmap YAMLs
+ type: list
+ elements: path
+ seccomp_profile_root:
+ description:
+ - Directory path for seccomp profiles (default is "/var/lib/kubelet/seccomp").
+ This option is not available with the remote Podman client
+ type: path
+ username:
+ description:
+ - The username and password to use to authenticate with the registry if required.
+ type: str
+ password:
+ description:
+ - The username and password to use to authenticate with the registry if required.
+ type: str
+ log_driver:
+ description:
+ - Set logging driver for all created containers.
+ type: str
+ log_level:
+ description:
+ - Set logging level for podman calls. Log messages above specified level
+ ("debug"|"info"|"warn"|"error"|"fatal"|"panic") (default "error")
+ type: str
+ choices:
+ - debug
+ - info
+ - warn
+ - error
+ - fatal
+ - panic
+ network:
+ description:
+ - List of the names of CNI networks the pod should join.
+ type: list
+ elements: str
+ state:
+ description:
+ - Start the pod after creating it, or to leave it created only.
+ type: str
+ choices:
+ - created
+ - started
+ - absent
+ required: True
+ tls_verify:
+ description:
+ - Require HTTPS and verify certificates when contacting registries (default is true).
+ If explicitly set to true, then TLS verification will be used. If set to false,
+ then TLS verification will not be used. If not specified, TLS verification will be
+ used unless the target registry is listed as an insecure registry in registries.conf.
+ type: bool
+ debug:
+ description:
+ - Enable debug for the module.
+ type: bool
+ recreate:
+ description:
+ - If pod already exists, delete it and run the new one.
+ type: bool
+ quiet:
+ description:
+ - Hide image pulls logs from output.
+ type: bool
+ userns:
+ description:
+ - Set the user namespace mode for all the containers in a pod.
+ It defaults to the PODMAN_USERNS environment variable.
+ An empty value ("") means user namespaces are disabled.
+ required: false
+ type: str
+'''
+
+EXAMPLES = '''
+- name: Play kube file
+ containers.podman.podman_play:
+ kube_file: ~/kube.yaml
+ state: started
+
+'''
+import re # noqa: F402
+try:
+ import yaml
+ HAS_YAML = True
+except ImportError:
+ HAS_YAML = False
+
+from ansible.module_utils.basic import AnsibleModule # noqa: F402
+
+
+class PodmanKubeManagement:
+
+ def __init__(self, module, executable):
+ self.module = module
+ self.actions = []
+ self.executable = executable
+ self.command = [self.executable, 'play', 'kube']
+ creds = []
+ # pod_name = extract_pod_name(module.params['kube_file'])
+ if self.module.params['username']:
+ creds += [self.module.params['username']]
+ if self.module.params['password']:
+ creds += [self.module.params['password']]
+ creds = ":".join(creds)
+ self.command.extend(['--creds=%s' % creds])
+ if self.module.params['network']:
+ networks = ",".join(self.module.params['network'])
+ self.command.extend(['--network=%s' % networks])
+ if self.module.params['configmap']:
+ configmaps = ",".join(self.module.params['configmap'])
+ self.command.extend(['--configmap=%s' % configmaps])
+ start = self.module.params['state'] == 'started'
+ self.command.extend(['--start=%s' % str(start).lower()])
+ for arg, param in {
+ '--authfile': 'authfile',
+ '--cert-dir': 'cert_dir',
+ '--log-driver': 'log_driver',
+ '--seccomp-profile-root': 'seccomp_profile_root',
+ '--tls-verify': 'tls_verify',
+ '--log-level': 'log_level',
+ '--userns': 'userns',
+ '--quiet': 'quiet',
+ }.items():
+ if self.module.params[param] is not None:
+ self.command += ["%s=%s" % (arg, self.module.params[param])]
+ self.command += [self.module.params['kube_file']]
+
+ def _command_run(self, cmd):
+ rc, out, err = self.module.run_command(cmd)
+ self.actions.append(" ".join(cmd))
+ if self.module.params['debug']:
+ self.module.log('PODMAN-PLAY-KUBE command: %s' % " ".join(cmd))
+ self.module.log('PODMAN-PLAY-KUBE stdout: %s' % out)
+ self.module.log('PODMAN-PLAY-KUBE stderr: %s' % err)
+ self.module.log('PODMAN-PLAY-KUBE rc: %s' % rc)
+ return rc, 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)
+ else:
+ with open(self.module.params['kube_file']) as text:
+ # the following formats are matched for a kube name:
+ # should match name field within metadata (2 or 4 spaces in front of name)
+ # the name can be written without quotes, in single or double quotes
+ # the name can contain -_
+ re_pod_name = re.compile(r'^\s{2,4}name: ["|\']?(?P<pod_name>[\w|\-|\_]+)["|\']?', re.MULTILINE)
+ re_pod = re_pod_name.search(text.read())
+ if re_pod:
+ pod_name = re_pod.group(1)
+ if not pod_name:
+ self.module.fail_json("Deployment doesn't have a name!")
+ # Find all pods
+ all_pods = ''
+ # In case of one pod or replicasets
+ for name in ("name=%s$", "name=%s-pod-*"):
+ cmd = [self.executable,
+ "pod", "ps", "-q", "--filter", name % pod_name]
+ rc, out, err = self._command_run(cmd)
+ all_pods += out
+ ids = list(set([i for i in all_pods.splitlines() if i]))
+ return ids
+
+ def remove_associated_pods(self, pods):
+ changed = False
+ out_all, err_all = '', ''
+ # Delete all pods
+ for pod_id in pods:
+ rc, out, err = self._command_run(
+ [self.executable, "pod", "rm", "-f", pod_id])
+ if rc != 0:
+ self.module.fail_json("Can NOT delete Pod %s" % pod_id)
+ else:
+ changed = True
+ out_all += out
+ err_all += err
+ return changed, out_all, err_all
+
+ def pod_recreate(self):
+ pods = self.discover_pods()
+ self.remove_associated_pods(pods)
+ # Create a pod
+ rc, out, err = self._command_run(self.command)
+ if rc != 0:
+ self.module.fail_json("Can NOT create Pod! Error: %s" % err)
+ return out, err
+
+ def play(self):
+ rc, out, err = self._command_run(self.command)
+ if rc != 0 and 'pod already exists' in err:
+ if self.module.params['recreate']:
+ out, err = self.pod_recreate()
+ changed = True
+ else:
+ changed = False
+ err = "\n".join([
+ i for i in err.splitlines() if 'pod already exists' not in i])
+ elif rc != 0:
+ self.module.fail_json(msg="Output: %s\nError=%s" % (out, err))
+ else:
+ changed = True
+ return changed, out, err
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ executable=dict(type='str', default='podman'),
+ kube_file=dict(type='path', required=True),
+ authfile=dict(type='path'),
+ cert_dir=dict(type='path'),
+ configmap=dict(type='list', elements='path'),
+ seccomp_profile_root=dict(type='path'),
+ username=dict(type='str'),
+ password=dict(type='str', no_log=True),
+ log_driver=dict(type='str'),
+ network=dict(type='list', elements='str'),
+ state=dict(
+ type='str',
+ choices=['started', 'created', 'absent'],
+ required=True),
+ tls_verify=dict(type='bool'),
+ debug=dict(type='bool'),
+ quiet=dict(type='bool'),
+ recreate=dict(type='bool'),
+ userns=dict(type='str'),
+ log_level=dict(
+ type='str',
+ choices=["debug", "info", "warn", "error", "fatal", "panic"]),
+ ),
+ supports_check_mode=True,
+ )
+
+ 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)
+ else:
+ changed, out, err = manage.play()
+ results = {
+ "changed": changed,
+ "stdout": out,
+ "stderr": err,
+ "actions": manage.actions
+ }
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_pod.py b/ansible_collections/containers/podman/plugins/modules/podman_pod.py
new file mode 100644
index 000000000..ab475de99
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_pod.py
@@ -0,0 +1,415 @@
+#!/usr/bin/python
+# Copyright (c) 2020 Red Hat
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# flake8: noqa: E501
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: podman_pod
+short_description: Manage Podman pods
+author:
+ - "Sagi Shnaidman (@sshnaidm)"
+version_added: '1.0.0'
+description:
+ - Manage podman pods.
+options:
+ state:
+ description:
+ - This variable is set for state
+ type: str
+ default: created
+ choices:
+ - created
+ - killed
+ - restarted
+ - absent
+ - started
+ - stopped
+ - paused
+ - unpaused
+ recreate:
+ description:
+ - Use with present and started states to force the re-creation of an
+ existing pod.
+ type: bool
+ default: False
+ add_host:
+ description:
+ - Add a host to the /etc/hosts file shared between all containers in the pod.
+ type: list
+ elements: str
+ required: false
+ cgroup_parent:
+ description:
+ - Path to cgroups under which the cgroup for the pod will be created. If the path
+ is not absolute, he path is considered to be relative to the cgroups path of the
+ init process. Cgroups will be created if they do not already exist.
+ type: str
+ required: false
+ cpus:
+ description:
+ - Set the total number of CPUs delegated to the pod.
+ Default is 0.000 which indicates that there is no limit on computation power.
+ required: false
+ type: str
+ cpuset_cpus:
+ description:
+ - Limit the CPUs to support execution. First CPU is numbered 0.
+ Unlike `cpus` this is of type string and parsed as a list of numbers. Format is 0-3,0,1
+ required: false
+ type: str
+ device:
+ description:
+ - Add a host device to the pod. Optional permissions parameter can be used to specify
+ device permissions. It is a combination of r for read, w for write, and m for mknod(2)
+ elements: str
+ required: false
+ type: list
+ device_read_bps:
+ description:
+ - Limit read rate (bytes per second) from a device (e.g. device-read-bps=/dev/sda:1mb)
+ elements: str
+ required: false
+ type: list
+ dns:
+ description:
+ - Set custom DNS servers in the /etc/resolv.conf file that will be shared between
+ all containers in the pod. A special option, "none" is allowed which disables
+ creation of /etc/resolv.conf for the pod.
+ type: list
+ elements: str
+ required: false
+ dns_opt:
+ description:
+ - Set custom DNS options in the /etc/resolv.conf file that will be shared between
+ all containers in the pod.
+ type: list
+ elements: str
+ required: false
+ dns_search:
+ description:
+ - Set custom DNS search domains in the /etc/resolv.conf file that will be shared
+ between all containers in the pod.
+ type: list
+ elements: str
+ required: false
+ generate_systemd:
+ description:
+ - Generate systemd unit file for container.
+ type: dict
+ default: {}
+ suboptions:
+ path:
+ description:
+ - Specify a path to the directory where unit files will be generated.
+ Required for this option. If it doesn't exist, the directory will be created.
+ type: str
+ required: false
+ restart_policy:
+ description:
+ - Specify a restart policy for the service. The restart-policy must be one of
+ "no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", or "always".
+ The default policy is "on-failure".
+ type: str
+ required: false
+ choices:
+ - 'no'
+ - 'on-success'
+ - 'on-failure'
+ - 'on-abnormal'
+ - 'on-watchdog'
+ - 'on-abort'
+ - 'always'
+ time:
+ description:
+ - Override the default stop timeout for the container with the given value.
+ type: int
+ required: false
+ no_header:
+ description:
+ - Do not generate the header including meta data such as the Podman version and the timestamp.
+ From podman version 3.1.0.
+ type: bool
+ default: false
+ names:
+ description:
+ - Use names of the containers for the start, stop, and description in the unit file.
+ Default is true.
+ type: bool
+ default: true
+ container_prefix:
+ description:
+ - Set the systemd unit name prefix for containers. The default is "container".
+ type: str
+ required: false
+ pod_prefix:
+ description:
+ - Set the systemd unit name prefix for pods. The default is "pod".
+ type: str
+ required: false
+ separator:
+ description:
+ - Set the systemd unit name separator between the name/id of a
+ container/pod and the prefix. The default is "-" (dash).
+ type: str
+ required: false
+ new:
+ description:
+ - Create containers and pods when the unit is started instead of
+ expecting them to exist. The default is "false".
+ Refer to podman-generate-systemd(1) for more information.
+ type: bool
+ default: false
+ after:
+ type: list
+ elements: str
+ required: false
+ description:
+ - Add the systemd unit after (After=) option, that ordering dependencies between the list of dependencies and this service.
+ wants:
+ type: list
+ elements: str
+ required: false
+ description:
+ - Add the systemd unit wants (Wants=) option, that this service is (weak) dependent on.
+ requires:
+ type: list
+ elements: str
+ required: false
+ description:
+ - Set the systemd unit requires (Requires=) option. Similar to wants, but declares a stronger requirement dependency.
+ gidmap:
+ description:
+ - GID map for the user namespace. Using this flag will run the container with
+ user namespace enabled. It conflicts with the `userns` and `subgidname` flags.
+ elements: str
+ required: false
+ type: list
+ hostname:
+ description:
+ - Set a hostname to the pod
+ type: str
+ required: false
+ infra:
+ description:
+ - Create an infra container and associate it with the pod. An infra container is
+ a lightweight container used to coordinate the shared kernel namespace of a pod.
+ Default is true.
+ type: bool
+ required: false
+ infra_conmon_pidfile:
+ description:
+ - Write the pid of the infra container's conmon process to a file. As conmon runs
+ in a separate process than Podman, this is necessary when using systemd to manage
+ Podman containers and pods.
+ type: str
+ required: false
+ infra_command:
+ description:
+ - The command that will be run to start the infra container. Default is "/pause".
+ type: str
+ required: false
+ infra_image:
+ description:
+ - The image that will be created for the infra container. Default is "k8s.gcr.io/pause:3.1".
+ type: str
+ required: false
+ infra_name:
+ description:
+ - The name that will be used for the pod's infra container.
+ type: str
+ required: false
+ ip:
+ description:
+ - Set a static IP for the pod's shared network.
+ type: str
+ required: false
+ label:
+ description:
+ - Add metadata to a pod, pass dictionary of label keys and values.
+ type: dict
+ required: false
+ label_file:
+ description:
+ - Read in a line delimited file of labels.
+ type: str
+ required: false
+ mac_address:
+ description:
+ - Set a static MAC address for the pod's shared network.
+ type: str
+ required: false
+ name:
+ description:
+ - Assign a name to the pod.
+ type: str
+ required: true
+ network:
+ description:
+ - Set network mode for the pod. Supported values are bridge (the default), host
+ (do not create a network namespace, all containers in the pod will use the host's
+ network), or a list of names of CNI networks to join.
+ type: list
+ elements: str
+ required: false
+ network_alias:
+ description:
+ - Add a network-scoped alias for the pod, setting the alias for all networks that the pod joins.
+ To set a name only for a specific network, use the alias option as described under the -`network` option.
+ Network aliases work only with the bridge networking mode.
+ This option can be specified multiple times.
+ elements: str
+ required: false
+ type: list
+ aliases:
+ - network_aliases
+ no_hosts:
+ description:
+ - Disable creation of /etc/hosts for the pod.
+ type: bool
+ required: false
+ pid:
+ description:
+ - Set the PID mode for the pod. The default is to create a private PID namespace
+ for the pod. Requires the PID namespace to be shared via `share` option.
+ required: false
+ type: str
+ pod_id_file:
+ description:
+ - Write the pod ID to the file.
+ type: str
+ required: false
+ publish:
+ description:
+ - Publish a port or range of ports from the pod to the host.
+ type: list
+ elements: str
+ required: false
+ aliases:
+ - ports
+ share:
+ description:
+ - A comma delimited list of kernel namespaces to share. If none or "" is specified,
+ no namespaces will be shared. The namespaces to choose from are ipc, net, pid,
+ user, uts.
+ type: str
+ required: false
+ subgidname:
+ description:
+ - Name for GID map from the /etc/subgid file. Using this flag will run the container
+ with user namespace enabled. This flag conflicts with `userns` and `gidmap`.
+ required: false
+ type: str
+ subuidname:
+ description:
+ - Name for UID map from the /etc/subuid file.
+ Using this flag will run the container with user namespace enabled.
+ This flag conflicts with `userns` and `uidmap`.
+ required: false
+ type: str
+ uidmap:
+ description:
+ - Run the container in a new user namespace using the supplied mapping.
+ This option conflicts with the `userns` and `subuidname` options.
+ This option provides a way to map host UIDs to container UIDs.
+ It can be passed several times to map different ranges.
+ elements: str
+ required: false
+ type: list
+ userns:
+ description:
+ - Set the user namespace mode for all the containers in a pod.
+ It defaults to the PODMAN_USERNS environment variable.
+ An empty value ("") means user namespaces are disabled.
+ required: false
+ type: str
+ volume:
+ description:
+ - Create a bind mount.
+ aliases:
+ - volumes
+ elements: str
+ required: false
+ type: list
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+ debug:
+ description:
+ - Return additional information which can be helpful for investigations.
+ type: bool
+ default: False
+
+requirements:
+ - "podman"
+
+'''
+
+RETURN = '''
+pod:
+ description: Pod inspection results for the given pod
+ built.
+ returned: always
+ type: dict
+ sample:
+ Config:
+ cgroupParent: /libpod_parent
+ created: '2020-06-14T15:16:12.230818767+03:00'
+ hostname: newpod
+ id: a5a5c6cdf8c72272fc5c33f787e8d7501e2fa0c1e92b2b602860defdafeeec58
+ infraConfig:
+ infraPortBindings: null
+ makeInfraContainer: true
+ labels: {}
+ lockID: 515
+ name: newpod
+ sharesCgroup: true
+ sharesIpc: true
+ sharesNet: true
+ sharesUts: true
+ Containers:
+ - id: dc70a947c7ae15198ec38b3c817587584085dee3919cbeb9969e3ab77ba10fd2
+ state: configured
+ State:
+ cgroupPath: /libpod_parent/a5a5c6cdf8c72272fc5c33f787e8d7501e2fa0c1e92b2b602860defdafeeec58
+ infraContainerID: dc70a947c7ae15198ec38b3c817587584085dee3919cbeb9969e3ab77ba10fd2
+ status: Created
+
+'''
+
+EXAMPLES = '''
+# What modules does for example
+- podman_pod:
+ name: pod1
+ state: started
+ ports:
+ - "4444:5555"
+
+# Connect random port from localhost to port 80 on pod2
+- name: Connect random port from localhost to port 80 on pod2
+ containers.podman.podman_pod:
+ name: pod2
+ state: started
+ publish: "127.0.0.1::80"
+'''
+from ansible.module_utils.basic import AnsibleModule # noqa: F402
+from ..module_utils.podman.podman_pod_lib import PodmanPodManager # noqa: F402
+from ..module_utils.podman.podman_pod_lib import ARGUMENTS_SPEC_POD # noqa: F402
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=ARGUMENTS_SPEC_POD
+ )
+ results = PodmanPodManager(module, module.params).execute()
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_pod_info.py b/ansible_collections/containers/podman/plugins/modules/podman_pod_info.py
new file mode 100644
index 000000000..8b2a4bf06
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_pod_info.py
@@ -0,0 +1,145 @@
+#!/usr/bin/python
+# Copyright (c) 2020 Red Hat
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r"""
+module: podman_pod_info
+author:
+ - "Sagi Shnaidman (@sshnaidm)"
+version_added: '1.0.0'
+short_description: Gather info about podman pods
+notes: []
+description:
+ - Gather info about podman pods with podman inspect command.
+requirements:
+ - "Podman installed on host"
+options:
+ name:
+ description:
+ - Name of the pod
+ type: str
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+"""
+
+EXAMPLES = r"""
+- name: Gather info about all present pods
+ containers.podman.podman_pod_info:
+
+- name: Gather info about specific pods
+ containers.podman.podman_pod_info:
+ name: special_pod
+"""
+
+RETURN = r"""
+pods:
+ description: Facts from all or specified pods
+ returned: always
+ type: list
+ sample: [
+ {
+ "Config": {
+ "id": "d9cb6dbb0....",
+ "name": "pod1",
+ "hostname": "pod1host",
+ "labels": {
+ },
+ "cgroupParent": "/libpod_parent",
+ "sharesCgroup": true,
+ "sharesIpc": true,
+ "sharesNet": true,
+ "sharesUts": true,
+ "infraConfig": {
+ "makeInfraContainer": true,
+ "infraPortBindings": [
+ {
+ "hostPort": 7777,
+ "containerPort": 7111,
+ "protocol": "tcp",
+ "hostIP": ""
+ }
+ ]
+ },
+ "created": "2020-07-13T20:29:12.572282186+03:00",
+ "lockID": 682
+ },
+ "State": {
+ "cgroupPath": "/libpod_parent/d9cb6dbb0....",
+ "infraContainerID": "ad46737bf....",
+ "status": "Created"
+ },
+ "Containers": [
+ {
+ "id": "ad46737bf....",
+ "state": "configured"
+ }
+ ]
+ }
+ ]
+"""
+
+import json
+from ansible.module_utils.basic import AnsibleModule
+
+
+def get_pod_info(module, executable, name):
+ command = [executable, 'pod', 'inspect']
+ pods = [name]
+ result = []
+ errs = []
+ rcs = []
+ if not name:
+ all_names = [executable, 'pod', 'ls', '-q']
+ rc, out, err = module.run_command(all_names)
+ if rc != 0:
+ module.fail_json(msg="Unable to get list of pods: %s" % err)
+ name = out.split()
+ if not name:
+ return [], [err], [rc]
+ pods = name
+ for pod in pods:
+ 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):
+ continue
+ result.append(json.loads(out))
+ return result, errs, rcs
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ executable=dict(type='str', default='podman'),
+ name=dict(type='str')
+ ),
+ supports_check_mode=True,
+ )
+
+ name = module.params['name']
+ executable = module.get_bin_path(module.params['executable'], required=True)
+
+ inspect_results, errs, rcs = get_pod_info(module, executable, name)
+
+ if len(rcs) > 1 and 0 not in rcs:
+ module.fail_json(msg="Failed to inspect pods", stderr="\n".join(errs))
+
+ results = {
+ "changed": False,
+ "pods": inspect_results,
+ "stderr": "\n".join(errs),
+ }
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_prune.py b/ansible_collections/containers/podman/plugins/modules/podman_prune.py
new file mode 100644
index 000000000..ee4c68a93
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_prune.py
@@ -0,0 +1,252 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+# Copyright (c) 2023, Roberto Alfieri <ralfieri@redhat.com>
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+module: podman_prune
+author:
+ - 'Roberto Alfieri (@rebtoor)'
+version_added: '1.10.0'
+short_description: Allows to prune various podman objects
+notes: []
+description:
+ - Allows to run C(podman container prune), C(podman image prune), C(podman network prune),
+ C(podman volume prune) and C(podman system prune)
+requirements:
+ - 'Podman installed on host'
+options:
+ executable:
+ description:
+ - Podman binary.
+ type: str
+ default: podman
+ container:
+ description:
+ - Whether to prune containers.
+ type: bool
+ default: false
+ container_filters:
+ description:
+ - A dictionary of filter values used for selecting containers to delete.
+ - 'For example, C(until: 24h).'
+ - See L(the podman documentation,
+ https://docs.podman.io/en/latest/markdown/podman-container-prune.1.html#filter-filters)
+ for more information on possible filters.
+ type: dict
+ image:
+ description:
+ - Whether to prune images.
+ type: bool
+ default: false
+ image_filters:
+ description:
+ - A dictionary of filter values used for selecting images to delete.
+ - 'You can also use C(dangling_only: false) to delete dangling and non-dangling images or C(external: true)
+ to delete images even when they are used by external containers.'
+ - See L(the podman documentation,
+ https://docs.podman.io/en/latest/markdown/podman-image-prune.1.html#filter-filters)
+ for more information on possible filters.
+ type: dict
+ network:
+ description:
+ - Whether to prune networks.
+ type: bool
+ default: false
+ network_filters:
+ description:
+ - A dictionary of filter values used for selecting networks to delete.
+ - See L(the podman documentation,
+ https://docs.podman.io/en/latest/markdown/podman-network-prune.1.html#filter)
+ for more information on possible filters.
+ type: dict
+ system:
+ description:
+ - Wheter to prune unused pods, containers, image, networks and volume data
+ type: bool
+ default: false
+ system_all:
+ description:
+ - Wheter to prune all unused images, not only dangling images.
+ type: bool
+ default: false
+ system_volumes:
+ description:
+ - Wheter to prune volumes currently unused by any container.
+ type: bool
+ default: false
+ volume:
+ description:
+ - Whether to prune volumes.
+ type: bool
+ default: false
+ volume_filters:
+ description:
+ - A dictionary of filter values used for selecting volumes to delete.
+ - See L(the podman documentation,
+ https://docs.podman.io/en/latest/markdown/podman-volume-prune.1.html#filter)
+ for more information on possible filters.
+ type: dict
+'''
+
+EXAMPLES = r'''
+- name: Prune containers older than 24h
+ containers.podman.podman_prune:
+ containers: true
+ containers_filters:
+ # only consider containers created more than 24 hours ago
+ until: 24h
+
+- name: Prune everything
+ containers.podman.podman_prune:
+ system: true
+
+- name: Prune everything (including non-dangling images)
+ containers.podman.podman_prune:
+ system: true
+ system_all: true
+ system_volumes: true
+'''
+
+RETURN = r'''
+# containers
+containers:
+ description:
+ - List of IDs of deleted containers.
+ returned: I(containers) is C(true)
+ type: list
+ elements: str
+ sample: []
+
+# images
+images:
+ description:
+ - List of IDs of deleted images.
+ returned: I(images) is C(true)
+ type: list
+ elements: str
+ sample: []
+
+# networks
+networks:
+ description:
+ - List of IDs of deleted networks.
+ returned: I(networks) is C(true)
+ type: list
+ elements: str
+ sample: []
+
+# volumes
+volumes:
+ description:
+ - List of IDs of deleted volumes.
+ returned: I(volumes) is C(true)
+ type: list
+ elements: str
+ sample: []
+
+# system
+system:
+ description:
+ - List of ID of deleted containers, volumes, images, network and total reclaimed space
+ returned: I(system) is C(true)
+ type: list
+ elements: str
+ sample: []
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def filtersPrepare(target, filters):
+ filter_out = []
+ if target == 'system':
+ for system_filter in filters:
+ filter_out.append(filters[system_filter])
+ else:
+ for common_filter in filters:
+ if isinstance(filters[common_filter], dict):
+ dict_filters = filters[common_filter]
+ for single_filter in dict_filters:
+ filter_out.append('--filter={label}={key}={value}'.format(label=common_filter, key=single_filter,
+ value=dict_filters[single_filter]))
+ else:
+ if target == 'image' and (common_filter in ('dangling_only', 'external')):
+ if common_filter == 'dangling_only' and not filters['dangling_only']:
+ filter_out.append('-a')
+ if common_filter == 'external' and filters['external']:
+ filter_out.append('--external')
+ else:
+ filter_out.append('--filter={label}={value}'.format(label=common_filter,
+ value=filters[common_filter]))
+
+ return filter_out
+
+
+def podmanExec(module, target, filters, executable):
+ command = [executable, target, 'prune', '--force']
+ if filters is not None:
+ command.extend(filtersPrepare(target, filters))
+ rc, out, err = module.run_command(command)
+ changed = bool(out)
+
+ if rc != 0:
+ module.fail_json(
+ msg='Error executing prune on {target}: {err}'.format(target=target, err=err))
+
+ return {
+ "changed": changed,
+ target: list(filter(None, out.split('\n'))),
+ "errors": err
+ }
+
+
+def main():
+ results = dict()
+ module_args = dict(
+ container=dict(type='bool', default=False),
+ container_filters=dict(type='dict'),
+ image=dict(type='bool', default=False),
+ image_filters=dict(type='dict'),
+ network=dict(type='bool', default=False),
+ network_filters=dict(type='dict'),
+ volume=dict(type='bool', default=False),
+ volume_filters=dict(type='dict'),
+ system=dict(type='bool', default=False),
+ system_all=dict(type='bool', default=False),
+ system_volumes=dict(type='bool', default=False),
+ executable=dict(type='str', default='podman')
+ )
+
+ module = AnsibleModule(
+ argument_spec=module_args
+ )
+
+ executable = module.get_bin_path(
+ module.params['executable'], required=True)
+
+ for target, filters in (
+ ('container', 'container_filters'), ('image', 'image_filters'), ('network', 'network_filters'),
+ ('volume', 'volume_filters')):
+ if module.params[target]:
+ results[target] = podmanExec(module, target, module.params[filters], executable)
+
+ if module.params['system']:
+ target = 'system'
+ system_filters = {}
+ if module.params['system_all']:
+ system_filters['system_all'] = '--all'
+ if module.params['system_volumes']:
+ system_filters['system_volumes'] = '--volumes'
+ results[target] = podmanExec(module, target, system_filters, executable)
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_save.py b/ansible_collections/containers/podman/plugins/modules/podman_save.py
new file mode 100644
index 000000000..bc7ce252c
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_save.py
@@ -0,0 +1,145 @@
+#!/usr/bin/python
+# coding: utf-8 -*-
+
+# Copyright (c) 2020, Sagi Shnaidman <sshnaidm@redhat.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+module: podman_save
+short_description: Saves podman image to tar file
+author: Sagi Shnaidman (@sshnaidm)
+description:
+ - podman save saves an image to either docker-archive, oci-archive, oci-dir
+ (directory with oci manifest type), or docker-dir (directory with v2s2 manifest type)
+ on the local machine, default is docker-archive.
+
+options:
+ image:
+ description:
+ - Image to save.
+ type: str
+ required: true
+ compress:
+ description:
+ - Compress tarball image layers when pushing to a directory using the 'dir' transport.
+ (default is same compression type, compressed or uncompressed, as source)
+ type: bool
+ dest:
+ description:
+ - Destination file to write image to.
+ type: str
+ required: true
+ aliases:
+ - path
+ format:
+ description:
+ - Save image to docker-archive, oci-archive (see containers-transports(5)), oci-dir
+ (oci transport), or docker-dir (dir transport with v2s2 manifest type).
+ type: str
+ choices:
+ - docker-archive
+ - oci-archive
+ - oci-dir
+ - docker-dir
+ multi_image_archive:
+ description:
+ - Allow for creating archives with more than one image. Additional names will be
+ interpreted as images instead of tags. Only supported for docker-archive.
+ type: bool
+ force:
+ description:
+ - Force saving to file even if it exists.
+ type: bool
+ default: True
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+requirements:
+ - "Podman installed on host"
+'''
+
+RETURN = '''
+'''
+
+EXAMPLES = '''
+# What modules does for example
+- containers.podman.podman_save:
+ dest: /path/to/tar/file
+ compress: true
+ format: oci-dir
+'''
+
+import os # noqa: E402
+from ansible.module_utils.basic import AnsibleModule # noqa: E402
+from ..module_utils.podman.common import remove_file_or_dir # noqa: E402
+
+
+def save(module, executable):
+ changed = False
+ command = [executable, 'save']
+ cmd_args = {
+ 'compress': ['--compress'],
+ 'dest': ['-o=%s' % module.params['dest']],
+ 'format': ['--format=%s' % module.params['format']],
+ 'multi_image_archive': ['--multi-image-archive'],
+ }
+ for param in module.params:
+ if module.params[param] is not None and param in cmd_args:
+ command += cmd_args[param]
+ command.append(module.params['image'])
+ if module.params['force']:
+ dest = module.params['dest']
+ if os.path.exists(dest):
+ changed = True
+ if module.check_mode:
+ return changed, '', ''
+ try:
+ remove_file_or_dir(dest)
+ except Exception as e:
+ module.fail_json(msg="Error deleting %s path: %s" % (dest, e))
+ else:
+ changed = not os.path.exists(module.params['dest'])
+ if module.check_mode:
+ return changed, '', ''
+ rc, out, err = module.run_command(command)
+ if rc != 0:
+ module.fail_json(msg="Error: %s" % (err))
+ return changed, out, err
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ image=dict(type='str', required=True),
+ compress=dict(type='bool'),
+ dest=dict(type='str', required=True, aliases=['path']),
+ format=dict(type='str', choices=['docker-archive', 'oci-archive', 'oci-dir', 'docker-dir']),
+ multi_image_archive=dict(type='bool'),
+ force=dict(type='bool', default=True),
+ executable=dict(type='str', default='podman')
+ ),
+ supports_check_mode=True,
+ )
+ if module.params['compress'] and module.params['format'] not in ['oci-dir', 'docker-dir']:
+ module.fail_json(msg="Compression is only supported for oci-dir and docker-dir format")
+
+ executable = module.get_bin_path(module.params['executable'], required=True)
+ changed, out, err = save(module, executable)
+
+ results = {
+ "changed": changed,
+ "stdout": out,
+ "stderr": err,
+ }
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_secret.py b/ansible_collections/containers/podman/plugins/modules/podman_secret.py
new file mode 100644
index 000000000..fc8ec1f1d
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_secret.py
@@ -0,0 +1,178 @@
+#!/usr/bin/python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: podman_secret
+author:
+ - "Aliaksandr Mianzhynski (@amenzhinsky)"
+version_added: '1.7.0'
+short_description: Manage podman secrets
+notes: []
+description:
+ - Manage podman secrets
+requirements:
+ - podman
+options:
+ data:
+ description:
+ - The value of the secret. Required when C(state) is C(present).
+ type: str
+ driver:
+ description:
+ - Override default secrets driver, currently podman uses C(file)
+ which is unencrypted.
+ type: str
+ driver_opts:
+ description:
+ - Driver-specific key-value options.
+ type: dict
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ type: str
+ default: 'podman'
+ force:
+ description:
+ - Use it when C(state) is C(present) to remove and recreate an existing secret.
+ type: bool
+ default: false
+ skip_existing:
+ description:
+ - Use it when C(state) is C(present) and secret with the same name already exists.
+ If set to C(true), the secret will NOT be recreated and remains as is.
+ type: bool
+ default: false
+ name:
+ description:
+ - The name of the secret.
+ required: True
+ type: str
+ state:
+ description:
+ - Whether to create or remove the named secret.
+ type: str
+ default: present
+ choices:
+ - absent
+ - present
+'''
+
+EXAMPLES = r"""
+- name: Create secret
+ containers.podman.podman_secret:
+ state: present
+ name: mysecret
+ data: "my super secret content"
+
+- name: Create container that uses the secret
+ containers.podman.podman_container:
+ name: showmysecret
+ image: docker.io/alpine:3.14
+ secrets:
+ - mysecret
+ detach: false
+ command: cat /run/secrets/mysecret
+ register: container
+
+- name: Output secret data
+ debug:
+ msg: '{{ container.stdout }}'
+
+- name: Remove secret
+ containers.podman.podman_secret:
+ state: absent
+ name: mysecret
+ """
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def podman_secret_create(module, executable, name, data, force, skip,
+ driver, driver_opts):
+ if force:
+ module.run_command([executable, 'secret', 'rm', name])
+ if skip:
+ rc, out, err = module.run_command(
+ [executable, 'secret', 'ls', "--format", "{{.Name}}"])
+ if name in [i.strip() for i in out.splitlines()]:
+ return {
+ "changed": False,
+ }
+
+ cmd = [executable, 'secret', 'create']
+ if driver:
+ cmd.append('--driver')
+ cmd.append(driver)
+ if driver_opts:
+ cmd.append('--driver-opts')
+ cmd.append(",".join("=".join(i) for i in driver_opts.items()))
+ cmd.append(name)
+ cmd.append('-')
+
+ rc, out, err = module.run_command(cmd, data=data, binary_data=True)
+ if rc != 0:
+ module.fail_json(msg="Unable to create secret: %s" % err)
+
+ return {
+ "changed": True,
+ }
+
+
+def podman_secret_remove(module, executable, name):
+ changed = False
+ rc, out, err = module.run_command([executable, 'secret', 'rm', name])
+ if rc == 0:
+ changed = True
+ elif 'no such secret' in err:
+ pass
+ else:
+ module.fail_json(msg="Unable to remove secret: %s" % err)
+
+ return {
+ "changed": changed,
+ }
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ executable=dict(type='str', default='podman'),
+ state=dict(type='str', default='present', choices=['absent', 'present']),
+ name=dict(type='str', required=True),
+ data=dict(type='str', no_log=True),
+ force=dict(type='bool', default=False),
+ skip_existing=dict(type='bool', default=False),
+ driver=dict(type='str'),
+ driver_opts=dict(type='dict'),
+ ),
+ )
+
+ state = module.params['state']
+ name = module.params['name']
+ executable = module.get_bin_path(module.params['executable'], required=True)
+
+ if state == 'present':
+ data = module.params['data']
+ if data is None:
+ raise Exception("'data' is required when 'state' is 'present'")
+ force = module.params['force']
+ skip = module.params['skip_existing']
+ driver = module.params['driver']
+ driver_opts = module.params['driver_opts']
+ results = podman_secret_create(module, executable,
+ name, data, force, skip,
+ driver, driver_opts)
+ else:
+ results = podman_secret_remove(module, executable, name)
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_tag.py b/ansible_collections/containers/podman/plugins/modules/podman_tag.py
new file mode 100644
index 000000000..39e799f6f
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_tag.py
@@ -0,0 +1,91 @@
+#!/usr/bin/python
+# coding: utf-8 -*-
+
+# Copyright (c) 2021, Christian Bourque <@ocafebabe>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+module: podman_tag
+short_description: Add an additional name to a local image
+author: Christian Bourque (@ocafebabe)
+description:
+ - podman tag adds one or more additional names to locally-stored image.
+options:
+ image:
+ description:
+ - Image to tag.
+ type: str
+ required: true
+ target_names:
+ description:
+ - Additional names.
+ type: list
+ elements: str
+ required: true
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+requirements:
+ - "Podman installed on host"
+'''
+
+RETURN = '''
+'''
+
+EXAMPLES = '''
+# What modules does for example
+- containers.podman.podman_tag:
+ image: docker.io/continuumio/miniconda3
+ target_names:
+ - miniconda3
+ - miniconda
+'''
+
+from ansible.module_utils.basic import AnsibleModule # noqa: E402
+
+
+def tag(module, executable):
+ changed = False
+ command = [executable, 'tag']
+ command.append(module.params['image'])
+ command.extend(module.params['target_names'])
+ if module.check_mode:
+ return changed, '', ''
+ rc, out, err = module.run_command(command)
+ if rc == 0:
+ changed = True
+ else:
+ module.fail_json(msg="Error tagging local image %s: %s" % (
+ module.params['image'], err))
+ return changed, out, err
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ image=dict(type='str', required=True),
+ target_names=dict(type='list', elements='str', required=True),
+ executable=dict(type='str', default='podman')
+ ),
+ supports_check_mode=True,
+ )
+
+ executable = module.get_bin_path(module.params['executable'], required=True)
+ changed, out, err = tag(module, executable)
+
+ results = {
+ "changed": changed,
+ "stdout": out,
+ "stderr": err,
+ }
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_volume.py b/ansible_collections/containers/podman/plugins/modules/podman_volume.py
new file mode 100644
index 000000000..c533091e1
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_volume.py
@@ -0,0 +1,484 @@
+#!/usr/bin/python
+# Copyright (c) 2020 Red Hat
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# flake8: noqa: E501
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: podman_volume
+short_description: Manage Podman volumes
+author:
+ - "Sagi Shnaidman (@sshnaidm)"
+version_added: '1.1.0'
+description:
+ - Manage Podman volumes
+options:
+ state:
+ description:
+ - State of volume, default 'present'
+ type: str
+ default: present
+ choices:
+ - present
+ - absent
+ recreate:
+ description:
+ - Recreate volume even if exists.
+ type: bool
+ default: false
+ name:
+ description:
+ - Name of volume.
+ type: str
+ required: true
+ label:
+ description:
+ - Add metadata to a pod volume (e.g., label com.example.key=value).
+ type: dict
+ required: false
+ driver:
+ description:
+ - Specify volume driver name (default local).
+ type: str
+ required: false
+ options:
+ description:
+ - Set driver specific options. For example 'device=tpmfs', 'type=tmpfs'.
+ UID and GID idempotency is not supported due to changes in podman.
+ type: list
+ elements: str
+ required: false
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+ debug:
+ description:
+ - Return additional information which can be helpful for investigations.
+ type: bool
+ default: False
+
+requirements:
+ - "podman"
+
+'''
+
+RETURN = '''
+volume:
+ description: Volume inspection results if exists.
+ returned: always
+ type: dict
+ sample:
+ CreatedAt: '2020-06-05T16:38:55.277628769+03:00'
+ Driver: local
+ Labels:
+ key.com: value
+ key.org: value2
+ Mountpoint: /home/user/.local/share/containers/storage/volumes/test/_data
+ Name: test
+ Options: {}
+ Scope: local
+
+'''
+
+EXAMPLES = '''
+# What modules does for example
+- podman_volume:
+ state: present
+ name: volume1
+ label:
+ key: value
+ key2: value2
+ options:
+ - "device=/dev/loop1"
+ - "type=ext4"
+'''
+# noqa: F402
+import json # noqa: F402
+
+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
+
+
+class PodmanVolumeModuleParams:
+ """Creates list of arguments for podman CLI command.
+
+ Arguments:
+ action {str} -- action type from 'create', 'delete'
+ params {dict} -- dictionary of module parameters
+
+ """
+
+ def __init__(self, action, params, podman_version, module):
+ self.params = params
+ self.action = action
+ self.podman_version = podman_version
+ self.module = module
+
+ def construct_command_from_params(self):
+ """Create a podman command from given module parameters.
+
+ Returns:
+ list -- list of byte strings for Popen command
+ """
+ if self.action in ['delete']:
+ return self._simple_action()
+ if self.action in ['create']:
+ return self._create_action()
+
+ def _simple_action(self):
+ if self.action == 'delete':
+ cmd = ['rm', '-f', self.params['name']]
+ return [to_bytes(i, errors='surrogate_or_strict') for i in cmd]
+
+ def _create_action(self):
+ cmd = [self.action, self.params['name']]
+ all_param_methods = [func for func in dir(self)
+ if callable(getattr(self, func))
+ and func.startswith("addparam")]
+ params_set = (i for i in self.params if self.params[i] is not None)
+ for param in params_set:
+ func_name = "_".join(["addparam", param])
+ if func_name in all_param_methods:
+ cmd = getattr(self, func_name)(cmd)
+ return [to_bytes(i, errors='surrogate_or_strict') for i in cmd]
+
+ def check_version(self, param, minv=None, maxv=None):
+ if minv and LooseVersion(minv) > LooseVersion(
+ self.podman_version):
+ self.module.fail_json(msg="Parameter %s is supported from podman "
+ "version %s only! Current version is %s" % (
+ param, minv, self.podman_version))
+ if maxv and LooseVersion(maxv) < LooseVersion(
+ self.podman_version):
+ self.module.fail_json(msg="Parameter %s is supported till podman "
+ "version %s only! Current version is %s" % (
+ param, minv, self.podman_version))
+
+ def addparam_label(self, c):
+ for label in self.params['label'].items():
+ c += ['--label', b'='.join(
+ [to_bytes(l, errors='surrogate_or_strict') for l in label])]
+ return c
+
+ def addparam_driver(self, c):
+ return c + ['--driver', self.params['driver']]
+
+ def addparam_options(self, c):
+ for opt in self.params['options']:
+ c += ['--opt', opt]
+ return c
+
+
+class PodmanVolumeDefaults:
+ def __init__(self, module, podman_version):
+ self.module = module
+ self.version = podman_version
+ self.defaults = {
+ 'driver': 'local',
+ 'label': {},
+ 'options': {}
+ }
+
+ def default_dict(self):
+ # make here any changes to self.defaults related to podman version
+ return self.defaults
+
+
+class PodmanVolumeDiff:
+ def __init__(self, module, info, podman_version):
+ self.module = module
+ self.version = podman_version
+ self.default_dict = None
+ self.info = lower_keys(info)
+ self.params = self.defaultize()
+ self.diff = {'before': {}, 'after': {}}
+ self.non_idempotent = {}
+
+ def defaultize(self):
+ params_with_defaults = {}
+ self.default_dict = PodmanVolumeDefaults(
+ self.module, self.version).default_dict()
+ for p in self.module.params:
+ if self.module.params[p] is None and p in self.default_dict:
+ params_with_defaults[p] = self.default_dict[p]
+ else:
+ params_with_defaults[p] = self.module.params[p]
+ return params_with_defaults
+
+ def _diff_update_and_compare(self, param_name, before, after):
+ if before != after:
+ self.diff['before'].update({param_name: before})
+ self.diff['after'].update({param_name: after})
+ return True
+ return False
+
+ def diffparam_label(self):
+ before = self.info['labels'] if 'labels' in self.info else {}
+ after = self.params['label']
+ return self._diff_update_and_compare('label', before, after)
+
+ def diffparam_driver(self):
+ before = self.info['driver']
+ after = self.params['driver']
+ return self._diff_update_and_compare('driver', before, after)
+
+ def diffparam_options(self):
+ before = self.info['options'] if 'options' in self.info else {}
+ # Removing GID and UID from options list
+ before.pop('uid', None)
+ before.pop('gid', None)
+ # Collecting all other options in the list
+ before = ["=".join((k, v)) for k, v in before.items()]
+ after = self.params['options']
+ # # For UID, GID
+ # if 'uid' in self.info or 'gid' in self.info:
+ # ids = []
+ # if 'uid' in self.info and self.info['uid']:
+ # before = [i for i in before if 'uid' not in i]
+ # before += ['uid=%s' % str(self.info['uid'])]
+ # if 'gid' in self.info and self.info['gid']:
+ # before = [i for i in before if 'gid' not in i]
+ # before += ['gid=%s' % str(self.info['gid'])]
+ # if self.params['options']:
+ # for opt in self.params['options']:
+ # if 'uid=' in opt or 'gid=' in opt:
+ # ids += opt.split("o=")[1].split(",")
+ # after = [i for i in after if 'gid' not in i and 'uid' not in i]
+ # after += ids
+ before, after = sorted(list(set(before))), sorted(list(set(after)))
+ return self._diff_update_and_compare('options', before, after)
+
+ def is_different(self):
+ diff_func_list = [func for func in dir(self)
+ if callable(getattr(self, func)) and func.startswith(
+ "diffparam")]
+ fail_fast = not bool(self.module._diff)
+ different = False
+ for func_name in diff_func_list:
+ dff_func = getattr(self, func_name)
+ if dff_func():
+ if fail_fast:
+ return True
+ else:
+ different = True
+ # Check non idempotent parameters
+ for p in self.non_idempotent:
+ if self.module.params[p] is not None and self.module.params[p] not in [{}, [], '']:
+ different = True
+ return different
+
+
+class PodmanVolume:
+ """Perform volume tasks.
+
+ Manages podman volume, inspects it and checks its current state
+ """
+
+ def __init__(self, module, name):
+ """Initialize PodmanVolume class.
+
+ Arguments:
+ module {obj} -- ansible module object
+ name {str} -- name of volume
+ """
+
+ super(PodmanVolume, self).__init__()
+ self.module = module
+ self.name = name
+ self.stdout, self.stderr = '', ''
+ self.info = self.get_info()
+ self.version = self._get_podman_version()
+ self.diff = {}
+ self.actions = []
+
+ @property
+ def exists(self):
+ """Check if volume exists."""
+ return bool(self.info != {})
+
+ @property
+ def different(self):
+ """Check if volume is different."""
+ diffcheck = PodmanVolumeDiff(
+ self.module,
+ self.info,
+ self.version)
+ is_different = diffcheck.is_different()
+ diffs = diffcheck.diff
+ if self.module._diff and is_different and diffs['before'] and diffs['after']:
+ self.diff['before'] = "\n".join(
+ ["%s - %s" % (k, v) for k, v in sorted(
+ diffs['before'].items())]) + "\n"
+ self.diff['after'] = "\n".join(
+ ["%s - %s" % (k, v) for k, v in sorted(
+ diffs['after'].items())]) + "\n"
+ return is_different
+
+ def get_info(self):
+ """Inspect volume and gather info about it."""
+ # pylint: disable=unused-variable
+ rc, out, err = self.module.run_command(
+ [self.module.params['executable'], b'volume', b'inspect', self.name])
+ return json.loads(out)[0] if rc == 0 else {}
+
+ def _get_podman_version(self):
+ # pylint: disable=unused-variable
+ rc, out, err = self.module.run_command(
+ [self.module.params['executable'], b'--version'])
+ if rc != 0 or not out or "version" not in out:
+ self.module.fail_json(msg="%s run failed!" %
+ self.module.params['executable'])
+ return out.split("version")[1].strip()
+
+ def _perform_action(self, action):
+ """Perform action with volume.
+
+ Arguments:
+ action {str} -- action to perform - create, stop, delete
+ """
+ b_command = PodmanVolumeModuleParams(action,
+ self.module.params,
+ self.version,
+ self.module,
+ ).construct_command_from_params()
+ full_cmd = " ".join([self.module.params['executable'], 'volume']
+ + [to_native(i) for i in b_command])
+ self.module.log("PODMAN-VOLUME-DEBUG: %s" % full_cmd)
+ self.actions.append(full_cmd)
+ if not self.module.check_mode:
+ rc, out, err = self.module.run_command(
+ [self.module.params['executable'], b'volume'] + b_command,
+ expand_user_and_vars=False)
+ self.stdout = out
+ self.stderr = err
+ if rc != 0:
+ self.module.fail_json(
+ msg="Can't %s volume %s" % (action, self.name),
+ stdout=out, stderr=err)
+
+ def delete(self):
+ """Delete the volume."""
+ self._perform_action('delete')
+
+ def create(self):
+ """Create the volume."""
+ self._perform_action('create')
+
+ def recreate(self):
+ """Recreate the volume."""
+ self.delete()
+ self.create()
+
+
+class PodmanVolumeManager:
+ """Module manager class.
+
+ Defines according to parameters what actions should be applied to volume
+ """
+
+ def __init__(self, module):
+ """Initialize PodmanManager class.
+
+ Arguments:
+ module {obj} -- ansible module object
+ """
+
+ super(PodmanVolumeManager, self).__init__()
+
+ self.module = module
+ self.results = {
+ 'changed': False,
+ 'actions': [],
+ 'volume': {},
+ }
+ self.name = self.module.params['name']
+ self.executable = \
+ self.module.get_bin_path(self.module.params['executable'],
+ required=True)
+ self.state = self.module.params['state']
+ self.recreate = self.module.params['recreate']
+ self.volume = PodmanVolume(self.module, self.name)
+
+ def update_volume_result(self, changed=True):
+ """Inspect the current volume, update results with last info, exit.
+
+ Keyword Arguments:
+ changed {bool} -- whether any action was performed
+ (default: {True})
+ """
+ facts = self.volume.get_info() if changed else self.volume.info
+ out, err = self.volume.stdout, self.volume.stderr
+ self.results.update({'changed': changed, 'volume': facts,
+ 'podman_actions': self.volume.actions},
+ stdout=out, stderr=err)
+ if self.volume.diff:
+ self.results.update({'diff': self.volume.diff})
+ if self.module.params['debug']:
+ self.results.update({'podman_version': self.volume.version})
+ self.module.exit_json(**self.results)
+
+ def execute(self):
+ """Execute the desired action according to map of actions & states."""
+ states_map = {
+ 'present': self.make_present,
+ 'absent': self.make_absent,
+ }
+ process_action = states_map[self.state]
+ process_action()
+ self.module.fail_json(msg="Unexpected logic error happened, "
+ "please contact maintainers ASAP!")
+
+ def make_present(self):
+ """Run actions if desired state is 'started'."""
+ if not self.volume.exists:
+ self.volume.create()
+ self.results['actions'].append('created %s' % self.volume.name)
+ self.update_volume_result()
+ elif self.recreate or self.volume.different:
+ self.volume.recreate()
+ self.results['actions'].append('recreated %s' %
+ self.volume.name)
+ self.update_volume_result()
+ else:
+ self.update_volume_result(changed=False)
+
+ def make_absent(self):
+ """Run actions if desired state is 'absent'."""
+ if not self.volume.exists:
+ self.results.update({'changed': False})
+ elif self.volume.exists:
+ self.volume.delete()
+ self.results['actions'].append('deleted %s' % self.volume.name)
+ self.results.update({'changed': True})
+ self.results.update({'volume': {},
+ 'podman_actions': self.volume.actions})
+ self.module.exit_json(**self.results)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(type='str', default="present",
+ choices=['present', 'absent']),
+ name=dict(type='str', required=True),
+ label=dict(type='dict', required=False),
+ driver=dict(type='str', required=False),
+ options=dict(type='list', elements='str', required=False),
+ recreate=dict(type='bool', default=False),
+ executable=dict(type='str', required=False, default='podman'),
+ debug=dict(type='bool', default=False),
+ ))
+
+ PodmanVolumeManager(module).execute()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_volume_info.py b/ansible_collections/containers/podman/plugins/modules/podman_volume_info.py
new file mode 100644
index 000000000..97b43b3ce
--- /dev/null
+++ b/ansible_collections/containers/podman/plugins/modules/podman_volume_info.py
@@ -0,0 +1,100 @@
+#!/usr/bin/python
+# Copyright (c) 2020 Red Hat
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+module: podman_volume_info
+author:
+ - "Sagi Shnaidman (@sshnaidm)"
+short_description: Gather info about podman volumes
+notes: []
+description:
+ - Gather info about podman volumes with podman inspect command.
+requirements:
+ - "Podman installed on host"
+options:
+ name:
+ description:
+ - Name of the volume
+ type: str
+ executable:
+ description:
+ - Path to C(podman) executable if it is not in the C($PATH) on the
+ machine running C(podman)
+ default: 'podman'
+ type: str
+'''
+
+EXAMPLES = r"""
+- name: Gather info about all present volumes
+ podman_volume_info:
+
+- name: Gather info about specific volume
+ podman_volume_info:
+ name: specific_volume
+"""
+
+RETURN = r"""
+volumes:
+ description: Facts from all or specified volumes
+ returned: always
+ type: list
+ sample: [
+ {
+ "name": "testvolume",
+ "labels": {},
+ "mountPoint": "/home/ansible/.local/share/testvolume/_data",
+ "driver": "local",
+ "options": {},
+ "scope": "local"
+ }
+ ]
+"""
+
+import json
+from ansible.module_utils.basic import AnsibleModule
+
+
+def get_volume_info(module, executable, name):
+ command = [executable, 'volume', 'inspect']
+ if name:
+ command.append(name)
+ else:
+ command.append("--all")
+ rc, out, err = module.run_command(command)
+ if rc != 0 or 'no such volume' in err:
+ module.fail_json(msg="Unable to gather info for %s: %s" % (name or 'all volumes', err))
+ if not out or json.loads(out) is None:
+ return [], out, err
+ return json.loads(out), out, err
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ executable=dict(type='str', default='podman'),
+ name=dict(type='str')
+ ),
+ supports_check_mode=True,
+ )
+
+ name = module.params['name']
+ executable = module.get_bin_path(module.params['executable'], required=True)
+
+ inspect_results, out, err = get_volume_info(module, executable, name)
+
+ results = {
+ "changed": False,
+ "volumes": inspect_results,
+ "stderr": err
+ }
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()