diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-26 06:22:20 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-26 06:22:20 +0000 |
commit | 18bd2207b6c1977e99a93673a7be099e23f0f547 (patch) | |
tree | 40fd9e5913462a88be6ba24be6953383c5b39874 /ansible_collections/containers/podman/plugins/modules | |
parent | Releasing progress-linux version 10.0.1+dfsg-1~progress7.99u1. (diff) | |
download | ansible-18bd2207b6c1977e99a93673a7be099e23f0f547.tar.xz ansible-18bd2207b6c1977e99a93673a7be099e23f0f547.zip |
Merging upstream version 10.1.0+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/containers/podman/plugins/modules')
7 files changed, 858 insertions, 112 deletions
diff --git a/ansible_collections/containers/podman/plugins/modules/podman_container.py b/ansible_collections/containers/podman/plugins/modules/podman_container.py index 75349f14e..b06c9ae9e 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_container.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_container.py @@ -79,6 +79,11 @@ options: - Add an annotation to the container. The format is key value, multiple times. type: dict + arch: + description: + - Set the architecture for the container. + Override the architecture, defaults to hosts, of the image to be pulled. For example, arm. + type: str attach: description: - Attach to STDIN, STDOUT or STDERR. The default in Podman is false. @@ -125,6 +130,10 @@ options: the cgroups path of the init process. Cgroups will be created if they do not already exist. type: path + cgroup_conf: + description: + - When running on cgroup v2, specify the cgroup file to write to and its value. + type: dict cgroupns: description: - Path to cgroups under which the cgroup for the container will be @@ -137,6 +146,10 @@ options: The disabled option will force the container to not create CGroups, and thus conflicts with CGroup options cgroupns and cgroup-parent. type: str + chrootdirs: + description: + - Path to a directory inside the container that is treated as a chroot directory. + type: str cidfile: description: - Write the container ID to the file @@ -196,6 +209,10 @@ options: - Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. type: str + decryption_key: + description: + - The "key-passphrase" to be used for decryption of images. Key can point to keys and/or certificates. + type: str delete_depend: description: - Remove selected container and recursively remove all containers that depend on it. @@ -234,6 +251,12 @@ options: (e.g. device /dev/sdc:/dev/xvdc:rwm) type: list elements: str + device_cgroup_rule: + description: + - Add a rule to the cgroup allowed devices list. + The rule is expected to be in the format specified in the Linux kernel + documentation admin-guide/cgroup-v1/devices. + type: str device_read_bps: description: - Limit read rate (bytes per second) from a device @@ -307,6 +330,10 @@ options: - Use all current host environment variables in container. Defaults to false. type: bool + env_merge: + description: + - Preprocess default environment variables for the containers + type: dict etc_hosts: description: - Dict of host-to-IP mappings, where each host name is a key in the @@ -436,6 +463,10 @@ options: - Run the container in a new user namespace using the supplied mapping. type: list elements: str + gpus: + description: + - GPU devices to add to the container. + type: str group_add: description: - Add additional groups to run as @@ -443,33 +474,70 @@ options: elements: str aliases: - groups + group_entry: + description: + - Customize the entry that is written to the /etc/group file within the container when --user is used. + type: str healthcheck: description: - Set or alter a healthcheck command for a container. type: str + aliases: + - health_cmd healthcheck_interval: description: - Set an interval for the healthchecks (a value of disable results in no automatic timer setup) (default "30s") type: str + aliases: + - health_interval healthcheck_retries: description: - The number of retries allowed before a healthcheck is considered to be unhealthy. The default value is 3. type: int + aliases: + - health_retries healthcheck_start_period: description: - The initialization time needed for a container to bootstrap. The value can be expressed in time format like 2m3s. The default value is 0s type: str + aliases: + - health_start_period + health_startup_cmd: + description: + - Set a startup healthcheck command for a container. + type: str + health_startup_interval: + description: + - Set an interval for the startup healthcheck. + type: str + health_startup_retries: + description: + - The number of attempts allowed before the startup healthcheck restarts the container. + If set to 0, the container is never restarted. The default is 0. + type: int + health_startup_success: + description: + - The number of successful runs required before the startup healthcheck succeeds + and the regular healthcheck begins. A value of 0 means that any success begins the regular healthcheck. + The default is 0. + type: int + health_startup_timeout: + description: + - The maximum time a startup healthcheck command has to complete before it is marked as failed. + type: str healthcheck_timeout: description: - The maximum time allowed to complete the healthcheck before an interval is considered failed. Like start-period, the value can be expressed in a time format such as 1m22s. The default value is 30s type: str + aliases: + - health_timeout healthcheck_failure_action: description: - The action to be taken when the container is considered unhealthy. The action must be one of @@ -481,6 +549,8 @@ options: - 'kill' - 'restart' - 'stop' + aliases: + - health_on_failure hooks_dir: description: - Each .json file in the path configures a hook for Podman containers. @@ -493,6 +563,11 @@ options: - Container host name. Sets the container host name that is available inside the container. type: str + hostuser: + description: + - Add a user account to /etc/passwd from the host to the container. + The Username or UID must exist on the host system. + type: str http_proxy: description: - By default proxy environment variables are passed into the container if @@ -522,6 +597,14 @@ options: - Run an init inside the container that forwards signals and reaps processes. The default is false. type: bool + init_ctr: + description: + - (Pods only). When using pods, create an init style container, + which is run after the infra container is started but before regular pod containers are started. + type: str + choices: + - 'once' + - 'always' init_path: description: - Path to the container-init binary. @@ -542,6 +625,10 @@ options: The address must be within the default CNI network's pool (default 10.88.0.0/16). type: str + ip6: + description: + - Specify a static IPv6 address for the container + type: str ipc: description: - Default is to create a private IPC namespace (POSIX SysV IPC) for the @@ -671,6 +758,12 @@ options: This is a limitation that will be removed in a later release. type: list elements: str + aliases: + - network_alias + no_healthcheck: + description: + - Disable any defined healthchecks for container. + type: bool no_hosts: description: - Do not create /etc/hosts for the container @@ -685,23 +778,64 @@ options: description: - Tune the host's OOM preferences for containers (accepts -1000 to 1000) type: int + os: + description: + - Override the OS, defaults to hosts, of the image to be pulled. For example, windows. + type: str + passwd: + description: + - Allow Podman to add entries to /etc/passwd and /etc/group when used in conjunction with the --user option. + This is used to override the Podman provided user setup in favor of entrypoint configurations + such as libnss-extrausers. + type: bool + passwd_entry: + description: + - Customize the entry that is written to the /etc/passwd file within the container when --passwd is used. + type: str + personality: + description: + - Personality sets the execution domain via Linux personality(2). + type: str pid: description: - Set the PID mode for the container type: str aliases: - pid_mode + pid_file: + description: + - When the pidfile location is specified, the container process' PID is written to the pidfile. + type: path pids_limit: description: - Tune the container's PIDs limit. Set -1 to have unlimited PIDs for the container. type: str + platform: + description: + - Specify the platform for selecting the image. + type: str pod: description: - Run container in an existing pod. If you want podman to make the pod for you, prefix the pod name with "new:" type: str + pod_id_file: + description: + - Run container in an existing pod and read the pod's ID from the specified file. + When a container is run within a pod which has an infra-container, + the infra-container starts first. + type: path + preserve_fd: + description: + - Pass down to the process the additional file descriptors specified in the comma separated list. + type: list + elements: str + preserve_fds: + description: + - Pass down to the process N additional file descriptors (in addition to 0, 1, 2). The total FDs are 3\+N. + type: str privileged: description: - Give extended privileges to this container. The default is false. @@ -724,6 +858,15 @@ options: - Publish all exposed ports to random ports on the host interfaces. The default is false. type: bool + pull: + description: + - Pull image policy. The default is 'missing'. + type: str + choices: + - 'missing' + - 'always' + - 'never' + - 'newer' quadlet_dir: description: - Path to the directory to write quadlet file in. @@ -740,6 +883,10 @@ options: options as a list of lines to add. type: list elements: str + rdt_class: + description: + - Rdt-class sets the class of service (CLOS or COS) for the container to run in. Requires root. + type: str read_only: description: - Mount the container's root filesystem as read only. Default is false @@ -779,6 +926,15 @@ options: - Seconds to wait before forcibly stopping the container when restarting. Use -1 for infinite wait. Applies to "restarted" status. type: str + retry: + description: + - Number of times to retry pulling or pushing images between the registry and local storage in case of failure. + Default is 3. + type: int + retry_delay: + description: + - Duration of delay between retry attempts when pulling or pushing images between the registry and local storage in case of failure. + type: str rm: description: - Automatically remove the container when it exits. The default is false. @@ -786,6 +942,11 @@ options: aliases: - remove - auto_remove + rmi: + description: + - After exit of the container, remove the image unless another container is using it. + Implies --rm on the new container. The default is false. + type: bool rootfs: description: - If true, the first argument refers to an exploded container on the file @@ -803,6 +964,10 @@ options: L(documentation,https://docs.podman.io/en/latest/markdown/podman-run.1.html#secret-secret-opt-opt) for more details. type: list elements: str + seccomp_policy: + description: + - Specify the policy to select the seccomp profile. + type: str security_opt: description: - Security Options. For example security_opt "seccomp=unconfined" @@ -817,6 +982,10 @@ options: If you omit the unit, the system uses bytes. If you omit the size entirely, the system uses 64m type: str + shm_size_systemd: + description: + - Size of systemd-specific tmpfs mounts such as /run, /run/lock, /var/log/journal and /tmp. + type: str sig_proxy: description: - Proxy signals sent to the podman run command to the container process. @@ -853,6 +1022,11 @@ options: description: - Run container in systemd mode. The default is true. type: str + timeout: + description: + - Maximum time (in seconds) a container is allowed to run before conmon sends it the kill signal. + By default containers run until they exit or are stopped by "podman stop". + type: int timezone: description: - Set timezone in container. This flag takes area-based timezones, @@ -861,6 +1035,10 @@ options: See /usr/share/zoneinfo/ for valid timezones. Remote connections use local containers.conf for defaults. type: str + tls_verify: + description: + - Require HTTPS and verify certificates when pulling images. + type: bool tmpfs: description: - Create a tmpfs mount. For example tmpfs @@ -882,6 +1060,20 @@ options: elements: str aliases: - ulimits + umask: + description: + - Set the umask inside the container. Defaults to 0022. + Remote connections use local containers.conf for defaults. + type: str + unsetenv: + description: + - Unset default environment variables for the container. + type: list + elements: str + unsetenv_all: + description: + - Unset all default environment variables for the container. + type: bool user: description: - Sets the username or UID used and optionally the groupname or GID for @@ -899,6 +1091,10 @@ options: description: - Set the UTS mode for the container type: str + variant: + description: + - Use VARIANT instead of the default architecture variant of the container image. + type: str volume: description: - Create a bind mount. If you specify, volume /HOST-DIR:/CONTAINER-DIR, diff --git a/ansible_collections/containers/podman/plugins/modules/podman_image.py b/ansible_collections/containers/podman/plugins/modules/podman_image.py index 7fcb0041a..a46a6c3c5 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_image.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_image.py @@ -42,6 +42,10 @@ DOCUMENTATION = r''' description: Whether or not to pull the image. default: True type: bool + pull_extra_args: + description: + - Extra arguments to pass to the pull command. + type: str push: description: Whether or not to push an image. default: False @@ -67,7 +71,8 @@ DOCUMENTATION = r''' - quadlet validate_certs: description: - - Require HTTPS and validate certificates when pulling or pushing. Also used during build if a pull or push is necessary. + - Require HTTPS and validate certificates when pulling or pushing. + Also used during build if a pull or push is necessary. type: bool aliases: - tlsverify @@ -94,9 +99,15 @@ DOCUMENTATION = r''' - build_args - buildargs suboptions: + container_file: + description: + - Content of the Containerfile to use for building the image. + Mutually exclusive with the C(file) option which is path to the existing Containerfile. + type: str file: description: - Path to the Containerfile if it is not in the build context directory. + Mutually exclusive with the C(container_file) option. type: path volume: description: @@ -105,7 +116,8 @@ DOCUMENTATION = r''' elements: str annotation: description: - - Dictionary of key=value pairs to add to the image. Only works with OCI images. Ignored for Docker containers. + - Dictionary of key=value pairs to add to the image. Only works with OCI images. + Ignored for Docker containers. type: dict force_rm: description: @@ -148,7 +160,7 @@ DOCUMENTATION = r''' type: bool format: description: - - Manifest type to use when pushing an image using the 'dir' transport (default is manifest type of source). + - Manifest type to use when pushing an image using the 'dir' transport (default is manifest type of source) type: str choices: - oci @@ -168,14 +180,19 @@ DOCUMENTATION = r''' - destination transport: description: - - Transport to use when pushing in image. If no transport is set, will attempt to push to a remote registry. + - Transport to use when pushing in image. If no transport is set, will attempt to push to a remote registry type: str choices: - dir + - docker - docker-archive - docker-daemon - oci-archive - ostree + extra_args: + description: + - Extra args to pass to push, if executed. Does not idempotently check for new push args. + type: str quadlet_dir: description: - Path to the directory to write quadlet file in. @@ -300,6 +317,15 @@ EXAMPLES = r""" name: nginx arch: amd64 +- name: Build a container from file inline + containers.podman.podman_image: + name: mycustom_image + state: build + build: + container_file: |- + FROM alpine:latest + CMD echo "Hello, World!" + - name: Create a quadlet file for an image containers.podman.podman_image: name: docker.io/library/alpine:latest @@ -333,7 +359,7 @@ RETURN = r""" "/app-entrypoint.sh" ], "Env": [ - "PATH=/opt/bitnami/java/bin:/opt/bitnami/wildfly/bin:/opt/bitnami/nami/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "PATH=/opt/bitnami/java/bin:/opt/bitnami/wildfly/bin:/opt/bitnami/nami/bin:...", "IMAGE_OS=debian-9", "NAMI_VERSION=1.0.0-1", "GPG_KEY_SERVERS_LIST=ha.pool.sks-keyservers.net", @@ -373,10 +399,10 @@ RETURN = r""" "Digest": "sha256:5a8ab28e314c2222de3feaf6dac94a0436a37fc08979d2722c99d2bef2619a9b", "GraphDriver": { "Data": { - "LowerDir": "/var/lib/containers/storage/overlay/142c1beadf1bb09fbd929465ec98c9dca3256638220450efb4214727d0d0680e/diff:/var/lib/containers/s", - "MergedDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99723ed787878dbfea/merged", - "UpperDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99723ed787878dbfea/diff", - "WorkDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99723ed787878dbfea/work" + "LowerDir": "/var/lib/containers/storage/overlay/142c1beadf1bb09fbd929465e..../diff:/var/lib/containers/s", + "MergedDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99/merged", + "UpperDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99/diff", + "WorkDir": "/var/lib/containers/storage/overlay/9aa10191f5bddb59e28508e721fdeb43505e5b395845fa99/work" }, "Name": "overlay" }, @@ -434,9 +460,12 @@ RETURN = r""" ] """ -import json -import re -import shlex +import json # noqa: E402 +import os # noqa: E402 +import re # noqa: E402 +import shlex # noqa: E402 +import tempfile # noqa: E402 +import time # noqa: E402 from ansible.module_utils._text import to_native from ansible.module_utils.basic import AnsibleModule @@ -456,6 +485,7 @@ class PodmanImageManager(object): self.executable = self.module.get_bin_path(module.params.get('executable'), required=True) self.tag = self.module.params.get('tag') self.pull = self.module.params.get('pull') + self.pull_extra_args = self.module.params.get('pull_extra_args') self.push = self.module.params.get('push') self.path = self.module.params.get('path') self.force = self.module.params.get('force') @@ -509,7 +539,7 @@ class PodmanImageManager(object): if not layer_ids: layer_ids = lines.splitlines() - return (layer_ids[-1]) + return layer_ids[-1] def present(self): image = self.find_image() @@ -520,9 +550,18 @@ class PodmanImageManager(object): digest_before = None if not image or self.force: - if self.path: + if self.state == 'build' or self.path: # Build the image - self.results['actions'].append('Built image {image_name} from {path}'.format(image_name=self.image_name, path=self.path)) + build_file = self.build.get('file') if self.build else None + container_file_txt = self.build.get('container_file') if self.build else None + if build_file and container_file_txt: + self.module.fail_json(msg='Cannot specify both build file and container file content!') + if not self.path and build_file: + self.path = os.path.dirname(build_file) + elif not self.path and not build_file and not container_file_txt: + self.module.fail_json(msg='Path to build context or file is required when building an image') + self.results['actions'].append('Built image {image_name} from {path}'.format( + image_name=self.image_name, path=self.path or 'default context')) if not self.module.check_mode: self.results['image'], self.results['stdout'] = self.build_image() image = self.results['image'] @@ -541,16 +580,8 @@ class PodmanImageManager(object): self.results['changed'] = True if self.push: - # Push the image - if '/' in self.image_name: - push_format_string = 'Pushed image {image_name}' - else: - push_format_string = 'Pushed image {image_name} to {dest}' - self.results['actions'].append(push_format_string.format(image_name=self.image_name, dest=self.push_args['dest'])) - self.results['changed'] = True - if not self.module.check_mode: - self.results['image'], output = self.push_image() - self.results['stdout'] += "\n" + output + self.results['image'], output = self.push_image() + self.results['stdout'] += "\n" + output if image and not self.results.get('image'): self.results['image'] = image @@ -654,13 +685,18 @@ class PodmanImageManager(object): if self.ca_cert_dir: args.extend(['--cert-dir', self.ca_cert_dir]) + if self.pull_extra_args: + args.extend(shlex.split(self.pull_extra_args)) + rc, out, err = self._run(args, ignore_errors=True) if rc != 0: if not self.pull: - self.module.fail_json(msg='Failed to find image {image_name} locally, image pull set to {pull_bool}'.format( - pull_bool=self.pull, image_name=image_name)) + self.module.fail_json( + msg='Failed to find image {image_name} locally, image pull set to {pull_bool}'.format( + pull_bool=self.pull, image_name=image_name)) else: - self.module.fail_json(msg='Failed to pull image {image_name}'.format(image_name=image_name)) + self.module.fail_json( + msg='Failed to pull image {image_name}'.format(image_name=image_name)) return self.inspect_image(out.strip()) def build_image(self): @@ -697,6 +733,17 @@ class PodmanImageManager(object): containerfile = self.build.get('file') if containerfile: args.extend(['--file', containerfile]) + container_file_txt = self.build.get('container_file') + if container_file_txt: + # create a temporarly file with the content of the Containerfile + if self.path: + container_file_path = os.path.join(self.path, 'Containerfile.generated_by_ansible_%s' % time.time()) + else: + container_file_path = os.path.join( + tempfile.gettempdir(), 'Containerfile.generated_by_ansible_%s' % time.time()) + with open(container_file_path, 'w') as f: + f.write(container_file_txt) + args.extend(['--file', container_file_path]) volume = self.build.get('volume') if volume: @@ -717,13 +764,16 @@ class PodmanImageManager(object): target = self.build.get('target') if target: args.extend(['--target', target]) - - args.append(self.path) + if self.path: + args.append(self.path) rc, out, err = self._run(args, ignore_errors=True) if rc != 0: - self.module.fail_json(msg="Failed to build image {image}: {out} {err}".format(image=self.image_name, out=out, err=err)) - + self.module.fail_json(msg="Failed to build image {image}: {out} {err}".format( + image=self.image_name, out=out, err=err)) + # remove the temporary file if it was created + if container_file_txt: + os.remove(container_file_path) last_id = self._get_id_from_output(out, startswith='-->') return self.inspect_image(last_id), out + err @@ -760,49 +810,55 @@ class PodmanImageManager(object): if sign_by_key: args.extend(['--sign-by', sign_by_key]) + push_extra_args = self.push_args.get('extra_args') + if push_extra_args: + args.extend(shlex.split(push_extra_args)) + args.append(self.image_name) # Build the destination argument dest = self.push_args.get('dest') - dest_format_string = '{dest}/{image_name}' - regexp = re.compile(r'/{name}(:{tag})?'.format(name=self.name, tag=self.tag)) - if not dest: - if '/' not in self.name: - self.module.fail_json(msg="'push_args['dest']' is required when pushing images that do not have the remote registry in the image name") - - # If the push destination contains the image name and/or the tag - # remove it and warn since it's not needed. - elif regexp.search(dest): - dest = regexp.sub('', dest) - self.module.warn("Image name and tag are automatically added to push_args['dest']. Destination changed to {dest}".format(dest=dest)) + transport = self.push_args.get('transport') - if dest and dest.endswith('/'): - dest = dest[:-1] + if dest is None: + dest = self.image_name - transport = self.push_args.get('transport') if transport: - if not dest: - self.module.fail_json("'push_args['transport'] requires 'push_args['dest'] but it was not provided.") if transport == 'docker': dest_format_string = '{transport}://{dest}' elif transport == 'ostree': dest_format_string = '{transport}:{name}@{dest}' else: dest_format_string = '{transport}:{dest}' - - dest_string = dest_format_string.format(transport=transport, name=self.name, dest=dest, image_name=self.image_name,) - - # Only append the destination argument if the image name is not a URL - if '/' not in self.name: - args.append(dest_string) - - rc, out, err = self._run(args, ignore_errors=True) - if rc != 0: - self.module.fail_json(msg="Failed to push image {image_name}: {err}".format(image_name=self.image_name, err=err)) - last_id = self._get_id_from_output( - out + err, contains=':', split_on=':') - - return self.inspect_image(last_id), out + err + if transport == 'docker-daemon' and ":" not in dest: + dest_format_string = '{transport}:{dest}:latest' + dest_string = dest_format_string.format(transport=transport, name=self.name, dest=dest) + else: + dest_string = dest + # In case of dest as a repository with org name only, append image name to it + if ":" not in dest and "@" not in dest and len(dest.rstrip("/").split("/")) == 2: + dest_string = dest.rstrip("/") + "/" + self.image_name + + if "/" not in dest_string and "@" not in dest_string and "docker-daemon" not in dest_string: + self.module.fail_json(msg="Destination must be a full URL or path to a directory.") + + args.append(dest_string) + self.module.log("PODMAN-IMAGE-DEBUG: Pushing image {image_name} to {dest_string}".format( + image_name=self.image_name, dest_string=dest_string)) + self.results['actions'].append(" ".join(args)) + self.results['podman_actions'].append(" ".join([self.executable] + args)) + self.results['changed'] = True + out, err = '', '' + if not self.module.check_mode: + rc, out, err = self._run(args, ignore_errors=True) + if rc != 0: + self.module.fail_json(msg="Failed to push image {image_name}".format( + image_name=self.image_name), + stdout=out, stderr=err, + actions=self.results['actions'], + podman_actions=self.results['podman_actions']) + + return self.inspect_image(self.image_name), out + err def remove_image(self, image_name=None): if image_name is None: @@ -813,7 +869,8 @@ class PodmanImageManager(object): args.append('--force') rc, out, err = self._run(args, ignore_errors=True) if rc != 0: - self.module.fail_json(msg='Failed to remove image {image_name}. {err}'.format(image_name=image_name, err=err)) + self.module.fail_json(msg='Failed to remove image {image_name}. {err}'.format( + image_name=image_name, err=err)) return out def remove_image_id(self, image_id=None): @@ -847,6 +904,7 @@ def main(): arch=dict(type='str'), tag=dict(type='str', default='latest'), pull=dict(type='bool', default=True), + pull_extra_args=dict(type='str'), push=dict(type='bool', default=False), path=dict(type='str'), force=dict(type='bool', default=False), @@ -868,6 +926,7 @@ def main(): annotation=dict(type='dict'), force_rm=dict(type='bool', default=False), file=dict(type='path'), + container_file=dict(type='str'), format=dict( type='str', choices=['oci', 'docker'], @@ -889,6 +948,7 @@ def main(): remove_signatures=dict(type='bool'), sign_by=dict(type='str'), dest=dict(type='str', aliases=['destination'],), + extra_args=dict(type='str'), transport=dict( type='str', choices=[ @@ -897,6 +957,7 @@ def main(): 'docker-daemon', 'oci-archive', 'ostree', + 'docker' ] ), ), diff --git a/ansible_collections/containers/podman/plugins/modules/podman_network.py b/ansible_collections/containers/podman/plugins/modules/podman_network.py index 37bfefede..7623fffc1 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_network.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_network.py @@ -33,6 +33,12 @@ options: description: - disable dns plugin (default "false") type: bool + dns: + description: + - Set network-scoped DNS resolver/nameserver for containers in this network. + If not set, the host servers from /etc/resolv.conf is used. + type: list + elements: str driver: description: - Driver to manage the network (default "bridge") @@ -61,11 +67,27 @@ options: description: - Allocate container IP from range type: str + ipam_driver: + description: + - Set the ipam driver (IP Address Management Driver) for the network. + When unset podman chooses an ipam driver automatically based on the network driver + type: str + choices: + - host-local + - dhcp + - none ipv6: description: - Enable IPv6 (Dual Stack) networking. You must pass a IPv6 subnet. The subnet option must be used with the ipv6 option. + Idempotency is not supported because it generates subnets randomly. type: bool + route: + description: + - A static route in the format <destination in CIDR notation>,<gateway>,<route metric (optional)>. + This route will be added to every container in this network. + type: list + elements: str subnet: description: - Subnet in CIDR format @@ -74,6 +96,29 @@ options: description: - Create a Macvlan connection based on this device type: str + net_config: + description: + - List of dictionaries with network configuration. + Each dictionary should contain 'subnet' and 'gateway' keys. + 'ip_range' is optional. + type: list + elements: dict + suboptions: + subnet: + description: + - Subnet in CIDR format + type: str + required: true + gateway: + description: + - Gateway for the subnet + type: str + required: true + ip_range: + description: + - Allocate container IP from range + type: str + required: false opt: description: - Add network options. Currently 'vlan' and 'mtu' are supported. @@ -297,6 +342,11 @@ class PodmanNetworkModuleParams: def addparam_gateway(self, c): return c + ['--gateway', self.params['gateway']] + def addparam_dns(self, c): + for dns in self.params['dns']: + c += ['--dns', dns] + return c + def addparam_driver(self, c): return c + ['--driver', self.params['driver']] @@ -312,6 +362,13 @@ class PodmanNetworkModuleParams: def addparam_macvlan(self, c): return c + ['--macvlan', self.params['macvlan']] + def addparam_net_config(self, c): + for net in self.params['net_config']: + for kw in ('subnet', 'gateway', 'ip_range'): + if kw in net and net[kw]: + c += ['--%s=%s' % (kw.replace('_', '-'), net[kw])] + return c + def addparam_interface_name(self, c): return c + ['--interface-name', self.params['interface_name']] @@ -326,6 +383,14 @@ class PodmanNetworkModuleParams: for k in opt])] return c + def addparam_route(self, c): + for route in self.params['route']: + c += ['--route', route] + return c + + def addparam_ipam_driver(self, c): + return c + ['--ipam-driver=%s' % self.params['ipam_driver']] + def addparam_disable_dns(self, c): return c + ['--disable-dns=%s' % self.params['disable_dns']] @@ -337,7 +402,6 @@ class PodmanNetworkDefaults: self.defaults = { 'driver': 'bridge', 'internal': False, - 'ipv6': False } def default_dict(self): @@ -385,32 +449,45 @@ class PodmanNetworkDiff: before = after = self.params['disable_dns'] return self._diff_update_and_compare('disable_dns', before, after) + def diffparam_dns(self): + before = self.info.get('network_dns_servers', []) + after = self.params['dns'] or [] + return self._diff_update_and_compare('dns', sorted(before), sorted(after)) + def diffparam_driver(self): # Currently only bridge is supported before = after = 'bridge' return self._diff_update_and_compare('driver', before, after) def diffparam_ipv6(self): - if LooseVersion(self.version) >= LooseVersion('4.0.0'): - before = self.info.get('ipv6_enabled', False) - after = self.params['ipv6'] - return self._diff_update_and_compare('ipv6', before, after) - before = after = '' - return self._diff_update_and_compare('ipv6', before, after) + # We don't support dual stack because it generates subnets randomly + return self._diff_update_and_compare('ipv6', '', '') def diffparam_gateway(self): # Disable idempotency of subnet for v4, subnets are added automatically # TODO(sshnaidm): check if it's still the issue in v5 - if LooseVersion(self.version) >= LooseVersion('4.0.0'): - return self._diff_update_and_compare('gateway', '', '') - try: - before = self.info['plugins'][0]['ipam']['ranges'][0][0]['gateway'] - except (IndexError, KeyError): - before = '' - after = before - if self.params['gateway'] is not None: + if LooseVersion(self.version) < LooseVersion('4.0.0'): + try: + before = self.info['plugins'][0]['ipam']['ranges'][0][0]['gateway'] + except (IndexError, KeyError): + before = '' + after = before + if self.params['gateway'] is not None: + after = self.params['gateway'] + return self._diff_update_and_compare('gateway', before, after) + else: + before_subs = self.info.get('subnets') after = self.params['gateway'] - return self._diff_update_and_compare('gateway', before, after) + if not before_subs: + before = None + if before_subs: + if len(before_subs) > 1 and after: + return self._diff_update_and_compare( + 'gateway', ",".join([i['gateway'] for i in before_subs]), after) + before = [i.get('gateway') for i in before_subs][0] + if not after: + after = before + return self._diff_update_and_compare('gateway', before, after) def diffparam_internal(self): if LooseVersion(self.version) >= LooseVersion('4.0.0'): @@ -429,21 +506,62 @@ class PodmanNetworkDiff: before = after = '' return self._diff_update_and_compare('ip_range', before, after) - def diffparam_subnet(self): - # Disable idempotency of subnet for v4, subnets are added automatically - # TODO(sshnaidm): check if it's still the issue in v5 - if LooseVersion(self.version) >= LooseVersion('4.0.0'): - return self._diff_update_and_compare('subnet', '', '') - try: - before = self.info['plugins'][0]['ipam']['ranges'][0][0]['subnet'] - except (IndexError, KeyError): + def diffparam_ipam_driver(self): + before = self.info.get("ipam_options", {}).get("driver", "") + after = self.params['ipam_driver'] + if not after: + after = before + return self._diff_update_and_compare('ipam_driver', before, after) + + def diffparam_net_config(self): + after = self.params['net_config'] + if not after: + return self._diff_update_and_compare('net_config', '', '') + before_subs = self.info.get('subnets', []) + if before_subs: + before = ":".join(sorted([",".join([i['subnet'], i['gateway']]).rstrip(",") for i in before_subs])) + else: before = '' - after = before - if self.params['subnet'] is not None: + after = ":".join(sorted([",".join([i['subnet'], i['gateway']]).rstrip(",") for i in after])) + return self._diff_update_and_compare('net_config', before, after) + + def diffparam_route(self): + routes = self.info.get('routes', []) + if routes: + before = [",".join([ + r['destination'], r['gateway'], str(r.get('metric', ''))]).rstrip(",") for r in routes] + else: + before = [] + after = self.params['route'] or [] + return self._diff_update_and_compare('route', sorted(before), sorted(after)) + + def diffparam_subnet(self): + # Disable idempotency of subnet for v3 and below + if LooseVersion(self.version) < LooseVersion('4.0.0'): + try: + before = self.info['plugins'][0]['ipam']['ranges'][0][0]['subnet'] + except (IndexError, KeyError): + before = '' + after = before + if self.params['subnet'] is not None: + after = self.params['subnet'] + if HAS_IP_ADDRESS_MODULE: + after = ipaddress.ip_network(after).compressed + return self._diff_update_and_compare('subnet', before, after) + else: + if self.params['ipv6'] is not None: + # We can't support dual stack, it generates subnets randomly + return self._diff_update_and_compare('subnet', '', '') after = self.params['subnet'] - if HAS_IP_ADDRESS_MODULE: - after = ipaddress.ip_network(after).compressed - return self._diff_update_and_compare('subnet', before, after) + if after is None: + # We can't guess what subnet was used before by default + return self._diff_update_and_compare('subnet', '', '') + before = self.info.get('subnets') + if before: + if len(before) > 1 and after: + return self._diff_update_and_compare('subnet', ",".join([i['subnet'] for i in before]), after) + before = [i['subnet'] for i in before][0] + return self._diff_update_and_compare('subnet', before, after) def diffparam_macvlan(self): before = after = '' @@ -694,12 +812,15 @@ def main(): choices=['present', 'absent', 'quadlet']), name=dict(type='str', required=True), disable_dns=dict(type='bool', required=False), + dns=dict(type='list', elements='str', required=False), driver=dict(type='str', required=False), force=dict(type='bool', default=False), gateway=dict(type='str', required=False), interface_name=dict(type='str', required=False), internal=dict(type='bool', required=False), ip_range=dict(type='str', required=False), + ipam_driver=dict(type='str', required=False, + choices=['host-local', 'dhcp', 'none']), ipv6=dict(type='bool', required=False), subnet=dict(type='str', required=False), macvlan=dict(type='str', required=False), @@ -715,14 +836,23 @@ def main(): executable=dict(type='str', required=False, default='podman'), debug=dict(type='bool', default=False), recreate=dict(type='bool', default=False), + route=dict(type='list', elements='str', required=False), quadlet_dir=dict(type='path', required=False), quadlet_filename=dict(type='str', required=False), quadlet_options=dict(type='list', elements='str', required=False), + net_config=dict(type='list', required=False, elements='dict', + options=dict( + subnet=dict(type='str', required=True), + gateway=dict(type='str', required=True), + ip_range=dict(type='str', required=False), + )), ), required_by=dict( # for IP range and GW to set 'subnet' is required ip_range=('subnet'), gateway=('subnet'), - )) + ), + # define or subnet or net config + mutually_exclusive=[['subnet', 'net_config']]) PodmanNetworkManager(module).execute() diff --git a/ansible_collections/containers/podman/plugins/modules/podman_pod.py b/ansible_collections/containers/podman/plugins/modules/podman_pod.py index a975921ea..cdf728243 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_pod.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_pod.py @@ -117,6 +117,8 @@ options: all containers in the pod. type: list elements: str + aliases: + - dns_option required: false dns_search: description: @@ -125,6 +127,14 @@ options: type: list elements: str required: false + exit_policy: + description: + - Set the exit policy of the pod when the last container exits. Supported policies are stop and continue + choices: + - stop + - continue + type: str + required: false generate_systemd: description: - Generate systemd unit file for container. @@ -227,6 +237,11 @@ options: elements: str required: false type: list + gpus: + description: + - GPU devices to add to the container ('all' to pass all GPUs). + type: str + required: false hostname: description: - Set a hostname to the pod @@ -266,6 +281,11 @@ options: - Set a static IP for the pod's shared network. type: str required: false + ip6: + description: + - Set a static IPv6 for the pod's shared network. + type: str + required: false label: description: - Add metadata to a pod, pass dictionary of label keys and values. @@ -357,6 +377,16 @@ options: options as a list of lines to add. type: list elements: str + restart_policy: + description: + - Restart policy to follow when containers exit. + type: str + security_opt: + description: + - Security options for the pod. + type: list + elements: str + required: false share: description: - A comma delimited list of kernel namespaces to share. If none or "" is specified, @@ -364,6 +394,30 @@ options: user, uts. type: str required: false + share_parent: + description: + - This boolean determines whether or not all containers entering the pod use the pod as their cgroup parent. + The default value of this option in Podman is true. + type: bool + required: false + shm_size: + description: + - Set the size of the /dev/shm shared memory space. + A unit can be b (bytes), k (kibibytes), m (mebibytes), or g (gibibytes). + If the unit is omitted, the system uses bytes. + If the size is omitted, the default is 64m. + When size is 0, there is no limit on the amount of memory used for IPC by the pod. + type: str + required: false + shm_size_systemd: + description: + - Size of systemd-specific tmpfs mounts such as /run, /run/lock, /var/log/journal and /tmp. + A unit can be b (bytes), k (kibibytes), m (mebibytes), or g (gibibytes). + If the unit is omitted, the system uses bytes. + If the size is omitted, the default is 64m. + When size is 0, the usage is limited to 50 percents of the host's available memory. + type: str + required: false subgidname: description: - Name for GID map from the /etc/subgid file. Using this flag will run the container @@ -377,6 +431,11 @@ options: This flag conflicts with `userns` and `uidmap`. required: false type: str + sysctl: + description: + - Set kernel parameters for the pod. + type: dict + required: false uidmap: description: - Run the container in a new user namespace using the supplied mapping. @@ -393,6 +452,11 @@ options: An empty value ("") means user namespaces are disabled. required: false type: str + uts: + description: + - Set the UTS namespace mode for the pod. + required: false + type: str volume: description: - Create a bind mount. @@ -401,6 +465,12 @@ options: elements: str required: false type: list + volumes_from: + description: + - Mount volumes from the specified container. + elements: str + required: false + type: list executable: description: - Path to C(podman) executable if it is not in the C($PATH) on the @@ -450,7 +520,7 @@ pod: ''' -EXAMPLES = ''' +EXAMPLES = r''' # What modules does for example - containers.podman.podman_pod: name: pod1 @@ -465,6 +535,62 @@ EXAMPLES = ''' state: started publish: "127.0.0.1::80" +# Full workflow example with pod and containers +- name: Create a pod with parameters + containers.podman.podman_pod: + name: mypod + state: created + network: host + share: net + userns: auto + security_opt: + - seccomp=unconfined + - apparmor=unconfined + hostname: mypod + dns: + - 1.1.1.1 + volumes: + - /tmp:/tmp/:ro + label: + key: cval + otherkey: kddkdk + somekey: someval + add_host: + - "google:5.5.5.5" + +- name: Create containers attached to the pod + containers.podman.podman_container: + name: "{{ item }}" + state: created + pod: mypod + image: alpine + command: sleep 1h + loop: + - "container1" + - "container2" + +- name: Start pod + containers.podman.podman_pod: + name: mypod + state: started + network: host + share: net + userns: auto + security_opt: + - seccomp=unconfined + - apparmor=unconfined + hostname: mypod + dns: + - 1.1.1.1 + volumes: + - /tmp:/tmp/:ro + label: + key: cval + otherkey: kddkdk + somekey: someval + add_host: + - "google:5.5.5.5" + # Create a Quadlet file for a pod - containers.podman.podman_pod: name: qpod diff --git a/ansible_collections/containers/podman/plugins/modules/podman_search.py b/ansible_collections/containers/podman/plugins/modules/podman_search.py new file mode 100644 index 000000000..128e3ce03 --- /dev/null +++ b/ansible_collections/containers/podman/plugins/modules/podman_search.py @@ -0,0 +1,131 @@ +#!/usr/bin/python +# Copyright (c) 2024 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +module: podman_search +author: + - Derek Waters (@derekwaters) +short_description: Search for remote images using podman +notes: + - Podman may required elevated privileges in order to run properly. +description: + - Search for remote images using C(podman) +options: + executable: + description: + - Path to C(podman) executable if it is not in the C($PATH) on the machine running C(podman) + default: 'podman' + type: str + term: + description: + - The search term to look for. Will search all default registries unless a registry is defined in the search term. + type: str + required: True + limit: + description: + - Limit the number of image results returned from the search (per image registry) + required: False + default: 25 + type: int + list_tags: + description: + - Whether or not to return the list of tags associated with each image + required: False + default: False + type: bool + +''' + +EXAMPLES = r""" +- name: Search for any rhel images + containers.podman.podman_search: + term: "rhel" + limit: 3 + +- name: Gather info on a specific remote image + containers.podman.podman_search: + term: "myimageregistry.com/ansible-automation-platform/ee-minimal-rhel8" + +- name: Gather tag info on a known remote image + containers.podman.podman_search: + term: "myimageregistry.com/ansible-automation-platform/ee-minimal-rhel8" + list_tags: True +""" + +RETURN = r""" +images: + description: info from all or specified images + returned: always + type: list + sample: [ + { + "Automated": "", + "Description": "Red Hat Enterprise Linux Atomic Image is a minimal, fully supported base image.", + "Index": "registry.access.redhat.com", + "Name": "registry.access.redhat.com/rhel7-atomic", + "Official": "", + "Stars": 0, + "Tags": ["1.0", "1.1", "1.1.1-devel"] + } + ] +""" + +import json + +from ansible.module_utils.basic import AnsibleModule + + +def search_images(module, executable, term, limit, list_tags): + command = [executable, 'search', term, '--format', 'json'] + command.extend(['--limit', "{0}".format(limit)]) + if list_tags: + command.extend(['--list-tags']) + + rc, out, err = module.run_command(command) + + if rc != 0: + module.fail_json(msg="Unable to gather info for '{0}': {1}".format(term, err)) + return out + + +def main(): + module = AnsibleModule( + argument_spec=dict( + executable=dict(type='str', default='podman'), + term=dict(type='str', required=True), + limit=dict(type='int', required=False, default=25), + list_tags=dict(type='bool', required=False, default=False) + ), + supports_check_mode=True, + ) + + executable = module.params['executable'] + term = module.params.get('term') + limit = module.params.get('limit') + list_tags = module.params.get('list_tags') + executable = module.get_bin_path(executable, required=True) + + result_str = search_images(module, executable, term, limit, list_tags) + if result_str == "": + results = [] + else: + try: + results = json.loads(result_str) + except json.decoder.JSONDecodeError: + module.fail_json(msg='Failed to parse JSON output from podman search: {out}'.format(out=result_str)) + + results = dict( + changed=False, + images=results + ) + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/containers/podman/plugins/modules/podman_secret.py b/ansible_collections/containers/podman/plugins/modules/podman_secret.py index a31aae9dc..76b10ad39 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_secret.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_secret.py @@ -21,6 +21,7 @@ options: data: description: - The value of the secret. Required when C(state) is C(present). + Mutually exclusive with C(env) and C(path). type: str driver: description: @@ -31,6 +32,11 @@ options: description: - Driver-specific key-value options. type: dict + env: + description: + - The name of the environment variable that contains the secret. + Mutually exclusive with C(data) and C(path). + type: str executable: description: - Path to C(podman) executable if it is not in the C($PATH) on the @@ -53,6 +59,11 @@ options: - The name of the secret. required: True type: str + path: + description: + - Path to the file that contains the secret. + Mutually exclusive with C(data) and C(env). + type: path state: description: - Whether to create or remove the named secret. @@ -67,7 +78,7 @@ options: type: dict debug: description: - - Enable debug mode for module. + - Enable debug mode for module. It prints secrets diff. type: bool default: False ''' @@ -99,6 +110,8 @@ EXAMPLES = r""" name: mysecret """ +import os + from ansible.module_utils.basic import AnsibleModule from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion from ansible_collections.containers.podman.plugins.module_utils.podman.common import get_podman_version @@ -116,14 +129,15 @@ def podman_secret_exists(module, executable, name, version): return rc == 0 -def need_update(module, executable, name, data, driver, driver_opts, debug, labels): - +def need_update(module, executable, name, data, path, env, skip, driver, driver_opts, debug, labels): cmd = [executable, 'secret', 'inspect', '--showsecret', name] rc, out, err = module.run_command(cmd) if rc != 0: if debug: module.log("PODMAN-SECRET-DEBUG: Unable to get secret info: %s" % err) return True + if skip: + return False try: secret = module.from_json(out)[0] # We support only file driver for now @@ -131,10 +145,37 @@ def need_update(module, executable, name, data, driver, driver_opts, debug, labe if debug: module.log("PODMAN-SECRET-DEBUG: Idempotency of driver %s is not supported" % driver) return True - if secret['SecretData'] != data: - diff['after'] = "<different-secret>" - diff['before'] = "<secret>" - return True + if data: + if secret['SecretData'] != data: + if debug: + diff['after'] = data + diff['before'] = secret['SecretData'] + else: + diff['after'] = "<different-secret>" + diff['before'] = "<secret>" + return True + if path: + with open(path, 'rb') as f: + text = f.read().decode('utf-8') + if secret['SecretData'] != text: + if debug: + diff['after'] = text + diff['before'] = secret['SecretData'] + else: + diff['after'] = "<different-secret>" + diff['before'] = "<secret>" + return True + if env: + env_data = os.environ.get(env) + if secret['SecretData'] != env_data: + if debug: + diff['after'] = env_data + diff['before'] = secret['SecretData'] + else: + diff['after'] = "<different-secret>" + diff['before'] = "<secret>" + return True + if driver_opts: for k, v in driver_opts.items(): if secret['Spec']['Driver']['Options'].get(k) != v: @@ -154,13 +195,13 @@ def need_update(module, executable, name, data, driver, driver_opts, debug, labe return False -def podman_secret_create(module, executable, name, data, force, skip, +def podman_secret_create(module, executable, name, data, path, env, force, skip, driver, driver_opts, debug, labels): podman_version = get_podman_version(module, fail=False) if (podman_version is not None and LooseVersion(podman_version) >= LooseVersion('4.7.0') and (driver is None or driver == 'file')): - if not skip and need_update(module, executable, name, data, driver, driver_opts, debug, labels): + if need_update(module, executable, name, data, path, env, skip, driver, driver_opts, debug, labels): podman_secret_remove(module, executable, name) else: return {"changed": False} @@ -182,9 +223,20 @@ def podman_secret_create(module, executable, name, data, force, skip, cmd.append('--label') cmd.append("=".join([k, v])) cmd.append(name) - cmd.append('-') + if data: + cmd.append('-') + elif path: + cmd.append(path) + elif env: + if os.environ.get(env) is None: + module.fail_json(msg="Environment variable %s is not set" % env) + cmd.append("--env") + cmd.append(env) - rc, out, err = module.run_command(cmd, data=data, binary_data=True) + if data: + rc, out, err = module.run_command(cmd, data=data, binary_data=True) + else: + rc, out, err = module.run_command(cmd) if rc != 0: module.fail_json(msg="Unable to create secret: %s" % err) @@ -219,6 +271,8 @@ def main(): state=dict(type='str', default='present', choices=['absent', 'present']), name=dict(type='str', required=True), data=dict(type='str', no_log=True), + env=dict(type='str'), + path=dict(type='path'), force=dict(type='bool', default=False), skip_existing=dict(type='bool', default=False), driver=dict(type='str'), @@ -226,6 +280,8 @@ def main(): labels=dict(type='dict'), debug=dict(type='bool', default=False), ), + required_if=[('state', 'present', ['path', 'env', 'data'], True)], + mutually_exclusive=[['path', 'env', 'data']], ) state = module.params['state'] @@ -234,16 +290,16 @@ def main(): if state == 'present': data = module.params['data'] - if data is None: - raise Exception("'data' is required when 'state' is 'present'") force = module.params['force'] skip = module.params['skip_existing'] driver = module.params['driver'] driver_opts = module.params['driver_opts'] debug = module.params['debug'] labels = module.params['labels'] + path = module.params['path'] + env = module.params['env'] results = podman_secret_create(module, executable, - name, data, force, skip, + name, data, path, env, force, skip, driver, driver_opts, debug, labels) else: results = podman_secret_remove(module, executable, name) diff --git a/ansible_collections/containers/podman/plugins/modules/podman_volume.py b/ansible_collections/containers/podman/plugins/modules/podman_volume.py index 0b990354a..cb958cc50 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_volume.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_volume.py @@ -24,6 +24,8 @@ options: choices: - present - absent + - mounted + - unmounted - quadlet recreate: description: @@ -131,6 +133,7 @@ EXAMPLES = ''' ''' # noqa: F402 import json # noqa: F402 +import os # noqa: F402 from ansible.module_utils.basic import AnsibleModule # noqa: F402 from ansible.module_utils._text import to_bytes, to_native # noqa: F402 @@ -160,7 +163,7 @@ class PodmanVolumeModuleParams: Returns: list -- list of byte strings for Popen command """ - if self.action in ['delete']: + if self.action in ['delete', 'mount', 'unmount']: return self._simple_action() if self.action in ['create']: return self._create_action() @@ -169,6 +172,12 @@ class PodmanVolumeModuleParams: if self.action == 'delete': cmd = ['rm', '-f', self.params['name']] return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] + if self.action == 'mount': + cmd = ['mount', self.params['name']] + return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] + if self.action == 'unmount': + cmd = ['unmount', self.params['name']] + return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] def _create_action(self): cmd = [self.action, self.params['name']] @@ -326,6 +335,7 @@ class PodmanVolume: self.module = module self.name = name self.stdout, self.stderr = '', '' + self.mount_point = None self.info = self.get_info() self.version = self._get_podman_version() self.diff = {} @@ -380,7 +390,7 @@ class PodmanVolume: """Perform action with volume. Arguments: - action {str} -- action to perform - create, stop, delete + action {str} -- action to perform - create, delete, mount, unmout """ b_command = PodmanVolumeModuleParams(action, self.module.params, @@ -389,11 +399,14 @@ class PodmanVolume: ).construct_command_from_params() full_cmd = " ".join([self.module.params['executable'], 'volume'] + [to_native(i) for i in b_command]) + # check if running not from root + if os.getuid() != 0 and action == 'mount': + full_cmd = f"{self.module.params['executable']} unshare {full_cmd}" self.module.log("PODMAN-VOLUME-DEBUG: %s" % full_cmd) self.actions.append(full_cmd) if not self.module.check_mode: rc, out, err = self.module.run_command( - [self.module.params['executable'], b'volume'] + b_command, + full_cmd, expand_user_and_vars=False) self.stdout = out self.stderr = err @@ -401,6 +414,9 @@ class PodmanVolume: self.module.fail_json( msg="Can't %s volume %s" % (action, self.name), stdout=out, stderr=err) + # in case of mount/unmount, return path to the volume from stdout + if action in ['mount']: + self.mount_point = out.strip() def delete(self): """Delete the volume.""" @@ -410,6 +426,14 @@ class PodmanVolume: """Create the volume.""" self._perform_action('create') + def mount(self): + """Delete the volume.""" + self._perform_action('mount') + + def unmount(self): + """Create the volume.""" + self._perform_action('unmount') + def recreate(self): """Recreate the volume.""" self.delete() @@ -468,6 +492,8 @@ class PodmanVolumeManager: states_map = { 'present': self.make_present, 'absent': self.make_absent, + 'mounted': self.make_mount, + 'unmounted': self.make_unmount, 'quadlet': self.make_quadlet, } process_action = states_map[self.state] @@ -501,6 +527,26 @@ class PodmanVolumeManager: 'podman_actions': self.volume.actions}) self.module.exit_json(**self.results) + def make_mount(self): + """Run actions if desired state is 'mounted'.""" + if not self.volume.exists: + self.volume.create() + self.results['actions'].append('created %s' % self.volume.name) + self.volume.mount() + self.results['actions'].append('mounted %s' % self.volume.name) + if self.volume.mount_point: + self.results.update({'mount_point': self.volume.mount_point}) + self.update_volume_result() + + def make_unmount(self): + """Run actions if desired state is 'unmounted'.""" + if self.volume.exists: + self.volume.unmount() + self.results['actions'].append('unmounted %s' % self.volume.name) + self.update_volume_result() + else: + self.module.fail_json(msg="Volume %s does not exist!" % self.name) + def make_quadlet(self): results_update = create_quadlet_state(self.module, "volume") self.results.update(results_update) @@ -511,7 +557,7 @@ def main(): module = AnsibleModule( argument_spec=dict( state=dict(type='str', default="present", - choices=['present', 'absent', 'quadlet']), + choices=['present', 'absent', 'mounted', 'unmounted', 'quadlet']), name=dict(type='str', required=True), label=dict(type='dict', required=False), driver=dict(type='str', required=False), |