1
0
Fork 0
mirror of https://github.com/containers/ansible-podman-collections.git synced 2026-02-03 23:01:48 +00:00
ansible-podman-collections/plugins/modules/podman_volume.py
Sergey f2e813671a
Run black -l 120 on all files, again (#943)
Signed-off-by: Sagi Shnaidman <sshnaidm@redhat.com>
2025-06-26 13:24:44 +03:00

596 lines
20 KiB
Python

#!/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_file_mode:
description:
- The permissions of the quadlet file.
- The O(quadlet_file_mode) can be specied as octal numbers or as a symbolic mode (for example, V(u+rwx) or V(u=rw,g=r,o=r)).
For octal numbers format, you must either add a leading zero so that Ansible's YAML parser knows it is an
octal number (like V(0644) or V(01777)) or quote it (like V('644') or V('1777')) so Ansible receives a string
and can do its own conversion from string into number. Giving Ansible a number without following one of these
rules will end up with a decimal number which will have unexpected results.
- If O(quadlet_file_mode) is not specified and the quadlet file B(does not) exist, the default V('0640') mask will be used
when setting the mode for the newly created file.
- If O(quadlet_file_mode) is not specified and the quadlet file B(does) exist, the mode of the existing file will be used.
- Specifying O(quadlet_file_mode) is the best way to ensure files are created with the correct permissions.
type: raw
required: false
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: '2023-11-30T16:41:31.310865559+02:00'
Driver: local
Labels: {}
LockNumber: 18
MountCount: 0
Mountpoint: /home/user/.local/share/containers/storage/volumes/volname/_data
Name: volname
NeedsChown: true
NeedsCopyUp: true
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_file_mode: '0640'
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_file_mode=dict(type="raw", required=False),
quadlet_options=dict(type="list", elements="str", required=False),
)
)
PodmanVolumeManager(module).execute()
if __name__ == "__main__":
main()