1from __future__ import (absolute_import, division, print_function)
2import json  # noqa: F402
3import shlex  # noqa: F402
4from distutils.version import LooseVersion  # noqa: F402
5
6from ansible.module_utils._text import to_bytes, to_native  # noqa: F402
7from ansible_collections.containers.podman.plugins.module_utils.podman.common import lower_keys
8from ansible_collections.containers.podman.plugins.module_utils.podman.common import generate_systemd
9
10__metaclass__ = type
11
12ARGUMENTS_SPEC_CONTAINER = dict(
13    name=dict(required=True, type='str'),
14    executable=dict(default='podman', type='str'),
15    state=dict(type='str', default='started', choices=[
16        'absent', 'present', 'stopped', 'started', 'created']),
17    image=dict(type='str'),
18    annotation=dict(type='dict'),
19    authfile=dict(type='path'),
20    blkio_weight=dict(type='int'),
21    blkio_weight_device=dict(type='dict'),
22    cap_add=dict(type='list', elements='str', aliases=['capabilities']),
23    cap_drop=dict(type='list', elements='str'),
24    cgroup_parent=dict(type='path'),
25    cgroupns=dict(type='str'),
26    cgroups=dict(type='str'),
27    cidfile=dict(type='path'),
28    cmd_args=dict(type='list', elements='str'),
29    conmon_pidfile=dict(type='path'),
30    command=dict(type='raw'),
31    cpu_period=dict(type='int'),
32    cpu_rt_period=dict(type='int'),
33    cpu_rt_runtime=dict(type='int'),
34    cpu_shares=dict(type='int'),
35    cpus=dict(type='str'),
36    cpuset_cpus=dict(type='str'),
37    cpuset_mems=dict(type='str'),
38    detach=dict(type='bool', default=True),
39    debug=dict(type='bool', default=False),
40    detach_keys=dict(type='str', no_log=False),
41    device=dict(type='list', elements='str'),
42    device_read_bps=dict(type='list', elements='str'),
43    device_read_iops=dict(type='list', elements='str'),
44    device_write_bps=dict(type='list', elements='str'),
45    device_write_iops=dict(type='list', elements='str'),
46    dns=dict(type='list', elements='str', aliases=['dns_servers']),
47    dns_option=dict(type='str', aliases=['dns_opts']),
48    dns_search=dict(type='str', aliases=['dns_search_domains']),
49    entrypoint=dict(type='str'),
50    env=dict(type='dict'),
51    env_file=dict(type='path'),
52    env_host=dict(type='bool'),
53    etc_hosts=dict(type='dict', aliases=['add_hosts']),
54    expose=dict(type='list', elements='str', aliases=[
55                'exposed', 'exposed_ports']),
56    force_restart=dict(type='bool', default=False,
57                       aliases=['restart']),
58    generate_systemd=dict(type='dict', default={}),
59    gidmap=dict(type='list', elements='str'),
60    group_add=dict(type='list', elements='str', aliases=['groups']),
61    healthcheck=dict(type='str'),
62    healthcheck_interval=dict(type='str'),
63    healthcheck_retries=dict(type='int'),
64    healthcheck_start_period=dict(type='str'),
65    healthcheck_timeout=dict(type='str'),
66    hostname=dict(type='str'),
67    http_proxy=dict(type='bool'),
68    image_volume=dict(type='str', choices=['bind', 'tmpfs', 'ignore']),
69    image_strict=dict(type='bool', default=False),
70    init=dict(type='bool'),
71    init_path=dict(type='str'),
72    interactive=dict(type='bool'),
73    ip=dict(type='str'),
74    ipc=dict(type='str', aliases=['ipc_mode']),
75    kernel_memory=dict(type='str'),
76    label=dict(type='dict', aliases=['labels']),
77    label_file=dict(type='str'),
78    log_driver=dict(type='str', choices=[
79        'k8s-file', 'journald', 'json-file']),
80    log_level=dict(
81        type='str',
82        choices=["debug", "info", "warn", "error", "fatal", "panic"]),
83    log_opt=dict(type='dict', aliases=['log_options'],
84                 options=dict(
85        max_size=dict(type='str'),
86        path=dict(type='str'),
87        tag=dict(type='str'))),
88    mac_address=dict(type='str'),
89    memory=dict(type='str'),
90    memory_reservation=dict(type='str'),
91    memory_swap=dict(type='str'),
92    memory_swappiness=dict(type='int'),
93    mount=dict(type='str'),
94    network=dict(type='list', elements='str', aliases=['net', 'network_mode']),
95    no_hosts=dict(type='bool'),
96    oom_kill_disable=dict(type='bool'),
97    oom_score_adj=dict(type='int'),
98    pid=dict(type='str', aliases=['pid_mode']),
99    pids_limit=dict(type='str'),
100    pod=dict(type='str'),
101    privileged=dict(type='bool'),
102    publish=dict(type='list', elements='str', aliases=[
103        'ports', 'published', 'published_ports']),
104    publish_all=dict(type='bool'),
105    read_only=dict(type='bool'),
106    read_only_tmpfs=dict(type='bool'),
107    recreate=dict(type='bool', default=False),
108    restart_policy=dict(type='str'),
109    rm=dict(type='bool', aliases=['remove', 'auto_remove']),
110    rootfs=dict(type='bool'),
111    secrets=dict(type='list', elements='str', no_log=True),
112    security_opt=dict(type='list', elements='str'),
113    shm_size=dict(type='str'),
114    sig_proxy=dict(type='bool'),
115    stop_signal=dict(type='int'),
116    stop_timeout=dict(type='int'),
117    subgidname=dict(type='str'),
118    subuidname=dict(type='str'),
119    sysctl=dict(type='dict'),
120    systemd=dict(type='str'),
121    timezone=dict(type='str'),
122    tmpfs=dict(type='dict'),
123    tty=dict(type='bool'),
124    uidmap=dict(type='list', elements='str'),
125    ulimit=dict(type='list', elements='str', aliases=['ulimits']),
126    user=dict(type='str'),
127    userns=dict(type='str', aliases=['userns_mode']),
128    uts=dict(type='str'),
129    volume=dict(type='list', elements='str', aliases=['volumes']),
130    volumes_from=dict(type='list', elements='str'),
131    workdir=dict(type='str', aliases=['working_dir'])
132)
133
134
135def init_options():
136    default = {}
137    opts = ARGUMENTS_SPEC_CONTAINER
138    for k, v in opts.items():
139        if 'default' in v:
140            default[k] = v['default']
141        else:
142            default[k] = None
143    return default
144
145
146def update_options(opts_dict, container):
147    def to_bool(x):
148        return str(x).lower() not in ['no', 'false']
149
150    aliases = {}
151    for k, v in ARGUMENTS_SPEC_CONTAINER.items():
152        if 'aliases' in v:
153            for alias in v['aliases']:
154                aliases[alias] = k
155    for k in list(container):
156        if k in aliases:
157            key = aliases[k]
158            container[key] = container.pop(k)
159        else:
160            key = k
161        if ARGUMENTS_SPEC_CONTAINER[key]['type'] == 'list' and not isinstance(container[key], list):
162            opts_dict[key] = [container[key]]
163        elif ARGUMENTS_SPEC_CONTAINER[key]['type'] == 'bool' and not isinstance(container[key], bool):
164            opts_dict[key] = to_bool(container[key])
165        elif ARGUMENTS_SPEC_CONTAINER[key]['type'] == 'int' and not isinstance(container[key], int):
166            opts_dict[key] = int(container[key])
167        else:
168            opts_dict[key] = container[key]
169
170    return opts_dict
171
172
173def set_container_opts(input_vars):
174    default_options_templ = init_options()
175    options_dict = update_options(default_options_templ, input_vars)
176    return options_dict
177
178
179class PodmanModuleParams:
180    """Creates list of arguments for podman CLI command.
181
182       Arguments:
183           action {str} -- action type from 'run', 'stop', 'create', 'delete',
184                           'start', 'restart'
185           params {dict} -- dictionary of module parameters
186
187       """
188
189    def __init__(self, action, params, podman_version, module):
190        self.params = params
191        self.action = action
192        self.podman_version = podman_version
193        self.module = module
194
195    def construct_command_from_params(self):
196        """Create a podman command from given module parameters.
197
198        Returns:
199           list -- list of byte strings for Popen command
200        """
201        if self.action in ['start', 'stop', 'delete', 'restart']:
202            return self.start_stop_delete()
203        if self.action in ['create', 'run']:
204            cmd = [self.action, '--name', self.params['name']]
205            all_param_methods = [func for func in dir(self)
206                                 if callable(getattr(self, func))
207                                 and func.startswith("addparam")]
208            params_set = (i for i in self.params if self.params[i] is not None)
209            for param in params_set:
210                func_name = "_".join(["addparam", param])
211                if func_name in all_param_methods:
212                    cmd = getattr(self, func_name)(cmd)
213            cmd.append(self.params['image'])
214            if self.params['command']:
215                if isinstance(self.params['command'], list):
216                    cmd += self.params['command']
217                else:
218                    cmd += self.params['command'].split()
219            return [to_bytes(i, errors='surrogate_or_strict') for i in cmd]
220
221    def start_stop_delete(self):
222
223        if self.action in ['stop', 'start', 'restart']:
224            cmd = [self.action, self.params['name']]
225            return [to_bytes(i, errors='surrogate_or_strict') for i in cmd]
226
227        if self.action == 'delete':
228            cmd = ['rm', '-f', self.params['name']]
229            return [to_bytes(i, errors='surrogate_or_strict') for i in cmd]
230
231    def check_version(self, param, minv=None, maxv=None):
232        if minv and LooseVersion(minv) > LooseVersion(
233                self.podman_version):
234            self.module.fail_json(msg="Parameter %s is supported from podman "
235                                  "version %s only! Current version is %s" % (
236                                      param, minv, self.podman_version))
237        if maxv and LooseVersion(maxv) < LooseVersion(
238                self.podman_version):
239            self.module.fail_json(msg="Parameter %s is supported till podman "
240                                  "version %s only! Current version is %s" % (
241                                      param, minv, self.podman_version))
242
243    def addparam_annotation(self, c):
244        for annotate in self.params['annotation'].items():
245            c += ['--annotation', '='.join(annotate)]
246        return c
247
248    def addparam_authfile(self, c):
249        return c + ['--authfile', self.params['authfile']]
250
251    def addparam_blkio_weight(self, c):
252        return c + ['--blkio-weight', self.params['blkio_weight']]
253
254    def addparam_blkio_weight_device(self, c):
255        for blkio in self.params['blkio_weight_device'].items():
256            c += ['--blkio-weight-device', ':'.join(blkio)]
257        return c
258
259    def addparam_cap_add(self, c):
260        for cap_add in self.params['cap_add']:
261            c += ['--cap-add', cap_add]
262        return c
263
264    def addparam_cap_drop(self, c):
265        for cap_drop in self.params['cap_drop']:
266            c += ['--cap-drop', cap_drop]
267        return c
268
269    def addparam_cgroups(self, c):
270        self.check_version('--cgroups', minv='1.6.0')
271        return c + ['--cgroups=%s' % self.params['cgroups']]
272
273    def addparam_cgroupns(self, c):
274        self.check_version('--cgroupns', minv='1.6.2')
275        return c + ['--cgroupns=%s' % self.params['cgroupns']]
276
277    def addparam_cgroup_parent(self, c):
278        return c + ['--cgroup-parent', self.params['cgroup_parent']]
279
280    def addparam_cidfile(self, c):
281        return c + ['--cidfile', self.params['cidfile']]
282
283    def addparam_conmon_pidfile(self, c):
284        return c + ['--conmon-pidfile', self.params['conmon_pidfile']]
285
286    def addparam_cpu_period(self, c):
287        return c + ['--cpu-period', self.params['cpu_period']]
288
289    def addparam_cpu_rt_period(self, c):
290        return c + ['--cpu-rt-period', self.params['cpu_rt_period']]
291
292    def addparam_cpu_rt_runtime(self, c):
293        return c + ['--cpu-rt-runtime', self.params['cpu_rt_runtime']]
294
295    def addparam_cpu_shares(self, c):
296        return c + ['--cpu-shares', self.params['cpu_shares']]
297
298    def addparam_cpus(self, c):
299        return c + ['--cpus', self.params['cpus']]
300
301    def addparam_cpuset_cpus(self, c):
302        return c + ['--cpuset-cpus', self.params['cpuset_cpus']]
303
304    def addparam_cpuset_mems(self, c):
305        return c + ['--cpuset-mems', self.params['cpuset_mems']]
306
307    def addparam_detach(self, c):
308        return c + ['--detach=%s' % self.params['detach']]
309
310    def addparam_detach_keys(self, c):
311        return c + ['--detach-keys', self.params['detach_keys']]
312
313    def addparam_device(self, c):
314        for dev in self.params['device']:
315            c += ['--device', dev]
316        return c
317
318    def addparam_device_read_bps(self, c):
319        for dev in self.params['device_read_bps']:
320            c += ['--device-read-bps', dev]
321        return c
322
323    def addparam_device_read_iops(self, c):
324        for dev in self.params['device_read_iops']:
325            c += ['--device-read-iops', dev]
326        return c
327
328    def addparam_device_write_bps(self, c):
329        for dev in self.params['device_write_bps']:
330            c += ['--device-write-bps', dev]
331        return c
332
333    def addparam_device_write_iops(self, c):
334        for dev in self.params['device_write_iops']:
335            c += ['--device-write-iops', dev]
336        return c
337
338    def addparam_dns(self, c):
339        return c + ['--dns', ','.join(self.params['dns'])]
340
341    def addparam_dns_option(self, c):
342        return c + ['--dns-option', self.params['dns_option']]
343
344    def addparam_dns_search(self, c):
345        return c + ['--dns-search', self.params['dns_search']]
346
347    def addparam_entrypoint(self, c):
348        return c + ['--entrypoint', self.params['entrypoint']]
349
350    def addparam_env(self, c):
351        for env_value in self.params['env'].items():
352            c += ['--env',
353                  b"=".join([to_bytes(k, errors='surrogate_or_strict')
354                             for k in env_value])]
355        return c
356
357    def addparam_env_file(self, c):
358        return c + ['--env-file', self.params['env_file']]
359
360    def addparam_env_host(self, c):
361        self.check_version('--env-host', minv='1.5.0')
362        return c + ['--env-host=%s' % self.params['env_host']]
363
364    def addparam_etc_hosts(self, c):
365        for host_ip in self.params['etc_hosts'].items():
366            c += ['--add-host', ':'.join(host_ip)]
367        return c
368
369    def addparam_expose(self, c):
370        for exp in self.params['expose']:
371            c += ['--expose', exp]
372        return c
373
374    def addparam_gidmap(self, c):
375        for gidmap in self.params['gidmap']:
376            c += ['--gidmap', gidmap]
377        return c
378
379    def addparam_group_add(self, c):
380        for g in self.params['group_add']:
381            c += ['--group-add', g]
382        return c
383
384    def addparam_healthcheck(self, c):
385        return c + ['--healthcheck-command', self.params['healthcheck']]
386
387    def addparam_healthcheck_interval(self, c):
388        return c + ['--healthcheck-interval',
389                    self.params['healthcheck_interval']]
390
391    def addparam_healthcheck_retries(self, c):
392        return c + ['--healthcheck-retries',
393                    self.params['healthcheck_retries']]
394
395    def addparam_healthcheck_start_period(self, c):
396        return c + ['--healthcheck-start-period',
397                    self.params['healthcheck_start_period']]
398
399    def addparam_healthcheck_timeout(self, c):
400        return c + ['--healthcheck-timeout',
401                    self.params['healthcheck_timeout']]
402
403    def addparam_hostname(self, c):
404        return c + ['--hostname', self.params['hostname']]
405
406    def addparam_http_proxy(self, c):
407        return c + ['--http-proxy=%s' % self.params['http_proxy']]
408
409    def addparam_image_volume(self, c):
410        return c + ['--image-volume', self.params['image_volume']]
411
412    def addparam_init(self, c):
413        if self.params['init']:
414            c += ['--init']
415        return c
416
417    def addparam_init_path(self, c):
418        return c + ['--init-path', self.params['init_path']]
419
420    def addparam_interactive(self, c):
421        return c + ['--interactive=%s' % self.params['interactive']]
422
423    def addparam_ip(self, c):
424        return c + ['--ip', self.params['ip']]
425
426    def addparam_ipc(self, c):
427        return c + ['--ipc', self.params['ipc']]
428
429    def addparam_kernel_memory(self, c):
430        return c + ['--kernel-memory', self.params['kernel_memory']]
431
432    def addparam_label(self, c):
433        for label in self.params['label'].items():
434            c += ['--label', b'='.join([to_bytes(la, errors='surrogate_or_strict')
435                                        for la in label])]
436        return c
437
438    def addparam_label_file(self, c):
439        return c + ['--label-file', self.params['label_file']]
440
441    def addparam_log_driver(self, c):
442        return c + ['--log-driver', self.params['log_driver']]
443
444    def addparam_log_opt(self, c):
445        for k, v in self.params['log_opt'].items():
446            if v is not None:
447                c += ['--log-opt',
448                      b"=".join([to_bytes(k.replace('max_size', 'max-size'),
449                                          errors='surrogate_or_strict'),
450                                 to_bytes(v,
451                                          errors='surrogate_or_strict')])]
452        return c
453
454    def addparam_log_level(self, c):
455        return c + ['--log-level', self.params['log_level']]
456
457    def addparam_mac_address(self, c):
458        return c + ['--mac-address', self.params['mac_address']]
459
460    def addparam_memory(self, c):
461        return c + ['--memory', self.params['memory']]
462
463    def addparam_memory_reservation(self, c):
464        return c + ['--memory-reservation', self.params['memory_reservation']]
465
466    def addparam_memory_swap(self, c):
467        return c + ['--memory-swap', self.params['memory_swap']]
468
469    def addparam_memory_swappiness(self, c):
470        return c + ['--memory-swappiness', self.params['memory_swappiness']]
471
472    def addparam_mount(self, c):
473        return c + ['--mount', self.params['mount']]
474
475    def addparam_network(self, c):
476        return c + ['--network', ",".join(self.params['network'])]
477
478    def addparam_no_hosts(self, c):
479        return c + ['--no-hosts=%s' % self.params['no_hosts']]
480
481    def addparam_oom_kill_disable(self, c):
482        return c + ['--oom-kill-disable=%s' % self.params['oom_kill_disable']]
483
484    def addparam_oom_score_adj(self, c):
485        return c + ['--oom-score-adj', self.params['oom_score_adj']]
486
487    def addparam_pid(self, c):
488        return c + ['--pid', self.params['pid']]
489
490    def addparam_pids_limit(self, c):
491        return c + ['--pids-limit', self.params['pids_limit']]
492
493    def addparam_pod(self, c):
494        return c + ['--pod', self.params['pod']]
495
496    def addparam_privileged(self, c):
497        return c + ['--privileged=%s' % self.params['privileged']]
498
499    def addparam_publish(self, c):
500        for pub in self.params['publish']:
501            c += ['--publish', pub]
502        return c
503
504    def addparam_publish_all(self, c):
505        return c + ['--publish-all=%s' % self.params['publish_all']]
506
507    def addparam_read_only(self, c):
508        return c + ['--read-only=%s' % self.params['read_only']]
509
510    def addparam_read_only_tmpfs(self, c):
511        return c + ['--read-only-tmpfs=%s' % self.params['read_only_tmpfs']]
512
513    def addparam_restart_policy(self, c):
514        return c + ['--restart=%s' % self.params['restart_policy']]
515
516    def addparam_rm(self, c):
517        if self.params['rm']:
518            c += ['--rm']
519        return c
520
521    def addparam_rootfs(self, c):
522        return c + ['--rootfs=%s' % self.params['rootfs']]
523
524    def addparam_secrets(self, c):
525        for secret in self.params['secrets']:
526            c += ['--secret', secret]
527        return c
528
529    def addparam_security_opt(self, c):
530        for secopt in self.params['security_opt']:
531            c += ['--security-opt', secopt]
532        return c
533
534    def addparam_shm_size(self, c):
535        return c + ['--shm-size', self.params['shm_size']]
536
537    def addparam_sig_proxy(self, c):
538        return c + ['--sig-proxy=%s' % self.params['sig_proxy']]
539
540    def addparam_stop_signal(self, c):
541        return c + ['--stop-signal', self.params['stop_signal']]
542
543    def addparam_stop_timeout(self, c):
544        return c + ['--stop-timeout', self.params['stop_timeout']]
545
546    def addparam_subgidname(self, c):
547        return c + ['--subgidname', self.params['subgidname']]
548
549    def addparam_subuidname(self, c):
550        return c + ['--subuidname', self.params['subuidname']]
551
552    def addparam_sysctl(self, c):
553        for sysctl in self.params['sysctl'].items():
554            c += ['--sysctl',
555                  b"=".join([to_bytes(k, errors='surrogate_or_strict')
556                             for k in sysctl])]
557        return c
558
559    def addparam_systemd(self, c):
560        return c + ['--systemd=%s' % str(self.params['systemd']).lower()]
561
562    def addparam_tmpfs(self, c):
563        for tmpfs in self.params['tmpfs'].items():
564            c += ['--tmpfs', ':'.join(tmpfs)]
565        return c
566
567    def addparam_timezone(self, c):
568        return c + ['--tz=%s' % self.params['timezone']]
569
570    def addparam_tty(self, c):
571        return c + ['--tty=%s' % self.params['tty']]
572
573    def addparam_uidmap(self, c):
574        for uidmap in self.params['uidmap']:
575            c += ['--uidmap', uidmap]
576        return c
577
578    def addparam_ulimit(self, c):
579        for u in self.params['ulimit']:
580            c += ['--ulimit', u]
581        return c
582
583    def addparam_user(self, c):
584        return c + ['--user', self.params['user']]
585
586    def addparam_userns(self, c):
587        return c + ['--userns', self.params['userns']]
588
589    def addparam_uts(self, c):
590        return c + ['--uts', self.params['uts']]
591
592    def addparam_volume(self, c):
593        for vol in self.params['volume']:
594            if vol:
595                c += ['--volume', vol]
596        return c
597
598    def addparam_volumes_from(self, c):
599        for vol in self.params['volumes_from']:
600            c += ['--volumes-from', vol]
601        return c
602
603    def addparam_workdir(self, c):
604        return c + ['--workdir', self.params['workdir']]
605
606    # Add your own args for podman command
607    def addparam_cmd_args(self, c):
608        return c + self.params['cmd_args']
609
610
611class PodmanDefaults:
612    def __init__(self, image_info, podman_version):
613        self.version = podman_version
614        self.image_info = image_info
615        self.defaults = {
616            "blkio_weight": 0,
617            "cgroups": "default",
618            "cidfile": "",
619            "cpus": 0.0,
620            "cpu_shares": 0,
621            "cpu_quota": 0,
622            "cpu_period": 0,
623            "cpu_rt_runtime": 0,
624            "cpu_rt_period": 0,
625            "cpuset_cpus": "",
626            "cpuset_mems": "",
627            "detach": True,
628            "device": [],
629            "env_host": False,
630            "etc_hosts": {},
631            "group_add": [],
632            "ipc": "",
633            "kernelmemory": "0",
634            "log_driver": "k8s-file",
635            "log_level": "error",
636            "memory": "0",
637            "memory_swap": "0",
638            "memory_reservation": "0",
639            # "memory_swappiness": -1,
640            "no_hosts": False,
641            # libpod issue with networks in inspection
642            "oom_score_adj": 0,
643            "pid": "",
644            "privileged": False,
645            "rm": False,
646            "security_opt": [],
647            "stop_signal": self.image_info['config'].get('stopsignal', "15"),
648            "tty": False,
649            "user": self.image_info.get('user', ''),
650            "workdir": self.image_info['config'].get('workingdir', '/'),
651            "uts": "",
652        }
653
654    def default_dict(self):
655        # make here any changes to self.defaults related to podman version
656        # https://github.com/containers/libpod/pull/5669
657        if (LooseVersion(self.version) >= LooseVersion('1.8.0')
658                and LooseVersion(self.version) < LooseVersion('1.9.0')):
659            self.defaults['cpu_shares'] = 1024
660        if (LooseVersion(self.version) >= LooseVersion('2.0.0')):
661            self.defaults['network'] = ["slirp4netns"]
662            self.defaults['ipc'] = "private"
663            self.defaults['uts'] = "private"
664            self.defaults['pid'] = "private"
665        if (LooseVersion(self.version) >= LooseVersion('3.0.0')):
666            self.defaults['log_level'] = "warning"
667        return self.defaults
668
669
670class PodmanContainerDiff:
671    def __init__(self, module, module_params, info, image_info, podman_version):
672        self.module = module
673        self.module_params = module_params
674        self.version = podman_version
675        self.default_dict = None
676        self.info = lower_keys(info)
677        self.image_info = lower_keys(image_info)
678        self.params = self.defaultize()
679        self.diff = {'before': {}, 'after': {}}
680        self.non_idempotent = {}
681
682    def defaultize(self):
683        params_with_defaults = {}
684        self.default_dict = PodmanDefaults(
685            self.image_info, self.version).default_dict()
686        for p in self.module_params:
687            if self.module_params[p] is None and p in self.default_dict:
688                params_with_defaults[p] = self.default_dict[p]
689            else:
690                params_with_defaults[p] = self.module_params[p]
691        return params_with_defaults
692
693    def _diff_update_and_compare(self, param_name, before, after):
694        if before != after:
695            self.diff['before'].update({param_name: before})
696            self.diff['after'].update({param_name: after})
697            return True
698        return False
699
700    def diffparam_annotation(self):
701        before = self.info['config']['annotations'] or {}
702        after = before.copy()
703        if self.module_params['annotation'] is not None:
704            after.update(self.params['annotation'])
705        return self._diff_update_and_compare('annotation', before, after)
706
707    def diffparam_env_host(self):
708        # It's impossible to get from inspest, recreate it if not default
709        before = False
710        after = self.params['env_host']
711        return self._diff_update_and_compare('env_host', before, after)
712
713    def diffparam_blkio_weight(self):
714        before = self.info['hostconfig']['blkioweight']
715        after = self.params['blkio_weight']
716        return self._diff_update_and_compare('blkio_weight', before, after)
717
718    def diffparam_blkio_weight_device(self):
719        before = self.info['hostconfig']['blkioweightdevice']
720        if before == [] and self.module_params['blkio_weight_device'] is None:
721            after = []
722        else:
723            after = self.params['blkio_weight_device']
724        return self._diff_update_and_compare('blkio_weight_device', before, after)
725
726    def diffparam_cap_add(self):
727        before = self.info['effectivecaps'] or []
728        before = [i.lower() for i in before]
729        after = []
730        if self.module_params['cap_add'] is not None:
731            for cap in self.module_params['cap_add']:
732                cap = cap.lower()
733                cap = cap if cap.startswith('cap_') else 'cap_' + cap
734                after.append(cap)
735        after += before
736        before, after = sorted(list(set(before))), sorted(list(set(after)))
737        return self._diff_update_and_compare('cap_add', before, after)
738
739    def diffparam_cap_drop(self):
740        before = self.info['effectivecaps'] or []
741        before = [i.lower() for i in before]
742        after = before[:]
743        if self.module_params['cap_drop'] is not None:
744            for cap in self.module_params['cap_drop']:
745                cap = cap.lower()
746                cap = cap if cap.startswith('cap_') else 'cap_' + cap
747                if cap in after:
748                    after.remove(cap)
749        before, after = sorted(list(set(before))), sorted(list(set(after)))
750        return self._diff_update_and_compare('cap_drop', before, after)
751
752    def diffparam_cgroup_parent(self):
753        before = self.info['hostconfig']['cgroupparent']
754        after = self.params['cgroup_parent']
755        if after is None:
756            after = before
757        return self._diff_update_and_compare('cgroup_parent', before, after)
758
759    def diffparam_cgroups(self):
760        # Cgroups output is not supported in all versions
761        if 'cgroups' in self.info['hostconfig']:
762            before = self.info['hostconfig']['cgroups']
763            after = self.params['cgroups']
764            return self._diff_update_and_compare('cgroups', before, after)
765        return False
766
767    def diffparam_cidfile(self):
768        before = self.info['hostconfig']['containeridfile']
769        after = self.params['cidfile']
770        labels = self.info['config']['labels'] or {}
771        # Ignore cidfile that is coming from systemd files
772        # https://github.com/containers/ansible-podman-collections/issues/276
773        if 'podman_systemd_unit' in labels:
774            after = before
775        return self._diff_update_and_compare('cidfile', before, after)
776
777    def diffparam_command(self):
778        # TODO(sshnaidm): to inspect image to get the default command
779        if self.module_params['command'] is not None:
780            before = self.info['config']['cmd']
781            after = self.params['command']
782            if isinstance(after, str):
783                after = shlex.split(after)
784            return self._diff_update_and_compare('command', before, after)
785        return False
786
787    def diffparam_conmon_pidfile(self):
788        before = self.info['conmonpidfile']
789        if self.module_params['conmon_pidfile'] is None:
790            after = before
791        else:
792            after = self.params['conmon_pidfile']
793        return self._diff_update_and_compare('conmon_pidfile', before, after)
794
795    def diffparam_cpu_period(self):
796        before = self.info['hostconfig']['cpuperiod']
797        after = self.params['cpu_period']
798        return self._diff_update_and_compare('cpu_period', before, after)
799
800    def diffparam_cpu_rt_period(self):
801        before = self.info['hostconfig']['cpurealtimeperiod']
802        after = self.params['cpu_rt_period']
803        return self._diff_update_and_compare('cpu_rt_period', before, after)
804
805    def diffparam_cpu_rt_runtime(self):
806        before = self.info['hostconfig']['cpurealtimeruntime']
807        after = self.params['cpu_rt_runtime']
808        return self._diff_update_and_compare('cpu_rt_runtime', before, after)
809
810    def diffparam_cpu_shares(self):
811        before = self.info['hostconfig']['cpushares']
812        after = self.params['cpu_shares']
813        return self._diff_update_and_compare('cpu_shares', before, after)
814
815    def diffparam_cpus(self):
816        before = int(self.info['hostconfig']['nanocpus']) / 1000000000
817        after = self.params['cpus']
818        return self._diff_update_and_compare('cpus', before, after)
819
820    def diffparam_cpuset_cpus(self):
821        before = self.info['hostconfig']['cpusetcpus']
822        after = self.params['cpuset_cpus']
823        return self._diff_update_and_compare('cpuset_cpus', before, after)
824
825    def diffparam_cpuset_mems(self):
826        before = self.info['hostconfig']['cpusetmems']
827        after = self.params['cpuset_mems']
828        return self._diff_update_and_compare('cpuset_mems', before, after)
829
830    def diffparam_device(self):
831        before = [":".join([i['pathonhost'], i['pathincontainer']])
832                  for i in self.info['hostconfig']['devices']]
833        after = [":".join(i.split(":")[:2]) for i in self.params['device']]
834        before, after = sorted(list(set(before))), sorted(list(set(after)))
835        return self._diff_update_and_compare('devices', before, after)
836
837    def diffparam_device_read_bps(self):
838        before = self.info['hostconfig']['blkiodevicereadbps'] or []
839        before = ["%s:%s" % (i['path'], i['rate']) for i in before]
840        after = self.params['device_read_bps'] or []
841        before, after = sorted(list(set(before))), sorted(list(set(after)))
842        return self._diff_update_and_compare('device_read_bps', before, after)
843
844    def diffparam_device_read_iops(self):
845        before = self.info['hostconfig']['blkiodevicereadiops'] or []
846        before = ["%s:%s" % (i['path'], i['rate']) for i in before]
847        after = self.params['device_read_iops'] or []
848        before, after = sorted(list(set(before))), sorted(list(set(after)))
849        return self._diff_update_and_compare('device_read_iops', before, after)
850
851    def diffparam_device_write_bps(self):
852        before = self.info['hostconfig']['blkiodevicewritebps'] or []
853        before = ["%s:%s" % (i['path'], i['rate']) for i in before]
854        after = self.params['device_write_bps'] or []
855        before, after = sorted(list(set(before))), sorted(list(set(after)))
856        return self._diff_update_and_compare('device_write_bps', before, after)
857
858    def diffparam_device_write_iops(self):
859        before = self.info['hostconfig']['blkiodevicewriteiops'] or []
860        before = ["%s:%s" % (i['path'], i['rate']) for i in before]
861        after = self.params['device_write_iops'] or []
862        before, after = sorted(list(set(before))), sorted(list(set(after)))
863        return self._diff_update_and_compare('device_write_iops', before, after)
864
865    # Limited idempotency, it can't guess default values
866    def diffparam_env(self):
867        env_before = self.info['config']['env'] or {}
868        before = {i.split("=")[0]: "=".join(i.split("=")[1:])
869                  for i in env_before}
870        after = before.copy()
871        if self.params['env']:
872            after.update({k: str(v) for k, v in self.params['env'].items()})
873        return self._diff_update_and_compare('env', before, after)
874
875    def diffparam_etc_hosts(self):
876        if self.info['hostconfig']['extrahosts']:
877            before = dict([i.split(":")
878                           for i in self.info['hostconfig']['extrahosts']])
879        else:
880            before = {}
881        after = self.params['etc_hosts']
882        return self._diff_update_and_compare('etc_hosts', before, after)
883
884    def diffparam_group_add(self):
885        before = self.info['hostconfig']['groupadd']
886        after = self.params['group_add']
887        return self._diff_update_and_compare('group_add', before, after)
888
889    # Healthcheck is only defined in container config if a healthcheck
890    # was configured; otherwise the config key isn't part of the config.
891    def diffparam_healthcheck(self):
892        if 'healthcheck' in self.info['config']:
893            # the "test" key is a list of 2 items where the first one is
894            # "CMD-SHELL" and the second one is the actual healthcheck command.
895            before = self.info['config']['healthcheck']['test'][1]
896        else:
897            before = ''
898        after = self.params['healthcheck'] or before
899        return self._diff_update_and_compare('healthcheck', before, after)
900
901    # Because of hostname is random generated, this parameter has partial idempotency only.
902    def diffparam_hostname(self):
903        before = self.info['config']['hostname']
904        after = self.params['hostname'] or before
905        return self._diff_update_and_compare('hostname', before, after)
906
907    def diffparam_image(self):
908        before_id = self.info['image']
909        after_id = self.image_info['id']
910        if before_id == after_id:
911            return self._diff_update_and_compare('image', before_id, after_id)
912        before = self.info['config']['image']
913        after = self.params['image']
914        mode = self.params['image_strict']
915        if mode is None or not mode:
916            # In a idempotency 'lite mode' assume all images from different registries are the same
917            before = before.replace(":latest", "")
918            after = after.replace(":latest", "")
919            before = before.split("/")[-1]
920            after = after.split("/")[-1]
921        else:
922            return self._diff_update_and_compare('image', before_id, after_id)
923        return self._diff_update_and_compare('image', before, after)
924
925    def diffparam_ipc(self):
926        before = self.info['hostconfig']['ipcmode']
927        after = self.params['ipc']
928        if self.params['pod'] and not self.module_params['ipc']:
929            after = before
930        return self._diff_update_and_compare('ipc', before, after)
931
932    def diffparam_label(self):
933        before = self.info['config']['labels'] or {}
934        after = self.image_info.get('labels') or {}
935        if self.params['label']:
936            after.update({
937                str(k).lower(): str(v)
938                for k, v in self.params['label'].items()
939            })
940        # Strip out labels that are coming from systemd files
941        # https://github.com/containers/ansible-podman-collections/issues/276
942        if 'podman_systemd_unit' in before:
943            after.pop('podman_systemd_unit', None)
944            before.pop('podman_systemd_unit', None)
945        return self._diff_update_and_compare('label', before, after)
946
947    def diffparam_log_driver(self):
948        before = self.info['hostconfig']['logconfig']['type']
949        after = self.params['log_driver']
950        return self._diff_update_and_compare('log_driver', before, after)
951
952    def diffparam_log_level(self):
953        excom = self.info['exitcommand']
954        if '--log-level' in excom:
955            before = excom[excom.index('--log-level') + 1].lower()
956        else:
957            before = self.params['log_level']
958        after = self.params['log_level']
959        return self._diff_update_and_compare('log_level', before, after)
960
961    # Parameter has limited idempotency, unable to guess the default log_path
962    def diffparam_log_opt(self):
963        before, after = {}, {}
964
965        # Log path
966        path_before = None
967        if 'logpath' in self.info:
968            path_before = self.info['logpath']
969        # For Podman v3
970        if ('logconfig' in self.info['hostconfig'] and
971                'path' in self.info['hostconfig']['logconfig']):
972            path_before = self.info['hostconfig']['logconfig']['path']
973        if path_before is not None:
974            if (self.module_params['log_opt'] and
975                    'path' in self.module_params['log_opt'] and
976                    self.module_params['log_opt']['path'] is not None):
977                path_after = self.params['log_opt']['path']
978            else:
979                path_after = path_before
980            if path_before != path_after:
981                before.update({'log-path': path_before})
982                after.update({'log-path': path_after})
983
984        # Log tag
985        tag_before = None
986        if 'logtag' in self.info:
987            tag_before = self.info['logtag']
988        # For Podman v3
989        if ('logconfig' in self.info['hostconfig'] and
990                'tag' in self.info['hostconfig']['logconfig']):
991            tag_before = self.info['hostconfig']['logconfig']['tag']
992        if tag_before is not None:
993            if (self.module_params['log_opt'] and
994                    'tag' in self.module_params['log_opt'] and
995                    self.module_params['log_opt']['tag'] is not None):
996                tag_after = self.params['log_opt']['tag']
997            else:
998                tag_after = ''
999            if tag_before != tag_after:
1000                before.update({'log-tag': tag_before})
1001                after.update({'log-tag': tag_after})
1002
1003        # Log size
1004        # For Podman v3
1005        # size_before = '0B'
1006        # TODO(sshnaidm): integrate B/KB/MB/GB calculation for sizes
1007        # if ('logconfig' in self.info['hostconfig'] and
1008        #         'size' in self.info['hostconfig']['logconfig']):
1009        #     size_before = self.info['hostconfig']['logconfig']['size']
1010        # if size_before != '0B':
1011        #     if (self.module_params['log_opt'] and
1012        #             'max_size' in self.module_params['log_opt'] and
1013        #             self.module_params['log_opt']['max_size'] is not None):
1014        #         size_after = self.params['log_opt']['max_size']
1015        #     else:
1016        #         size_after = ''
1017        #     if size_before != size_after:
1018        #         before.update({'log-size': size_before})
1019        #         after.update({'log-size': size_after})
1020
1021        return self._diff_update_and_compare('log_opt', before, after)
1022
1023    def diffparam_mac_address(self):
1024        before = str(self.info['networksettings']['macaddress'])
1025        if self.module_params['mac_address'] is not None:
1026            after = self.params['mac_address']
1027        else:
1028            after = before
1029        return self._diff_update_and_compare('mac_address', before, after)
1030
1031    def diffparam_memory(self):
1032        before = str(self.info['hostconfig']['memory'])
1033        after = self.params['memory']
1034        return self._diff_update_and_compare('memory', before, after)
1035
1036    def diffparam_memory_swap(self):
1037        # By default it's twice memory parameter
1038        before = str(self.info['hostconfig']['memoryswap'])
1039        after = self.params['memory_swap']
1040        if (self.module_params['memory_swap'] is None
1041                and self.params['memory'] != 0
1042                and self.params['memory'].isdigit()):
1043            after = str(int(self.params['memory']) * 2)
1044        return self._diff_update_and_compare('memory_swap', before, after)
1045
1046    def diffparam_memory_reservation(self):
1047        before = str(self.info['hostconfig']['memoryreservation'])
1048        after = self.params['memory_reservation']
1049        return self._diff_update_and_compare('memory_reservation', before, after)
1050
1051    def diffparam_network(self):
1052        net_mode_before = self.info['hostconfig']['networkmode']
1053        net_mode_after = ''
1054        before = list(self.info['networksettings'].get('networks', {}))
1055        # Remove default 'podman' network in v3 for comparison
1056        if before == ['podman']:
1057            before = []
1058        # Special case for options for slirp4netns rootless networking from v2
1059        if net_mode_before == 'slirp4netns' and 'createcommand' in self.info['config']:
1060            cr_com = self.info['config']['createcommand']
1061            if '--network' in cr_com:
1062                cr_net = cr_com[cr_com.index('--network') + 1].lower()
1063                if 'slirp4netns:' in cr_net:
1064                    before = [cr_net]
1065        after = self.params['network'] or []
1066        # If container is in pod and no networks are provided
1067        if not self.module_params['network'] and self.params['pod']:
1068            after = before
1069            return self._diff_update_and_compare('network', before, after)
1070        # Check special network modes
1071        if after in [['bridge'], ['host'], ['slirp4netns'], ['none']]:
1072            net_mode_after = after[0]
1073        # If changes are only for network mode and container has no networks
1074        if net_mode_after and not before:
1075            # Remove differences between v1 and v2
1076            net_mode_after = net_mode_after.replace('bridge', 'default')
1077            net_mode_after = net_mode_after.replace('slirp4netns', 'default')
1078            net_mode_before = net_mode_before.replace('bridge', 'default')
1079            net_mode_before = net_mode_before.replace('slirp4netns', 'default')
1080            return self._diff_update_and_compare('network', net_mode_before, net_mode_after)
1081        # If container is attached to network of a different container
1082        if "container" in net_mode_before:
1083            for netw in after:
1084                if "container" in netw:
1085                    before = after = netw
1086        before, after = sorted(list(set(before))), sorted(list(set(after)))
1087        return self._diff_update_and_compare('network', before, after)
1088
1089    def diffparam_oom_score_adj(self):
1090        before = self.info['hostconfig']['oomscoreadj']
1091        after = self.params['oom_score_adj']
1092        return self._diff_update_and_compare('oom_score_adj', before, after)
1093
1094    def diffparam_privileged(self):
1095        before = self.info['hostconfig']['privileged']
1096        after = self.params['privileged']
1097        return self._diff_update_and_compare('privileged', before, after)
1098
1099    def diffparam_pid(self):
1100        before = self.info['hostconfig']['pidmode']
1101        after = self.params['pid']
1102        return self._diff_update_and_compare('pid', before, after)
1103
1104    # TODO(sshnaidm) Need to add port ranges support
1105    def diffparam_publish(self):
1106        def compose(p, h):
1107            s = ":".join(
1108                [str(h["hostport"]), p.replace('/tcp', '')]
1109            ).strip(":")
1110            if h['hostip']:
1111                return ":".join([h['hostip'], s])
1112            return s
1113
1114        ports = self.info['hostconfig']['portbindings']
1115        before = []
1116        for port, hosts in ports.items():
1117            for h in hosts:
1118                before.append(compose(port, h))
1119        after = self.params['publish'] or []
1120        if self.params['publish_all']:
1121            image_ports = self.image_info['config'].get('exposedports', {})
1122            if image_ports:
1123                after += list(image_ports.keys())
1124        after = [
1125            i.replace("/tcp", "").replace("[", "").replace("]", "")
1126            for i in after]
1127        # No support for port ranges yet
1128        for ports in after:
1129            if "-" in ports:
1130                return self._diff_update_and_compare('publish', '', '')
1131        before, after = sorted(list(set(before))), sorted(list(set(after)))
1132        return self._diff_update_and_compare('publish', before, after)
1133
1134    def diffparam_rm(self):
1135        before = self.info['hostconfig']['autoremove']
1136        after = self.params['rm']
1137        return self._diff_update_and_compare('rm', before, after)
1138
1139    def diffparam_security_opt(self):
1140        before = self.info['hostconfig']['securityopt']
1141        # In rootful containers with apparmor there is a default security opt
1142        before = [o for o in before if 'apparmor=containers-default' not in o]
1143        after = self.params['security_opt']
1144        before, after = sorted(list(set(before))), sorted(list(set(after)))
1145        return self._diff_update_and_compare('security_opt', before, after)
1146
1147    def diffparam_stop_signal(self):
1148        signals = {
1149            "sighup": "1",
1150            "sigint": "2",
1151            "sigquit": "3",
1152            "sigill": "4",
1153            "sigtrap": "5",
1154            "sigabrt": "6",
1155            "sigiot": "6",
1156            "sigbus": "7",
1157            "sigfpe": "8",
1158            "sigkill": "9",
1159            "sigusr1": "10",
1160            "sigsegv": "11",
1161            "sigusr2": "12",
1162            "sigpipe": "13",
1163            "sigalrm": "14",
1164            "sigterm": "15",
1165            "sigstkflt": "16",
1166            "sigchld": "17",
1167            "sigcont": "18",
1168            "sigstop": "19",
1169            "sigtstp": "20",
1170            "sigttin": "21",
1171            "sigttou": "22",
1172            "sigurg": "23",
1173            "sigxcpu": "24",
1174            "sigxfsz": "25",
1175            "sigvtalrm": "26",
1176            "sigprof": "27",
1177            "sigwinch": "28",
1178            "sigio": "29",
1179            "sigpwr": "30",
1180            "sigsys": "31",
1181            "sigrtmin+3": "37"
1182        }
1183        before = str(self.info['config']['stopsignal'])
1184        if not before.isdigit():
1185            before = signals[before.lower()]
1186        after = str(self.params['stop_signal'])
1187        if not after.isdigit():
1188            after = signals[after.lower()]
1189        return self._diff_update_and_compare('stop_signal', before, after)
1190
1191    def diffparam_timezone(self):
1192        before = self.info['config'].get('timezone')
1193        after = self.params['timezone']
1194        return self._diff_update_and_compare('timezone', before, after)
1195
1196    def diffparam_tty(self):
1197        before = self.info['config']['tty']
1198        after = self.params['tty']
1199        return self._diff_update_and_compare('tty', before, after)
1200
1201    def diffparam_user(self):
1202        before = self.info['config']['user']
1203        after = self.params['user']
1204        return self._diff_update_and_compare('user', before, after)
1205
1206    def diffparam_ulimit(self):
1207        after = self.params['ulimit'] or []
1208        # In case of latest podman
1209        if 'createcommand' in self.info['config']:
1210            ulimits = []
1211            for k, c in enumerate(self.info['config']['createcommand']):
1212                if c == '--ulimit':
1213                    ulimits.append(self.info['config']['createcommand'][k + 1])
1214            before = ulimits
1215            before, after = sorted(before), sorted(after)
1216            return self._diff_update_and_compare('ulimit', before, after)
1217        if after:
1218            ulimits = self.info['hostconfig']['ulimits']
1219            before = {
1220                u['name'].replace('rlimit_', ''): "%s:%s" % (u['soft'], u['hard']) for u in ulimits}
1221            after = {i.split('=')[0]: i.split('=')[1]
1222                     for i in self.params['ulimit']}
1223            new_before = []
1224            new_after = []
1225            for u in list(after.keys()):
1226                # We don't support unlimited ulimits because it depends on platform
1227                if u in before and "-1" not in after[u]:
1228                    new_before.append([u, before[u]])
1229                    new_after.append([u, after[u]])
1230            return self._diff_update_and_compare('ulimit', new_before, new_after)
1231        return self._diff_update_and_compare('ulimit', '', '')
1232
1233    def diffparam_uts(self):
1234        before = self.info['hostconfig']['utsmode']
1235        after = self.params['uts']
1236        if self.params['pod'] and not self.module_params['uts']:
1237            after = before
1238        return self._diff_update_and_compare('uts', before, after)
1239
1240    def diffparam_volume(self):
1241        def clean_volume(x):
1242            '''Remove trailing and double slashes from volumes.'''
1243            if not x.rstrip("/"):
1244                return "/"
1245            return x.replace("//", "/").rstrip("/")
1246
1247        before = self.info['mounts']
1248        before_local_vols = []
1249        if before:
1250            volumes = []
1251            local_vols = []
1252            for m in before:
1253                if m['type'] != 'volume':
1254                    volumes.append([m['source'], m['destination']])
1255                elif m['type'] == 'volume':
1256                    local_vols.append([m['name'], m['destination']])
1257            before = [":".join(v) for v in volumes]
1258            before_local_vols = [":".join(v) for v in local_vols]
1259        if self.params['volume'] is not None:
1260            after = [":".join(
1261                [clean_volume(i) for i in v.split(":")[:2]]
1262            ) for v in self.params['volume']]
1263        else:
1264            after = []
1265        if before_local_vols:
1266            after = list(set(after).difference(before_local_vols))
1267        before, after = sorted(list(set(before))), sorted(list(set(after)))
1268        return self._diff_update_and_compare('volume', before, after)
1269
1270    def diffparam_volumes_from(self):
1271        # Possibly volumesfrom is not in config
1272        before = self.info['hostconfig'].get('volumesfrom', []) or []
1273        after = self.params['volumes_from'] or []
1274        return self._diff_update_and_compare('volumes_from', before, after)
1275
1276    def diffparam_workdir(self):
1277        before = self.info['config']['workingdir']
1278        after = self.params['workdir']
1279        return self._diff_update_and_compare('workdir', before, after)
1280
1281    def is_different(self):
1282        diff_func_list = [func for func in dir(self)
1283                          if callable(getattr(self, func)) and func.startswith(
1284                              "diffparam")]
1285        fail_fast = not bool(self.module._diff)
1286        different = False
1287        for func_name in diff_func_list:
1288            dff_func = getattr(self, func_name)
1289            if dff_func():
1290                if fail_fast:
1291                    return True
1292                different = True
1293        # Check non idempotent parameters
1294        for p in self.non_idempotent:
1295            if self.module_params[p] is not None and self.module_params[p] not in [{}, [], '']:
1296                different = True
1297        return different
1298
1299
1300def ensure_image_exists(module, image, module_params):
1301    """If image is passed, ensure it exists, if not - pull it or fail.
1302
1303    Arguments:
1304        module {obj} -- ansible module object
1305        image {str} -- name of image
1306
1307    Returns:
1308        list -- list of image actions - if it pulled or nothing was done
1309    """
1310    image_actions = []
1311    module_exec = module_params['executable']
1312    if not image:
1313        return image_actions
1314    rc, out, err = module.run_command([module_exec, 'image', 'exists', image])
1315    if rc == 0:
1316        return image_actions
1317    rc, out, err = module.run_command([module_exec, 'image', 'pull', image])
1318    if rc != 0:
1319        module.fail_json(msg="Can't pull image %s" % image, stdout=out,
1320                         stderr=err)
1321    image_actions.append("pulled image %s" % image)
1322    return image_actions
1323
1324
1325class PodmanContainer:
1326    """Perform container tasks.
1327
1328    Manages podman container, inspects it and checks its current state
1329    """
1330
1331    def __init__(self, module, name, module_params):
1332        """Initialize PodmanContainer class.
1333
1334        Arguments:
1335            module {obj} -- ansible module object
1336            name {str} -- name of container
1337        """
1338
1339        self.module = module
1340        self.module_params = module_params
1341        self.name = name
1342        self.stdout, self.stderr = '', ''
1343        self.info = self.get_info()
1344        self.version = self._get_podman_version()
1345        self.diff = {}
1346        self.actions = []
1347
1348    @property
1349    def exists(self):
1350        """Check if container exists."""
1351        return bool(self.info != {})
1352
1353    @property
1354    def different(self):
1355        """Check if container is different."""
1356        diffcheck = PodmanContainerDiff(
1357            self.module,
1358            self.module_params,
1359            self.info,
1360            self.get_image_info(),
1361            self.version)
1362        is_different = diffcheck.is_different()
1363        diffs = diffcheck.diff
1364        if self.module._diff and is_different and diffs['before'] and diffs['after']:
1365            self.diff['before'] = "\n".join(
1366                ["%s - %s" % (k, v) for k, v in sorted(
1367                    diffs['before'].items())]) + "\n"
1368            self.diff['after'] = "\n".join(
1369                ["%s - %s" % (k, v) for k, v in sorted(
1370                    diffs['after'].items())]) + "\n"
1371        return is_different
1372
1373    @property
1374    def running(self):
1375        """Return True if container is running now."""
1376        return self.exists and self.info['State']['Running']
1377
1378    @property
1379    def stopped(self):
1380        """Return True if container exists and is not running now."""
1381        return self.exists and not self.info['State']['Running']
1382
1383    def get_info(self):
1384        """Inspect container and gather info about it."""
1385        # pylint: disable=unused-variable
1386        rc, out, err = self.module.run_command(
1387            [self.module_params['executable'], b'container', b'inspect', self.name])
1388        return json.loads(out)[0] if rc == 0 else {}
1389
1390    def get_image_info(self):
1391        """Inspect container image and gather info about it."""
1392        # pylint: disable=unused-variable
1393        rc, out, err = self.module.run_command(
1394            [self.module_params['executable'], b'image', b'inspect', self.module_params['image']])
1395        return json.loads(out)[0] if rc == 0 else {}
1396
1397    def _get_podman_version(self):
1398        # pylint: disable=unused-variable
1399        rc, out, err = self.module.run_command(
1400            [self.module_params['executable'], b'--version'])
1401        if rc != 0 or not out or "version" not in out:
1402            self.module.fail_json(msg="%s run failed!" %
1403                                  self.module_params['executable'])
1404        return out.split("version")[1].strip()
1405
1406    def _perform_action(self, action):
1407        """Perform action with container.
1408
1409        Arguments:
1410            action {str} -- action to perform - start, create, stop, run,
1411                            delete, restart
1412        """
1413        b_command = PodmanModuleParams(action,
1414                                       self.module_params,
1415                                       self.version,
1416                                       self.module,
1417                                       ).construct_command_from_params()
1418        if action == 'create':
1419            b_command.remove(b'--detach=True')
1420        full_cmd = " ".join([self.module_params['executable']]
1421                            + [to_native(i) for i in b_command])
1422        self.actions.append(full_cmd)
1423        if self.module.check_mode:
1424            self.module.log(
1425                "PODMAN-CONTAINER-DEBUG (check_mode): %s" % full_cmd)
1426        else:
1427            rc, out, err = self.module.run_command(
1428                [self.module_params['executable'], b'container'] + b_command,
1429                expand_user_and_vars=False)
1430            self.module.log("PODMAN-CONTAINER-DEBUG: %s" % full_cmd)
1431            if self.module_params['debug']:
1432                self.module.log("PODMAN-CONTAINER-DEBUG STDOUT: %s" % out)
1433                self.module.log("PODMAN-CONTAINER-DEBUG STDERR: %s" % err)
1434                self.module.log("PODMAN-CONTAINER-DEBUG RC: %s" % rc)
1435            self.stdout = out
1436            self.stderr = err
1437            if rc != 0:
1438                self.module.fail_json(
1439                    msg="Can't %s container %s" % (action, self.name),
1440                    stdout=out, stderr=err)
1441
1442    def run(self):
1443        """Run the container."""
1444        self._perform_action('run')
1445
1446    def delete(self):
1447        """Delete the container."""
1448        self._perform_action('delete')
1449
1450    def stop(self):
1451        """Stop the container."""
1452        self._perform_action('stop')
1453
1454    def start(self):
1455        """Start the container."""
1456        self._perform_action('start')
1457
1458    def restart(self):
1459        """Restart the container."""
1460        self._perform_action('restart')
1461
1462    def create(self):
1463        """Create the container."""
1464        self._perform_action('create')
1465
1466    def recreate(self):
1467        """Recreate the container."""
1468        if self.running:
1469            self.stop()
1470        self.delete()
1471        self.create()
1472
1473    def recreate_run(self):
1474        """Recreate and run the container."""
1475        if self.running:
1476            self.stop()
1477        self.delete()
1478        self.run()
1479
1480
1481class PodmanManager:
1482    """Module manager class.
1483
1484    Defines according to parameters what actions should be applied to container
1485    """
1486
1487    def __init__(self, module, params):
1488        """Initialize PodmanManager class.
1489
1490        Arguments:
1491            module {obj} -- ansible module object
1492        """
1493
1494        self.module = module
1495        self.results = {
1496            'changed': False,
1497            'actions': [],
1498            'container': {},
1499        }
1500        self.module_params = params
1501        self.name = self.module_params['name']
1502        self.executable = \
1503            self.module.get_bin_path(self.module_params['executable'],
1504                                     required=True)
1505        self.image = self.module_params['image']
1506        image_actions = ensure_image_exists(
1507            self.module, self.image, self.module_params)
1508        self.results['actions'] += image_actions
1509        self.state = self.module_params['state']
1510        self.restart = self.module_params['force_restart']
1511        self.recreate = self.module_params['recreate']
1512        self.container = PodmanContainer(
1513            self.module, self.name, self.module_params)
1514
1515    def update_container_result(self, changed=True):
1516        """Inspect the current container, update results with last info, exit.
1517
1518        Keyword Arguments:
1519            changed {bool} -- whether any action was performed
1520                              (default: {True})
1521        """
1522        facts = self.container.get_info() if changed else self.container.info
1523        out, err = self.container.stdout, self.container.stderr
1524        self.results.update({'changed': changed, 'container': facts,
1525                             'podman_actions': self.container.actions},
1526                            stdout=out, stderr=err)
1527        if self.container.diff:
1528            self.results.update({'diff': self.container.diff})
1529        if self.module.params['debug'] or self.module_params['debug']:
1530            self.results.update({'podman_version': self.container.version})
1531        self.results.update(
1532            {'podman_systemd': generate_systemd(self.module, self.module_params, self.name)})
1533
1534    def make_started(self):
1535        """Run actions if desired state is 'started'."""
1536        if not self.image:
1537            if not self.container.exists:
1538                self.module.fail_json(msg='Cannot start container when image'
1539                                          ' is not specified!')
1540            if self.restart:
1541                self.container.restart()
1542                self.results['actions'].append('restarted %s' %
1543                                               self.container.name)
1544            else:
1545                self.container.start()
1546                self.results['actions'].append('started %s' %
1547                                               self.container.name)
1548            self.update_container_result()
1549            return
1550        if self.container.exists and self.restart:
1551            if self.container.running:
1552                self.container.restart()
1553                self.results['actions'].append('restarted %s' %
1554                                               self.container.name)
1555            else:
1556                self.container.start()
1557                self.results['actions'].append('started %s' %
1558                                               self.container.name)
1559            self.update_container_result()
1560            return
1561        if self.container.running and \
1562                (self.container.different or self.recreate):
1563            self.container.recreate_run()
1564            self.results['actions'].append('recreated %s' %
1565                                           self.container.name)
1566            self.update_container_result()
1567            return
1568        elif self.container.running and not self.container.different:
1569            if self.restart:
1570                self.container.restart()
1571                self.results['actions'].append('restarted %s' %
1572                                               self.container.name)
1573                self.update_container_result()
1574                return
1575            self.update_container_result(changed=False)
1576            return
1577        elif not self.container.exists:
1578            self.container.run()
1579            self.results['actions'].append('started %s' % self.container.name)
1580            self.update_container_result()
1581            return
1582        elif self.container.stopped and self.container.different:
1583            self.container.recreate_run()
1584            self.results['actions'].append('recreated %s' %
1585                                           self.container.name)
1586            self.update_container_result()
1587            return
1588        elif self.container.stopped and not self.container.different:
1589            self.container.start()
1590            self.results['actions'].append('started %s' % self.container.name)
1591            self.update_container_result()
1592            return
1593
1594    def make_created(self):
1595        """Run actions if desired state is 'created'."""
1596        if not self.container.exists and not self.image:
1597            self.module.fail_json(msg='Cannot create container when image'
1598                                      ' is not specified!')
1599        if not self.container.exists:
1600            self.container.create()
1601            self.results['actions'].append('created %s' % self.container.name)
1602            self.update_container_result()
1603            return
1604        else:
1605            if (self.container.different or self.recreate):
1606                self.container.recreate()
1607                self.results['actions'].append('recreated %s' %
1608                                               self.container.name)
1609                if self.container.running:
1610                    self.container.start()
1611                    self.results['actions'].append('started %s' %
1612                                                   self.container.name)
1613                self.update_container_result()
1614                return
1615            elif self.restart:
1616                if self.container.running:
1617                    self.container.restart()
1618                    self.results['actions'].append('restarted %s' %
1619                                                   self.container.name)
1620                else:
1621                    self.container.start()
1622                    self.results['actions'].append('started %s' %
1623                                                   self.container.name)
1624                self.update_container_result()
1625                return
1626            self.update_container_result(changed=False)
1627            return
1628
1629    def make_stopped(self):
1630        """Run actions if desired state is 'stopped'."""
1631        if not self.container.exists and not self.image:
1632            self.module.fail_json(msg='Cannot create container when image'
1633                                      ' is not specified!')
1634        if not self.container.exists:
1635            self.container.create()
1636            self.results['actions'].append('created %s' % self.container.name)
1637            self.update_container_result()
1638            return
1639        if self.container.stopped:
1640            self.update_container_result(changed=False)
1641            return
1642        elif self.container.running:
1643            self.container.stop()
1644            self.results['actions'].append('stopped %s' % self.container.name)
1645            self.update_container_result()
1646            return
1647
1648    def make_absent(self):
1649        """Run actions if desired state is 'absent'."""
1650        if not self.container.exists:
1651            self.results.update({'changed': False})
1652        elif self.container.exists:
1653            self.container.delete()
1654            self.results['actions'].append('deleted %s' % self.container.name)
1655            self.results.update({'changed': True})
1656        self.results.update({'container': {},
1657                             'podman_actions': self.container.actions})
1658
1659    def execute(self):
1660        """Execute the desired action according to map of actions & states."""
1661        states_map = {
1662            'present': self.make_created,
1663            'started': self.make_started,
1664            'absent': self.make_absent,
1665            'stopped': self.make_stopped,
1666            'created': self.make_created,
1667        }
1668        process_action = states_map[self.state]
1669        process_action()
1670        return self.results
1671