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