diff --git a/.github/workflows/galaxy_build.yml b/.github/workflows/galaxy_build.yml new file mode 100644 index 0000000..1a65f2c --- /dev/null +++ b/.github/workflows/galaxy_build.yml @@ -0,0 +1,33 @@ +name: Build and Push Docker Image + +on: [push] + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: my-docker-hub-namespace/my-docker-hub-repository + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ce9ec38 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "github-actions.workflows.pinned.workflows": [ + ".github/workflows/galaxy_build.yml" + ], + "ansible.python.interpreterPath": "/bin/python" +} \ No newline at end of file diff --git a/FILES.json b/FILES.json new file mode 100644 index 0000000..3162ffc --- /dev/null +++ b/FILES.json @@ -0,0 +1,418 @@ +{ + "files": [ + { + "name": ".", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "init", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", + "format": 1 + }, + { + "name": "ansible.cfg", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3778693a5812e76856af01260ffab7ac20e5085df08b40df57ab9217d7688806", + "format": 1 + }, + { + "name": "LICENSE", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986", + "format": 1 + }, + { + "name": "plugin_requirements.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "725b6f164b22194af91a12f26d723a840145e781333a6ab888073adaa7cba0dd", + "format": 1 + }, + { + "name": "plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/modules", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ffbb1879c4568465d0e6991da98214e2b8406d8423909615f9739102c4be950e", + "format": 1 + }, + { + "name": "requirements.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "837870eaf6e4820554b6d45dbbd6ffe3b162b2b45663fe3ccb4bbae513d31f98", + "format": 1 + }, + { + "name": ".github", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".github/workflows", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".github/workflows/galaxy_build.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d5228a19aaa0ff672613f5e1e19144b4dce265bf92fc994729615d3af8dfd711", + "format": 1 + }, + { + "name": ".vscode", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".vscode/settings.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d452ca019a0d0153f56fe94e50489817fec7543e9718021ccad9151551c775e6", + "format": 1 + }, + { + "name": "meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "meta/runtime.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "87e75c08f1d3fa24f3d2d0b4d6f6211bf420bcdea328a8d130d4e1b5ecae1fc4", + "format": 1 + }, + { + "name": "meta/execution-environment.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ab557b7c85d579f1e2cb9cdbefe1f569f8597ea30116a2f7c2e3a3ee3f7dcad5", + "format": 1 + }, + { + "name": "meta/requirements.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "roles", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/requirements.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4afbb4914354238ce63783d01164eb3d40d4db7a40611f6c939800d80a6baf60", + "format": 1 + }, + { + "name": "roles/install", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/install/README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cc751d75266f757a38189f15d54e548dc87706f14a39756995c27b49eaca8a07", + "format": 1 + }, + { + "name": "roles/install/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/install/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6db1f48df83eb02c41da019269558cb69c20a98337044574e2499179b681b765", + "format": 1 + }, + { + "name": "roles/install/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/install/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6f9874ab441648ce20f7d6a89187aec54da36b48aca2d40eb2eb1f84d4b39713", + "format": 1 + }, + { + "name": "roles/install/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/install/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7242a395a62f8660116ed87f0b7e47fcc2b3dde5ed2c714ba711c717bd6fa25d", + "format": 1 + }, + { + "name": "roles/set_zone", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/set_zone/README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cc751d75266f757a38189f15d54e548dc87706f14a39756995c27b49eaca8a07", + "format": 1 + }, + { + "name": "roles/set_zone/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/set_zone/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7242a395a62f8660116ed87f0b7e47fcc2b3dde5ed2c714ba711c717bd6fa25d", + "format": 1 + }, + { + "name": "roles/set_zone/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/set_zone/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6db1f48df83eb02c41da019269558cb69c20a98337044574e2499179b681b765", + "format": 1 + }, + { + "name": "roles/set_zone/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/set_zone/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a5354cf9a7f800d1d93252fac683fb05dcc3659c19b93204fa2ad1b23ad32cf5", + "format": 1 + }, + { + "name": "roles/set_zone/templates", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/set_zone/templates/forwardzone.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9f93ffb16cd223e0f9d3e1136fe281adea319d9fdbe6192dbbb28acdd98c71a3", + "format": 1 + }, + { + "name": "roles/set_zone/templates/reversezone.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bc534bc00ab7832fc3d2c8b5451ce0b7c54e4827f47ea1da6ed03dd0cd82cabb", + "format": 1 + }, + { + "name": "roles/config", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/config/README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cc751d75266f757a38189f15d54e548dc87706f14a39756995c27b49eaca8a07", + "format": 1 + }, + { + "name": "roles/config/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/config/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "04cfd9f8a3050f924ca4dd9daab6540dc8da41454e21dafd9ff5d236b4e2e8d8", + "format": 1 + }, + { + "name": "roles/config/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/config/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6db1f48df83eb02c41da019269558cb69c20a98337044574e2499179b681b765", + "format": 1 + }, + { + "name": "roles/config/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/config/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "90452aa59d7c53dd73ac4a39e5d8e634984893e0cf919a7de6f3456870e0edaa", + "format": 1 + }, + { + "name": "roles/config/templates", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/config/templates/named.conf.options.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d55ab043cca9066c08c62d6a58448165bda8f6b4ab6370a4e6804192f447a9e3", + "format": 1 + }, + { + "name": "roles/config/templates/named.conf.local.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6871092987d0cec3bd3e6808acfb64ad0b433b569f3c04df2ff7516cc3ceee9e", + "format": 1 + }, + { + "name": "roles/update", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/update/README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cc751d75266f757a38189f15d54e548dc87706f14a39756995c27b49eaca8a07", + "format": 1 + }, + { + "name": "roles/update/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/update/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7242a395a62f8660116ed87f0b7e47fcc2b3dde5ed2c714ba711c717bd6fa25d", + "format": 1 + }, + { + "name": "roles/update/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/update/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6db1f48df83eb02c41da019269558cb69c20a98337044574e2499179b681b765", + "format": 1 + }, + { + "name": "roles/update/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "roles/update/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cb3e5333d5e8d9bbbb617900af144ae78326ed9d83b471e6e30c2293bec6acef", + "format": 1 + } + ], + "format": 1 +} \ No newline at end of file diff --git a/MANIFEST.json b/MANIFEST.json new file mode 100644 index 0000000..a500289 --- /dev/null +++ b/MANIFEST.json @@ -0,0 +1,39 @@ +{ + "collection_info": { + "namespace": "ji_podhead", + "name": "host_prototypes", + "version": "0.0.5", + "authors": [ + "ji-podhead (https://https://www.github.com/ji-podhead)" + ], + "readme": "README.md", + "tags": [ + "host_prototypes", + "random_cloud_init_config", + "random_mac_and_random_pass", + "random_ssh_keys", + "dynamic_host_creation" + + ], + "description": "a little collection that creates hosts from prototypes. it also creates private,public keys, random mac and random pass", + "license": [ + "MIT" + ], + "license_file": null, + "dependencies": { + "community.general": ">=0.0.0" + }, + "repository": "https://www.github.com/the-pod-shop/host_prototypes", + "documentation": null, + "homepage": null, + "issues": null + }, + "file_manifest_file": { + "name": "FILES.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4dba77037edf0a9797cb6442131087053481bb08275b2bc024b03c8ec315b777", + "format": 1 + }, + "format": 1 +} \ No newline at end of file diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..3c89542 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,6 @@ +[defaults] +enable_plugins = host_list, script, auto, yaml, ini, toml, community.hashi_vault +inventory = inventory.yml +private_key_file = ~/.ssh/id_ed25519 +plugin_search_path = plugins + diff --git a/galaxy.yml b/galaxy.yml new file mode 100644 index 0000000..85df06f --- /dev/null +++ b/galaxy.yml @@ -0,0 +1,21 @@ +namespace: "ji_podhead" +name: "lil_bind" +description: "a little collection that creates podman bind 9 container and lets you configure zones" +version: "0.0.1" +readme: "README.md" +authors: + - "ji-podhead (https://https://www.github.com/ji-podhead)" +dependencies: + community.general: ">=0.0.0" +license: + - "MIT" +tags: + - dns + - bind9 + - podman + - baremetal + +repository: "https://www.github.com/the-pod-shop/lil_bind" + + + diff --git a/init b/init new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/init @@ -0,0 +1 @@ + diff --git a/ji_podhead-lil_bind-0.0.1.tar.gz b/ji_podhead-lil_bind-0.0.1.tar.gz new file mode 100644 index 0000000..e15b89f Binary files /dev/null and b/ji_podhead-lil_bind-0.0.1.tar.gz differ diff --git a/meta/execution-environment.yml b/meta/execution-environment.yml new file mode 100644 index 0000000..663349a --- /dev/null +++ b/meta/execution-environment.yml @@ -0,0 +1,6 @@ +#/meta/execution-environment.yml +--- +dependencies: + + python: meta/requirements.txt +version: 1 \ No newline at end of file diff --git a/meta/requirements.txt b/meta/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/meta/runtime.yml b/meta/runtime.yml new file mode 100644 index 0000000..3e513cc --- /dev/null +++ b/meta/runtime.yml @@ -0,0 +1,3 @@ +--- +#requires_python: ">=3.8.0" +requires_ansible: ">=2.10" diff --git a/plugin_requirements.json b/plugin_requirements.json new file mode 100644 index 0000000..7f44983 --- /dev/null +++ b/plugin_requirements.json @@ -0,0 +1,11 @@ +{ + "python": { + "redfish": "latest", + "pyyaml": "latest", + "pandas": "latest", + "matplotlib": "latest" + }, + "apt": { + "ansible": "latest" + } +} \ No newline at end of file diff --git a/plugins/become/podman_unshare.py b/plugins/become/podman_unshare.py new file mode 100644 index 0000000..6453f23 --- /dev/null +++ b/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/plugins/connection/__init__.py b/plugins/connection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/connection/buildah.py b/plugins/connection/buildah.py new file mode 100644 index 0000000..69fc63c --- /dev/null +++ b/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/plugins/connection/podman.py b/plugins/connection/podman.py new file mode 100644 index 0000000..2ade918 --- /dev/null +++ b/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/plugins/module_utils/__init__.py b/plugins/module_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/module_utils/podman/__init__.py b/plugins/module_utils/podman/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/module_utils/podman/common.py b/plugins/module_utils/podman/common.py new file mode 100644 index 0000000..ec06af2 --- /dev/null +++ b/plugins/module_utils/podman/common.py @@ -0,0 +1,432 @@ +# 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) + +ARGUMENTS_OPTS_DICT = { + '--attach': ['--attach', '-a'], + '--cpu-shares': ['--cpu-shares', '-c'], + '--detach': ['--detach', '-d'], + '--env': ['--env', '-e'], + '--hostname': ['--hostname', '-h'], + '--interactive': ['--interactive', '-i'], + '--label': ['--label', '-l'], + '--memory': ['--memory', '-m'], + '--network': ['--network', '--net'], + '--publish': ['--publish', '-p'], + '--publish-all': ['--publish-all', '-P'], + '--quiet': ['--quiet', '-q'], + '--tty': ['--tty', '-t'], + '--user': ['--user', '-u'], + '--volume': ['--volume', '-v'], + '--workdir': ['--workdir', '-w'], +} + + +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('restart_sec') is not None: + command.extend(['--restart-sec=%s' % sysconf['restart_sec']]) + if (sysconf.get('stop_timeout') is not None) or (sysconf.get('time') is not None): + # Select correct parameter name based on version + arg_name = 'stop-timeout' if gt4ver else 'time' + arg_value = sysconf.get('stop_timeout') if sysconf.get('stop_timeout') is not None else sysconf.get('time') + command.extend(['--%s=%s' % (arg_name, arg_value)]) + if sysconf.get('start_timeout') is not None: + command.extend(['--start-timeout=%s' % sysconf['start_timeout']]) + 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 compare_systemd_file_content(file_path, file_content): + if not os.path.exists(file_path): + # File does not exist, so all lines in file_content are different + return '', file_content + # Read the file + with open(file_path, 'r') as unit_file: + current_unit_file_content = unit_file.read() + + # Function to remove comments from file content + def remove_comments(content): + return "\n".join([line for line in content.splitlines() if not line.startswith('#')]) + + # Remove comments from both file contents before comparison + current_unit_file_content_nocmnt = remove_comments(current_unit_file_content) + unit_content_nocmnt = remove_comments(file_content) + if current_unit_file_content_nocmnt == unit_content_nocmnt: + return None + + # Get the different lines between the two contents + diff_in_file = [line + for line in unit_content_nocmnt.splitlines() + if line not in current_unit_file_content_nocmnt.splitlines()] + diff_in_string = [line + for line in current_unit_file_content_nocmnt.splitlines() + if line not in unit_content_nocmnt.splitlines()] + + return diff_in_string, diff_in_file + + +def generate_systemd(module, module_params, name, version): + result = { + 'changed': False, + 'systemd': {}, + 'diff': {}, + } + 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) + if sysconf: + module.fail_json(msg="Error generating systemd: %s" % err) + return result + else: + try: + data = json.loads(systemd) + result['systemd'] = data + if sysconf.get('path'): + full_path = os.path.expanduser(sysconf['path']) + if not os.path.exists(full_path): + os.makedirs(full_path) + result['changed'] = True + 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" + if not os.path.exists(os.path.join(full_path, file_name)): + result['changed'] = True + if result['diff'].get('before') is None: + result['diff'] = {'before': {}, 'after': {}} + result['diff']['before'].update( + {'systemd_{file_name}.service'.format(file_name=file_name): ''}) + result['diff']['after'].update( + {'systemd_{file_name}.service'.format(file_name=file_name): file_content}) + + else: + diff_ = compare_systemd_file_content(os.path.join(full_path, file_name), file_content) + if diff_: + result['changed'] = True + if result['diff'].get('before') is None: + result['diff'] = {'before': {}, 'after': {}} + result['diff']['before'].update( + {'systemd_{file_name}.service'.format(file_name=file_name): "\n".join(diff_[0])}) + result['diff']['after'].update( + {'systemd_{file_name}.service'.format(file_name=file_name): "\n".join(diff_[1])}) + with open(os.path.join(full_path, file_name), 'w') as f: + f.write(file_content) + diff_before = "\n".join( + ["{j} - {k}".format(j=j, k=k) + for j, k in result['diff'].get('before', {}).items() if 'PIDFile' not in k]).strip() + diff_after = "\n".join( + ["{j} - {k}".format(j=j, k=k) + for j, k in result['diff'].get('after', {}).items() if 'PIDFile' not in k]).strip() + if diff_before or diff_after: + result['diff']['before'] = diff_before + "\n" + result['diff']['after'] = diff_after + "\n" + else: + result['diff'] = {} + return result + except Exception as e: + module.log( + "PODMAN-CONTAINER-DEBUG: Error writing systemd: %s" % e) + if sysconf: + module.fail_json(msg="Error writing systemd: %s" % e) + return result + + +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]) + + +def get_podman_version(module, fail=True): + executable = module.params['executable'] if module.params['executable'] else 'podman' + rc, out, err = module.run_command( + [executable, b'--version']) + if rc != 0 or not out or "version" not in out: + if fail: + module.fail_json(msg="'%s --version' run failed! Error: %s" % + (executable, err)) + return None + return out.split("version")[1].strip() + + +def createcommand(argument, info_config, boolean_type=False): + """Returns list of values for given argument from CreateCommand + from Podman container inspect output. + + Args: + argument (str): argument name + info_config (dict): dictionary with container info + boolean_type (bool): if True, then argument is boolean type + + Returns: + + all_values: list of values for given argument from createcommand + """ + if "createcommand" not in info_config: + return [] + cr_com = info_config["createcommand"] + argument_values = ARGUMENTS_OPTS_DICT.get(argument, [argument]) + all_values = [] + # Remove command args from the list + container_cmd = info_config.get("cmd") + if container_cmd and container_cmd == cr_com[-len(container_cmd):]: + cr_com = cr_com[:-len(container_cmd)] + for arg in argument_values: + for ind, cr_opt in enumerate(cr_com): + if arg == cr_opt: + if boolean_type: + # This is a boolean argument and doesn't have value + return [True] + if not cr_com[ind + 1].startswith("-"): + # This is a key=value argument + all_values.append(cr_com[ind + 1]) + else: + # This is also a false/true switching argument + return [True] + if cr_opt.startswith("%s=" % arg): + all_values.append(cr_opt.split("=", 1)[1]) + return all_values + + +def diff_generic(params, info_config, module_arg, cmd_arg, boolean_type=False): + """ + Generic diff function for module arguments from CreateCommand + in Podman inspection output. + + Args: + params (dict): module parameters + info_config (dict): dictionary with container info + module_arg (str): module argument name + cmd_arg (str): command line argument name + boolean_type (bool): if True, then argument is boolean type + + Returns: + bool: True if there is a difference, False otherwise + + """ + before = createcommand(cmd_arg, info_config, boolean_type=boolean_type) + if before == []: + before = None + after = params[module_arg] + if boolean_type and (before, after) in [(None, False), (False, None)]: + before, after = False, False + return before, after + if before is None and after is None: + return before, after + if after is not None: + if isinstance(after, list): + after = ",".join(sorted([str(i).lower() for i in after])) + if before: + before = ",".join(sorted([str(i).lower() for i in before])) + else: + before = '' + elif isinstance(after, dict): + after = ",".join(sorted( + [str(k).lower() + "=" + str(v).lower() for k, v in after.items() if v is not None])) + if before: + before = ",".join(sorted([j.lower() for j in before])) + else: + before = '' + elif isinstance(after, bool): + after = str(after).capitalize() + if before is not None: + before = str(before[0]).capitalize() + elif isinstance(after, int): + after = str(after) + if before is not None: + before = str(before[0]) + else: + before = before[0] if before else None + else: + before = ",".join(sorted(before)) if len(before) > 1 else before[0] + return before, after diff --git a/plugins/module_utils/podman/podman_container_lib.py b/plugins/module_utils/podman/podman_container_lib.py new file mode 100644 index 0000000..febc19e --- /dev/null +++ b/plugins/module_utils/podman/podman_container_lib.py @@ -0,0 +1,1919 @@ +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 diff_generic +from ansible_collections.containers.podman.plugins.module_utils.podman.common import createcommand +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import ContainerQuadlet + + +__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', 'quadlet']), + image=dict(type='str'), + annotation=dict(type='dict'), + arch=dict(type='str'), + attach=dict(type='list', elements='str', choices=['stdout', 'stderr', 'stdin']), + authfile=dict(type='path'), + blkio_weight=dict(type='int'), + blkio_weight_device=dict(type='dict'), + cap_add=dict(type='list', elements='str', aliases=['capabilities']), + cap_drop=dict(type='list', elements='str'), + cgroup_conf=dict(type='dict'), + cgroup_parent=dict(type='path'), + cgroupns=dict(type='str'), + cgroups=dict(type='str'), + chrootdirs=dict(type='str'), + cidfile=dict(type='path'), + cmd_args=dict(type='list', elements='str'), + conmon_pidfile=dict(type='path'), + command=dict(type='raw'), + cpu_period=dict(type='int'), + cpu_quota=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'), + decryption_key=dict(type='str', no_log=False), + delete_depend=dict(type='bool'), + delete_time=dict(type='str'), + delete_volumes=dict(type='bool'), + 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_cgroup_rule=dict(type='str'), + device_read_bps=dict(type='list', elements='str'), + device_read_iops=dict(type='list', elements='str'), + device_write_bps=dict(type='list', elements='str'), + 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='list', elements='path', aliases=['env_files']), + env_host=dict(type='bool'), + env_merge=dict(type='dict'), + etc_hosts=dict(type='dict', aliases=['add_hosts']), + expose=dict(type='list', elements='str', aliases=[ + 'exposed', 'exposed_ports']), + force_restart=dict(type='bool', default=False, + aliases=['restart']), + force_delete=dict(type='bool', default=True), + generate_systemd=dict(type='dict', default={}), + gidmap=dict(type='list', elements='str'), + gpus=dict(type='str'), + group_add=dict(type='list', elements='str', aliases=['groups']), + group_entry=dict(type='str'), + healthcheck=dict(type='str', aliases=['health_cmd']), + healthcheck_interval=dict(type='str', aliases=['health_interval']), + healthcheck_retries=dict(type='int', aliases=['health_retries']), + healthcheck_start_period=dict(type='str', aliases=['health_start_period']), + health_startup_cmd=dict(type='str'), + health_startup_interval=dict(type='str'), + health_startup_retries=dict(type='int'), + health_startup_success=dict(type='int'), + health_startup_timeout=dict(type='str'), + healthcheck_timeout=dict(type='str', aliases=['health_timeout']), + healthcheck_failure_action=dict(type='str', choices=[ + 'none', 'kill', 'restart', 'stop'], aliases=['health_on_failure']), + hooks_dir=dict(type='list', elements='str'), + hostname=dict(type='str'), + hostuser=dict(type='str'), + http_proxy=dict(type='bool'), + image_volume=dict(type='str', choices=['bind', 'tmpfs', 'ignore']), + image_strict=dict(type='bool', default=False), + init=dict(type='bool'), + init_ctr=dict(type='str', choices=['once', 'always']), + init_path=dict(type='str'), + interactive=dict(type='bool'), + ip=dict(type='str'), + ip6=dict(type='str'), + ipc=dict(type='str', aliases=['ipc_mode']), + kernel_memory=dict(type='str'), + label=dict(type='dict', aliases=['labels']), + 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', aliases=['network_alias']), + no_healthcheck=dict(type='bool'), + no_hosts=dict(type='bool'), + oom_kill_disable=dict(type='bool'), + oom_score_adj=dict(type='int'), + os=dict(type='str'), + passwd=dict(type='bool', no_log=False), + passwd_entry=dict(type='str', no_log=False), + personality=dict(type='str'), + pid=dict(type='str', aliases=['pid_mode']), + pid_file=dict(type='path'), + pids_limit=dict(type='str'), + platform=dict(type='str'), + pod=dict(type='str'), + pod_id_file=dict(type='path'), + preserve_fd=dict(type='list', elements='str'), + preserve_fds=dict(type='str'), + privileged=dict(type='bool'), + publish=dict(type='list', elements='str', aliases=[ + 'ports', 'published', 'published_ports']), + publish_all=dict(type='bool'), + pull=dict(type='str', choices=['always', 'missing', 'never', 'newer']), + quadlet_dir=dict(type='path'), + quadlet_filename=dict(type='str'), + quadlet_options=dict(type='list', elements='str'), + rdt_class=dict(type='str'), + read_only=dict(type='bool'), + read_only_tmpfs=dict(type='bool'), + recreate=dict(type='bool', default=False), + requires=dict(type='list', elements='str'), + restart_policy=dict(type='str'), + restart_time=dict(type='str'), + retry=dict(type='int'), + retry_delay=dict(type='str'), + rm=dict(type='bool', aliases=['remove', 'auto_remove']), + rmi=dict(type='bool'), + rootfs=dict(type='bool'), + seccomp_policy=dict(type='str'), + secrets=dict(type='list', elements='str', no_log=True), + sdnotify=dict(type='str'), + security_opt=dict(type='list', elements='str'), + shm_size=dict(type='str'), + shm_size_systemd=dict(type='str'), + sig_proxy=dict(type='bool'), + stop_signal=dict(type='int'), + stop_timeout=dict(type='int'), + stop_time=dict(type='str'), + subgidname=dict(type='str'), + subuidname=dict(type='str'), + sysctl=dict(type='dict'), + systemd=dict(type='str'), + timeout=dict(type='int'), + timezone=dict(type='str'), + tls_verify=dict(type='bool'), + tmpfs=dict(type='dict'), + tty=dict(type='bool'), + uidmap=dict(type='list', elements='str'), + ulimit=dict(type='list', elements='str', aliases=['ulimits']), + umask=dict(type='str'), + unsetenv=dict(type='list', elements='str'), + unsetenv_all=dict(type='bool'), + user=dict(type='str'), + userns=dict(type='str', aliases=['userns_mode']), + uts=dict(type='str'), + variant=dict(type='str'), + volume=dict(type='list', elements='str', aliases=['volumes']), + volumes_from=dict(type='list', elements='str'), + workdir=dict(type='str', aliases=['working_dir']) +) + + +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): + + def complete_params(cmd): + if self.params['attach'] and self.action == 'start': + cmd.append('--attach') + if self.params['detach'] is False and self.action == 'start' and '--attach' not in cmd: + cmd.append('--attach') + if self.params['detach_keys'] and self.action == 'start': + cmd += ['--detach-keys', self.params['detach_keys']] + if self.params['sig_proxy'] and self.action == 'start': + cmd.append('--sig-proxy') + if self.params['stop_time'] and self.action == 'stop': + cmd += ['--time', self.params['stop_time']] + if self.params['restart_time'] and self.action == 'restart': + cmd += ['--time', self.params['restart_time']] + if self.params['delete_depend'] and self.action == 'delete': + cmd.append('--depend') + if self.params['delete_time'] and self.action == 'delete': + cmd += ['--time', self.params['delete_time']] + if self.params['delete_volumes'] and self.action == 'delete': + cmd.append('--volumes') + if self.params['force_delete'] and self.action == 'delete': + cmd.append('--force') + return cmd + + if self.action in ['stop', 'start', 'restart']: + cmd = complete_params([self.action]) + [self.params['name']] + return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] + + if self.action == 'delete': + cmd = complete_params(['rm']) + [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_arch(self, c): + return c + ['--arch=%s' % self.params['arch']] + + def addparam_attach(self, c): + for attach in self.params['attach']: + c += ['--attach=%s' % attach] + 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_cgroup_conf(self, c): + for cgroup in self.params['cgroup_conf'].items(): + c += ['--cgroup-conf=%s' % '='.join([str(i) for i in cgroup])] + return c + + def addparam_chrootdirs(self, c): + return c + ['--chrootdirs', self.params['chrootdirs']] + + def addparam_cidfile(self, c): + return c + ['--cidfile', self.params['cidfile']] + + 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_quota(self, c): + return c + ['--cpu-quota', self.params['cpu_quota']] + + 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_decryption_key(self, c): + return c + ['--decryption-key=%s' % self.params['decryption_key']] + + def addparam_detach(self, c): + # Remove detach from create command and don't set if attach is true + if self.action == 'create' or self.params['attach']: + return 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_cgroup_rule(self, c): + return c + ['--device-cgroup-rule=%s' % self.params['device_cgroup_rule']] + + def addparam_device_read_bps(self, c): + for dev in self.params['device_read_bps']: + c += ['--device-read-bps', dev] + 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=%s' % 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): + for env_file in self.params['env_file']: + c += ['--env-file', env_file] + return c + + def addparam_env_host(self, c): + self.check_version('--env-host', minv='1.5.0') + return c + ['--env-host=%s' % self.params['env_host']] + + # Exception for etc_hosts and add-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_env_merge(self, c): + for env_merge in self.params['env_merge'].items(): + c += ['--env-merge', + b"=".join([to_bytes(k, errors='surrogate_or_strict') + for k in env_merge])] + return c + + def addparam_expose(self, c): + for exp in self.params['expose']: + c += ['--expose', exp] + return c + + def addparam_gidmap(self, c): + for gidmap in self.params['gidmap']: + c += ['--gidmap', gidmap] + return c + + def addparam_gpus(self, c): + return c + ['--gpus', self.params['gpus']] + + def addparam_group_add(self, c): + for g in self.params['group_add']: + c += ['--group-add', g] + return c + + def addparam_group_entry(self, c): + return c + ['--group-entry', self.params['group_entry']] + + # Exception for healthcheck and healthcheck-command + 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_health_startup_cmd(self, c): + return c + ['--health-startup-command', self.params['health_startup_cmd']] + + def addparam_health_startup_interval(self, c): + return c + ['--health-startup-interval', self.params['health_startup_interval']] + + def addparam_healthcheck_timeout(self, c): + return c + ['--healthcheck-timeout', + self.params['healthcheck_timeout']] + + def addparam_health_startup_retries(self, c): + return c + ['--health-startup-retries', self.params['health_startup_retries']] + + def addparam_health_startup_success(self, c): + return c + ['--health-startup-success', self.params['health_startup_success']] + + def addparam_health_startup_timeout(self, c): + return c + ['--health-startup-timeout', self.params['health_startup_timeout']] + + def addparam_healthcheck_failure_action(self, c): + return c + ['--health-on-failure', + self.params['healthcheck_failure_action']] + + 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_hostuser(self, c): + return c + ['--hostuser', self.params['hostuser']] + + 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_init_ctr(self, c): + return c + ['--init-ctr', self.params['init_ctr']] + + def addparam_interactive(self, c): + return c + ['--interactive=%s' % self.params['interactive']] + + def addparam_ip(self, c): + return c + ['--ip', self.params['ip']] + + def addparam_ip6(self, c): + return c + ['--ip6', self.params['ip6']] + + def addparam_ipc(self, c): + return c + ['--ipc', self.params['ipc']] + + 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'])] + + # Exception for network_aliases and network-alias + 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_no_healthcheck(self, c): + if self.params['no_healthcheck']: + c += ['--no-healthcheck'] + return c + + def addparam_oom_kill_disable(self, c): + return c + ['--oom-kill-disable=%s' % self.params['oom_kill_disable']] + + def addparam_oom_score_adj(self, c): + return c + ['--oom-score-adj', self.params['oom_score_adj']] + + def addparam_os(self, c): + return c + ['--os', self.params['os']] + + def addparam_passwd(self, c): + if self.params['passwd']: + c += ['--passwd'] + return c + + def addparam_passwd_entry(self, c): + return c + ['--passwd-entry', self.params['passwd_entry']] + + def addparam_personality(self, c): + return c + ['--personality', self.params['personality']] + + def addparam_pid(self, c): + return c + ['--pid', self.params['pid']] + + def addparam_pid_file(self, c): + return c + ['--pid-file', self.params['pid_file']] + + def addparam_pids_limit(self, c): + return c + ['--pids-limit', self.params['pids_limit']] + + def addparam_platform(self, c): + return c + ['--platform', self.params['platform']] + + def addparam_pod(self, c): + return c + ['--pod', self.params['pod']] + + def addparam_pod_id_file(self, c): + return c + ['--pod-id-file', self.params['pod_id_file']] + + def addparam_preserve_fd(self, c): + for fd in self.params['preserve_fd']: + c += ['--preserve-fd', fd] + return c + + def addparam_preserve_fds(self, c): + return c + ['--preserve-fds', self.params['preserve_fds']] + + def addparam_privileged(self, c): + return c + ['--privileged=%s' % self.params['privileged']] + + 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_pull(self, c): + return c + ['--pull=%s' % self.params['pull']] + + def addparam_rdt_class(self, c): + return c + ['--rdt-class', self.params['rdt_class']] + + def addparam_read_only(self, c): + return c + ['--read-only=%s' % self.params['read_only']] + + 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'])] + + # Exception for restart_policy and restart + def addparam_restart_policy(self, c): + return c + ['--restart=%s' % self.params['restart_policy']] + + def addparam_retry(self, c): + return c + ['--retry', self.params['retry']] + + def addparam_retry_delay(self, c): + return c + ['--retry-delay', self.params['retry_delay']] + + def addparam_rm(self, c): + if self.params['rm']: + c += ['--rm'] + return c + + def addparam_rmi(self, c): + if self.params['rmi']: + c += ['--rmi'] + return c + + def addparam_rootfs(self, c): + return c + ['--rootfs=%s' % self.params['rootfs']] + + def addparam_sdnotify(self, c): + return c + ['--sdnotify=%s' % self.params['sdnotify']] + + def addparam_seccomp_policy(self, c): + return c + ['--seccomp-policy', self.params['seccomp_policy']] + + # Exception for secrets and secret + 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_shm_size_systemd(self, c): + return c + ['--shm-size-systemd', self.params['shm_size_systemd']] + + def addparam_sig_proxy(self, c): + return c + ['--sig-proxy=%s' % self.params['sig_proxy']] + + 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_timeout(self, c): + return c + ['--timeout', self.params['timeout']] + + # Exception for timezone and tz + def addparam_timezone(self, c): + return c + ['--tz=%s' % self.params['timezone']] + + def addparam_tls_verify(self, c): + return c + ['--tls-verify=%s' % self.params['tls_verify']] + + def addparam_tmpfs(self, c): + for tmpfs in self.params['tmpfs'].items(): + c += ['--tmpfs', ':'.join(tmpfs)] + return c + + def addparam_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_umask(self, c): + return c + ['--umask', self.params['umask']] + + def addparam_unsetenv(self, c): + for unsetenv in self.params['unsetenv']: + c += ['--unsetenv', unsetenv] + return c + + def addparam_unsetenv_all(self, c): + if self.params['unsetenv_all']: + c += ['--unsetenv-all'] + return c + + def addparam_user(self, c): + return c + ['--user', self.params['user']] + + def addparam_userns(self, c): + return c + ['--userns', self.params['userns']] + + def addparam_uts(self, c): + return c + ['--uts', self.params['uts']] + + def addparam_variant(self, c): + return c + ['--variant', self.params['variant']] + + def addparam_volume(self, c): + for vol in self.params['volume']: + if vol: + 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 = { + "detach": True, + "log_level": "error", + "tty": False, + } + + 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('3.0.0')): + self.defaults['log_level'] = "warning" + 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 _diff_generic(self, module_arg, cmd_arg, boolean_type=False): + """ + Generic diff function for module arguments from CreateCommand + in Podman inspection output. + + Args: + module_arg (str): module argument name + cmd_arg (str): command line argument name + boolean_type (bool): if True, then argument is boolean type + + Returns: + bool: True if there is a difference, False otherwise + + """ + info_config = self.info["config"] + before, after = diff_generic(self.params, info_config, module_arg, cmd_arg, boolean_type) + return self._diff_update_and_compare(module_arg, before, after) + + def diffparam_annotation(self): + before = self.info['config']['annotations'] or {} + after = before.copy() + if self.module_params['annotation'] is not None: + after.update(self.params['annotation']) + return self._diff_update_and_compare('annotation', before, after) + + def diffparam_arch(self): + return self._diff_generic('arch', '--arch') + + def diffparam_authfile(self): + return self._diff_generic('authfile', '--authfile') + + def diffparam_blkio_weight(self): + return self._diff_generic('blkio_weight', '--blkio-weight') + + def diffparam_blkio_weight_device(self): + return self._diff_generic('blkio_weight_device', '--blkio-weight-device') + + def diffparam_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_conf(self): + return self._diff_generic('cgroup_conf', '--cgroup-conf') + + def diffparam_cgroup_parent(self): + return self._diff_generic('cgroup_parent', '--cgroup-parent') + + def diffparam_cgroupns(self): + return self._diff_generic('cgroupns', '--cgroupns') + + # Disabling idemotency check for cgroups as it's added by systemd generator + # https://github.com/containers/ansible-podman-collections/issues/775 + # def diffparam_cgroups(self): + # return self._diff_generic('cgroups', '--cgroups') + + def diffparam_chrootdirs(self): + return self._diff_generic('chrootdirs', '--chrootdirs') + + # Disabling idemotency check for cidfile as it's added by systemd generator + # https://github.com/containers/ansible-podman-collections/issues/775 + # def diffparam_cidfile(self): + # return self._diff_generic('cidfile', '--cidfile') + + def diffparam_command(self): + def _join_quotes(com_list): + result = [] + buffer = [] + in_quotes = False + + for item in com_list: + if item.startswith('"') and not in_quotes: + buffer.append(item) + in_quotes = True + elif item.endswith('"') and in_quotes: + buffer.append(item) + result.append(' '.join(buffer).strip('"')) + buffer = [] + in_quotes = False + elif in_quotes: + buffer.append(item) + else: + result.append(item) + if in_quotes: + result.extend(buffer) + + return result + + # 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'] + before = _join_quotes(before) + if isinstance(after, list): + after = [str(i) for i in after] + if isinstance(after, str): + after = shlex.split(after) + return self._diff_update_and_compare('command', before, after) + return False + + def diffparam_conmon_pidfile(self): + return self._diff_generic('conmon_pidfile', '--conmon-pidfile') + + def diffparam_cpu_period(self): + return self._diff_generic('cpu_period', '--cpu-period') + + def diffparam_cpu_quota(self): + return self._diff_generic('cpu_quota', '--cpu-quota') + + def diffparam_cpu_rt_period(self): + return self._diff_generic('cpu_rt_period', '--cpu-rt-period') + + def diffparam_cpu_rt_runtime(self): + return self._diff_generic('cpu_rt_runtime', '--cpu-rt-runtime') + + def diffparam_cpu_shares(self): + return self._diff_generic('cpu_shares', '--cpu-shares') + + def diffparam_cpus(self): + return self._diff_generic('cpus', '--cpus') + + def diffparam_cpuset_cpus(self): + return self._diff_generic('cpuset_cpus', '--cpuset-cpus') + + def diffparam_cpuset_mems(self): + return self._diff_generic('cpuset_mems', '--cpuset-mems') + + def diffparam_decryption_key(self): + return self._diff_generic('decryption_key', '--decryption-key') + + def diffparam_device(self): + return self._diff_generic('device', '--device') + + def diffparam_device_cgroup_rule(self): + return self._diff_generic('device_cgroup_rule', '--device-cgroup-rule') + + def diffparam_device_read_bps(self): + return self._diff_generic('device_read_bps', '--device-read-bps') + + def diffparam_device_read_iops(self): + return self._diff_generic('device_read_iops', '--device-read-iops') + + def diffparam_device_write_bps(self): + return self._diff_generic('device_write_bps', '--device-write-bps') + + def diffparam_device_write_iops(self): + return self._diff_generic('device_write_iops', '--device-write-iops') + + def diffparam_dns(self): + return self._diff_generic('dns', '--dns') + + def diffparam_dns_option(self): + return self._diff_generic('dns_option', '--dns-option') + + def diffparam_dns_search(self): + return self._diff_generic('dns_search', '--dns-search') + + def diffparam_env(self): + return self._diff_generic('env', '--env') + + def diffparam_env_file(self): + return self._diff_generic('env_file', '--env-file') + + def diffparam_env_merge(self): + return self._diff_generic('env_merge', '--env-merge') + + def diffparam_env_host(self): + return self._diff_generic('env_host', '--env-host') + + def diffparam_etc_hosts(self): + if self.info['hostconfig']['extrahosts']: + before = dict([i.split(":", 1) + for i in self.info['hostconfig']['extrahosts']]) + else: + before = {} + after = self.params['etc_hosts'] or {} + return self._diff_update_and_compare('etc_hosts', before, after) + + def diffparam_expose(self): + return self._diff_generic('expose', '--expose') + + def diffparam_gidmap(self): + return self._diff_generic('gidmap', '--gidmap') + + def diffparam_gpus(self): + return self._diff_generic('gpus', '--gpus') + + def diffparam_group_add(self): + return self._diff_generic('group_add', '--group-add') + + def diffparam_group_entry(self): + return self._diff_generic('group_entry', '--group-entry') + + # Healthcheck is only defined in container config if a healthcheck + # was configured; otherwise the config key isn't part of the config. + def diffparam_healthcheck(self): + before = '' + 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. + if len(self.info['config']['healthcheck']['test']) > 1: + before = self.info['config']['healthcheck']['test'][1] + after = self.params['healthcheck'] or before + return self._diff_update_and_compare('healthcheck', before, after) + + def diffparam_healthcheck_failure_action(self): + if 'healthcheckonfailureaction' in self.info['config']: + before = self.info['config']['healthcheckonfailureaction'] + else: + before = '' + after = self.params['healthcheck_failure_action'] or before + return self._diff_update_and_compare('healthcheckonfailureaction', before, after) + + def diffparam_healthcheck_interval(self): + return self._diff_generic('healthcheck_interval', '--healthcheck-interval') + + def diffparam_healthcheck_retries(self): + return self._diff_generic('healthcheck_retries', '--healthcheck-retries') + + def diffparam_healthcheck_start_period(self): + return self._diff_generic('healthcheck_start_period', '--healthcheck-start-period') + + def diffparam_health_startup_cmd(self): + return self._diff_generic('health_startup_cmd', '--health-startup-cmd') + + def diffparam_health_startup_interval(self): + return self._diff_generic('health_startup_interval', '--health-startup-interval') + + def diffparam_health_startup_retries(self): + return self._diff_generic('health_startup_retries', '--health-startup-retries') + + def diffparam_health_startup_success(self): + return self._diff_generic('health_startup_success', '--health-startup-success') + + def diffparam_health_startup_timeout(self): + return self._diff_generic('health_startup_timeout', '--health-startup-timeout') + + def diffparam_healthcheck_timeout(self): + return self._diff_generic('healthcheck_timeout', '--healthcheck-timeout') + + def diffparam_hooks_dir(self): + return self._diff_generic('hooks_dir', '--hooks-dir') + + def diffparam_hostname(self): + return self._diff_generic('hostname', '--hostname') + + def diffparam_hostuser(self): + return self._diff_generic('hostuser', '--hostuser') + + def diffparam_http_proxy(self): + return self._diff_generic('http_proxy', '--http-proxy') + + def diffparam_image(self): + before_id = self.info['image'] or self.info['rootfs'] + 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_image_volume(self): + return self._diff_generic('image_volume', '--image-volume') + + def diffparam_init(self): + return self._diff_generic('init', '--init', boolean_type=True) + + def diffparam_init_ctr(self): + return self._diff_generic('init_ctr', '--init-ctr') + + def diffparam_init_path(self): + return self._diff_generic('init_path', '--init-path') + + def diffparam_interactive(self): + return self._diff_generic('interactive', '--interactive') + + def diffparam_ip(self): + return self._diff_generic('ip', '--ip') + + def diffparam_ip6(self): + return self._diff_generic('ip6', '--ip6') + + def diffparam_ipc(self): + return self._diff_generic('ipc', '--ipc') + + 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_label_file(self): + return self._diff_generic('label_file', '--label-file') + + def diffparam_log_driver(self): + return self._diff_generic('log_driver', '--log-driver') + + def diffparam_log_opt(self): + return self._diff_generic('log_opt', '--log-opt') + + def diffparam_mac_address(self): + return self._diff_generic('mac_address', '--mac-address') + + def diffparam_memory(self): + return self._diff_generic('memory', '--memory') + + def diffparam_memory_reservation(self): + return self._diff_generic('memory_reservation', '--memory-reservation') + + def diffparam_memory_swap(self): + return self._diff_generic('memory_swap', '--memory-swap') + + def diffparam_memory_swappiness(self): + return self._diff_generic('memory_swappiness', '--memory-swappiness') + + def diffparam_mount(self): + return self._diff_generic('mount', '--mount') + + def diffparam_network(self): + return self._diff_generic('network', '--network') + + def diffparam_network_aliases(self): + return self._diff_generic('network_aliases', '--network-alias') + + def diffparam_no_healthcheck(self): + return self._diff_generic('no_healthcheck', '--no-healthcheck', boolean_type=True) + + def diffparam_no_hosts(self): + return self._diff_generic('no_hosts', '--no-hosts') + + def diffparam_oom_kill_disable(self): + return self._diff_generic('oom_kill_disable', '--oom-kill-disable') + + def diffparam_oom_score_adj(self): + return self._diff_generic('oom_score_adj', '--oom-score-adj') + + def diffparam_os(self): + return self._diff_generic('os', '--os') + + def diffparam_passwd(self): + return self._diff_generic('passwd', '--passwd', boolean_type=True) + + def diffparam_passwd_entry(self): + return self._diff_generic('passwd_entry', '--passwd-entry') + + def diffparam_personality(self): + return self._diff_generic('personality', '--personality') + + def diffparam_pid(self): + return self._diff_generic('pid', '--pid') + + def diffparam_pid_file(self): + return self._diff_generic('pid_file', '--pid-file') + + def diffparam_pids_limit(self): + return self._diff_generic('pids_limit', '--pids-limit') + + def diffparam_platform(self): + return self._diff_generic('platform', '--platform') + + # def diffparam_pod(self): + # return self._diff_generic('pod', '--pod') + + def diffparam_pod_id_file(self): + return self._diff_generic('pod_id_file', '--pod-id-file') + + def diffparam_privileged(self): + return self._diff_generic('privileged', '--privileged') + + def diffparam_publish(self): + return self._diff_generic('publish', '--publish') + + def diffparam_publish_all(self): + return self._diff_generic('publish_all', '--publish-all') + + def diffparam_pull(self): + return self._diff_generic('pull', '--pull') + + def diffparam_rdt_class(self): + return self._diff_generic('rdt_class', '--rdt-class') + + def diffparam_read_only(self): + return self._diff_generic('read_only', '--read-only') + + def diffparam_read_only_tmpfs(self): + return self._diff_generic('read_only_tmpfs', '--read-only-tmpfs') + + def diffparam_requires(self): + return self._diff_generic('requires', '--requires') + + def diffparam_restart_policy(self): + return self._diff_generic('restart_policy', '--restart') + + def diffparam_retry(self): + return self._diff_generic('retry', '--retry') + + def diffparam_retry_delay(self): + return self._diff_generic('retry_delay', '--retry-delay') + + def diffparam_rootfs(self): + return self._diff_generic('rootfs', '--rootfs') + + # Disabling idemotency check for sdnotify as it's added by systemd generator + # https://github.com/containers/ansible-podman-collections/issues/775 + # def diffparam_sdnotify(self): + # return self._diff_generic('sdnotify', '--sdnotify') + + def diffparam_rm(self): + before = self.info['hostconfig']['autoremove'] + after = self.params['rm'] + if after is None: + return self._diff_update_and_compare('rm', '', '') + return self._diff_update_and_compare('rm', before, after) + + def diffparam_rmi(self): + return self._diff_generic('rmi', '--rmi', boolean_type=True) + + def diffparam_seccomp_policy(self): + return self._diff_generic('seccomp_policy', '--seccomp-policy') + + def diffparam_secrets(self): + return self._diff_generic('secrets', '--secret') + + def diffparam_security_opt(self): + return self._diff_generic('security_opt', '--security-opt') + + def diffparam_shm_size(self): + return self._diff_generic('shm_size', '--shm-size') + + def diffparam_shm_size_systemd(self): + return self._diff_generic('shm_size_systemd', '--shm-size-systemd') + + def diffparam_stop_signal(self): + return self._diff_generic('stop_signal', '--stop-signal') + + def diffparam_stop_timeout(self): + return self._diff_generic('stop_timeout', '--stop-timeout') + + def diffparam_subgidname(self): + return self._diff_generic('subgidname', '--subgidname') + + def diffparam_subuidname(self): + return self._diff_generic('subuidname', '--subuidname') + + def diffparam_sysctl(self): + return self._diff_generic('sysctl', '--sysctl') + + def diffparam_systemd(self): + return self._diff_generic('systemd', '--systemd') + + def diffparam_timeout(self): + return self._diff_generic('timeout', '--timeout') + + def diffparam_timezone(self): + return self._diff_generic('timezone', '--tz') + + def diffparam_tls_verify(self): + return self._diff_generic('tls_verify', '--tls-verify') + + def diffparam_tty(self): + before = self.info['config']['tty'] + after = self.params['tty'] + return self._diff_update_and_compare('tty', before, after) + + def diffparam_tmpfs(self): + return self._diff_generic('tmpfs', '--tmpfs') + + def diffparam_uidmap(self): + return self._diff_generic('uidmap', '--uidmap') + + def diffparam_ulimit(self): + return self._diff_generic('ulimit', '--ulimit') + + def diffparam_umask(self): + return self._diff_generic('umask', '--umask') + + def diffparam_unsetenv(self): + return self._diff_generic('unsetenv', '--unsetenv') + + def diffparam_unsetenv_all(self): + return self._diff_generic('unsetenv_all', '--unsetenv-all', boolean_type=True) + + def diffparam_user(self): + return self._diff_generic('user', '--user') + + def diffparam_userns(self): + return self._diff_generic('userns', '--userns') + + def diffparam_uts(self): + return self._diff_generic('uts', '--uts') + + def diffparam_variant(self): + return self._diff_generic('variant', '--variant') + + def diffparam_volume(self): + def clean_volume(x): + '''Remove trailing and double slashes from volumes.''' + if not x.rstrip("/"): + return "/" + return x.replace("//", "/").rstrip("/") + + before = createcommand('--volume', self.info['config']) + if before == []: + before = None + after = self.params['volume'] + if after is not None: + after = [":".join( + [clean_volume(i) for i in v.split(":")[:2]]) for v in self.params['volume']] + if before is not None: + before = [":".join([clean_volume(i) for i in v.split(":")[:2]]) for v in before] + if before is None and after is None: + return self._diff_update_and_compare('volume', before, after) + if after is not None: + after = ",".join(sorted([str(i).lower() for i in after])) + if before: + before = ",".join(sorted([str(i).lower() for i in before])) + return self._diff_update_and_compare('volume', before, after) + + def diffparam_volumes_from(self): + return self._diff_generic('volumes_from', '--volumes-from') + + def diffparam_workdir(self): + return self._diff_generic('workdir', '--workdir') + + 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() + 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="Container %s exited with code %s when %sed" % (self.name, rc, action), + 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}) + sysd = generate_systemd(self.module, + self.module_params, + self.name, + self.container.version) + self.results['changed'] = changed or sysd['changed'] + self.results.update( + {'podman_systemd': sysd['systemd']}) + if sysd['diff']: + if 'diff' not in self.results: + self.results.update({'diff': sysd['diff']}) + else: + self.results['diff']['before'] += sysd['diff']['before'] + self.results['diff']['after'] += sysd['diff']['after'] + quadlet = ContainerQuadlet(self.module_params) + quadlet_content = quadlet.create_quadlet_content() + self.results.update({'podman_quadlet': quadlet_content}) + + def make_started(self): + """Run actions if desired state is 'started'.""" + 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 or self.recreate): + 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 make_quadlet(self): + results_update = create_quadlet_state(self.module, "container") + self.results.update(results_update) + + def execute(self): + """Execute the desired action according to map of actions & states.""" + states_map = { + 'present': self.make_created, + 'started': self.make_started, + 'absent': self.make_absent, + 'stopped': self.make_stopped, + 'created': self.make_created, + 'quadlet': self.make_quadlet, + } + process_action = states_map[self.state] + process_action() + return self.results diff --git a/plugins/module_utils/podman/podman_pod_lib.py b/plugins/module_utils/podman/podman_pod_lib.py new file mode 100644 index 0000000..8f315a5 --- /dev/null +++ b/plugins/module_utils/podman/podman_pod_lib.py @@ -0,0 +1,1013 @@ +from __future__ import (absolute_import, division, print_function) +import json # noqa: F402 + +from ansible.module_utils._text import to_bytes, to_native +from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion +from ansible_collections.containers.podman.plugins.module_utils.podman.common import lower_keys +from ansible_collections.containers.podman.plugins.module_utils.podman.common import generate_systemd +from ansible_collections.containers.podman.plugins.module_utils.podman.common import delete_systemd +from ansible_collections.containers.podman.plugins.module_utils.podman.common import diff_generic +from ansible_collections.containers.podman.plugins.module_utils.podman.common import createcommand +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state, PodQuadlet + + +__metaclass__ = type + +ARGUMENTS_SPEC_POD = dict( + state=dict( + type='str', + default="created", + choices=[ + 'created', + 'killed', + 'restarted', + 'absent', + 'started', + 'stopped', + 'paused', + 'unpaused', + 'quadlet' + ]), + recreate=dict(type='bool', default=False), + add_host=dict(type='list', required=False, elements='str'), + blkio_weight=dict(type='str', required=False), + blkio_weight_device=dict(type='list', elements='str', required=False), + cgroup_parent=dict(type='str', required=False), + cpus=dict(type='str', required=False), + cpuset_cpus=dict(type='str', required=False), + cpuset_mems=dict(type='str', required=False), + cpu_shares=dict(type='str', required=False), + device=dict(type='list', elements='str', required=False), + device_read_bps=dict(type='list', elements='str', required=False), + device_write_bps=dict(type='list', elements='str', required=False), + dns=dict(type='list', elements='str', required=False), + dns_opt=dict(type='list', elements='str', aliases=['dns_option'], required=False), + dns_search=dict(type='list', elements='str', required=False), + exit_policy=dict(type='str', required=False, choices=['continue', 'stop']), + generate_systemd=dict(type='dict', default={}), + gidmap=dict(type='list', elements='str', required=False), + gpus=dict(type='str', required=False), + hostname=dict(type='str', required=False), + infra=dict(type='bool', required=False), + infra_conmon_pidfile=dict(type='str', required=False), + 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), + ip6=dict(type='str', required=False), + label=dict(type='dict', required=False), + label_file=dict(type='str', required=False), + mac_address=dict(type='str', required=False), + memory=dict(type='str', required=False), + memory_swap=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']), + quadlet_dir=dict(type='path'), + quadlet_filename=dict(type='str'), + quadlet_options=dict(type='list', elements='str'), + restart_policy=dict(type='str', required=False), + security_opt=dict(type='list', elements='str', required=False), + share=dict(type='str', required=False), + share_parent=dict(type='bool', required=False), + shm_size=dict(type='str', required=False), + shm_size_systemd=dict(type='str', required=False), + subgidname=dict(type='str', required=False), + subuidname=dict(type='str', required=False), + sysctl=dict(type='dict', required=False), + uidmap=dict(type='list', elements='str', required=False), + userns=dict(type='str', required=False), + uts=dict(type='str', required=False), + volume=dict(type='list', elements='str', aliases=['volumes'], + required=False), + volumes_from=dict(type='list', elements='str', required=False), + executable=dict(type='str', required=False, default='podman'), + debug=dict(type='bool', default=False), +) + + +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_blkio_weight(self, c): + self.check_version('--blkio-weight', minv='4.3.0') + return c + ['--blkio-weight', self.params['blkio_weight']] + + def addparam_blkio_weight_device(self, c): + self.check_version('--blkio-weight-device', minv='4.3.0') + for dev in self.params['blkio_weight_device']: + c += ['--blkio-weight-device', dev] + return c + + def addparam_cgroup_parent(self, c): + return c + ['--cgroup-parent', self.params['cgroup_parent']] + + def addparam_cpus(self, c): + self.check_version('--cpus', minv='4.2.0') + return c + ['--cpus', self.params['cpus']] + + def addparam_cpuset_cpus(self, c): + self.check_version('--cpus', minv='4.2.0') + return c + ['--cpuset-cpus', self.params['cpuset_cpus']] + + def addparam_cpuset_mems(self, c): + self.check_version('--cpuset-mems', minv='4.3.0') + return c + ['--cpuset-mems', self.params['cpuset_mems']] + + def addparam_cpu_shares(self, c): + self.check_version('--cpu-shares', minv='4.3.0') + return c + ['--cpu-shares', self.params['cpu_shares']] + + def addparam_device(self, c): + for dev in self.params['device']: + c += ['--device', dev] + return c + + def addparam_device_read_bps(self, c): + self.check_version('--device-read-bps', minv='4.3.0') + for dev in self.params['device_read_bps']: + c += ['--device-read-bps', dev] + return c + + def addparam_device_write_bps(self, c): + self.check_version('--device-write-bps', minv='4.3.0') + for dev in self.params['device_write_bps']: + c += ['--device-write-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-option', g] + return c + + def addparam_dns_search(self, c): + for g in self.params['dns_search']: + c += ['--dns-search', g] + return c + + def addparam_exit_policy(self, c): + return c + ['--exit-policy=%s' % self.params['exit_policy']] + + def addparam_gidmap(self, c): + for gidmap in self.params['gidmap']: + c += ['--gidmap', gidmap] + return c + + def addparam_gpus(self, c): + return c + ['--gpus', self.params['gpus']] + + def addparam_hostname(self, c): + return c + ['--hostname', self.params['hostname']] + + 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_ip6(self, c): + return c + ['--ip6', self.params['ip6']] + + def addparam_label(self, c): + for label in self.params['label'].items(): + c += ['--label', b'='.join( + [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_memory(self, c): + self.check_version('--memory', minv='4.2.0') + return c + ['--memory', self.params['memory']] + + def addparam_memory_swap(self, c): + self.check_version('--memory-swap', minv='4.3.0') + return c + ['--memory-swap', self.params['memory_swap']] + + 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_restart_policy(self, c): + return c + ['--restart=%s' % self.params['restart_policy']] + + def addparam_security_opt(self, c): + for g in self.params['security_opt']: + c += ['--security-opt', g] + return c + + def addparam_share(self, c): + return c + ['--share', self.params['share']] + + def addparam_share_parent(self, c): + if self.params['share_parent'] is not None: + return c + ['--share-parent=%s' % self.params['share_parent']] + return c + + def addparam_shm_size(self, c): + return c + ['--shm-size=%s' % self.params['shm_size']] + + def addparam_shm_size_systemd(self, c): + return c + ['--shm-size-systemd=%s' % self.params['shm_size_systemd']] + + def addparam_subgidname(self, c): + return c + ['--subgidname', self.params['subgidname']] + + def addparam_subuidname(self, c): + return c + ['--subuidname', self.params['subuidname']] + + def addparam_sysctl(self, c): + for k, v in self.params['sysctl'].items(): + c += ['--sysctl', "%s=%s" % (k, v)] + return c + + def addparam_uidmap(self, c): + for uidmap in self.params['uidmap']: + c += ['--uidmap', uidmap] + return c + + def addparam_userns(self, c): + return c + ['--userns', self.params['userns']] + + def addparam_uts(self, c): + return c + ['--uts', self.params['uts']] + + def addparam_volume(self, c): + for vol in self.params['volume']: + if vol: + c += ['--volume', vol] + return c + + def addparam_volumes_from(self, c): + for vol in self.params['volumes_from']: + c += ['--volumes-from', vol] + return c + + +class PodmanPodDefaults: + def __init__(self, module, podman_version): + self.module = module + self.version = podman_version + self.defaults = { + '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 _diff_generic(self, module_arg, cmd_arg, boolean_type=False): + """ + Generic diff function for module arguments from CreateCommand + in Podman inspection output. + + Args: + module_arg (str): module argument name + cmd_arg (str): command line argument name + boolean_type (bool): if True, then argument is boolean type + + Returns: + bool: True if there is a difference, False otherwise + + """ + info_config = self.info + before, after = diff_generic(self.params, info_config, module_arg, cmd_arg, boolean_type) + return self._diff_update_and_compare(module_arg, before, after) + + def diffparam_add_host(self): + return self._diff_generic('add_host', '--add-host') + + def diffparam_blkio_weight(self): + return self._diff_generic('blkio_weight', '--blkio-weight') + + def diffparam_blkio_weight_device(self): + return self._diff_generic('blkio_weight_device', '--blkio-weight-device') + + def diffparam_cgroup_parent(self): + return self._diff_generic('cgroup_parent', '--cgroup-parent') + + def diffparam_cpu_shares(self): + return self._diff_generic('cpu_shares', '--cpu-shares') + + def diffparam_cpus(self): + return self._diff_generic('cpus', '--cpus') + + def diffparam_cpuset_cpus(self): + return self._diff_generic('cpuset_cpus', '--cpuset-cpus') + + def diffparam_cpuset_mems(self): + return self._diff_generic('cpuset_mems', '--cpuset-mems') + + def diffparam_device(self): + return self._diff_generic('device', '--device') + + def diffparam_device_read_bps(self): + return self._diff_generic('device_read_bps', '--device-read-bps') + + def diffparam_device_write_bps(self): + return self._diff_generic('device_write_bps', '--device-write-bps') + + def diffparam_dns(self): + return self._diff_generic('dns', '--dns') + + def diffparam_dns_opt(self): + return self._diff_generic('dns_opt', '--dns-option') + + def diffparam_dns_search(self): + return self._diff_generic('dns_search', '--dns-search') + + # Disabling idemotency check for exit policy as it's added by systemd generator + # https://github.com/containers/ansible-podman-collections/issues/774 + # def diffparam_exit_policy(self): + # return self._diff_generic('exit_policy', '--exit-policy') + + def diffparam_gidmap(self): + return self._diff_generic('gidmap', '--gidmap') + + def diffparam_gpus(self): + return self._diff_generic('gpus', '--gpus') + + def diffparam_hostname(self): + return self._diff_generic('hostname', '--hostname') + + # 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) + + def diffparam_infra_command(self): + return self._diff_generic('infra_command', '--infra-command') + + # Disabling idemotency check for infra_conmon_pidfile as it's added by systemd generator + # https://github.com/containers/ansible-podman-collections/issues/774 + # def diffparam_infra_conmon_pidfile(self): + # return self._diff_generic('infra_conmon_pidfile', '--infra-conmon-pidfile') + + def diffparam_infra_image(self): + return self._diff_generic('infra_image', '--infra-image') + + def diffparam_infra_name(self): + return self._diff_generic('infra_name', '--infra-name') + + def diffparam_ip(self): + return self._diff_generic('ip', '--ip') + + def diffparam_ip6(self): + return self._diff_generic('ip6', '--ip6') + + def diffparam_label(self): + if 'config' in self.info and 'labels' in self.info['config']: + 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) + + def diffparam_label_file(self): + return self._diff_generic('label_file', '--label-file') + + def diffparam_mac_address(self): + return self._diff_generic('mac_address', '--mac-address') + + def diffparam_memory(self): + return self._diff_generic('memory', '--memory') + + def diffparam_memory_swap(self): + return self._diff_generic('memory_swap', '--memory-swap') + + def diffparam_network(self): + return self._diff_generic('network', '--network') + + def diffparam_network_alias(self): + return self._diff_generic('network_alias', '--network-alias') + + def diffparam_no_hosts(self): + return self._diff_generic('no_hosts', '--no-hosts', boolean_type=True) + + def diffparam_pid(self): + return self._diff_generic('pid', '--pid') + + # Disabling idemotency check for pod id file as it's added by systemd generator + # https://github.com/containers/ansible-podman-collections/issues/774 + # def diffparam_pod_id_file(self): + # return self._diff_generic('pod_id_file', '--pod-id-file') + + def diffparam_publish(self): + return self._diff_generic('publish', '--publish') + + def diffparam_restart_policy(self): + return self._diff_generic('restart_policy', '--restart') + + def diffparam_security_opt(self): + return self._diff_generic('security_opt', '--security-opt') + + def diffparam_share(self): + return self._diff_generic('share', '--share') + + def diffparam_share_parent(self): + return self._diff_generic('share_parent', '--share-parent') + + def diffparam_shm_size(self): + return self._diff_generic('shm_size', '--shm-size') + + def diffparam_shm_size_systemd(self): + return self._diff_generic('shm_size_systemd', '--shm-size-systemd') + + def diffparam_subgidname(self): + return self._diff_generic('subgidname', '--subgidname') + + def diffparam_subuidname(self): + return self._diff_generic('subuidname', '--subuidname') + + def diffparam_sysctl(self): + return self._diff_generic('sysctl', '--sysctl') + + def diffparam_uidmap(self): + return self._diff_generic('uidmap', '--uidmap') + + def diffparam_userns(self): + return self._diff_generic('userns', '--userns') + + def diffparam_uts(self): + return self._diff_generic('uts', '--uts') + + def diffparam_volume(self): + def clean_volume(x): + '''Remove trailing and double slashes from volumes.''' + if not x.rstrip("/"): + return "/" + return x.replace("//", "/").rstrip("/") + + before = createcommand('--volume', self.info) + if before == []: + before = None + after = self.params['volume'] + if after is not None: + after = [":".join( + [clean_volume(i) for i in v.split(":")[:2]]) for v in self.params['volume']] + if before is not None: + before = [":".join([clean_volume(i) for i in v.split(":")[:2]]) for v in before] + self.module.log("PODMAN Before: %s and After: %s" % (before, after)) + if before is None and after is None: + return self._diff_update_and_compare('volume', before, after) + if after is not None: + after = ",".join(sorted([str(i).lower() for i in after])) + if before: + before = ",".join(sorted([str(i).lower() for i in before])) + return self._diff_update_and_compare('volume', before, after) + + def diffparam_volumes_from(self): + return self._diff_generic('volumes_from', '--volumes-from') + + def is_different(self): + diff_func_list = [func for func in dir(self) + 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]) + if rc == 0: + info = json.loads(out) + # from podman 5 onwards, this is a list of dicts, + # before it was just a single dict when querying + # a single pod + if isinstance(info, list): + return info[0] + else: + return info + return {} + + def get_ps(self): + """Inspect pod process and gather info about it.""" + # 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 + if isinstance(facts, list): + facts = facts[0] + out, err = self.pod.stdout, self.pod.stderr + self.results.update({'changed': changed, 'pod': facts, + 'podman_actions': self.pod.actions}, + 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}) + sysd = generate_systemd(self.module, + self.module_params, + self.name, + self.pod.version) + self.results['changed'] = changed or sysd['changed'] + self.results.update( + {'podman_systemd': sysd['systemd']}) + if sysd['diff']: + if 'diff' not in self.results: + self.results.update({'diff': sysd['diff']}) + else: + self.results['diff']['before'] += sysd['diff']['before'] + self.results['diff']['after'] += sysd['diff']['after'] + quadlet = PodQuadlet(self.module_params) + quadlet_content = quadlet.create_quadlet_content() + self.results.update({'podman_quadlet': quadlet_content}) + + def execute(self): + """Execute the desired action according to map of actions & states.""" + 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, + 'quadlet': self.make_quadlet, + } + 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}) + + def make_quadlet(self): + results_update = create_quadlet_state(self.module, "pod") + self.results.update(results_update) diff --git a/plugins/module_utils/podman/quadlet.py b/plugins/module_utils/podman/quadlet.py new file mode 100644 index 0000000..88b8239 --- /dev/null +++ b/plugins/module_utils/podman/quadlet.py @@ -0,0 +1,719 @@ +# Copyright (c) 2024 Sagi Shnaidman (@sshnaidm) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os + +from ansible_collections.containers.podman.plugins.module_utils.podman.common import compare_systemd_file_content + +QUADLET_ROOT_PATH = "/etc/containers/systemd/" +QUADLET_NON_ROOT_PATH = "~/.config/containers/systemd/" + + +class Quadlet: + param_map = {} + + def __init__(self, section: str, params: dict): + self.section = section + self.custom_params = self.custom_prepare_params(params) + self.dict_params = self.prepare_params() + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for specific Quadlet types. + """ + # This should be implemented in child classes if needed. + return params + + def prepare_params(self) -> dict: + """ + Convert parameter values as per param_map. + """ + processed_params = [] + for param_key, quadlet_key in self.param_map.items(): + value = self.custom_params.get(param_key) + if value is not None: + if isinstance(value, list): + # Add an entry for each item in the list + for item in value: + processed_params.append([quadlet_key, item]) + else: + if isinstance(value, bool): + value = str(value).lower() + # Add a single entry for the key + processed_params.append([quadlet_key, value]) + return processed_params + + def create_quadlet_content(self) -> str: + """ + Construct the quadlet content as a string. + """ + custom_user_options = self.custom_params.get("quadlet_options") + custom_text = "\n" + "\n".join(custom_user_options) if custom_user_options else "" + return f"[{self.section}]\n" + "\n".join( + f"{key}={value}" for key, value in self.dict_params + ) + custom_text + "\n" + + def write_to_file(self, path: str): + """ + Write the quadlet content to a file at the specified path. + """ + content = self.create_quadlet_content() + with open(path, 'w') as file: + file.write(content) + + +class ContainerQuadlet(Quadlet): + param_map = { + 'cap_add': 'AddCapability', + 'device': 'AddDevice', + 'annotation': 'Annotation', + 'name': 'ContainerName', + # the following are not implemented yet in Podman module + 'AutoUpdate': 'AutoUpdate', + 'ContainersConfModule': 'ContainersConfModule', + # end of not implemented yet + 'dns': 'DNS', + 'dns_option': 'DNSOption', + 'dns_search': 'DNSSearch', + 'cap_drop': 'DropCapability', + 'entrypoint': 'Entrypoint', + 'env': 'Environment', + 'env_file': 'EnvironmentFile', + 'env_host': 'EnvironmentHost', + 'command': 'Exec', + 'expose': 'ExposeHostPort', + 'gidmap': 'GIDMap', + 'global_args': 'GlobalArgs', + 'group': 'Group', # Does not exist in module parameters + 'healthcheck': 'HealthCmd', + 'healthcheck_interval': 'HealthInterval', + 'healthcheck_failure_action': 'HealthOnFailure', + 'healthcheck_retries': 'HealthRetries', + 'healthcheck_start_period': 'HealthStartPeriod', + 'healthcheck_timeout': 'HealthTimeout', + 'health_startup_cmd': 'HealthStartupCmd', + 'health_startup_interval': 'HealthStartupInterval', + 'health_startup_retries': 'HealthStartupRetries', + 'health_startup_success': 'HealthStartupSuccess', + 'health_startup_timeout': 'HealthStartupTimeout', + 'hostname': 'HostName', + 'image': 'Image', + 'ip': 'IP', + 'ip6': 'IP6', + 'label': 'Label', + 'log_driver': 'LogDriver', + "Mask": "Mask", # add it in security_opt + 'mount': 'Mount', + 'network': 'Network', + 'no_new_privileges': 'NoNewPrivileges', + 'sdnotify': 'Notify', + 'pids_limit': 'PidsLimit', + 'pod': 'Pod', + 'publish': 'PublishPort', + "pull": "Pull", + 'read_only': 'ReadOnly', + 'read_only_tmpfs': 'ReadOnlyTmpfs', + 'rootfs': 'Rootfs', + 'init': 'RunInit', + 'SeccompProfile': 'SeccompProfile', + 'secrets': 'Secret', + # All these are in security_opt + 'SecurityLabelDisable': 'SecurityLabelDisable', + 'SecurityLabelFileType': 'SecurityLabelFileType', + 'SecurityLabelLevel': 'SecurityLabelLevel', + 'SecurityLabelNested': 'SecurityLabelNested', + 'SecurityLabelType': 'SecurityLabelType', + 'shm_size': 'ShmSize', + 'stop_timeout': 'StopTimeout', + 'subgidname': 'SubGIDMap', + 'subuidname': 'SubUIDMap', + 'sysctl': 'Sysctl', + 'timezone': 'Timezone', + 'tmpfs': 'Tmpfs', + 'uidmap': 'UIDMap', + 'ulimit': 'Ulimit', + 'Unmask': 'Unmask', # --security-opt unmask=ALL + 'user': 'User', + 'userns': 'UserNS', + 'volume': 'Volume', + 'workdir': 'WorkingDir', + 'podman_args': 'PodmanArgs', + } + + def __init__(self, params: dict): + super().__init__("Container", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for container-specific parameters. + """ + # Work on params in params_map and convert them to a right form + if params["annotation"]: + params['annotation'] = ["%s=%s" % + (k, v) for k, v in params['annotation'].items()] + if params["cap_add"]: + params["cap_add"] = " ".join(params["cap_add"]) + if params["cap_drop"]: + params["cap_drop"] = " ".join(params["cap_drop"]) + if params["command"]: + params["command"] = (" ".join([str(j) for j in params["command"]]) + if isinstance(params["command"], list) + else params["command"]) + if params["label"]: + params["label"] = ["%s=%s" % (k, v) for k, v in params["label"].items()] + if params["env"]: + params["env"] = ["%s=%s" % (k, v) for k, v in params["env"].items()] + if params["sysctl"]: + params["sysctl"] = ["%s=%s" % (k, v) for k, v in params["sysctl"].items()] + if params["tmpfs"]: + params["tmpfs"] = ["%s:%s" % (k, v) if v else k for k, v in params["tmpfs"].items()] + + # Work on params which are not in the param_map but can be calculated + params["global_args"] = [] + if params["user"] and len(str(params["user"]).split(":")) > 1: + user, group = params["user"].split(":") + params["user"] = user + params["group"] = group + if params["security_opt"]: + if "no-new-privileges" in params["security_opt"]: + params["no_new_privileges"] = True + params["security_opt"].remove("no-new-privileges") + if params["log_level"]: + params["global_args"].append(f"--log-level {params['log_level']}") + if params["debug"]: + params["global_args"].append("--log-level debug") + + # Work on params which are not in the param_map and add them to PodmanArgs + params["podman_args"] = [] + if params["arch"]: + params["podman_args"].append(f"--arch {params['arch']}") + if params["authfile"]: + params["podman_args"].append(f"--authfile {params['authfile']}") + if params["attach"]: + for attach in params["attach"]: + params["podman_args"].append(f"--attach {attach}") + if params["blkio_weight"]: + params["podman_args"].append(f"--blkio-weight {params['blkio_weight']}") + if params["blkio_weight_device"]: + params["podman_args"].append(" ".join([ + f"--blkio-weight-device {':'.join(blkio)}" for blkio in params["blkio_weight_device"].items()])) + if params["cgroupns"]: + params["podman_args"].append(f"--cgroupns {params['cgroupns']}") + if params["cgroup_conf"]: + for k, v in params["cgroup_conf"].items(): + params["podman_args"].append(f"--cgroup-conf {k}={v}") + if params["cgroup_parent"]: + params["podman_args"].append(f"--cgroup-parent {params['cgroup_parent']}") + if params["chrootdirs"]: + params["podman_args"].append(f"--chrootdirs {params['chrootdirs']}") + if params["cidfile"]: + params["podman_args"].append(f"--cidfile {params['cidfile']}") + if params["conmon_pidfile"]: + params["podman_args"].append(f"--conmon-pidfile {params['conmon_pidfile']}") + if params["cpuset_cpus"]: + params["podman_args"].append(f"--cpuset-cpus {params['cpuset_cpus']}") + if params["cpuset_mems"]: + params["podman_args"].append(f"--cpuset-mems {params['cpuset_mems']}") + if params["cpu_period"]: + params["podman_args"].append(f"--cpu-period {params['cpu_period']}") + if params["cpu_quota"]: + params["podman_args"].append(f"--cpu-quota {params['cpu_quota']}") + if params["cpu_rt_period"]: + params["podman_args"].append(f"--cpu-rt-period {params['cpu_rt_period']}") + if params["cpu_rt_runtime"]: + params["podman_args"].append(f"--cpu-rt-runtime {params['cpu_rt_runtime']}") + if params["cpu_shares"]: + params["podman_args"].append(f"--cpu-shares {params['cpu_shares']}") + if params["decryption_key"]: + params["podman_args"].append(f"--decryption-key {params['decryption_key']}") + if params["device_cgroup_rule"]: + params["podman_args"].append(f"--device-cgroup-rule {params['device_cgroup_rule']}") + if params["device_read_bps"]: + for i in params["device_read_bps"]: + params["podman_args"].append(f"--device-read-bps {i}") + if params["device_read_iops"]: + for i in params["device_read_iops"]: + params["podman_args"].append(f"--device-read-iops {i}") + if params["device_write_bps"]: + for i in params["device_write_bps"]: + params["podman_args"].append(f"--device-write-bps {i}") + if params["device_write_iops"]: + for i in params["device_write_iops"]: + params["podman_args"].append(f"--device-write-iops {i}") + if params["etc_hosts"]: + for host_ip in params['etc_hosts'].items(): + params["podman_args"].append(f"--add-host {':'.join(host_ip)}") + if params["env_merge"]: + for k, v in params["env_merge"].items(): + params["podman_args"].append(f"--env {k}={v}") + if params["gpus"]: + params["podman_args"].append(f"--gpus {params['gpus']}") + if params["group_entry"]: + params["podman_args"].append(f"--group-entry {params['group_entry']}") + if params["hostuser"]: + params["podman_args"].append(f"--hostuser {params['hostuser']}") + if params["hooks_dir"]: + for hook in params["hooks_dir"]: + params["podman_args"].append(f"--hooks-dir {hook}") + if params["http_proxy"]: + params["podman_args"].append(f"--http-proxy {params['http_proxy']}") + if params["image_volume"]: + params["podman_args"].append(f"--image-volume {params['image_volume']}") + if params["init_ctr"]: + params["podman_args"].append(f"--init-ctr {params['init_ctr']}") + if params["init_path"]: + params["podman_args"].append(f"--init-path {params['init_path']}") + if params["interactive"]: + params["podman_args"].append("--interactive") + if params["ipc"]: + params["podman_args"].append(f"--ipc {params['ipc']}") + if params["kernel_memory"]: + params["podman_args"].append(f"--kernel-memory {params['kernel_memory']}") + if params["label_file"]: + params["podman_args"].append(f"--label-file {params['label_file']}") + if params["log_opt"]: + for k, v in params['log_opt'].items(): + params["podman_args"].append(f"--log-opt {k.replace('max_size', 'max-size')}={v}") + if params["mac_address"]: + params["podman_args"].append(f"--mac-address {params['mac_address']}") + if params["memory"]: + params["podman_args"].append(f"--memory {params['memory']}") + if params["memory_reservation"]: + params["podman_args"].append(f"--memory-reservation {params['memory_reservation']}") + if params["memory_swap"]: + params["podman_args"].append(f"--memory-swap {params['memory_swap']}") + if params["memory_swappiness"]: + params["podman_args"].append(f"--memory-swappiness {params['memory_swappiness']}") + if params["network_aliases"]: + for alias in params["network_aliases"]: + params["podman_args"].append(f"--network-alias {alias}") + if params["no_healthcheck"]: + params["podman_args"].append("--no-healthcheck") + if params["no_hosts"] is not None: + params["podman_args"].append(f"--no-hosts={params['no_hosts']}") + if params["oom_kill_disable"]: + params["podman_args"].append(f"--oom-kill-disable={params['oom_kill_disable']}") + if params["oom_score_adj"]: + params["podman_args"].append(f"--oom-score-adj {params['oom_score_adj']}") + if params["os"]: + params["podman_args"].append(f"--os {params['os']}") + if params["passwd"]: + params["podman_args"].append("--passwd") + if params["passwd_entry"]: + params["podman_args"].append(f"--passwd-entry {params['passwd_entry']}") + if params["personality"]: + params["podman_args"].append(f"--personality {params['personality']}") + if params["pid"]: + params["podman_args"].append(f"--pid {params['pid']}") + if params["pid_file"]: + params["podman_args"].append(f"--pid-file {params['pid_file']}") + if params["preserve_fd"]: + for pres in params["preserve_fd"]: + params["podman_args"].append(f"--preserve-fd {pres}") + if params["preserve_fds"]: + params["podman_args"].append(f"--preserve-fds {params['preserve_fds']}") + if params["privileged"]: + params["podman_args"].append("--privileged") + if params["publish_all"]: + params["podman_args"].append("--publish-all") + if params["rdt_class"]: + params["podman_args"].append(f"--rdt-class {params['rdt_class']}") + if params["requires"]: + params["podman_args"].append(f"--requires {','.join(params['requires'])}") + if params["restart_policy"]: + params["podman_args"].append(f"--restart {params['restart_policy']}") + if params["retry"]: + params["podman_args"].append(f"--retry {params['retry']}") + if params["retry_delay"]: + params["podman_args"].append(f"--retry-delay {params['retry_delay']}") + if params["rm"]: + params["podman_args"].append("--rm") + if params["rmi"]: + params["podman_args"].append("--rmi") + if params["seccomp_policy"]: + params["podman_args"].append(f"--seccomp-policy {params['seccomp_policy']}") + if params["security_opt"]: + for security_opt in params["security_opt"]: + params["podman_args"].append(f"--security-opt {security_opt}") + if params["shm_size_systemd"]: + params["podman_args"].append(f"--shm-size-systemd {params['shm_size_systemd']}") + if params["sig_proxy"]: + params["podman_args"].append(f"--sig-proxy {params['sig_proxy']}") + if params["stop_signal"]: + params["podman_args"].append(f"--stop-signal {params['stop_signal']}") + if params["systemd"]: + params["podman_args"].append(f"--systemd={str(params['systemd']).lower()}") + if params["timeout"]: + params["podman_args"].append(f"--timeout {params['timeout']}") + if params["tls_verify"]: + params["podman_args"].append(f"--tls-verify={str(params['tls_verify']).lower()}") + if params["tty"]: + params["podman_args"].append("--tty") + if params["umask"]: + params["podman_args"].append(f"--umask {params['umask']}") + if params["unsetenv"]: + for unset in params["unsetenv"]: + params["podman_args"].append(f"--unsetenv {unset}") + if params["unsetenv_all"]: + params["podman_args"].append("--unsetenv-all") + if params["uts"]: + params["podman_args"].append(f"--uts {params['uts']}") + if params["variant"]: + params["podman_args"].append(f"--variant {params['variant']}") + if params["volumes_from"]: + for volume in params["volumes_from"]: + params["podman_args"].append(f"--volumes-from {volume}") + if params["cmd_args"]: + params["podman_args"].append(params["cmd_args"]) + + # Return params with custom processing applied + return params + + +class NetworkQuadlet(Quadlet): + param_map = { + 'name': 'NetworkName', + 'internal': 'Internal', + 'driver': 'Driver', + 'gateway': 'Gateway', + 'disable_dns': 'DisableDNS', + 'subnet': 'Subnet', + 'ip_range': 'IPRange', + 'ipv6': 'IPv6', + "opt": "Options", + # Add more parameter mappings specific to networks + 'ContainersConfModule': 'ContainersConfModule', + "dns": "DNS", + "ipam_driver": "IPAMDriver", + "Label": "Label", + "global_args": "GlobalArgs", + "podman_args": "PodmanArgs", + } + + def __init__(self, params: dict): + super().__init__("Network", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for network-specific parameters. + """ + # Work on params in params_map and convert them to a right form + if params["debug"]: + params["global_args"].append("--log-level debug") + if params["opt"]: + new_opt = [] + for k, v in params["opt"].items(): + if v is not None: + new_opt.append(f"{k}={v}") + params["opt"] = new_opt + return params + + +# This is a inherited class that represents a Quadlet file for the Podman pod +class PodQuadlet(Quadlet): + param_map = { + 'name': 'PodName', + "network": "Network", + "publish": "PublishPort", + "volume": "Volume", + 'ContainersConfModule': 'ContainersConfModule', + "global_args": "GlobalArgs", + "podman_args": "PodmanArgs", + } + + def __init__(self, params: dict): + super().__init__("Pod", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for pod-specific parameters. + """ + # Work on params in params_map and convert them to a right form + params["global_args"] = [] + params["podman_args"] = [] + + if params["add_host"]: + for host in params['add_host']: + params["podman_args"].append(f"--add-host {host}") + if params["cgroup_parent"]: + params["podman_args"].append(f"--cgroup-parent {params['cgroup_parent']}") + if params["blkio_weight"]: + params["podman_args"].append(f"--blkio-weight {params['blkio_weight']}") + if params["blkio_weight_device"]: + params["podman_args"].append(" ".join([ + f"--blkio-weight-device {':'.join(blkio)}" for blkio in params["blkio_weight_device"].items()])) + if params["cpuset_cpus"]: + params["podman_args"].append(f"--cpuset-cpus {params['cpuset_cpus']}") + if params["cpuset_mems"]: + params["podman_args"].append(f"--cpuset-mems {params['cpuset_mems']}") + if params["cpu_shares"]: + params["podman_args"].append(f"--cpu-shares {params['cpu_shares']}") + if params["cpus"]: + params["podman_args"].append(f"--cpus {params['cpus']}") + if params["device"]: + for device in params["device"]: + params["podman_args"].append(f"--device {device}") + if params["device_read_bps"]: + for i in params["device_read_bps"]: + params["podman_args"].append(f"--device-read-bps {i}") + if params["device_write_bps"]: + for i in params["device_write_bps"]: + params["podman_args"].append(f"--device-write-bps {i}") + if params["dns"]: + for dns in params["dns"]: + params["podman_args"].append(f"--dns {dns}") + if params["dns_opt"]: + for dns_option in params["dns_opt"]: + params["podman_args"].append(f"--dns-option {dns_option}") + if params["dns_search"]: + for dns_search in params["dns_search"]: + params["podman_args"].append(f"--dns-search {dns_search}") + if params["gidmap"]: + for gidmap in params["gidmap"]: + params["podman_args"].append(f"--gidmap {gidmap}") + if params["exit_policy"]: + params["podman_args"].append(f"--exit-policy={params['exit_policy']}") + if params["gpus"]: + params["podman_args"].append(f"--gpus {params['gpus']}") + if params["hostname"]: + params["podman_args"].append(f"--hostname {params['hostname']}") + if params["infra"]: + params["podman_args"].append(f"--infra {params['infra']}") + if params["infra_command"]: + params["podman_args"].append(f"--infra-command {params['infra_command']}") + if params["infra_conmon_pidfile"]: + params["podman_args"].append(f"--infra-conmon-pidfile {params['infra_conmon_pidfile']}") + if params["infra_image"]: + params["podman_args"].append(f"--infra-image {params['infra_image']}") + if params["infra_name"]: + params["podman_args"].append(f"--infra-name {params['infra_name']}") + if params["ip"]: + params["podman_args"].append(f"--ip {params['ip']}") + if params["ip6"]: + params["podman_args"].append(f"--ip6 {params['ip6']}") + if params["label"]: + for label, label_v in params["label"].items(): + params["podman_args"].append(f"--label {label}={label_v}") + if params["label_file"]: + params["podman_args"].append(f"--label-file {params['label_file']}") + if params["mac_address"]: + params["podman_args"].append(f"--mac-address {params['mac_address']}") + if params["memory"]: + params["podman_args"].append(f"--memory {params['memory']}") + if params["memory_swap"]: + params["podman_args"].append(f"--memory-swap {params['memory_swap']}") + if params["no_hosts"]: + params["podman_args"].append(f"--no-hosts {params['no_hosts']}") + if params["pid"]: + params["podman_args"].append(f"--pid {params['pid']}") + if params["pod_id_file"]: + params["podman_args"].append(f"--pod-id-file {params['pod_id_file']}") + if params["restart_policy"]: + params["podman_args"].append(f"--restart={params['restart_policy']}") + if params["security_opt"]: + for security_opt in params["security_opt"]: + params["podman_args"].append(f"--security-opt {security_opt}") + if params["share"]: + params["podman_args"].append(f"--share {params['share']}") + if params["share_parent"] is not None: + params["podman_args"].append(f"--share-parent={str(params['share_parent']).lower()}") + if params["shm_size"]: + params["podman_args"].append(f"--shm-size {params['shm_size']}") + if params["shm_size_systemd"]: + params["podman_args"].append(f"--shm-size-systemd {params['shm_size_systemd']}") + if params["subgidname"]: + params["podman_args"].append(f"--subgidname {params['subgidname']}") + if params["subuidname"]: + params["podman_args"].append(f"--subuidname {params['subuidname']}") + if params["sysctl"]: + for k, v in params["sysctl"].items(): + params["podman_args"].append(f"--sysctl {k}={v}") + if params["uidmap"]: + for uidmap in params["uidmap"]: + params["podman_args"].append(f"--uidmap {uidmap}") + if params["userns"]: + params["podman_args"].append(f"--userns {params['userns']}") + if params["uts"]: + params["podman_args"].append(f"--uts {params['uts']}") + if params["volumes_from"]: + for volume in params["volumes_from"]: + params["podman_args"].append(f"--volumes-from {volume}") + if params["debug"]: + params["global_args"].append("--log-level debug") + + return params + + +# This is a inherited class that represents a Quadlet file for the Podman volume +class VolumeQuadlet(Quadlet): + param_map = { + 'name': 'VolumeName', + 'driver': 'Driver', + 'label': 'Label', + # 'opt': 'Options', + 'ContainersConfModule': 'ContainersConfModule', + 'global_args': 'GlobalArgs', + 'podman_args': 'PodmanArgs', + } + + def __init__(self, params: dict): + super().__init__("Volume", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for volume-specific parameters. + """ + # Work on params in params_map and convert them to a right form + params["global_args"] = [] + params["podman_args"] = [] + + if params["debug"]: + params["global_args"].append("--log-level debug") + if params["label"]: + params["label"] = ["%s=%s" % (k, v) for k, v in params["label"].items()] + if params["options"]: + for opt in params["options"]: + params["podman_args"].append(f"--opt {opt}") + + return params + + +# This is a inherited class that represents a Quadlet file for the Podman kube +class KubeQuadlet(Quadlet): + param_map = { + 'configmap': 'ConfigMap', + 'log_driver': 'LogDriver', + 'network': 'Network', + 'kube_file': 'Yaml', + 'userns': 'UserNS', + 'AutoUpdate': 'AutoUpdate', + 'ExitCodePropagation': 'ExitCodePropagation', + 'KubeDownForce': 'KubeDownForce', + 'PublishPort': 'PublishPort', + 'SetWorkingDirectory': 'SetWorkingDirectory', + 'ContainersConfModule': 'ContainersConfModule', + 'global_args': 'GlobalArgs', + 'podman_args': 'PodmanArgs', + } + + def __init__(self, params: dict): + super().__init__("Kube", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for kube-specific parameters. + """ + # Work on params in params_map and convert them to a right form + params["global_args"] = [] + params["podman_args"] = [] + + if params["debug"]: + params["global_args"].append("--log-level debug") + + return params + + +# This is a inherited class that represents a Quadlet file for the Podman image +class ImageQuadlet(Quadlet): + param_map = { + 'AllTags': 'AllTags', + 'arch': 'Arch', + 'authfile': 'AuthFile', + 'ca_cert_dir': 'CertDir', + 'creds': 'Creds', + 'DecryptionKey': 'DecryptionKey', + 'name': 'Image', + 'ImageTag': 'ImageTag', + 'OS': 'OS', + 'validate_certs': 'TLSVerify', + 'Variant': 'Variant', + 'ContainersConfModule': 'ContainersConfModule', + 'global_args': 'GlobalArgs', + 'podman_args': 'PodmanArgs', + } + + def __init__(self, params: dict): + super().__init__("Image", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for image-specific parameters. + """ + # Work on params in params_map and convert them to a right form + params["global_args"] = [] + params["podman_args"] = [] + + if params["username"] and params["password"]: + params["creds"] = f"{params['username']}:{params['password']}" + # if params['validate_certs'] is not None: + # params['validate_certs'] = str(params['validate_certs']).lower() + + return params + + +def check_quadlet_directory(module, quadlet_dir): + '''Check if the directory exists and is writable. If not, fail the module.''' + if not os.path.exists(quadlet_dir): + try: + os.makedirs(quadlet_dir) + except Exception as e: + module.fail_json( + msg="Directory for quadlet_file can't be created: %s" % e) + if not os.access(quadlet_dir, os.W_OK): + module.fail_json( + msg="Directory for quadlet_file is not writable: %s" % quadlet_dir) + + +def create_quadlet_state(module, issuer): + '''Create a quadlet file for the specified issuer.''' + class_map = { + "container": ContainerQuadlet, + "network": NetworkQuadlet, + "pod": PodQuadlet, + "volume": VolumeQuadlet, + "kube": KubeQuadlet, + "image": ImageQuadlet, + } + # Let's detect which user is running + user = "root" if os.geteuid() == 0 else "user" + quadlet_dir = module.params.get('quadlet_dir') + if not quadlet_dir: + if user == "root": + quadlet_dir = QUADLET_ROOT_PATH + else: + quadlet_dir = os.path.expanduser(QUADLET_NON_ROOT_PATH) + # Create a filename based on the issuer + if not module.params.get('name') and not module.params.get('quadlet_filename'): + module.fail_json(msg=f"Filename for {issuer} is required for creating a quadlet file.") + if issuer == "image": + name = module.params['name'].split("/")[-1].split(":")[0] + else: + name = module.params.get('name') + quad_file_name = module.params['quadlet_filename'] + if quad_file_name and not quad_file_name.endswith(f".{issuer}"): + quad_file_name = f"{quad_file_name}.{issuer}" + filename = quad_file_name or f"{name}.{issuer}" + quadlet_file_path = os.path.join(quadlet_dir, filename) + # Check if the directory exists and is writable + check_quadlet_directory(module, quadlet_dir) + # Check if file already exists and if it's different + quadlet = class_map[issuer](module.params) + quadlet_content = quadlet.create_quadlet_content() + file_diff = compare_systemd_file_content(quadlet_file_path, quadlet_content) + if bool(file_diff): + quadlet.write_to_file(quadlet_file_path) + results_update = { + 'changed': True, + "diff": { + "before": "\n".join(file_diff[0]) if isinstance(file_diff[0], list) else file_diff[0] + "\n", + "after": "\n".join(file_diff[1]) if isinstance(file_diff[1], list) else file_diff[1] + "\n", + }} + else: + results_update = {} + return results_update + +# Check with following command: +# QUADLET_UNIT_DIRS= /usr/lib/systemd/system-generators/podman-system-generator {--user} --dryrun diff --git a/plugins/modules/__init__.py b/plugins/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/modules/podman_container.py b/plugins/modules/podman_container.py new file mode 100644 index 0000000..b06c9ae --- /dev/null +++ b/plugins/modules/podman_container.py @@ -0,0 +1,1377 @@ +#!/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 + - I(quadlet) - Write a quadlet file with the specified configuration. + Requires the C(quadlet_dir) option to be set. + type: str + default: started + choices: + - absent + - present + - stopped + - started + - created + - quadlet + 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 + 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. + type: list + elements: str + choices: + - stdin + - stdout + - stderr + 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 + 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 + 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 + 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 + 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 CFS (Completely Fair Scheduler) period + type: int + cpu_quota: + description: + - Limit the CPU CFS (Completely Fair Scheduler) quota + 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 + 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. + Applies to "delete" command. + type: bool + delete_time: + description: + - Seconds to wait before forcibly stopping the container. Use -1 for infinite wait. + Applies to "delete" command. + type: str + delete_volumes: + description: + - Remove anonymous volumes associated with the container. + This does not include named volumes created with podman volume create, + or the --volume option of podman run and podman create. + type: bool + 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 [:][:] + (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 + (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. + The file must be present on the REMOTE machine where actual podman is + running, not on the controller machine where Ansible is executing. + If you need to copy the file from controller to remote machine, use the + copy or slurp module. + type: list + elements: path + aliases: + - env_files + env_host: + description: + - 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 + 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 + force_delete: + description: + - Force deletion of container when it's being deleted. + type: bool + default: True + 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' + restart_sec: + description: Set the systemd service restartsec value. + type: int + required: false + start_timeout: + description: Override the default start timeout for the container with the given value. + type: int + required: false + stop_timeout: + description: + - Override the default stop timeout for the container with the given value. Called `time` before version 4. + type: int + required: false + aliases: + - time + 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 + gpus: + description: + - GPU devices to add to the container. + type: str + group_add: + description: + - Add additional groups to run as + type: list + 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 + "none", "kill", "restart", or "stop". + The default policy is "none". + type: str + choices: + - 'none' + - 'kill' + - 'restart' + - 'stop' + aliases: + - health_on_failure + 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 + 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 + 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_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. + 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:'. + 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 + container + type: str + aliases: + - ipc_mode + kernel_memory: + description: + - Kernel memory limit + (format [], 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: reuse another container's network stack + * host use the podman host network stack. + * | connect to a user-defined network + * ns: 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 + 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 + 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 + 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. + 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 + 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. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes C(name) value. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual container args + options as a list of lines to add. + type: list + elements: str + 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 + 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 + restart_time: + description: + - 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. + type: bool + 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 + 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 + seccomp_policy: + description: + - Specify the policy to select the seccomp profile. + type: 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 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 + 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. + 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_time: + description: + - Seconds to wait before forcibly stopping the container. Use -1 for infinite wait. + Applies to "stopped" status. + type: str + 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 + 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, + 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 + tls_verify: + description: + - Require HTTPS and verify certificates when pulling images. + type: bool + 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 + 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 + 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 + 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, + 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 + stop_timeout: 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 + +- name: Run container with complex command with quotes + containers.podman.podman_container: + name: mycontainer + image: certbot/certbot + command: + - renew + - --deploy-hook + - "echo 1 > /var/lib/letsencrypt/complete" + +- name: Create a Quadlet file + containers.podman.podman_container: + name: quadlet-container + image: nginx + state: quadlet + quadlet_filename: custome-container + device: "/dev/sda:/dev/xvda:rwm" + ports: + - "8080:80" + volumes: + - "/var/www:/usr/share/nginx/html" + quadlet_options: + - "AutoUpdate=registry" + - "Pull=true" + - | + [Install] + WantedBy=default.target +""" + +RETURN = r""" +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/plugins/modules/podman_container_exec.py b/plugins/modules/podman_container_exec.py new file mode 100644 index 0000000..1827b0c --- /dev/null +++ b/plugins/modules/podman_container_exec.py @@ -0,0 +1,254 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2023, Takuya Nishimura <@nishipy> +# 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_exec +author: + - Takuya Nishimura (@nishipy) +short_description: Executes a command in a running container. +description: + - Executes a command in a running container. +options: + name: + description: + - Name of the container where the command is executed. + type: str + required: true + command: + description: + - The command to run in the container. + - One of the I(command) or I(args) is required. + type: str + argv: + description: + - Passes the command as a list rather than a string. + - One of the I(command) or I(args) is required. + type: list + elements: str + detach: + description: + - If true, the command runs in the background. + - The exec session is automatically removed when it completes. + type: bool + default: false + env: + description: + - Set environment variables. + type: dict + executable: + description: + - The path to the podman executable. + type: str + default: podman + privileged: + description: + - Give extended privileges to the container. + type: bool + default: false + tty: + description: + - Allocate a pseudo-TTY. + type: bool + default: false + user: + description: + - The username or UID used and, optionally, the groupname or GID for the specified command. + - Both user and group may be symbolic or numeric. + type: str + workdir: + description: + - Working directory inside the container. + type: str +requirements: + - podman +notes: + - See L(the Podman documentation,https://docs.podman.io/en/latest/markdown/podman-exec.1.html) for details of podman-exec(1). +''' + +EXAMPLES = r''' +- name: Execute a command with workdir + containers.podman.podman_container_exec: + name: ubi8 + command: "cat redhat-release" + workdir: /etc + +- name: Execute a command with a list of args and environment variables + containers.podman.podman_container_exec: + name: test_container + argv: + - /bin/sh + - -c + - echo $HELLO $BYE + env: + HELLO: hello world + BYE: goodbye world + +- name: Execute command in background by using detach + containers.podman.podman_container_exec: + name: detach_container + command: "cat redhat-release" + detach: true +''' + +RETURN = r''' +stdout: + type: str + returned: success + description: + - The standard output of the command executed in the container. +stderr: + type: str + returned: success + description: + - The standard output of the command executed in the container. +rc: + type: int + returned: success + sample: 0 + description: + - The exit code of the command executed in the container. +exec_id: + type: str + returned: success and I(detach=true) + sample: f99002e34c1087fd1aa08d5027e455bf7c2d6b74f019069acf6462a96ddf2a47 + description: + - The ID of the exec session. +''' + + +import shlex +from ansible.module_utils.six import string_types +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.containers.podman.plugins.module_utils.podman.common import run_podman_command + + +def run_container_exec(module: AnsibleModule) -> dict: + ''' + Execute podman-container-exec for the given options + ''' + exec_with_args = ['container', 'exec'] + # podman_container_exec always returns changed=true + changed = True + exec_options = [] + + name = module.params['name'] + argv = module.params['argv'] + command = module.params['command'] + detach = module.params['detach'] + env = module.params['env'] + privileged = module.params['privileged'] + tty = module.params['tty'] + user = module.params['user'] + workdir = module.params['workdir'] + executable = module.params['executable'] + + if command is not None: + argv = shlex.split(command) + + if detach: + exec_options.append('--detach') + + if env is not None: + for key, value in env.items(): + if not isinstance(value, string_types): + module.fail_json( + msg="Specify string value %s on the env field" % (value)) + + to_text(value, errors='surrogate_or_strict') + exec_options += ['--env', + '%s=%s' % (key, value)] + + if privileged: + exec_options.append('--privileged') + + if tty: + exec_options.append('--tty') + + if user is not None: + exec_options += ['--user', + to_text(user, errors='surrogate_or_strict')] + + if workdir is not None: + exec_options += ['--workdir', + to_text(workdir, errors='surrogate_or_strict')] + + exec_options.append(name) + exec_options.extend(argv) + + exec_with_args.extend(exec_options) + + rc, stdout, stderr = run_podman_command( + module=module, executable=executable, args=exec_with_args, ignore_errors=True) + + result = { + 'changed': changed, + 'podman_command': exec_options, + 'rc': rc, + 'stdout': stdout, + 'stderr': stderr, + } + + if detach: + result['exec_id'] = stdout.replace('\n', '') + + return result + + +def main(): + argument_spec = { + 'name': { + 'type': 'str', + 'required': True, + }, + 'command': { + 'type': 'str', + }, + 'argv': { + 'type': 'list', + 'elements': 'str', + }, + 'detach': { + 'type': 'bool', + 'default': False, + }, + 'executable': { + 'type': 'str', + 'default': 'podman', + }, + 'env': { + 'type': 'dict', + }, + 'privileged': { + 'type': 'bool', + 'default': False, + }, + 'tty': { + 'type': 'bool', + 'default': False, + }, + 'user': { + 'type': 'str', + }, + 'workdir': { + 'type': 'str', + }, + } + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=[('argv', 'command')], + ) + + result = run_container_exec(module) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/podman_container_info.py b/plugins/modules/podman_container_info.py new file mode 100644 index 0000000..dd361c4 --- /dev/null +++ b/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 specified 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/plugins/modules/podman_containers.py b/plugins/modules/podman_containers.py new file mode 100644 index 0000000..7f418a6 --- /dev/null +++ b/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.10-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/plugins/modules/podman_export.py b/plugins/modules/podman_export.py new file mode 100644 index 0000000..dda0099 --- /dev/null +++ b/plugins/modules/podman_export.py @@ -0,0 +1,127 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2021, Sagi Shnaidman +# 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 + volume: + description: + - Volume to export. + type: str + 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 +- containers.podman.podman_export: + dest: /path/to/tar/file + volume: volume-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 + export_type = '' + command = [] + if module.params['container']: + export_type = 'container' + command = [executable, 'export'] + else: + export_type = 'volume' + command = [executable, 'volume', 'export'] + + command += ['-o=%s' % module.params['dest'], module.params[export_type]] + 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 %s %s: %s" % (export_type, + 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'), + volume=dict(type='str'), + force=dict(type='bool', default=True), + executable=dict(type='str', default='podman') + ), + supports_check_mode=True, + mutually_exclusive=[ + ('container', 'volume'), + ], + required_one_of=[ + ('container', 'volume'), + ], + ) + + 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/plugins/modules/podman_generate_systemd.py b/plugins/modules/podman_generate_systemd.py new file mode 100644 index 0000000..b27c16c --- /dev/null +++ b/plugins/modules/podman_generate_systemd.py @@ -0,0 +1,603 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# 2022, Sébastien Gendre +# 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 + force: + description: + - Replace the systemd unit file(s) even if it already exists. + - This works with dest option. + type: bool + default: false + 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 + scope: user + 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 +from ansible_collections.containers.podman.plugins.module_utils.podman.common import compare_systemd_file_content + +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 information 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 directory + 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, + ) + + if module.params['force']: + # Force to replace the existing unit file + need_to_write_file = True + else: + # See if we need to write the unit file, default yes + need_to_write_file = bool(compare_systemd_file_content( + unit_file_full_path, unit_content)) + + # 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, + }, + 'force': { + '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/plugins/modules/podman_image.py b/plugins/modules/podman_image.py new file mode 100644 index 0000000..e0b2986 --- /dev/null +++ b/plugins/modules/podman_image.py @@ -0,0 +1,988 @@ +#!/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 architecture 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 + 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 + 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 + - quadlet + 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: + 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: + - 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 + target: + description: + - Specify the target build stage to build. + 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 + - 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. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + required: false + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes image name without prefixes and tags. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual network args + options as a list of lines to add. + type: list + elements: str + required: false +''' + +EXAMPLES = r""" +- 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 + +- 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 + state: quadlet + quadlet_dir: /etc/containers/systemd + quadlet_filename: alpine-latest + quadlet_options: + - Variant=arm/v7 + - | + [Install] + WantedBy=default.target +""" + +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:...", + "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 " + }, + "User": "1001" + }, + "Created": "2019-04-10T05:48:03.553887623Z", + "Digest": "sha256:5a8ab28e314c2222de3feaf6dac94a0436a37fc08979d2722c99d2bef2619a9b", + "GraphDriver": { + "Data": { + "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" + }, + "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 ", + "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 " + }, + "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 # 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 +from ansible_collections.containers.podman.plugins.module_utils.podman.common import run_podman_command +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state + + +class PodmanImageManager(object): + + 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.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') + 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() + + if self.state == 'quadlet': + self.make_quadlet() + + def _run(self, args, expected_rc=0, ignore_errors=False): + cmd = " ".join([self.executable] + + [to_native(i) for i in args]) + 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.state == 'build' or self.path: + # Build the image + 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'] + 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: + self.results['image'], output = self.push_image() + self.results['stdout'] += "\n" + output + if image and not self.results.get('image'): + self.results['image'] = image + + 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 make_quadlet(self): + results_update = create_quadlet_state(self.module, "image") + self.results.update(results_update) + self.module.exit_json(**self.results) + + def find_image(self, image_name=None): + if image_name is None: + image_name = self.image_name + args = ['image', 'ls', image_name, '--format', 'json'] + rc, images, err = self._run(args, ignore_errors=True) + try: + images = json.loads(images) + except json.decoder.JSONDecodeError: + self.module.fail_json(msg='Failed to parse JSON output from podman image ls: {out}'.format(out=images)) + if len(images) == 0: + # Let's find out if image exists + rc, out, err = self._run(['image', 'exists', image_name], ignore_errors=True) + if rc == 0: + inspect_json = self.inspect_image(image_name) + else: + return None + 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 or inspect_json + 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) + try: + image_data = json.loads(image_data) + except json.decoder.JSONDecodeError: + self.module.fail_json(msg='Failed to parse JSON output from podman inspect: {out}'.format(out=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]) + + 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)) + 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]) + 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: + 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)) + + target = self.build.get('target') + if target: + args.extend(['--target', target]) + 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)) + # 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 + + 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]) + + 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') + transport = self.push_args.get('transport') + + if dest is None: + dest = self.image_name + + if transport: + if transport == 'docker': + dest_format_string = '{transport}://{dest}' + elif transport == 'ostree': + dest_format_string = '{transport}:{name}@{dest}' + else: + dest_format_string = '{transport}:{dest}' + 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['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: + 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), + pull_extra_args=dict(type='str'), + 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', 'quadlet']), + validate_certs=dict(type='bool', aliases=['tlsverify', 'tls_verify']), + executable=dict(type='str', default='podman'), + auth_file=dict(type='path', aliases=['authfile']), + username=dict(type='str'), + password=dict(type='str', no_log=True), + ca_cert_dir=dict(type='path'), + quadlet_dir=dict(type='path', required=False), + quadlet_filename=dict(type='str'), + quadlet_options=dict(type='list', elements='str', required=False), + build=dict( + type='dict', + aliases=['build_args', 'buildargs'], + default={}, + options=dict( + 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'], + 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'), + target=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'],), + extra_args=dict(type='str'), + transport=dict( + type='str', + choices=[ + 'dir', + 'docker-archive', + 'docker-daemon', + 'oci-archive', + 'ostree', + 'docker' + ] + ), + ), + ), + ), + 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/plugins/modules/podman_image_info.py b/plugins/modules/podman_image_info.py new file mode 100644 index 0000000..02b0f9e --- /dev/null +++ b/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: list + 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 " + } + }, + "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 " + }, + "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/plugins/modules/podman_import.py b/plugins/modules/podman_import.py new file mode 100644 index 0000000..6a408c0 --- /dev/null +++ b/plugins/modules/podman_import.py @@ -0,0 +1,194 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2021, Sagi Shnaidman +# 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 + volume: + description: + - Volume to import, cannot be used with change and commit_message + 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 +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" +- containers.podman.podman_import: + src: /path/to/tar/file + volume: myvolume +''' + +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 volume_load(module, executable): + changed = True + command = [executable, 'volume', 'import', module.params['volume'], module.params['src']] + src = module.params['src'] + if module.check_mode: + return changed, '', '', '', command + rc, out, err = module.run_command(command) + if rc != 0: + module.fail_json(msg="Error importing volume %s: %s" % (src, err)) + rc, out2, err2 = module.run_command([executable, 'volume', 'inspect', module.params['volume']]) + if rc != 0: + module.fail_json(msg="Volume %s inspection failed: %s" % (module.params['volume'], err2)) + try: + info = json.loads(out2)[0] + except Exception as e: + module.fail_json(msg="Could not parse JSON from volume %s: %s" % (module.params['volume'], 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'), + volume=dict(type='str', required=False), + ), + mutually_exclusive=[ + ('volume', 'commit_message'), + ('volume', 'change'), + ], + supports_check_mode=True, + ) + + executable = module.get_bin_path(module.params['executable'], required=True) + volume_info = '' + image_info = '' + if module.params['volume']: + changed, out, err, volume_info, command = volume_load(module, executable) + else: + changed, out, err, image_info, command = load(module, executable) + + results = { + "changed": changed, + "stdout": out, + "stderr": err, + "image": image_info, + "volume": volume_info, + "podman_command": " ".join(command) + } + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/podman_load.py b/plugins/modules/podman_load.py new file mode 100644 index 0000000..4fa7bde --- /dev/null +++ b/plugins/modules/podman_load.py @@ -0,0 +1,199 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2020, Sagi Shnaidman +# 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 " + } + }, + "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 " + }, + "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/plugins/modules/podman_login.py b/plugins/modules/podman_login.py new file mode 100644 index 0000000..25bdb8d --- /dev/null +++ b/plugins/modules/podman_login.py @@ -0,0 +1,181 @@ +#!/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 quay.io 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'], + ) + ) + + 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/plugins/modules/podman_login_info.py b/plugins/modules/podman_login_info.py new file mode 100644 index 0000000..739adb1 --- /dev/null +++ b/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/plugins/modules/podman_logout.py b/plugins/modules/podman_logout.py new file mode 100644 index 0000000..d5816a9 --- /dev/null +++ b/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/plugins/modules/podman_network.py b/plugins/modules/podman_network.py new file mode 100644 index 0000000..7623fff --- /dev/null +++ b/plugins/modules/podman_network.py @@ -0,0 +1,861 @@ +#!/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 + 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") + type: str + force: + description: + - Remove all containers that use the network. + If the container is running, it is stopped and removed. + default: False + type: bool + gateway: + description: + - IPv4 or IPv6 gateway for the subnet + type: str + interface_name: + description: + - For bridge, it uses the bridge interface name. + For macvlan, it is the parent device on the host (it is the same + as 'opt.parent') + type: str + internal: + description: + - Restrict external access from this network (default "false") + type: bool + ip_range: + 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 ,,. + This route will be added to every container in this network. + type: list + elements: str + subnet: + description: + - Subnet in CIDR format + type: str + macvlan: + 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. + 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 + (it is the same as 'interface' in that case). + 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 + - quadlet + recreate: + description: + - Recreate network even if exists. + type: bool + default: false + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + required: false + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes I(name) value. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual network args + options as a list of lines to add. + type: list + elements: str + required: false +""" + +EXAMPLES = r""" +- 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 + +- name: Create Quadlet file for podman network + containers.podman.podman_network: + name: podman_network + state: quadlet + quadlet_options: + - IPv6=true + - Label="ipv6 network" +""" + +RETURN = r""" +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 +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 +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state + + +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._delete_action() + if self.action in ['create']: + return self._create_action() + + def _delete_action(self): + cmd = ['rm', self.params['name']] + if self.params['force']: + cmd += ['--force'] + 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_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']] + + 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_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']] + + 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_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']] + + +class PodmanNetworkDefaults: + def __init__(self, module, podman_version): + self.module = module + self.version = podman_version + self.defaults = { + 'driver': 'bridge', + 'internal': 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'] + # compare only if set explicitly + if self.params['disable_dns'] is None: + after = before + 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_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): + # 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'): + 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'] + 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'): + 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_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 = ":".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 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 = '' + 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, + 'quadlet': self.make_quadlet, + } + 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 make_quadlet(self): + results_update = create_quadlet_state(self.module, "network") + self.results.update(results_update) + self.module.exit_json(**self.results) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + state=dict(type='str', default="present", + choices=['present', 'absent', '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), + 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), + 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() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/podman_network_info.py b/plugins/modules/podman_network_info.py new file mode 100644 index 0000000..a9e18cd --- /dev/null +++ b/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/plugins/modules/podman_play.py b/plugins/modules/podman_play.py new file mode 100644 index 0000000..66138ef --- /dev/null +++ b/plugins/modules/podman_play.py @@ -0,0 +1,448 @@ +#!/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 + annotation: + description: + - Add an annotation to the container or pod. + type: dict + aliases: + - annotations + 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 + build: + description: + - Build images even if they are found in the local storage. + - It is required to exist subdirectories matching the image names to be build. + type: bool + 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 + context_dir: + description: + - Use path as the build context directory for each image. + Requires build option be true. + type: 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_opt: + description: + - Logging driver specific options. Set custom logging configuration. + 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. + This option is currently supported only by the journald log driver in Podman. + type: str + required: false + 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 + - quadlet + 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 + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + required: false + quadlet_filename: + description: + - Name of quadlet file to write. Must be specified if state is quadlet. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual network args + options as a list of lines to add. + type: list + elements: str + required: false +''' + +EXAMPLES = ''' +- name: Play kube file + containers.podman.podman_play: + kube_file: ~/kube.yaml + state: started + +- name: Recreate pod from a kube file with options + containers.podman.podman_play: + kube_file: ~/kube.yaml + state: started + recreate: true + annotations: + greeting: hello + greet_to: world + userns: host + log_opt: + path: /tmp/my-container.log + max_size: 10mb + +- name: Create a Quadlet file + containers.podman.podman_play: + kube_file: ~/kube.yaml + state: quadlet + annotations: + greeting: hello + greet_to: world + userns: host + quadlet_filename: kube-pod + quadlet_options: + - "SetWorkingDirectory=yaml" + - "ExitCodePropagation=any" +''' +import re # noqa: F402 +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + +from ansible.module_utils.basic import AnsibleModule # noqa: F402 +from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion, get_podman_version +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state # noqa: F402 + + +class PodmanKubeManagement: + + def __init__(self, module, executable): + self.module = module + self.actions = [] + self.executable = executable + self.command = [self.executable, 'play', 'kube'] + self.version = get_podman_version(module) + creds = [] + # pod_name = extract_pod_name(module.params['kube_file']) + if self.module.params['annotation']: + for k, v in self.module.params['annotation'].items(): + self.command.extend(['--annotation', '{k}={v}'.format(k=k, v=v)]) + 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]) + if self.module.params['log_opt']: + for k, v in self.module.params['log_opt'].items(): + self.command.extend(['--log-opt', '{k}={v}'.format(k=k.replace('_', '-'), v=v)]) + start = self.module.params['state'] == 'started' + self.command.extend(['--start=%s' % str(start).lower()]) + for arg, param in { + '--authfile': 'authfile', + '--build': 'build', + '--cert-dir': 'cert_dir', + '--context-dir': 'context_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 tear_down_pods(self): + ''' + Tear down the pod and contaiers by using --down option in kube play + which is supported since Podman 3.4.0 + ''' + changed = False + kube_file = self.module.params['kube_file'] + + rc, out, err = self._command_run([self.executable, "kube", "play", "--down", kube_file]) + if rc != 0: + self.module.fail_json(msg="Failed to delete Pod with %s" % (kube_file)) + else: + changed = True + + return changed, out, err + + def discover_pods(self): + pod_name = '' + if self.module.params['kube_file']: + if HAS_YAML: + with open(self.module.params['kube_file']) as f: + pods = list(yaml.safe_load_all(f)) + for pod in pods: + if 'metadata' in pod and pod['kind'] in ['Deployment', 'Pod']: + pod_name = pod['metadata'].get('name') + else: + with open(self.module.params['kube_file']) as text: + # the following formats are matched for a kube name: + # 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[\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("This Kube file doesn't have Pod or Deployment!") + # 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): + if self.version is not None and LooseVersion(self.version) >= LooseVersion('3.4.0'): + self.tear_down_pods() + else: + pods = self.discover_pods() + self.remove_associated_pods(pods) + + # Create a pod + rc, out, err = self._command_run(self.command) + if rc != 0: + 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 make_quadlet(self): + results = {"changed": False} + results_update = create_quadlet_state(self.module, "kube") + results.update(results_update) + self.module.exit_json(**results) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + annotation=dict(type='dict', aliases=['annotations']), + executable=dict(type='str', default='podman'), + kube_file=dict(type='path', required=True), + authfile=dict(type='path'), + build=dict(type='bool'), + cert_dir=dict(type='path'), + configmap=dict(type='list', elements='path'), + context_dir=dict(type='path'), + seccomp_profile_root=dict(type='path'), + username=dict(type='str'), + password=dict(type='str', no_log=True), + log_driver=dict(type='str'), + log_opt=dict(type='dict', aliases=['log_options'], options=dict( + path=dict(type='str'), + max_size=dict(type='str'), + tag=dict(type='str'))), + network=dict(type='list', elements='str'), + state=dict( + type='str', + choices=['started', 'created', 'absent', 'quadlet'], + 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"]), + quadlet_dir=dict(type='path', required=False), + quadlet_filename=dict(type='str', required=False), + quadlet_options=dict(type='list', elements='str', required=False), + ), + supports_check_mode=True, + required_if=[ + ('state', 'quadlet', ['quadlet_filename']), + ], + ) + + executable = module.get_bin_path( + module.params['executable'], required=True) + manage = PodmanKubeManagement(module, executable) + if module.params['state'] == 'absent': + if manage.version is not None and LooseVersion(manage.version) > LooseVersion('3.4.0'): + manage.module.log(msg="version: %s, kube file %s" % (manage.version, manage.module.params['kube_file'])) + changed, out, err = manage.tear_down_pods() + else: + pods = manage.discover_pods() + changed, out, err = manage.remove_associated_pods(pods) + elif module.params['state'] == 'quadlet': + manage.make_quadlet() + else: + changed, out, err = manage.play() + results = { + "changed": changed, + "stdout": out, + "stderr": err, + "actions": manage.actions + } + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/podman_pod.py b/plugins/modules/podman_pod.py new file mode 100644 index 0000000..cdf7282 --- /dev/null +++ b/plugins/modules/podman_pod.py @@ -0,0 +1,616 @@ +#!/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 + - quadlet + 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 + blkio_weight: + description: + - Block IO relative weight. The weight is a value between 10 and 1000. + - This option is not supported on cgroups V1 rootless systems. + type: str + required: false + blkio_weight_device: + description: + - Block IO relative device weight. + 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 + cpuset_mems: + description: + - Memory nodes in which to allow execution (0-3, 0,1). Only effective on NUMA systems. + required: false + type: str + cpu_shares: + description: + - CPU shares (relative weight). + 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 + device_write_bps: + description: + - Limit write rate (in bytes per second) to a device. + type: list + elements: str + required: false + 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 + aliases: + - dns_option + 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 + 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. + 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' + restart_sec: + description: Set the systemd service restartsec value. + type: int + required: false + start_timeout: + description: Override the default start timeout for the container with the given value. + type: int + required: false + stop_timeout: + description: + - Override the default stop timeout for the container with the given value. Called `time` before version 4. + type: int + required: false + aliases: + - time + 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 + 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 + 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 + 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. + 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 + memory: + description: + - Set memory limit. + - A unit can be b (bytes), k (kibibytes), m (mebibytes), or g (gibibytes). + type: str + required: false + memory_swap: + description: + - Set limit value equal to memory plus swap. + - A unit can be b (bytes), k (kibibytes), m (mebibytes), or g (gibibytes). + 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 + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes I(name) value. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual container args + options as a list of lines to add. + type: list + elements: str + 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, + no namespaces will be shared. The namespaces to choose from are ipc, net, pid, + 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 + 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 + 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. + 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 + uts: + description: + - Set the UTS namespace mode for the pod. + required: false + type: str + volume: + description: + - Create a bind mount. + aliases: + - volumes + 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 + 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 = r''' +# What modules does for example +- containers.podman.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" + +# 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 + state: quadlet + ports: + - "4444:5555" + volume: + - /var/run/docker.sock:/var/run/docker.sock + quadlet_dir: /custom/dir +''' +from ansible.module_utils.basic import AnsibleModule # noqa: F402 +from ..module_utils.podman.podman_pod_lib import PodmanPodManager # noqa: F402 +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/plugins/modules/podman_pod_info.py b/plugins/modules/podman_pod_info.py new file mode 100644 index 0000000..8597ae9 --- /dev/null +++ b/plugins/modules/podman_pod_info.py @@ -0,0 +1,148 @@ +#!/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] + data = json.loads(out) if out else None + if isinstance(data, list) and data: + data = data[0] + if not out or data is None or not data: + continue + result.append(data) + 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/plugins/modules/podman_prune.py b/plugins/modules/podman_prune.py new file mode 100644 index 0000000..3fe3b75 --- /dev/null +++ b/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 + +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: + - Whether to prune unused pods, containers, image, networks and volume data + type: bool + default: false + system_all: + description: + - Whether to prune all unused images, not only dangling images. + type: bool + default: false + system_volumes: + description: + - Whether 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/plugins/modules/podman_runlabel.py b/plugins/modules/podman_runlabel.py new file mode 100644 index 0000000..e5b6cf3 --- /dev/null +++ b/plugins/modules/podman_runlabel.py @@ -0,0 +1,86 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2023, Pavel Dostal <@pdostal> +# 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_runlabel +short_description: Run given label from given image +author: Pavel Dostal (@pdostal) +description: + - podman container runlabel runs selected label from given image +options: + image: + description: + - Image to get the label from. + type: str + required: true + label: + description: + - Label to run. + 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 +requirements: + - "Podman installed on host" +''' + +RETURN = ''' +''' + +EXAMPLES = ''' +# What modules does for example +- containers.podman.podman_runlabel: + image: docker.io/continuumio/miniconda3 + label: INSTALL +''' + +from ansible.module_utils.basic import AnsibleModule # noqa: E402 + + +def runlabel(module, executable): + changed = False + command = [executable, 'container', 'runlabel'] + command.append(module.params['label']) + command.append(module.params['image']) + rc, out, err = module.run_command(command) + if rc == 0: + changed = True + else: + module.fail_json(msg="Error running the runlabel from image %s: %s" % ( + module.params['image'], err)) + return changed, out, err + + +def main(): + module = AnsibleModule( + argument_spec=dict( + image=dict(type='str', required=True), + label=dict(type='str', required=True), + executable=dict(type='str', default='podman') + ), + supports_check_mode=False, + ) + + executable = module.get_bin_path(module.params['executable'], required=True) + changed, out, err = runlabel(module, executable) + + results = { + "changed": changed, + "stdout": out, + "stderr": err + } + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/podman_save.py b/plugins/modules/podman_save.py new file mode 100644 index 0000000..e23f310 --- /dev/null +++ b/plugins/modules/podman_save.py @@ -0,0 +1,152 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2020, Sagi Shnaidman +# 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: list + elements: 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: + image: nginx + dest: /tmp/file123.tar +- containers.podman.podman_save: + image: + - nginx + - fedora + dest: /tmp/file456.tar + multi_image_archive: true +''' + +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] + for img in module.params['image']: + command.append(img) + 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='list', elements='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/plugins/modules/podman_search.py b/plugins/modules/podman_search.py new file mode 100644 index 0000000..128e3ce --- /dev/null +++ b/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/plugins/modules/podman_secret.py b/plugins/modules/podman_secret.py new file mode 100644 index 0000000..76b10ad --- /dev/null +++ b/plugins/modules/podman_secret.py @@ -0,0 +1,311 @@ +#!/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). + Mutually exclusive with C(env) and C(path). + 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 + 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 + 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 + 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. + type: str + default: present + choices: + - absent + - present + labels: + description: + - Labels to set on the secret. + type: dict + debug: + description: + - Enable debug mode for module. It prints secrets diff. + type: bool + default: False +''' + +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 + """ + +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 + +diff = {"before": '', "after": ''} + + +def podman_secret_exists(module, executable, name, version): + if version is None or LooseVersion(version) < LooseVersion('4.5.0'): + rc, out, err = module.run_command( + [executable, 'secret', 'ls', "--format", "{{.Name}}"]) + return name in [i.strip() for i in out.splitlines()] + rc, out, err = module.run_command( + [executable, 'secret', 'exists', name]) + return rc == 0 + + +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 + if (driver and driver != 'file') or secret['Spec']['Driver']['Name'] != 'file': + if debug: + module.log("PODMAN-SECRET-DEBUG: Idempotency of driver %s is not supported" % driver) + return True + if data: + if secret['SecretData'] != data: + if debug: + diff['after'] = data + diff['before'] = secret['SecretData'] + else: + diff['after'] = "" + diff['before'] = "" + 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'] = "" + diff['before'] = "" + 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'] = "" + diff['before'] = "" + return True + + if driver_opts: + for k, v in driver_opts.items(): + if secret['Spec']['Driver']['Options'].get(k) != v: + diff['after'] = "=".join([k, v]) + diff['before'] = "=".join( + [k, secret['Spec']['Driver']['Options'].get(k)]) + return True + if labels: + for k, v in labels.items(): + if secret['Spec']['Labels'].get(k) != v: + diff['after'] = "=".join([k, v]) + diff['before'] = "=".join( + [k, secret['Spec']['Labels'].get(k)]) + return True + except Exception: + return True + return False + + +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 need_update(module, executable, name, data, path, env, skip, driver, driver_opts, debug, labels): + podman_secret_remove(module, executable, name) + else: + return {"changed": False} + else: + if force: + podman_secret_remove(module, executable, name) + if skip and podman_secret_exists(module, executable, name, podman_version): + 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())) + if labels: + for k, v in labels.items(): + cmd.append('--label') + cmd.append("=".join([k, v])) + cmd.append(name) + 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) + + 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) + + return { + "changed": True, + "diff": { + "before": diff['before'] + "\n", + "after": diff['after'] + "\n", + }, + } + + +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), + 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'), + driver_opts=dict(type='dict'), + 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'] + name = module.params['name'] + executable = module.get_bin_path(module.params['executable'], required=True) + + if state == 'present': + data = module.params['data'] + 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, path, env, force, skip, + driver, driver_opts, debug, labels) + else: + results = podman_secret_remove(module, executable, name) + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/podman_secret_info.py b/plugins/modules/podman_secret_info.py new file mode 100644 index 0000000..ebe8542 --- /dev/null +++ b/plugins/modules/podman_secret_info.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# Copyright (c) 2024 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_secret_info +author: + - "Sagi Shnaidman (@sshnaidm)" +short_description: Gather info about podman secrets +notes: [] +description: + - Gather info about podman secrets with podman inspect command. +requirements: + - "Podman installed on host" +options: + name: + description: + - Name of the secret + type: str + showsecret: + description: + - Show secret data value + type: bool + default: 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 +''' + +EXAMPLES = r""" +- name: Gather info about all present secrets + podman_secret_info: + +- name: Gather info about specific secret + podman_secret_info: + name: specific_secret +""" + +RETURN = r""" +secrets: + description: Facts from all or specified secrets + returned: always + type: list + sample: [ + { + "ID": "06068c676e9a7f1c7dc0da8dd", + "CreatedAt": "2024-01-28T20:32:08.31857841+02:00", + "UpdatedAt": "2024-01-28T20:32:08.31857841+02:00", + "Spec": { + "Name": "secret_name", + "Driver": { + "Name": "file", + "Options": { + "path": "/home/user/.local/share/containers/storage/secrets/filedriver" + } + }, + "Labels": {} + } + } + ] +""" + +import json +from ansible.module_utils.basic import AnsibleModule + + +def get_secret_info(module, executable, show, name): + command = [executable, 'secret', 'inspect'] + if show: + command.append('--showsecret') + if name: + command.append(name) + else: + all_names = [executable, 'secret', 'ls', '-q'] + rc, out, err = module.run_command(all_names) + name = out.split() + if not name: + return [], out, err + command.extend(name) + rc, out, err = module.run_command(command) + if rc != 0 or 'no secret with name or id' in err: + module.fail_json(msg="Unable to gather info for %s: %s" % (name or 'all secrets', 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'), + showsecret=dict(type='bool', default=False), + ), + supports_check_mode=True, + ) + + name = module.params['name'] + showsecret = module.params['showsecret'] + executable = module.get_bin_path(module.params['executable'], required=True) + + inspect_results, out, err = get_secret_info(module, executable, showsecret, name) + + results = { + "changed": False, + "secrets": inspect_results, + "stderr": err, + } + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/podman_tag.py b/plugins/modules/podman_tag.py new file mode 100644 index 0000000..39e799f --- /dev/null +++ b/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/plugins/modules/podman_volume.py b/plugins/modules/podman_volume.py new file mode 100644 index 0000000..cb958cc --- /dev/null +++ b/plugins/modules/podman_volume.py @@ -0,0 +1,577 @@ +#!/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 + - mounted + - unmounted + - quadlet + 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 + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + required: false + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes I(name) value. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual network args + options as a list of lines to add. + type: list + elements: str + required: false + +requirements: + - "podman" + +''' + +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 +- name: Create a volume + containers.podman.podman_volume: + state: present + name: volume1 + label: + key: value + key2: value2 + options: + - "device=/dev/loop1" + - "type=ext4" + +- name: Create a Quadlet file for a volume + containers.podman.podman_volume: + state: quadlet + name: quadlet_volume + quadlet_filename: custom-name + quadlet_options: + - Group=192 + - Copy=true + - Image=quay.io/centos/centos:latest + +''' +# noqa: F402 +import json # noqa: F402 +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 +from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion +from ansible_collections.containers.podman.plugins.module_utils.podman.common import lower_keys +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state + + +class PodmanVolumeModuleParams: + """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', 'mount', 'unmount']: + 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] + 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']] + 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.mount_point = None + 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]) + if rc == 0: + data = json.loads(out) + if data: + data = data[0] + if data.get("Name") == self.name: + return data + return {} + + 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, delete, mount, unmout + """ + 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]) + # 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( + full_cmd, + 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) + # 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.""" + self._perform_action('delete') + + def create(self): + """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() + 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, + 'mounted': self.make_mount, + 'unmounted': self.make_unmount, + 'quadlet': self.make_quadlet, + } + 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 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) + self.module.exit_json(**self.results) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + state=dict(type='str', default="present", + choices=['present', 'absent', 'mounted', 'unmounted', 'quadlet']), + 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), + quadlet_dir=dict(type='path', required=False), + quadlet_filename=dict(type='str', required=False), + quadlet_options=dict(type='list', elements='str', required=False), + )) + + PodmanVolumeManager(module).execute() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/podman_volume_info.py b/plugins/modules/podman_volume_info.py new file mode 100644 index 0000000..97b43b3 --- /dev/null +++ b/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() diff --git a/plugins/modules/stringloop.py b/plugins/modules/stringloop.py new file mode 100644 index 0000000..d6488ef --- /dev/null +++ b/plugins/modules/stringloop.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# ji_podhead/host_prototypes/utils/generate_mac_adress.py + +DOCUMENTATION = ''' +--- +module: generate_mac_adress +short_description: Generiert eine MAC-Adresse +description: + - Generiert eine zufällige MAC-Adresse +options: + - name: + description: + - Der Name der MAC-Adresse + required: true + type: str +author: + - Ihr Name +''' + + +import os +import json +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +from ansible.module_utils.facts import ansible_facts, get_all_facts +import tempfile +import re +#from hvac import Client as hvac_client +#from ..module_utils.hvac import Client as hvac_client +import urllib.request +import urllib.parse +import json + + + + +class AnsiblePrint: + def __init__(self, module): + self.status = {"log":[], "return":None} + self.module = module + + def log(self, string): + string = str(string) + self.status["log"].append(string) + self.module.log(string) + # self.module.info(string) + + def fail(self, msg, err): + self.status["return"] = {"type": "Error", "value": err} + to_native(self.status) + self.module.fail_json(msg=msg) + +def main(): + module = AnsibleModule( + argument_spec=dict( + string=dict(default=''), + replacements=dict(required=True), # {replacements:{pre:"some sintr to put before replacement", post:"some string to put after replacement"}} + + ), + ) + ansiblePrint = AnsiblePrint(module=module) + string = module.params['string'] + replacements = module.params['replacements'] + + try: + with tempfile.TemporaryFile(mode='w+t') as tmp: + replacements=(replacements[0] + "{" + replacements[1:-2] + "}" + replacements[-1]).replace("\\\" \\\"","\\\", \\\"") + replacements = json.loads(replacements) + tmp.write(replacements) + tmp.seek(0) + replacements = json.load(tmp) + newstring="" + for key,val in replacements.items(): + #if(targets[0] is "all" or targets.__contains__(key) ): + # Setze den Vault-Token + ansiblePrint.log("--------------") + ansiblePrint.log("adding to secrets: key= " + key + " value= " + val) + secrets[key]=val + ansiblePrint.log(antwort) + + + ansiblePrint.log(newstring) + + secrets={} + ansiblePrint.status["return"]=secrets + + except Exception as e: + ansiblePrint.fail(e,"error",) + module.exit_json(result=ansiblePrint.status) + module._log_to_syslog + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..bdcb7ee --- /dev/null +++ b/requirements.yml @@ -0,0 +1,5 @@ +# the collection ilo-ansible-collection also depends on community.general being upto date. +# Use the following command to install: +# ansible-galaxy install -r requirements.yml +collections: + - community.general \ No newline at end of file diff --git a/roles/config/README.md b/roles/config/README.md new file mode 100644 index 0000000..225dd44 --- /dev/null +++ b/roles/config/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/roles/config/defaults/main.yml b/roles/config/defaults/main.yml new file mode 100644 index 0000000..8732dee --- /dev/null +++ b/roles/config/defaults/main.yml @@ -0,0 +1,4 @@ +--- +domains: [] +forwarders: [] +subnets: [] \ No newline at end of file diff --git a/roles/config/meta/main.yml b/roles/config/meta/main.yml new file mode 100644 index 0000000..92fe6c8 --- /dev/null +++ b/roles/config/meta/main.yml @@ -0,0 +1,54 @@ +galaxy_info: + author: ji-podhead + description: just a role for kvm and proxmox + company: the-pod-shop + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.1 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. + dependencies: + - name: jm1.libvirt + version: "2024.5.30" \ No newline at end of file diff --git a/roles/config/tasks/main copy.yml b/roles/config/tasks/main copy.yml new file mode 100644 index 0000000..111e0cc --- /dev/null +++ b/roles/config/tasks/main copy.yml @@ -0,0 +1,55 @@ + + + +- name: config + containers.podman.podman_container_exec: + name: dns + argv: + - /bin/sh + - -c + - "echo // include '/etc/bind/zones.rfc1918'; \ + {% for domain in domains %} \ + zone '{{domain}}' IN { \ + type master; \ + file '/etc/bind/zones/{{domain}}.de'; \ + allow-query { any; }; \ + # allow-update { any; }; \ + }; \ + zone '0.116.10.in-addr.arpa' IN { \ + type master; \ + file /etc/bind/zones/{{domain}}.rev; \ + allow-query { any; }; \ + # allow-update { any; }; \ + }; \ + {% endfor %} + > /etc/bind/named.conf.local" + + - "echo acl local-lan { \ + localhost; \ + # 192.168.2.0/24; \ + # 192.168.120.0/24; \ + {% for subnet in subnets %} \ + {{ subnet }}; \ + {% endfor %} \ + }; \ + options { \ + directory '/var/cache/bind'; \ + forwarders { \ + {% for forwarder in forwarders %} \ + {{forwarder }}; \ + {% endfor %} \ + #100.100.100.100; \ + # the tailscale dns \ + }; \ + allow-query { local-lan; 100.0.0.0/8 }; # Erlaubt Anfragen nur von den in local-lan definierten Netzwerken \ + dnssec-validation auto; \ + auth-nxdomain no; // conform to RFC1035 \ + listen-on-v6 { any; }; \ + recursion no; // we set that to no to avoid unnecessary traffic \ + querylog yes; // Enable for debugging \ + version 'not available'; // Disable for security \ + }; > /etc/bind/named.conf.options" + # env: + # named_conf_local: "{{ lookup('template', 'named_conf_local.j2') }}" + # named_conf_options: "{{ lookup('template', 'named_conf_options.j2') }}" + diff --git a/roles/config/tasks/main.yml b/roles/config/tasks/main.yml new file mode 100644 index 0000000..a2fc484 --- /dev/null +++ b/roles/config/tasks/main.yml @@ -0,0 +1,89 @@ +- name: fick dich doch + debug: + msg: "fick dich doch" +- name: ficvkdich zweimal + debug: + msg: "{{domains}}" +- name: get REVERSED_IP + register: REVERSED_IP + ansible.builtin.shell: | + {% for item in domains %} + echo {{ item.ip }} | awk -F. '{print $3"."$2"."$1}' + {% endfor %} +- name: get ip Cut + register: CUT_IP + ansible.builtin.shell: | + {% for item in domains %} + echo {{ item.ip }} | awk -F. '{print $1"."$2"."$3}' + {% endfor %} + +- name: test REVERSED_IP + debug: + msg: "{{REVERSED_IP}}" + + +- name: config + loop: "{{domains}}" + loop_control: + loop_var: item + index_var: i2 + + containers.podman.podman_container_exec: + name: "{{container_name}}" + argv: + - /bin/sh + - -c + - | + + echo ---------------------- named_conf_local ---------------------- + cat << 'EOF' >> /etc/bind/named.conf.local + zone "{{item.domain}}" IN { + type master; + file "/etc/bind/zones/{{item.domain}}"; + allow-query { any; }; + allow-update { any; }; + }; + zone "{{- REVERSED_IP.stdout_lines[i2]}}.in-addr.arpa" IN { + type master; + file "/etc/bind/zones/{{item.domain}}.rev"; + allow-query { any; }; + allow-update { any; }; + }; + EOF + cat /etc/bind/named.conf.local +- name: config + containers.podman.podman_container_exec: + name: "{{container_name}}" + argv: + - /bin/sh + - -c + - | + echo ---------------------- named_conf_options ---------------------- + cat << 'EOF' > /etc/bind/named.conf.options + acl local-lan { + localhost; + {% for subnet in subnets %} + {{- subnet }}; + {% endfor %} + }; + options { + directory "/var/cache/bind"; + forwarders { + {% for forwarder in forwarders %} + {{- forwarder }}; + {% endfor %} + }; + allow-query { + {% for query in allow_queries %} + {{- query }}; + {% endfor %} + }; + dnssec-validation auto; + auth-nxdomain no; // conform to RFC1035 + listen-on-v6 { any; }; + recursion no; // we set that to no to avoid unnecessary traffic + querylog yes; // Enable for debugging + version "not available"; // Disable for security + }; + EOF + cat etc/bind/named.conf.options diff --git a/roles/config/templates/named_conf_local copy 2.j2 b/roles/config/templates/named_conf_local copy 2.j2 new file mode 100644 index 0000000..2389de4 --- /dev/null +++ b/roles/config/templates/named_conf_local copy 2.j2 @@ -0,0 +1,16 @@ +/include "/etc/bind/zones.rfc1918"; \ +% for domain in domains %} \ +one "{{domain}}" IN { \ + type master; \ + file "/etc/bind/zones/{{domain}}.de"; \ + allow-query { any; }; \ + allow-update { any; }; \ +; \ +one "0.116.10.in-addr.arpa" IN { \ + type master; \ + file /etc/bind/zones/{{domain}}.rev; \ + allow-query { any; }; \ + allow-update { any; }; \ +; \ +% endfor %} \ + \ \ No newline at end of file diff --git a/roles/config/templates/named_conf_local copy.j2 b/roles/config/templates/named_conf_local copy.j2 new file mode 100644 index 0000000..2389de4 --- /dev/null +++ b/roles/config/templates/named_conf_local copy.j2 @@ -0,0 +1,16 @@ +/include "/etc/bind/zones.rfc1918"; \ +% for domain in domains %} \ +one "{{domain}}" IN { \ + type master; \ + file "/etc/bind/zones/{{domain}}.de"; \ + allow-query { any; }; \ + allow-update { any; }; \ +; \ +one "0.116.10.in-addr.arpa" IN { \ + type master; \ + file /etc/bind/zones/{{domain}}.rev; \ + allow-query { any; }; \ + allow-update { any; }; \ +; \ +% endfor %} \ + \ \ No newline at end of file diff --git a/roles/config/templates/named_conf_local.j2 b/roles/config/templates/named_conf_local.j2 new file mode 100644 index 0000000..66c5e9a --- /dev/null +++ b/roles/config/templates/named_conf_local.j2 @@ -0,0 +1,15 @@ +//include '/etc/bind/zones.rfc1918'; +% for domain in domains %} +one '{{domain}}' IN { + type master; + file '/etc/bind/zones/{{domain}}.de'; + allow-query { any; }; + allow-update { any; }; +; +one '0.116.10.in-addr.arpa' IN { + type master; + file /etc/bind/zones/{{domain}}.rev; + allow-query { any; }; + allow-update { any; }; +; +% endfor %} diff --git a/roles/config/templates/named_conf_options.j2 b/roles/config/templates/named_conf_options.j2 new file mode 100644 index 0000000..79c8b65 --- /dev/null +++ b/roles/config/templates/named_conf_options.j2 @@ -0,0 +1,25 @@ + acl local-lan { + localhost; + # 192.168.2.0/24; + # 192.168.120.0/24; + {% for subnet in subnets %} + {{ subnet }}; + {% endfor %} + }; + options { + directory '/var/cache/bind'; + forwarders { + {% for forwarder in forwarders %} + {{forwarder }}; + {% endfor %} + #100.100.100.100; + # the tailscale dns + }; + allow-query { local-lan; 100.0.0.0/8 }; # Erlaubt Anfragen nur von den in local-lan definierten Netzwerken + dnssec-validation auto; + auth-nxdomain no; // conform to RFC1035 + listen-on-v6 { any; }; + recursion no; // we set that to no to avoid unnecessary traffic + querylog yes; // Enable for debugging + version 'not available'; // Disable for security + }; \ No newline at end of file diff --git a/roles/install/README.md b/roles/install/README.md new file mode 100644 index 0000000..225dd44 --- /dev/null +++ b/roles/install/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/roles/install/defaults/main.yml b/roles/install/defaults/main.yml new file mode 100644 index 0000000..26dfbe0 --- /dev/null +++ b/roles/install/defaults/main.yml @@ -0,0 +1,5 @@ +--- +container_name: "dns" +container_ip: "{{1.1.1.1}}" +dns_admin: "admin" +dns_domain: "dns.com" diff --git a/roles/install/meta/main.yml b/roles/install/meta/main.yml new file mode 100644 index 0000000..92fe6c8 --- /dev/null +++ b/roles/install/meta/main.yml @@ -0,0 +1,54 @@ +galaxy_info: + author: ji-podhead + description: just a role for kvm and proxmox + company: the-pod-shop + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.1 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. + dependencies: + - name: jm1.libvirt + version: "2024.5.30" \ No newline at end of file diff --git a/roles/install/tasks/main.yml b/roles/install/tasks/main.yml new file mode 100644 index 0000000..7192e0a --- /dev/null +++ b/roles/install/tasks/main.yml @@ -0,0 +1,12 @@ + +- name: Create yamls + ansible.builtin.command: podman rm -f {{container_name}} + +- name: install bind9 + containers.podman.podman_container: + name: "{{container_name}}" + image: docker.io/ubuntu/bind9:latest + state: started + ports: + - "30053:53" + - "30053:53/udp" \ No newline at end of file diff --git a/roles/requirements.yml b/roles/requirements.yml new file mode 100644 index 0000000..e9a5e33 --- /dev/null +++ b/roles/requirements.yml @@ -0,0 +1,4 @@ +- src: https://github.com/ansible-community/ansible-vault.git + name: ansible-community.ansible-vault + scm: git + version: master \ No newline at end of file diff --git a/roles/set_zones/README.md b/roles/set_zones/README.md new file mode 100644 index 0000000..225dd44 --- /dev/null +++ b/roles/set_zones/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/roles/set_zones/defaults/main.yml b/roles/set_zones/defaults/main.yml new file mode 100644 index 0000000..6da76cd --- /dev/null +++ b/roles/set_zones/defaults/main.yml @@ -0,0 +1,11 @@ +--- +serial: 2 +refrefsh: 604800 +retry: 86400 +expire: 2419200 +ncttl: 604800 +dns_domain: "lil_bind.com" +dns_admin: "admin@lil_bind.com" + + + diff --git a/roles/set_zones/meta/main.yml b/roles/set_zones/meta/main.yml new file mode 100644 index 0000000..92fe6c8 --- /dev/null +++ b/roles/set_zones/meta/main.yml @@ -0,0 +1,54 @@ +galaxy_info: + author: ji-podhead + description: just a role for kvm and proxmox + company: the-pod-shop + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.1 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. + dependencies: + - name: jm1.libvirt + version: "2024.5.30" \ No newline at end of file diff --git a/roles/set_zones/tasks/main.yml b/roles/set_zones/tasks/main.yml new file mode 100644 index 0000000..dab09a6 --- /dev/null +++ b/roles/set_zones/tasks/main.yml @@ -0,0 +1,104 @@ +- name: get REVERSED_IP + register: REVERSED_IP + ansible.builtin.shell: | + {% for item in domains %} + echo {{ item.ip }} | awk -F. '{print $3"."$2"."$1}' + {% endfor %} +- name: get ip Cut + register: CUT_IP + ansible.builtin.shell: | + {% for item in domains %} + echo {{ item.ip }} | awk -F. '{print $1"."$2"."$3}' + {% endfor %} +- name: get last Octet + register: last_octet + ansible.builtin.shell: | + {% for item in domains %} + echo {{ item.ip }} | awk -F. '{print $4}' + {% endfor %} +- name: test REVERSED_IP + debug: + msg: "{{REVERSED_IP}}" +- name: Create folder + ignore_errors: yes + containers.podman.podman_container_exec: + name: dns + argv: + - /bin/sh + - -c + - | + if [ -d "/etc/bind/zones" ]; then + rm -r /etc/bind/zones + mkdir /etc/bind/zones + else + mkdir /etc/bind/zones + fi + chmod +x /etc/bind/zones + +- name: forward zone + loop: "{{domains}}" + loop_control: + loop_var: item + index_var: i2 + containers.podman.podman_container_exec: + name: "{{container_name}}" + argv: + - /bin/sh + - -c + - | + echo ------------------------ zone {{ item.domain }} ------------------------ + echo ---------> forward <--------- + cat << 'EOF' > /etc/bind/zones/{{ item.domain }} + ; + ; BIND data file for local loopback interface + ; + $TTL 604800 + @ IN SOA {{dns_domain}}. {{dns_admin}}. ( + {{serial}} + {{refrefsh}} + {{retry}} + {{expire}} + {{ncttl}} ) + ; + @ IN NS {{ dns_domain }}. + {{item.domain}}. IN A {{ item.ip }} + {% for sub in item.sub_domains %} + {{ sub.sub_domain }}.{{ item.domain }}. IN A {{CUT_IP.stdout_lines[i2]}}.{{ sub.ip }} + {% endfor %} + EOF + cat /etc/bind/zones/{{ item.domain }} + +- name: reverse zone + loop: "{{domains}}" + loop_control: + loop_var: item + index_var: i2 + containers.podman.podman_container_exec: + name: "{{container_name}}" + argv: + - /bin/sh + - -c + - | + echo ---------> reverse <--------- + cat << 'EOF' > /etc/bind/zones/{{ item.domain }}.rev + ; + ; BIND reverse data file for local loopback interface + ; + $TTL 604800 + @ IN SOA {{dns_domain}}. {{dns_admin}}. ( + {{serial}} + {{refrefsh}} + {{retry}} + {{expire}} + {{ncttl}} ) + ; + @ IN NS {{ dns_domain }}. + {{last_octet.stdout_lines[i2]}} IN PTR {{ item.domain }}. + {% for sub in item.sub_domains %} + {{ sub.ip }} IN PTR {{ sub.sub_domain }}.{{ item.domain }}. + {% endfor %} + + EOF + cat /etc/bind/zones/{{ item.domain }}.rev + echo ------------------------------------------------------------------- + diff --git a/roles/set_zones/templates/forwardzone.j2 b/roles/set_zones/templates/forwardzone.j2 new file mode 100644 index 0000000..f9554b4 --- /dev/null +++ b/roles/set_zones/templates/forwardzone.j2 @@ -0,0 +1,17 @@ + ; + ; BIND data file for local loopback interface + ; + $TTL 604800 + @ IN SOA "{{domains.name}}". root."{{domains.name}}". ( + 2 ; Serial + 604800 ; Refresh + 86400 ; Retry + 2419200 ; Expire + 604800 ) ; Negative Cache TTL + ; + @ IN NS bindserver.{{ip}}. + ; A record for name server + bindserver IN A '{{domains.ip}}' + @ IN NS localhost. + @ IN A '{{domains.ip}}' + @ IN AAAA ::1 \ No newline at end of file diff --git a/roles/set_zones/templates/reversezone.j2 b/roles/set_zones/templates/reversezone.j2 new file mode 100644 index 0000000..0acc6fe --- /dev/null +++ b/roles/set_zones/templates/reversezone.j2 @@ -0,0 +1,17 @@ + ; + ; BIND reverse data file for local loopback interface + ; + $TTL 604800 + @ IN SOA "{{domain.name}}". root."{{domain.name}}". ( + 1 ; Serial + 604800 ; Refresh + 86400 ; Retry + 2419200 ; Expire + 604800 ) ; Negative Cache TTL + ; + ; Name server record + @ IN NS bindserver.{{domain}}. + ; A record for name server + bindserver IN A '{{domain.ip}}' + '{{ip | reverse}}'.in-addr.arpa. IN PTR tele.pod.com. + @ IN NS localhost. diff --git a/roles/update/README.md b/roles/update/README.md new file mode 100644 index 0000000..225dd44 --- /dev/null +++ b/roles/update/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/roles/update/defaults/main.yml b/roles/update/defaults/main.yml new file mode 100644 index 0000000..e35604c --- /dev/null +++ b/roles/update/defaults/main.yml @@ -0,0 +1,8 @@ +--- +yaml_path: "./yamlgen" +group: "" +store_yaml: "False" +inventory_path: "./inventory" +target_group: prototypes +target_group_state: present +target_group_gid: 1000 \ No newline at end of file diff --git a/roles/update/meta/main.yml b/roles/update/meta/main.yml new file mode 100644 index 0000000..92fe6c8 --- /dev/null +++ b/roles/update/meta/main.yml @@ -0,0 +1,54 @@ +galaxy_info: + author: ji-podhead + description: just a role for kvm and proxmox + company: the-pod-shop + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.1 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. + dependencies: + - name: jm1.libvirt + version: "2024.5.30" \ No newline at end of file diff --git a/roles/update/tasks/main.yml b/roles/update/tasks/main.yml new file mode 100644 index 0000000..e5993f7 --- /dev/null +++ b/roles/update/tasks/main.yml @@ -0,0 +1,17 @@ +- name: Create yamls + containers.podman.podman_container_exec: + name: dns + argv: + - /bin/sh + - -c + - | + {% for item in domains %} + /usr/bin/named-checkzone {{item.domain}} /etc/bind/zones/{{item.domain}} + /usr/bin/named-checkzone {{item.domain}}.rev /etc/bind/zones/{{item.domain}}.rev + {% endfor %} + named-checkconf /etc/bind/named.conf.options + /usr/bin/named-checkconf + # /etc/init.d/named restart +- name: restart container + ansible.builtin.command: | + podman restart {{container_name}} \ No newline at end of file