Idempotency for podman_container

Introduce partial idepmpotency for running podman_container

- Replace all options by their defaults it they're not set
- Force lower case on all podman inspection data
- Add a class with methods for every parameter to check its value
and compare it with module argument
- Add check_mode support and podman_actions

Change-Id: I1ae93dff1e10a1a696bb171996a75a0db6c34fa3
This commit is contained in:
Sagi Shnaidman 2019-10-15 00:21:14 +03:00
parent cb41764f05
commit 1212544a28
3 changed files with 793 additions and 115 deletions

View File

@ -23,6 +23,8 @@ __metaclass__ = type
import json import json
import yaml import yaml
from packaging import version
from ansible.module_utils.basic import AnsibleModule, env_fallback from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible.module_utils._text import to_bytes, to_native from ansible.module_utils._text import to_bytes, to_native
@ -101,7 +103,7 @@ options:
(Not available for remote commands) You can also override the default (Not available for remote commands) You can also override the default
path of the authentication file by setting the ``REGISTRY_AUTH_FILE`` path of the authentication file by setting the ``REGISTRY_AUTH_FILE``
environment variable. ``export REGISTRY_AUTH_FILE=path`` environment variable. ``export REGISTRY_AUTH_FILE=path``
type: str type: path
blkio_weight: blkio_weight:
description: description:
- Block IO weight (relative weight) accepts a weight value between 10 and - Block IO weight (relative weight) accepts a weight value between 10 and
@ -128,7 +130,22 @@ options:
If the path is not absolute, the path is considered to be relative to 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 the cgroups path of the init process. Cgroups will be created if they
do not already exist. do not already exist.
type: path
cgroupns:
description:
- Path to cgroups under which the cgroup for the container will be
created.
type: str 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
choices:
- default
- disabled
cidfile: cidfile:
description: description:
- Write the container ID to the file - Write the container ID to the file
@ -154,9 +171,17 @@ options:
description: description:
- Limit the CPU real-time period in microseconds - Limit the CPU real-time period in microseconds
type: int 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: cpu_rt_runtime:
description: description:
- Limit the CPU real-time runtime in microseconds - 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 type: int
cpu_shares: cpu_shares:
description: description:
@ -180,6 +205,11 @@ options:
- Run container in detach mode - Run container in detach mode
type: bool type: bool
default: True default: True
debug:
description:
- Return additional information which can be helpful for investigations.
type: bool
default: False
detach_keys: detach_keys:
description: description:
- Override the key sequence for detaching a container. Format is a single - Override the key sequence for detaching a container. Format is a single
@ -190,27 +220,28 @@ options:
- Add a host device to the container. - Add a host device to the container.
The format is <device-on-host>[:<device-on-container>][:<permissions>] The format is <device-on-host>[:<device-on-container>][:<permissions>]
(e.g. device /dev/sdc:/dev/xvdc:rwm) (e.g. device /dev/sdc:/dev/xvdc:rwm)
type: str type: list
elements: str
device_read_bps: device_read_bps:
description: description:
- Limit read rate (bytes per second) from a device - Limit read rate (bytes per second) from a device
(e.g. device-read-bps /dev/sda:1mb) (e.g. device-read-bps /dev/sda:1mb)
type: str type: list
device_read_iops: device_read_iops:
description: description:
- Limit read rate (IO per second) from a device - Limit read rate (IO per second) from a device
(e.g. device-read-iops /dev/sda:1000) (e.g. device-read-iops /dev/sda:1000)
type: str type: list
device_write_bps: device_write_bps:
description: description:
- Limit write rate (bytes per second) to a device - Limit write rate (bytes per second) to a device
(e.g. device-write-bps /dev/sda:1mb) (e.g. device-write-bps /dev/sda:1mb)
type: str type: list
device_write_iops: device_write_iops:
description: description:
- Limit write rate (IO per second) to a device - Limit write rate (IO per second) to a device
(e.g. device-write-iops /dev/sda:1000) (e.g. device-write-iops /dev/sda:1000)
type: str type: list
dns: dns:
description: description:
- Set custom DNS servers - Set custom DNS servers
@ -240,6 +271,11 @@ options:
description: description:
- Read in a line delimited file of environment variables - Read in a line delimited file of environment variables
type: path type: path
env_host:
description:
- Use all current host environment variables in container.
Defaults to false.
type: bool
etc_hosts: etc_hosts:
description: description:
- Dict of host-to-IP mappings, where each host name is a key in the - Dict of host-to-IP mappings, where each host name is a key in the
@ -271,7 +307,7 @@ options:
group_add: group_add:
description: description:
- Add additional groups to run as - Add additional groups to run as
type: str type: list
healthcheck: healthcheck:
description: description:
- Set or alter a healthcheck command for a container. - Set or alter a healthcheck command for a container.
@ -356,6 +392,7 @@ options:
description: description:
- Kernel memory limit - Kernel memory limit
(format <number>[<unit>], where unit = b, k, m or g) (format <number>[<unit>], where unit = b, k, m or g)
Note - idempotency is supported for integers only.
type: str type: str
label: label:
description: description:
@ -385,10 +422,12 @@ options:
memory: memory:
description: description:
- Memory limit (format 10k, where unit = b, k, m or g) - Memory limit (format 10k, where unit = b, k, m or g)
Note - idempotency is supported for integers only.
type: str type: str
memory_reservation: memory_reservation:
description: description:
- Memory soft limit (format 100m, where unit = b, k, m or g) - Memory soft limit (format 100m, where unit = b, k, m or g)
Note - idempotency is supported for integers only.
type: str type: str
memory_swap: memory_swap:
description: description:
@ -396,6 +435,7 @@ options:
(--memory) flag. (--memory) flag.
The swap LIMIT should always be larger than -m (--memory) value. 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 By default, the swap LIMIT will be set to double the value of --memory
Note - idempotency is supported for integers only.
type: str type: str
memory_swappiness: memory_swappiness:
description: description:
@ -419,7 +459,8 @@ options:
* ns:<path> path to a network namespace to join * ns:<path> path to a network namespace to join
* slirp4netns use slirp4netns to create a user network stack. * slirp4netns use slirp4netns to create a user network stack.
This is the default for rootless containers This is the default for rootless containers
type: str type: list
elements: str
aliases: aliases:
- net - net
no_hosts: no_hosts:
@ -512,7 +553,8 @@ options:
security_opt: security_opt:
description: description:
- Security Options. For example security_opt "seccomp=unconfined" - Security Options. For example security_opt "seccomp=unconfined"
type: str type: list
elements: str
shm_size: shm_size:
description: description:
- Size of /dev/shm. The format is <number><unit>. number must be greater - Size of /dev/shm. The format is <number><unit>. number must be greater
@ -530,7 +572,7 @@ options:
stop_signal: stop_signal:
description: description:
- Signal to stop a container. Default is SIGTERM. - Signal to stop a container. Default is SIGTERM.
type: str type: int
stop_timeout: stop_timeout:
description: description:
- Timeout (in seconds) to stop a container. Default is 10. - Timeout (in seconds) to stop a container. Default is 10.
@ -813,9 +855,12 @@ class PodmanModuleParams:
params {dict} -- dictionary of module parameters params {dict} -- dictionary of module parameters
""" """
def __init__(self, action, params):
def __init__(self, action, params, podman_version, module):
self.params = params self.params = params
self.action = action self.action = action
self.podman_version = podman_version
self.module = module
def construct_command_from_params(self): def construct_command_from_params(self):
"""Create a podman command from given module parameters. """Create a podman command from given module parameters.
@ -853,19 +898,26 @@ class PodmanModuleParams:
cmd = ['rm', '-f', self.params['name']] cmd = ['rm', '-f', self.params['name']]
return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] return [to_bytes(i, errors='surrogate_or_strict') for i in cmd]
def addparam_detach(self, c): def check_version(self, param, minv=None, maxv=None):
return c + ['--detach=%s' % self.params['detach']] if minv and version.parse(minv) > version.parse(
self.podman_version):
def addparam_etc_hosts(self, c): self.module.fail_json(msg="Parameter %s is supported from podman "
for host_ip in self.params['etc_hosts'].items(): "version %s only! Current version is %s" % (
c += ['--add-host', ':'.join(host_ip)] param, minv, self.podman_version))
return c if maxv and version.parse(maxv) < version.parse(
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): def addparam_annotation(self, c):
for annotate in self.params['annotation'].items(): for annotate in self.params['annotation'].items():
c += ['--annotation', '='.join(annotate)] c += ['--annotation', '='.join(annotate)]
return c return c
def addparam_authfile(self, c):
return c + ['--authfile', self.params['authfile']]
def addparam_blkio_weight(self, c): def addparam_blkio_weight(self, c):
return c + ['--blkio-weight', self.params['blkio_weight']] return c + ['--blkio-weight', self.params['blkio_weight']]
@ -884,6 +936,14 @@ class PodmanModuleParams:
c += ['--cap-drop', cap_drop] c += ['--cap-drop', cap_drop]
return c 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): def addparam_cgroup_parent(self, c):
return c + ['--cgroup-parent', self.params['cgroup_parent']] return c + ['--cgroup-parent', self.params['cgroup_parent']]
@ -896,6 +956,9 @@ class PodmanModuleParams:
def addparam_cpu_period(self, c): def addparam_cpu_period(self, c):
return c + ['--cpu-period', self.params['cpu_period']] return c + ['--cpu-period', self.params['cpu_period']]
def addparam_cpu_rt_period(self, c):
return c + ['--cpu-rt-period', self.params['cpu_rt_period']]
def addparam_cpu_rt_runtime(self, c): def addparam_cpu_rt_runtime(self, c):
return c + ['--cpu-rt-runtime', self.params['cpu_rt_runtime']] return c + ['--cpu-rt-runtime', self.params['cpu_rt_runtime']]
@ -911,23 +974,36 @@ class PodmanModuleParams:
def addparam_cpuset_mems(self, c): def addparam_cpuset_mems(self, c):
return c + ['--cpuset-mems', self.params['cpuset_mems']] return c + ['--cpuset-mems', self.params['cpuset_mems']]
def addparam_detach(self, c):
return c + ['--detach=%s' % self.params['detach']]
def addparam_detach_keys(self, c): def addparam_detach_keys(self, c):
return c + ['--detach-keys', self.params['detach_keys']] return c + ['--detach-keys', self.params['detach_keys']]
def addparam_device(self, c): def addparam_device(self, c):
return c + ['--device', self.params['device']] for dev in self.params['device']:
c += ['--device', dev]
return c
def addparam_device_read_bps(self, c): def addparam_device_read_bps(self, c):
return c + ['--device-read-bps', self.params['device_read_bps']] for dev in self.params['device_read_bps']:
c += ['--device-read-bps', dev]
return c
def addparam_device_read_iops(self, c): def addparam_device_read_iops(self, c):
return c + ['--device-read-iops', self.params['device_read_iops']] for dev in self.params['device_read_iops']:
c += ['--device-read-iops', dev]
return c
def addparam_device_write_bps(self, c): def addparam_device_write_bps(self, c):
return c + ['--device-write-bps', self.params['device_write_bps']] for dev in self.params['device_write_bps']:
c += ['--device-write-bps', dev]
return c
def addparam_device_write_iops(self, c): def addparam_device_write_iops(self, c):
return c + ['--device-write-iops', self.params['device_write_iops']] for dev in self.params['device_write_iops']:
c += ['--device-write-iops', dev]
return c
def addparam_dns(self, c): def addparam_dns(self, c):
return c + ['--dns', ','.join(self.params['dns'])] return c + ['--dns', ','.join(self.params['dns'])]
@ -951,6 +1027,15 @@ class PodmanModuleParams:
def addparam_env_file(self, c): def addparam_env_file(self, c):
return c + ['--env-file', self.params['env_file']] return c + ['--env-file', self.params['env_file']]
def addparam_env_host(self, c):
self.check_version('--env-host', minv='1.5.0')
return c + ['--env-host=%s' % self.params['env_host']]
def addparam_etc_hosts(self, c):
for host_ip in self.params['etc_hosts'].items():
c += ['--add-host', ':'.join(host_ip)]
return c
def addparam_expose(self, c): def addparam_expose(self, c):
for exp in self.params['expose']: for exp in self.params['expose']:
c += ['--expose', exp] c += ['--expose', exp]
@ -960,7 +1045,9 @@ class PodmanModuleParams:
return c + ['--gidmap', self.params['gidmap']] return c + ['--gidmap', self.params['gidmap']]
def addparam_group_add(self, c): def addparam_group_add(self, c):
return c + ['--group-add', self.params['group_add']] for g in self.params['group_add']:
c += ['--group-add', g]
return c
def addparam_healthcheck(self, c): def addparam_healthcheck(self, c):
return c + ['--healthcheck', self.params['healthcheck']] return c + ['--healthcheck', self.params['healthcheck']]
@ -1010,7 +1097,8 @@ class PodmanModuleParams:
def addparam_label(self, c): def addparam_label(self, c):
for label in self.params['label'].items(): for label in self.params['label'].items():
c += ['--label', '='.join(label)] c += ['--label', b'='.join([to_bytes(l, errors='surrogate_or_strict')
for l in label])]
return c return c
def addparam_label_file(self, c): def addparam_label_file(self, c):
@ -1038,7 +1126,7 @@ class PodmanModuleParams:
return c + ['--mount', self.params['mount']] return c + ['--mount', self.params['mount']]
def addparam_network(self, c): def addparam_network(self, c):
return c + ['--network', self.params['network']] return c + ['--network', ",".join(self.params['network'])]
def addparam_no_hosts(self, c): def addparam_no_hosts(self, c):
return c + ['--no-hosts=%s' % self.params['no_hosts']] return c + ['--no-hosts=%s' % self.params['no_hosts']]
@ -1085,7 +1173,9 @@ class PodmanModuleParams:
return c + ['--rootfs=%s' % self.params['rootfs']] return c + ['--rootfs=%s' % self.params['rootfs']]
def addparam_security_opt(self, c): def addparam_security_opt(self, c):
return c + ['--security-opt', self.params['security_opt']] for secopt in self.params['security_opt']:
c += ['--security-opt', secopt]
return c
def addparam_shm_size(self, c): def addparam_shm_size(self, c):
return c + ['--shm-size', self.params['shm_size']] return c + ['--shm-size', self.params['shm_size']]
@ -1161,6 +1251,507 @@ class PodmanModuleParams:
return c + self.params['cmd_args'] return c + self.params['cmd_args']
class PodmanDefaults:
def __init__(self, module, podman_version):
self.module = module
self.version = podman_version
self.defaults = {
"annotation": [
"io.container.manager",
"io.kubernetes.cri-o.containertype",
"io.kubernetes.cri-o.created",
"io.kubernetes.cri-o.tty",
"io.podman.annotations.autoremove",
"io.podman.annotations.init",
"io.podman.annotations.privileged",
"io.podman.annotations.publish-all",
"org.opencontainers.image.stopsignal",
],
"blkio_weight": 0,
"cap_add": ['cap_audit_write',
'cap_chown',
'cap_dac_override',
'cap_fowner',
'cap_fsetid',
'cap_kill',
'cap_mknod',
'cap_net_bind_service',
'cap_net_raw',
'cap_setfcap',
'cap_setgid',
'cap_setpcap',
'cap_setuid',
'cap_sys_chroot'],
"cgroups": "default",
"cgroup_parent": "",
"cidfile": "",
"cpus": 0.0,
"cpu_shares": 0,
"cpu_quota": 0,
"cpu_period": 0,
"cpu_rt_runtime": 0,
"cpu_rt_period": 0,
"cpuset_cpus": "",
"cpuset_mems": "",
"detach": True,
"device": [],
"env": [
"path",
"term",
"hostname",
"container",
"home"
],
"env_host": False,
"etc_hosts": [],
"group_add": [],
"ipc": "",
"kernelmemory": "0",
"log_driver": "k8s-file",
"memory": "0",
"memory_swap": "0",
"memory_reservation": "0",
# "memory_swappiness": -1,
"no_hosts": False,
"oom_score_adj": 0,
"pid": "",
"privileged": False,
"rm": False,
"security_opt": [],
"stop_signal": 15,
"tty": False,
"user": "",
"uts": "",
"volume": [],
"workdir": "/",
}
def default_dict(self):
# make here any changes to self.defaults related to podman version
if version.parse(self.version) < version.parse("1.6.0"):
self.defaults["env"] = [
"path",
"term",
"hostname",
"container",
]
return self.defaults
class PodmanContainerDiff:
def __init__(self, module, info, podman_version):
self.module = module
self.version = podman_version
self.default_dict = None
self.info = json.loads(json.dumps(info).lower())
self.params = self.defaultize()
self.diff = {'before': {}, 'after': {}}
self.non_idempotent = {
'env_file',
'env_host',
'stop_timeout',
"network", # Issue in libpod
"ulimit", # Defaults depend on user and platform, impossible to guess
}
self.all_caps = ['cap_audit_control',
'cap_audit_read',
'cap_audit_write',
'cap_block_suspend',
'cap_chown',
'cap_dac_override',
'cap_dac_read_search',
'cap_fowner',
'cap_fsetid',
'cap_ipc_lock',
'cap_ipc_owner',
'cap_kill',
'cap_lease',
'cap_linux_immutable',
'cap_mac_admin',
'cap_mac_override',
'cap_mknod',
'cap_net_admin',
'cap_net_bind_service',
'cap_net_broadcast',
'cap_net_raw',
'cap_setfcap',
'cap_setgid',
'cap_setpcap',
'cap_setuid',
'cap_sys_admin',
'cap_sys_boot',
'cap_sys_chroot',
'cap_sys_module',
'cap_sys_nice',
'cap_sys_pacct',
'cap_sys_ptrace',
'cap_sys_rawio',
'cap_sys_resource',
'cap_sys_time',
'cap_sys_tty_config',
'cap_syslog',
'cap_wake_alarm']
def defaultize(self):
params_with_defaults = {}
self.default_dict = PodmanDefaults(
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_annotation(self):
# before = self.info['config']['annotations']
# after = before.copy()
# if (self.params['annotation'] and self.params['annotation'] != self.default_dict['annotation']):
# after.update(self.params['annotation'])
# for key in list(after):
# if (key not in self.params['annotation'] and key not in self.default_dict['annotation']):
# del after[key]
# return self._diff_update_and_compare('annotation', before, after)
def diffparam_env_host(self):
# It's impossible to get from inspest, recreate it if not default
before = False
after = self.params['env_host']
return self._diff_update_and_compare('env_host', before, after)
def diffparam_blkio_weight(self):
before = self.info['hostconfig']['blkioweight']
after = self.params['blkio_weight']
return self._diff_update_and_compare('blkio_weight', before, after)
def diffparam_blkio_weight_device(self):
before = self.info['hostconfig']['blkioweightdevice']
if before == [] and self.module.params['blkio_weight_device'] is None:
after = []
else:
after = self.params['blkio_weight_device']
return self._diff_update_and_compare('blkio_weight_device', before, after)
def diffparam_cap_add(self):
before = self.info['effectivecaps']
after = self.default_dict['cap_add']
if self.module.params['cap_add'] is not None:
after += ["cap_" + i.lower()
for i in self.module.params['cap_add']]
if self.params['privileged']:
after = self.all_caps
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']
after = self.default_dict['cap_add']
if self.module.params['cap_drop'] is not None:
for c in ["cap_" + i.lower() for i in self.module.params['cap_drop']]:
if c in after:
after.remove(c)
if self.params['privileged']:
after = self.all_caps
before, after = sorted(list(set(before))), sorted(list(set(after)))
return self._diff_update_and_compare('cap_add', before, after)
def diffparam_cgroup_parent(self):
before = self.info['hostconfig']['cgroupparent']
after = self.params['cgroup_parent']
return self._diff_update_and_compare('cgroup_parent', before, after)
def diffparam_cgroups(self):
# Cgroups output is not supported in all versions
if 'cgroups' in self.info['hostconfig']:
before = self.info['hostconfig']['cgroups']
after = self.params['cgroups']
return self._diff_update_and_compare('cgroups', before, after)
return False
def diffparam_cidfile(self):
before = self.info['hostconfig']['containeridfile']
after = self.params['cidfile']
return self._diff_update_and_compare('cidfile', before, after)
def diffparam_command(self):
before = self.info['config']['cmd']
after = self.params['command']
if isinstance(after, str):
after = after.split()
return self._diff_update_and_compare('command', before, after)
def diffparam_conmon_pidfile(self):
before = self.info['conmonpidfile']
if self.module.params['conmon_pidfile'] is None:
after = before
else:
after = self.params['conmon_pidfile']
return self._diff_update_and_compare('conmon_pidfile', before, after)
def diffparam_cpu_period(self):
before = self.info['hostconfig']['cpuperiod']
after = self.params['cpu_period']
return self._diff_update_and_compare('cpu_period', before, after)
def diffparam_cpu_rt_period(self):
before = self.info['hostconfig']['cpurealtimeperiod']
after = self.params['cpu_rt_period']
return self._diff_update_and_compare('cpu_rt_period', before, after)
def diffparam_cpu_rt_runtime(self):
before = self.info['hostconfig']['cpurealtimeruntime']
after = self.params['cpu_rt_runtime']
return self._diff_update_and_compare('cpu_rt_runtime', before, after)
def diffparam_cpu_shares(self):
before = self.info['hostconfig']['cpushares']
after = self.params['cpu_shares']
return self._diff_update_and_compare('cpu_shares', before, after)
def diffparam_cpus(self):
before = int(self.info['hostconfig']['nanocpus']) / 1000000000
after = self.params['cpus']
return self._diff_update_and_compare('cpus', before, after)
def diffparam_cpuset_cpus(self):
before = self.info['hostconfig']['cpusetcpus']
after = self.params['cpuset_cpus']
return self._diff_update_and_compare('cpuset_cpus', before, after)
def diffparam_cpuset_mems(self):
before = self.info['hostconfig']['cpusetmems']
after = self.params['cpuset_mems']
return self._diff_update_and_compare('cpuset_mems', before, after)
def diffparam_device(self):
before = [":".join([i['pathonhost'], i['pathincontainer']])
for i in self.info['hostconfig']['devices']]
after = [":".join(i.split(":")[:2]) for i in self.params['device']]
before, after = sorted(list(set(before))), sorted(list(set(after)))
return self._diff_update_and_compare('devices', before, after)
def diffparam_device_read_bps(self):
before = self.info['hostconfig']['blkiodevicereadbps'] or []
before = ["%s:%s" % (i['path'], i['rate']) for i in before]
after = self.params['device_read_bps'] or []
before, after = sorted(list(set(before))), sorted(list(set(after)))
return self._diff_update_and_compare('device_read_bps', before, after)
def diffparam_device_read_iops(self):
before = self.info['hostconfig']['blkiodevicereadiops'] or []
before = ["%s:%s" % (i['path'], i['rate']) for i in before]
after = self.params['device_read_iops'] or []
before, after = sorted(list(set(before))), sorted(list(set(after)))
return self._diff_update_and_compare('device_read_iops', before, after)
def diffparam_device_write_bps(self):
before = self.info['hostconfig']['blkiodevicewritebps'] or []
before = ["%s:%s" % (i['path'], i['rate']) for i in before]
after = self.params['device_write_bps'] or []
before, after = sorted(list(set(before))), sorted(list(set(after)))
return self._diff_update_and_compare('device_write_bps', before, after)
def diffparam_device_write_iops(self):
before = self.info['hostconfig']['blkiodevicewriteiops'] or []
before = ["%s:%s" % (i['path'], i['rate']) for i in before]
after = self.params['device_write_iops'] or []
before, after = sorted(list(set(before))), sorted(list(set(after)))
return self._diff_update_and_compare('device_write_iops', before, after)
# Limited idempotency, it can't guess default values
def diffparam_env(self):
keys = [i.split("=")[0] for i in self.info['config']['env']]
if self.module.params['env'] is None:
if sorted(keys) == sorted(self.default_dict['env']):
before = after = self.info['config']['env']
else:
before = self.info['config']['env']
after = ["=".join([i, "default"]) for i in self.default_dict['env']]
return self._diff_update_and_compare('env', before, after)
before = {i.split("=")[0]: i.split("=")[1] for i in self.info['config']['env']}
after = {}
for key in self.default_dict['env']:
after[key] = before[key]
for d_key in self.module.params['env']:
after[d_key.lower()] = self.module.params['env'][d_key].lower()
return self._diff_update_and_compare('env', before, after)
def diffparam_etc_hosts(self):
if self.info['hostconfig']['extrahosts']:
before = dict([i.split(":") for i in self.info['hostconfig']['extrahosts']])
else:
before = []
after = self.params['etc_hosts']
return self._diff_update_and_compare('etc_hosts', before, after)
def diffparam_group_add(self):
before = self.info['hostconfig']['groupadd']
after = self.params['group_add']
return self._diff_update_and_compare('group_add', before, after)
# Because of hostname is random generated, this parameter has partial idempotency only.
def diffparam_hostname(self):
before = self.info['config']['hostname']
after = self.params['hostname'] or before
return self._diff_update_and_compare('hostname', before, after)
def diffparam_image(self):
before = self.info['config']['image'].replace(
"docker.io/library/", "").replace(
"docker.io/", "").replace(
":latest", "")
after = self.params['image']
return self._diff_update_and_compare('image', before, after)
def diffparam_ipc(self):
before = self.info['hostconfig']['ipcmode']
after = self.params['ipc']
return self._diff_update_and_compare('ipc', before, after)
def diffparam_label(self):
before = self.info['config']['labels']
after = self.params['label'] if not self.params['label'] else {
k: str(v) for k, v in self.params['label'].items()
}
return self._diff_update_and_compare('label', before, after)
def diffparam_log_driver(self):
before = self.info['hostconfig']['logconfig']['type']
after = self.params['log_driver']
return self._diff_update_and_compare('log_driver', before, after)
# Parameter has limited idempotency, unable to guess the default log_path
def diffparam_log_opt(self):
before = self.info['logpath']
if self.module.params['log_opt'] in [None, '']:
after = before
else:
after = self.params['log_opt'].split("=")[1]
return self._diff_update_and_compare('log_opt', before, after)
def diffparam_memory(self):
before = str(self.info['hostconfig']['memory'])
after = self.params['memory']
return self._diff_update_and_compare('memory', before, after)
def diffparam_memory_swap(self):
# By default it's twice memory parameter
before = str(self.info['hostconfig']['memoryswap'])
after = self.params['memory_swap']
if (self.module.params['memory_swap'] is None and
self.params['memory'] != 0 and
self.params['memory'].isdigit()):
after = str(int(self.params['memory']) * 2)
return self._diff_update_and_compare('memory_swap', before, after)
def diffparam_memory_reservation(self):
before = str(self.info['hostconfig']['memoryreservation'])
after = self.params['memory_reservation']
return self._diff_update_and_compare('memory_reservation', before, after)
def diffparam_no_hosts(self):
before = not bool(self.info['hostspath'])
after = self.params['no_hosts']
return self._diff_update_and_compare('no_hosts', before, after)
def diffparam_oom_score_adj(self):
before = self.info['hostconfig']['oomscoreadj']
after = self.params['oom_score_adj']
return self._diff_update_and_compare('oom_score_adj', before, after)
def diffparam_privileged(self):
before = self.info['hostconfig']['privileged']
after = self.params['privileged']
return self._diff_update_and_compare('privileged', before, after)
def diffparam_pid(self):
before = self.info['hostconfig']['pidmode']
after = self.params['pid']
return self._diff_update_and_compare('pid', before, after)
def diffparam_rm(self):
before = self.info['hostconfig']['autoremove']
after = self.params['rm']
return self._diff_update_and_compare('rm', before, after)
def diffparam_security_opt(self):
before = self.info['hostconfig']['securityopt']
after = self.params['security_opt']
before, after = sorted(list(set(before))), sorted(list(set(after)))
return self._diff_update_and_compare('security_opt', before, after)
def diffparam_stop_signal(self):
before = self.info['config']['stopsignal']
after = self.params['stop_signal']
return self._diff_update_and_compare('stop_signal', before, after)
def diffparam_tty(self):
before = self.info['config']['tty']
after = self.params['tty']
return self._diff_update_and_compare('tty', before, after)
def diffparam_user(self):
before = self.info['config']['user']
after = self.params['user']
return self._diff_update_and_compare('user', before, after)
def diffparam_uts(self):
before = self.info['hostconfig']['utsmode']
after = self.params['uts']
return self._diff_update_and_compare('uts', before, after)
def diffparam_volume(self):
before = self.info['mounts']
if before:
volumes = []
for m in before:
if m['type'] == 'volume':
volumes.append([m['name'], m['destination']])
else:
volumes.append([m['source'], m['destination']])
before = [":".join(v) for v in volumes]
# Ignore volumes option for idempotency
after = [":".join(v.split(":")[:2]) for v in self.params['volume']]
before, after = sorted(list(set(before))), sorted(list(set(after)))
return self._diff_update_and_compare('volume', before, after)
def diffparam_volumes_from(self):
before = self.info['hostconfig']['volumesfrom']
after = self.params['volumes_from']
return self._diff_update_and_compare('volumes_from', before, after)
def diffparam_workdir(self):
before = self.info['config']['workingdir']
after = self.params['workdir']
return self._diff_update_and_compare('workdir', before, after)
def is_different(self):
diff_func_list = [func for func in dir(self)
if callable(getattr(self, func)) and func.startswith(
"diffparam")]
fail_fast = not bool(self.module._diff)
different = False
for func_name in diff_func_list:
dff_func = getattr(self, func_name)
if dff_func():
if fail_fast:
return True
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
def ensure_image_exists(module, image): def ensure_image_exists(module, image):
"""If image is passed, ensure it exists, if not - pull it or fail. """If image is passed, ensure it exists, if not - pull it or fail.
@ -1205,6 +1796,9 @@ class PodmanContainer:
self.name = name self.name = name
self.stdout, self.stderr = '', '' self.stdout, self.stderr = '', ''
self.info = self.get_info() self.info = self.get_info()
self.version = self._get_podman_version()
self.diff = {}
self.actions = []
@property @property
def exists(self): def exists(self):
@ -1214,9 +1808,17 @@ class PodmanContainer:
@property @property
def different(self): def different(self):
"""Check if container is different.""" """Check if container is different."""
# TODO(sshnaidm): implement difference calculation between input vars diffcheck = PodmanContainerDiff(self.module, self.info, self.version)
# and current container to understand if we need to recreate it is_different = diffcheck.is_different()
return True 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 @property
def running(self): def running(self):
@ -1234,6 +1836,13 @@ class PodmanContainer:
[self.module.params['executable'], b'container', b'inspect', self.name]) [self.module.params['executable'], b'container', b'inspect', self.name])
return json.loads(out)[0] if rc == 0 else {} return json.loads(out)[0] if rc == 0 else {}
def _get_podman_version(self):
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): def _perform_action(self, action):
"""Perform action with container. """Perform action with container.
@ -1241,19 +1850,25 @@ class PodmanContainer:
action {str} -- action to perform - start, create, stop, run, action {str} -- action to perform - start, create, stop, run,
delete delete
""" """
b_command = PodmanModuleParams(action, self.module.params b_command = PodmanModuleParams(action,
self.module.params,
self.version,
self.module,
).construct_command_from_params() ).construct_command_from_params()
self.module.log("PODMAN-CONTAINER-DEBUG: " + full_cmd = " ".join([self.module.params['executable']] +
"%s" % " ".join([to_native(i) for i in b_command])) [to_native(i) for i in b_command])
rc, out, err = self.module.run_command( self.module.log("PODMAN-CONTAINER-DEBUG: %s" % full_cmd)
[self.module.params['executable'], b'container'] + b_command, self.actions.append(full_cmd)
expand_user_and_vars=False) if not self.module.check_mode:
self.stdout = out rc, out, err = self.module.run_command(
self.stderr = err [self.module.params['executable'], b'container'] + b_command,
if rc != 0: expand_user_and_vars=False)
self.module.fail_json( self.stdout = out
msg="Can't %s container %s" % (action, self.name), self.stderr = err
stdout=out, stderr=err) if rc != 0:
self.module.fail_json(
msg="Can't %s container %s" % (action, self.name),
stdout=out, stderr=err)
def run(self): def run(self):
"""Run the container.""" """Run the container."""
@ -1326,11 +1941,15 @@ class PodmanManager:
changed {bool} -- whether any action was performed changed {bool} -- whether any action was performed
(default: {True}) (default: {True})
""" """
facts = self.container.get_info() facts = self.container.get_info() if changed else self.container.info
out, err = self.container.stdout, self.container.stderr out, err = self.container.stdout, self.container.stderr
self.results.update({'changed': changed, 'container': facts, self.results.update({'changed': changed, 'container': facts,
'ansible_facts': {'podman_container': facts}}, 'podman_actions': self.container.actions},
stdout=out, stderr=err) stdout=out, stderr=err)
if self.container.diff:
self.results.update({'diff': self.container.diff})
if self.module.params['debug']:
self.results.update({'podman_version': self.container.version})
self.module.exit_json(**self.results) self.module.exit_json(**self.results)
def make_started(self): def make_started(self):
@ -1347,7 +1966,7 @@ class PodmanManager:
self.results['actions'].append('restarted %s' % self.results['actions'].append('restarted %s' %
self.container.name) self.container.name)
self.update_container_result() self.update_container_result()
self.module.exit_json(**self.results) self.update_container_result(changed=False)
elif not self.container.exists: elif not self.container.exists:
self.container.run() self.container.run()
self.results['actions'].append('started %s' % self.container.name) self.results['actions'].append('started %s' % self.container.name)
@ -1387,7 +2006,7 @@ class PodmanManager:
self.results['actions'].append('deleted %s' % self.container.name) self.results['actions'].append('deleted %s' % self.container.name)
self.results.update({'changed': True}) self.results.update({'changed': True})
self.results.update({'container': {}, self.results.update({'container': {},
'ansible_facts': {'podman_container': {}}}) 'podman_actions': self.container.actions})
self.module.exit_json(**self.results) self.module.exit_json(**self.results)
def execute(self): def execute(self):
@ -1411,6 +2030,7 @@ def main():
['no_hosts', 'etc_hosts'], ['no_hosts', 'etc_hosts'],
), ),
supports_check_mode=True,
) )
# work on input vars # work on input vars
if module.params['state'] in ['started', 'present'] and \ if module.params['state'] in ['started', 'present'] and \

View File

@ -71,17 +71,11 @@
assert: assert:
that: that:
- image is changed - image is changed
- image.ansible_facts is defined
- image.ansible_facts.podman_container is defined
- image.ansible_facts.podman_container['State']['Running']
- image.container is defined - image.container is defined
- image.container['State']['Running'] - image.container['State']['Running']
- "'pulled image alpine:3.7' in image.actions" - "'pulled image alpine:3.7' in image.actions"
- "'started container' in image.actions" - "'started container' in image.actions"
- image2 is changed - image2 is changed
- image2.ansible_facts is defined
- image2.ansible_facts.podman_container is defined
- image2.ansible_facts.podman_container['State']['Running']
- image2.container is defined - image2.container is defined
- image2.container['State']['Running'] - image2.container['State']['Running']
- "'pulled image alpine:3.7' not in image2.actions" - "'pulled image alpine:3.7' not in image2.actions"
@ -118,9 +112,8 @@
assert: assert:
that: that:
- recreated is changed - recreated is changed
- recreated.ansible_facts is defined - recreated.container is defined
- recreated.ansible_facts.podman_container is defined - recreated.container['State']['Running']
- recreated.ansible_facts.podman_container['State']['Running']
- "'recreated container' in recreated.actions" - "'recreated container' in recreated.actions"
fail_msg: Force recreate test failed! fail_msg: Force recreate test failed!
success_msg: Force recreate test passed! success_msg: Force recreate test passed!
@ -141,14 +134,12 @@
assert: assert:
that: that:
- stopped is changed - stopped is changed
- stopped.ansible_facts is defined - stopped.container is defined
- stopped.ansible_facts.podman_container is defined - not stopped.container['State']['Running']
- not stopped.ansible_facts.podman_container['State']['Running']
- "'stopped container' in stopped.actions" - "'stopped container' in stopped.actions"
- stopped_again is not changed - stopped_again is not changed
- stopped_again.ansible_facts is defined - stopped_again.container is defined
- stopped_again.ansible_facts.podman_container is defined - not stopped_again.container['State']['Running']
- not stopped_again.ansible_facts.podman_container['State']['Running']
- stopped_again.actions == [] - stopped_again.actions == []
fail_msg: Stopping container test failed! fail_msg: Stopping container test failed!
success_msg: Stopping container test passed! success_msg: Stopping container test passed!
@ -169,14 +160,12 @@
assert: assert:
that: that:
- deleted is changed - deleted is changed
- deleted.ansible_facts is defined - deleted.container is defined
- deleted.ansible_facts.podman_container is defined - deleted.container == {}
- deleted.ansible_facts.podman_container == {}
- "'deleted container' in deleted.actions" - "'deleted container' in deleted.actions"
- deleted_again is not changed - deleted_again is not changed
- deleted_again.ansible_facts is defined - deleted_again.container is defined
- deleted_again.ansible_facts.podman_container is defined - deleted_again.container == {}
- deleted_again.ansible_facts.podman_container == {}
- deleted_again.actions == [] - deleted_again.actions == []
fail_msg: Deleting stopped container test failed! fail_msg: Deleting stopped container test failed!
success_msg: Deleting stopped container test passed! success_msg: Deleting stopped container test passed!
@ -193,10 +182,9 @@
assert: assert:
that: that:
- created is changed - created is changed
- created.ansible_facts is defined - created.container is defined
- created.ansible_facts.podman_container is defined - created.container != {}
- created.ansible_facts.podman_container != {} - not created.container['State']['Running']
- not created.ansible_facts.podman_container['State']['Running']
- "'created container' in created.actions" - "'created container' in created.actions"
fail_msg: "Creating stopped container test failed!" fail_msg: "Creating stopped container test failed!"
success_msg: "Creating stopped container test passed!" success_msg: "Creating stopped container test passed!"
@ -218,9 +206,6 @@
assert: assert:
that: that:
- started is changed - started is changed
- started.ansible_facts is defined
- started.ansible_facts.podman_container is defined
- started.ansible_facts.podman_container['State']['Running']
- started.container is defined - started.container is defined
- started.container['State']['Running'] - started.container['State']['Running']
- "'pulled image alpine:3.7' not in started.actions" - "'pulled image alpine:3.7' not in started.actions"
@ -241,14 +226,12 @@
assert: assert:
that: that:
- deleted is changed - deleted is changed
- deleted.ansible_facts is defined - deleted.container is defined
- deleted.ansible_facts.podman_container is defined - deleted.container == {}
- deleted.ansible_facts.podman_container == {}
- "'deleted container' in deleted.actions" - "'deleted container' in deleted.actions"
- deleted_again is not changed - deleted_again is not changed
- deleted_again.ansible_facts is defined - deleted_again.container is defined
- deleted_again.ansible_facts.podman_container is defined - deleted_again.container == {}
- deleted_again.ansible_facts.podman_container == {}
- deleted_again.actions == [] - deleted_again.actions == []
fail_msg: Deleting started container test failed! fail_msg: Deleting started container test failed!
success_msg: Deleting started container test passed! success_msg: Deleting started container test passed!
@ -293,54 +276,112 @@
assert: assert:
that: that:
- test is changed - test is changed
- test.ansible_facts is defined - test.container is defined
- test.ansible_facts.podman_container is defined - test.container != {}
- test.ansible_facts.podman_container != {} - test.container['State']['Running']
- test.ansible_facts.podman_container['State']['Running']
# test capabilities # test capabilities
- "'CAP_SYS_TIME' in test.ansible_facts.podman_container['BoundingCaps']" - "'CAP_SYS_TIME' in test.container['BoundingCaps']"
- "'CAP_NET_ADMIN' in test.ansible_facts.podman_container['BoundingCaps']" - "'CAP_NET_ADMIN' in test.container['BoundingCaps']"
# test annotations # test annotations
- test.ansible_facts.podman_container['Config']['Annotations']['this'] is defined - test.container['Config']['Annotations']['this'] is defined
- test.ansible_facts.podman_container['Config']['Annotations']['this'] == "annotation_value" - test.container['Config']['Annotations']['this'] == "annotation_value"
# test DNS # test DNS
- >- - >-
(test.ansible_facts.podman_container['HostConfig']['Dns'] is defined and (test.container['HostConfig']['Dns'] is defined and
test.ansible_facts.podman_container['HostConfig']['Dns'] == ['1.1.1.1', '8.8.4.4']) or test.container['HostConfig']['Dns'] == ['1.1.1.1', '8.8.4.4']) or
(test.ansible_facts.podman_container['HostConfig']['DNS'] is defined and (test.container['HostConfig']['DNS'] is defined and
test.ansible_facts.podman_container['HostConfig']['DNS'] == ['1.1.1.1', '8.8.4.4']) test.container['HostConfig']['DNS'] == ['1.1.1.1', '8.8.4.4'])
# test ports # test ports
- test.ansible_facts.podman_container['NetworkSettings']['Ports']|length == 2 - test.container['NetworkSettings']['Ports']|length == 2
# test working dir # test working dir
- test.ansible_facts.podman_container['Config']['WorkingDir'] == "/bin" - test.container['Config']['WorkingDir'] == "/bin"
# test dns search # test dns search
- >- - >-
(test.ansible_facts.podman_container['HostConfig']['DnsSearch'] is defined and (test.container['HostConfig']['DnsSearch'] is defined and
test.ansible_facts.podman_container['HostConfig']['DnsSearch'] == ['example.com']) or test.container['HostConfig']['DnsSearch'] == ['example.com']) or
(test.ansible_facts.podman_container['HostConfig']['DNSSearch'] is defined and (test.container['HostConfig']['DNSSearch'] is defined and
test.ansible_facts.podman_container['HostConfig']['DNSSearch'] == ['example.com']) test.container['HostConfig']['DNSSearch'] == ['example.com'])
# test environment variables # test environment variables
- "'FOO=bar' in test.ansible_facts.podman_container['Config']['Env']" - "'FOO=bar' in test.container['Config']['Env']"
- "'BAR=foo' in test.ansible_facts.podman_container['Config']['Env']" - "'BAR=foo' in test.container['Config']['Env']"
- "'TEST=1' in test.ansible_facts.podman_container['Config']['Env']" - "'TEST=1' in test.container['Config']['Env']"
- "'BOOL=False' in test.ansible_facts.podman_container['Config']['Env']" - "'BOOL=False' in test.container['Config']['Env']"
# test labels # test labels
- test.ansible_facts.podman_container['Config']['Labels'] | length == 2 - test.container['Config']['Labels'] | length == 2
- test.ansible_facts.podman_container['Config']['Labels']['somelabel'] == "labelvalue" - test.container['Config']['Labels']['somelabel'] == "labelvalue"
- test.ansible_facts.podman_container['Config']['Labels']['otheralbe'] == "othervalue" - test.container['Config']['Labels']['otheralbe'] == "othervalue"
# test mounts # test mounts
- >- - >-
(test.ansible_facts.podman_container['Mounts'][0]['Destination'] is defined and (test.container['Mounts'][0]['Destination'] is defined and
'/data' in test.ansible_facts.podman_container['Mounts'] | map(attribute='Destination') | list) or '/data' in test.container['Mounts'] | map(attribute='Destination') | list) or
(test.ansible_facts.podman_container['Mounts'][0]['destination'] is defined and (test.container['Mounts'][0]['destination'] is defined and
'/data' in test.ansible_facts.podman_container['Mounts'] | map(attribute='destination') | list) '/data' in test.container['Mounts'] | map(attribute='destination') | list)
- >- - >-
(test.ansible_facts.podman_container['Mounts'][0]['Source'] is defined and (test.container['Mounts'][0]['Source'] is defined and
'/tmp' in test.ansible_facts.podman_container['Mounts'] | map(attribute='Source') | list) or '/tmp' in test.container['Mounts'] | map(attribute='Source') | list) or
(test.ansible_facts.podman_container['Mounts'][0]['source'] is defined and (test.container['Mounts'][0]['source'] is defined and
'/tmp' in test.ansible_facts.podman_container['Mounts'] | map(attribute='source') | list) '/tmp' in test.container['Mounts'] | map(attribute='source') | list)
fail_msg: Parameters container test failed! fail_msg: Parameters container test failed!
success_msg: Parameters container test passed! success_msg: Parameters container test passed!
- name: Check basic idempotency of running container
podman_container:
name: testidem
image: alpine
state: present
command: sleep 20m
- name: Check basic idempotency of running container - run it again
podman_container:
name: testidem
image: alpine
state: present
command: sleep 20m
register: idem
- name: Check that nothing was changed
assert:
that:
- not idem.changed
- name: Run changed container (with tty enabled)
podman_container:
name: testidem
image: alpine
state: present
command: sleep 20m
tty: true
register: idem1
- name: Check that container is recreated when changed
assert:
that:
- idem1 is changed
- name: Run changed container without specifying an option, use defaults
podman_container:
name: testidem
image: alpine
state: present
command: sleep 20m
register: idem2
- name: Check that container is recreated when changed to default value
assert:
that:
- idem2 is changed
- name: Remove container
podman_container:
name: testidem
state: absent
register: remove
- name: Check podman_actions
assert:
that:
- "'podman rm -f testidem' in remove.podman_actions"
always: always:
- name: Delete all container leftovers from tests - name: Delete all container leftovers from tests
podman_container: podman_container:

View File

@ -16,7 +16,24 @@
- name: Prepare - name: Prepare
hosts: all hosts: all
gather_facts: true
roles: roles:
- role: test_deps - role: test_deps
test_deps_extra_packages: test_deps_extra_packages:
- podman - podman
post_tasks:
- name: Check podman version
command: podman version
register: p_ver
changed_when: false
- name: Print podman version
debug:
msg: |
podman version:
{{ p_ver.stdout }}
Testing with ansible {{ ansible_version.full }}
with python {{ ansible_python_version }}
on host {{ ansible_distribution }} {{ ansible_distribution_version }}