diff --git a/.gitignore b/.gitignore index 7611f2f28..dca2ce003 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ cover nosetests.xml .testrepository .venv +.stestr +tripleo_ansible.egg-info/ +__pycache__ +build # Editors *~ diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 000000000..1649a44cb --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=${TEST_PATH:-./tripleo_ansible/tests/} +top_dir=./ diff --git a/ansible-requirements.txt b/ansible-requirements.txt new file mode 100644 index 000000000..a89eb19de --- /dev/null +++ b/ansible-requirements.txt @@ -0,0 +1 @@ +ansible>=2.8 diff --git a/bindep.txt b/bindep.txt index 668bd4047..b5c04d700 100644 --- a/bindep.txt +++ b/bindep.txt @@ -19,7 +19,9 @@ git [platform:rpm] libffi-devel [platform:rpm] openssl-devel [platform:rpm] python-devel [platform:rpm !platform:rhel-8 !platform:centos-8] -python3-devel [platform:rpm !platform:rhel-7 !platform:centos-7] +python3-devel [platform:rpm !platform:rhel-7 !platform:centos-7] +PyYAML [platform:rpm !platform:rhel-8 !platform:centos-8] +python3-pyyaml [platform:rpm !platform:rhel-7 !platform:centos-7] python2-dnf [platform:fedora] # For SELinux diff --git a/doc/source/roles/role-tripleo-container-manage.rst b/doc/source/roles/role-tripleo-container-manage.rst new file mode 100644 index 000000000..3593307c0 --- /dev/null +++ b/doc/source/roles/role-tripleo-container-manage.rst @@ -0,0 +1,6 @@ +=============================== +Role - tripleo-container-manage +=============================== + +.. ansibleautoplugin:: + :role: tripleo_ansible/roles/tripleo-container-manage diff --git a/test-requirements.txt b/test-requirements.txt index e15dd0c3c..93e532ef6 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,2 +1,5 @@ pre-commit # MIT netaddr # BSD +mock>=2.0.0 # BSD +stestr>=2.0.0 # Apache-2.0 +oslotest>=3.2.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini index 1fa3872fc..6c3c54622 100644 --- a/tox.ini +++ b/tox.ini @@ -26,8 +26,13 @@ setenv = PYTHONWARNINGS=ignore:DEPRECATION::pip._internal.cli.base_command,ignore::UserWarning PIP_DISABLE_PIP_VERSION_CHECK=1 sitepackages = True -deps = -r {toxinidir}/test-requirements.txt -whitelist_externals = bash +deps = + -r {toxinidir}/test-requirements.txt + -r {toxinidir}/ansible-requirements.txt +commands = stestr run {posargs} +whitelist_externals = + bash + tox [testenv:bindep] # Do not install any requirements. We want this to be fast and work even if @@ -123,3 +128,10 @@ commands = echo -e '\n\nNo molecule tests have been executed\nSee https://docs.openstack.org/tripleo-ansible/latest/contributing.html#local-testing-of-new-roles\n\n'; \ fi" {[testenv:linters]commands} + +[testenv:modules] +deps= + {[testenv:linters]deps} +commands = + bash -c "cd {toxinidir}/tripleo_ansible/ansible_plugins/tests; molecule test --all;" + {[testenv:linters]commands} diff --git a/tripleo_ansible/ansible_plugins/filter/helpers.py b/tripleo_ansible/ansible_plugins/filter/helpers.py new file mode 100644 index 000000000..f94350e3b --- /dev/null +++ b/tripleo_ansible/ansible_plugins/filter/helpers.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ast +import json +import six + +from collections import OrderedDict +from operator import itemgetter + + +# cmp() doesn't exist on python3 +if six.PY3: + def cmp(a, b): + return 0 if a == b else 1 + + +class FilterModule(object): + def filters(self): + return { + 'singledict': self.singledict, + 'subsort': self.subsort, + 'needs_delete': self.needs_delete, + 'haskey': self.haskey, + 'list_of_keys': self.list_of_keys, + 'container_exec_cmd': self.container_exec_cmd + } + + def subsort(self, dict_to_sort, attribute, null_value=0): + """Sort a hash from a sub-element. + + This filter will return an dictionary ordered by the attribute + part of each item. + """ + for k, v in dict_to_sort.items(): + if attribute not in v: + dict_to_sort[k][attribute] = null_value + + data = {} + for d in dict_to_sort.items(): + if d[1][attribute] not in data: + data[d[1][attribute]] = [] + data[d[1][attribute]].append({d[0]: d[1]}) + + sorted_list = sorted( + data.items(), + key=lambda x: x[0] + ) + ordered_dict = {} + for o, v in sorted_list: + ordered_dict[o] = v + return ordered_dict + + def singledict(self, list_to_convert): + """Generate a single dictionary from a list of dictionaries. + + This filter will return a single dictionary from a list of + dictionaries. + """ + return_dict = {} + for i in list_to_convert: + return_dict.update(i) + return return_dict + + def needs_delete(self, container_infos, config, config_id): + """Returns a list of containers which need to be removed. + + This filter will check which containers need to be removed for these + reasons: no config_data, updated config_data or container not + part of the global config. + """ + to_delete = [] + to_skip = [] + installed_containers = [] + for c in container_infos: + c_name = c['Name'] + installed_containers.append(c_name) + + # Don't delete containers not managed by tripleo-ansible + if (c['Config']['Labels'] is None + or c['Config']['Labels'].get( + 'managed_by') != 'tripleo_ansible'): + to_skip += [c_name] + continue + + # Only remove containers managed in this config_id + if (c['Config']['Labels'] is None + or c['Config']['Labels'].get('config_id') != config_id): + to_skip += [c_name] + continue + + # Remove containers with no config_data + # e.g. broken config containers + if (c['Config']['Labels'] is not None + and 'config_data' not in c['Config']['Labels']): + to_delete += [c_name] + continue + + # Remove containers managed by tripleo-ansible that aren't in + # config e.g. containers not needed anymore and removed by an + # upgrade. Note: we don't cleanup paunch-managed containers. + if c_name not in config: + to_delete += [c_name] + continue + + for c_name, config_data in config.items(): + # don't try to remove a container which doesn't exist + if c_name not in installed_containers: + continue + + # already tagged to be removed + if c_name in to_delete: + continue + + if c_name in to_skip: + continue + + # Remove containers managed by tripleo-ansible when config_data + # changed. Since we already cleaned the containers not in config, + # this check needs to be in that loop. + # e.g. new TRIPLEO_CONFIG_HASH during a minor update + try: + c_facts = [c['Config']['Labels']['config_data'] + for c in container_infos if c_name == c['Name']] + except KeyError: + continue + + # Build c_facts so it can be compared later with config_data + c_facts = ast.literal_eval(c_facts[0]) if ( + len(c_facts)) == 1 else dict() + + if cmp(c_facts, config_data) != 0: + to_delete += [c_name] + + return to_delete + + def haskey(self, batched_container_data, attribute, value=None, + reverse=False, any=False): + """Return container data with a specific config key. + + This filter will take a list of dictionaries (batched_container_data) + and will return the dictionnaries which have a certain key given + in parameter with 'attribute'. + If reverse is set to True, the returned list won't contain dictionaries + which have the attribute. + If any is set to True, the returned list will match any value in + the list of values for "value" parameter which has to be a list. + """ + return_list = [] + for container in batched_container_data: + for k, v in json.loads(json.dumps(container)).items(): + if attribute in v and not reverse: + if value is None: + return_list.append({k: v}) + else: + if isinstance(value, list) and any: + if v[attribute] in value: + return_list.append({k: v}) + elif any: + raise TypeError("value has to be a list if any is " + "set to True.") + else: + if v[attribute] == value: + return_list.append({k: v}) + if attribute not in v and reverse: + return_list.append({k: v}) + return return_list + + def list_of_keys(self, keys_to_list): + """Return a list of keys from a list of dictionaries. + + This filter takes in input a list of dictionaries and for each of them + it will add the key to list_of_keys and returns it. + """ + list_of_keys = [] + for i in keys_to_list: + for k, v in i.items(): + list_of_keys.append(k) + return list_of_keys + + def list_or_dict_arg(self, data, cmd, key, arg): + """Utility to build a command and its argument with list or dict data. + + The key can be a dictionary or a list, the returned arguments will be + a list where each item is the argument name and the item data. + """ + if key not in data: + return + value = data[key] + if isinstance(value, dict): + for k, v in sorted(value.items()): + if v: + cmd.append('%s=%s=%s' % (arg, k, v)) + elif k: + cmd.append('%s=%s' % (arg, k)) + elif isinstance(value, list): + for v in value: + if v: + cmd.append('%s=%s' % (arg, v)) + + def container_exec_cmd(self, data, cli='podman'): + """Return a list of all the arguments to execute a container exec. + + This filter takes in input the container exec data and the cli name + to return the full command in a list of arguments that will be used + by Ansible command module. + """ + cmd = [cli, 'exec'] + cmd.append('--user=%s' % data.get('user', 'root')) + if 'privileged' in data: + cmd.append('--privileged=%s' % str(data['privileged']).lower()) + self.list_or_dict_arg(data, cmd, 'environment', '--env') + cmd.extend(data['command']) + return cmd diff --git a/tripleo_ansible/ansible_plugins/modules/podman_container.py b/tripleo_ansible/ansible_plugins/modules/podman_container.py index 4d3de6a17..f82fd74d4 100644 --- a/tripleo_ansible/ansible_plugins/modules/podman_container.py +++ b/tripleo_ansible/ansible_plugins/modules/podman_container.py @@ -21,9 +21,10 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type import json +from distutils.version import LooseVersion import yaml -from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_bytes, to_native ANSIBLE_METADATA = { @@ -101,7 +102,7 @@ options: (Not available for remote commands) You can also override the default path of the authentication file by setting the ``REGISTRY_AUTH_FILE`` environment variable. ``export REGISTRY_AUTH_FILE=path`` - type: str + type: path blkio_weight: description: - Block IO weight (relative weight) accepts a weight value between 10 and @@ -128,7 +129,22 @@ options: If the path is not absolute, the path is considered to be relative to the cgroups path of the init process. Cgroups will be created if they do not already exist. + type: path + cgroupns: + description: + - Path to cgroups under which the cgroup for the container will be + created. type: str + cgroups: + description: + - Determines whether the container will create CGroups. + Valid values are enabled and disabled, which the default being enabled. + The disabled option will force the container to not create CGroups, + and thus conflicts with CGroup options cgroupns and cgroup-parent. + type: str + choices: + - default + - disabled cidfile: description: - Write the container ID to the file @@ -154,9 +170,17 @@ options: description: - Limit the CPU real-time period in microseconds type: int + cpu_rt_period: + description: + - Limit the CPU real-time period in microseconds. + Limit the container's Real Time CPU usage. This flag tell the kernel to + restrict the container's Real Time CPU usage to the period you specify. + type: int cpu_rt_runtime: description: - - Limit the CPU real-time runtime in microseconds + - Limit the CPU real-time runtime in microseconds. + This flag tells the kernel to limit the amount of time in a given CPU + period Real Time tasks may consume. type: int cpu_shares: description: @@ -180,6 +204,11 @@ options: - Run container in detach mode type: bool default: True + debug: + description: + - Return additional information which can be helpful for investigations. + type: bool + default: False detach_keys: description: - Override the key sequence for detaching a container. Format is a single @@ -190,27 +219,28 @@ options: - Add a host device to the container. The format is [:][:] (e.g. device /dev/sdc:/dev/xvdc:rwm) - type: str + type: list + elements: str device_read_bps: description: - Limit read rate (bytes per second) from a device (e.g. device-read-bps /dev/sda:1mb) - type: str + type: list device_read_iops: description: - Limit read rate (IO per second) from a device (e.g. device-read-iops /dev/sda:1000) - type: str + type: list device_write_bps: description: - Limit write rate (bytes per second) to a device (e.g. device-write-bps /dev/sda:1mb) - type: str + type: list device_write_iops: description: - Limit write rate (IO per second) to a device (e.g. device-write-iops /dev/sda:1000) - type: str + type: list dns: description: - Set custom DNS servers @@ -240,6 +270,11 @@ options: description: - Read in a line delimited file of environment variables type: path + env_host: + description: + - Use all current host environment variables in container. + Defaults to false. + type: bool etc_hosts: description: - Dict of host-to-IP mappings, where each host name is a key in the @@ -271,7 +306,7 @@ options: group_add: description: - Add additional groups to run as - type: str + type: list healthcheck: description: - Set or alter a healthcheck command for a container. @@ -356,6 +391,7 @@ options: description: - Kernel memory limit (format [], where unit = b, k, m or g) + Note - idempotency is supported for integers only. type: str label: description: @@ -385,10 +421,12 @@ options: memory: description: - Memory limit (format 10k, where unit = b, k, m or g) + Note - idempotency is supported for integers only. type: str memory_reservation: description: - Memory soft limit (format 100m, where unit = b, k, m or g) + Note - idempotency is supported for integers only. type: str memory_swap: description: @@ -396,6 +434,7 @@ options: (--memory) flag. The swap LIMIT should always be larger than -m (--memory) value. By default, the swap LIMIT will be set to double the value of --memory + Note - idempotency is supported for integers only. type: str memory_swappiness: description: @@ -419,7 +458,8 @@ options: * ns: path to a network namespace to join * slirp4netns use slirp4netns to create a user network stack. This is the default for rootless containers - type: str + type: list + elements: str aliases: - net no_hosts: @@ -512,7 +552,8 @@ options: security_opt: description: - Security Options. For example security_opt "seccomp=unconfined" - type: str + type: list + elements: str shm_size: description: - Size of /dev/shm. The format is . number must be greater @@ -530,7 +571,7 @@ options: stop_signal: description: - Signal to stop a container. Default is SIGTERM. - type: str + type: int stop_timeout: description: - Timeout (in seconds) to stop a container. Default is 10. @@ -813,9 +854,12 @@ class PodmanModuleParams: params {dict} -- dictionary of module parameters """ - def __init__(self, action, params): + + def __init__(self, action, params, podman_version, module): self.params = params self.action = action + self.podman_version = podman_version + self.module = module def construct_command_from_params(self): """Create a podman command from given module parameters. @@ -828,8 +872,8 @@ class PodmanModuleParams: if self.action in ['create', 'run']: cmd = [self.action, '--name', self.params['name']] all_param_methods = [func for func in dir(self) - if callable(getattr(self, func)) and - func.startswith("addparam")] + if callable(getattr(self, func)) + and func.startswith("addparam")] params_set = (i for i in self.params if self.params[i] is not None) for param in params_set: func_name = "_".join(["addparam", param]) @@ -853,19 +897,26 @@ class PodmanModuleParams: cmd = ['rm', '-f', self.params['name']] return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] - def addparam_detach(self, c): - return c + ['--detach=%s' % self.params['detach']] - - def addparam_etc_hosts(self, c): - for host_ip in self.params['etc_hosts'].items(): - c += ['--add-host', ':'.join(host_ip)] - return c + def check_version(self, param, minv=None, maxv=None): + if minv and LooseVersion(minv) > LooseVersion( + self.podman_version): + self.module.fail_json(msg="Parameter %s is supported from podman " + "version %s only! Current version is %s" % ( + param, minv, self.podman_version)) + if maxv and LooseVersion(maxv) < LooseVersion( + self.podman_version): + self.module.fail_json(msg="Parameter %s is supported till podman " + "version %s only! Current version is %s" % ( + param, minv, self.podman_version)) def addparam_annotation(self, c): for annotate in self.params['annotation'].items(): c += ['--annotation', '='.join(annotate)] return c + def addparam_authfile(self, c): + return c + ['--authfile', self.params['authfile']] + def addparam_blkio_weight(self, c): return c + ['--blkio-weight', self.params['blkio_weight']] @@ -884,6 +935,14 @@ class PodmanModuleParams: c += ['--cap-drop', cap_drop] return c + def addparam_cgroups(self, c): + self.check_version('--cgroups', minv='1.6.0') + return c + ['--cgroups=%s' % self.params['cgroups']] + + def addparam_cgroupns(self, c): + self.check_version('--cgroupns', minv='1.6.2') + return c + ['--cgroupns=%s' % self.params['cgroupns']] + def addparam_cgroup_parent(self, c): return c + ['--cgroup-parent', self.params['cgroup_parent']] @@ -896,6 +955,9 @@ class PodmanModuleParams: def addparam_cpu_period(self, c): return c + ['--cpu-period', self.params['cpu_period']] + def addparam_cpu_rt_period(self, c): + return c + ['--cpu-rt-period', self.params['cpu_rt_period']] + def addparam_cpu_rt_runtime(self, c): return c + ['--cpu-rt-runtime', self.params['cpu_rt_runtime']] @@ -911,23 +973,36 @@ class PodmanModuleParams: def addparam_cpuset_mems(self, c): return c + ['--cpuset-mems', self.params['cpuset_mems']] + def addparam_detach(self, c): + return c + ['--detach=%s' % self.params['detach']] + def addparam_detach_keys(self, c): return c + ['--detach-keys', self.params['detach_keys']] def addparam_device(self, c): - return c + ['--device', self.params['device']] + for dev in self.params['device']: + c += ['--device', dev] + return c def addparam_device_read_bps(self, c): - return c + ['--device-read-bps', self.params['device_read_bps']] + for dev in self.params['device_read_bps']: + c += ['--device-read-bps', dev] + return c def addparam_device_read_iops(self, c): - return c + ['--device-read-iops', self.params['device_read_iops']] + for dev in self.params['device_read_iops']: + c += ['--device-read-iops', dev] + return c def addparam_device_write_bps(self, c): - return c + ['--device-write-bps', self.params['device_write_bps']] + for dev in self.params['device_write_bps']: + c += ['--device-write-bps', dev] + return c def addparam_device_write_iops(self, c): - return c + ['--device-write-iops', self.params['device_write_iops']] + for dev in self.params['device_write_iops']: + c += ['--device-write-iops', dev] + return c def addparam_dns(self, c): return c + ['--dns', ','.join(self.params['dns'])] @@ -951,6 +1026,15 @@ class PodmanModuleParams: def addparam_env_file(self, c): return c + ['--env-file', self.params['env_file']] + def addparam_env_host(self, c): + self.check_version('--env-host', minv='1.5.0') + return c + ['--env-host=%s' % self.params['env_host']] + + def addparam_etc_hosts(self, c): + for host_ip in self.params['etc_hosts'].items(): + c += ['--add-host', ':'.join(host_ip)] + return c + def addparam_expose(self, c): for exp in self.params['expose']: c += ['--expose', exp] @@ -960,7 +1044,9 @@ class PodmanModuleParams: return c + ['--gidmap', self.params['gidmap']] def addparam_group_add(self, c): - return c + ['--group-add', self.params['group_add']] + for g in self.params['group_add']: + c += ['--group-add', g] + return c def addparam_healthcheck(self, c): return c + ['--healthcheck', self.params['healthcheck']] @@ -1010,7 +1096,8 @@ class PodmanModuleParams: def addparam_label(self, c): for label in self.params['label'].items(): - c += ['--label', '='.join(label)] + c += ['--label', b'='.join([to_bytes(l, errors='surrogate_or_strict') + for l in label])] return c def addparam_label_file(self, c): @@ -1038,7 +1125,7 @@ class PodmanModuleParams: return c + ['--mount', self.params['mount']] def addparam_network(self, c): - return c + ['--network', self.params['network']] + return c + ['--network', ",".join(self.params['network'])] def addparam_no_hosts(self, c): return c + ['--no-hosts=%s' % self.params['no_hosts']] @@ -1085,7 +1172,9 @@ class PodmanModuleParams: return c + ['--rootfs=%s' % self.params['rootfs']] def addparam_security_opt(self, c): - return c + ['--security-opt', self.params['security_opt']] + for secopt in self.params['security_opt']: + c += ['--security-opt', secopt] + return c def addparam_shm_size(self, c): return c + ['--shm-size', self.params['shm_size']] @@ -1161,6 +1250,436 @@ class PodmanModuleParams: return c + self.params['cmd_args'] +class PodmanDefaults: + def __init__(self, module, podman_version): + self.module = module + self.version = podman_version + self.defaults = { + "blkio_weight": 0, + "cgroups": "default", + "cgroup_parent": "", + "cidfile": "", + "cpus": 0.0, + "cpu_shares": 0, + "cpu_quota": 0, + "cpu_period": 0, + "cpu_rt_runtime": 0, + "cpu_rt_period": 0, + "cpuset_cpus": "", + "cpuset_mems": "", + "detach": True, + "device": [], + "env_host": False, + "etc_hosts": {}, + "group_add": [], + "ipc": "", + "kernelmemory": "0", + "log_driver": "k8s-file", + "memory": "0", + "memory_swap": "0", + "memory_reservation": "0", + # "memory_swappiness": -1, + "no_hosts": False, + # libpod issue with networks in inspection + "network": ["default"], + "oom_score_adj": 0, + "pid": "", + "privileged": False, + "rm": False, + "security_opt": [], + "stop_signal": 15, + "tty": False, + "user": "", + "uts": "", + "volume": [], + "workdir": "/", + } + + def default_dict(self): + # make here any changes to self.defaults related to podman version + return self.defaults + + +class PodmanContainerDiff: + def __init__(self, module, info, podman_version): + self.module = module + self.version = podman_version + self.default_dict = None + self.info = yaml.safe_load(json.dumps(info).lower()) + self.params = self.defaultize() + self.diff = {'before': {}, 'after': {}} + self.non_idempotent = { + 'env_file', + 'env_host', + "ulimit", # Defaults depend on user and platform, impossible to guess + } + + def defaultize(self): + params_with_defaults = {} + self.default_dict = PodmanDefaults( + self.module, self.version).default_dict() + for p in self.module.params: + if self.module.params[p] is None and p in self.default_dict: + params_with_defaults[p] = self.default_dict[p] + else: + params_with_defaults[p] = self.module.params[p] + return params_with_defaults + + def _diff_update_and_compare(self, param_name, before, after): + if before != after: + self.diff['before'].update({param_name: before}) + self.diff['after'].update({param_name: after}) + return True + return False + + def diffparam_annotation(self): + before = self.info['config']['annotations'] or {} + after = before.copy() + if self.module.params['annotation'] is not None: + after.update(self.params['annotation']) + return self._diff_update_and_compare('annotation', before, after) + + def diffparam_env_host(self): + # It's impossible to get from inspest, recreate it if not default + before = False + after = self.params['env_host'] + return self._diff_update_and_compare('env_host', before, after) + + def diffparam_blkio_weight(self): + before = self.info['hostconfig']['blkioweight'] + after = self.params['blkio_weight'] + return self._diff_update_and_compare('blkio_weight', before, after) + + def diffparam_blkio_weight_device(self): + before = self.info['hostconfig']['blkioweightdevice'] + if before == [] and self.module.params['blkio_weight_device'] is None: + after = [] + else: + after = self.params['blkio_weight_device'] + return self._diff_update_and_compare('blkio_weight_device', before, after) + + def diffparam_cap_add(self): + before = self.info['effectivecaps'] or [] + after = [] + if self.module.params['cap_add'] is not None: + after += ["cap_" + i.lower() + for i in self.module.params['cap_add']] + after += before + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('cap_add', before, after) + + def diffparam_cap_drop(self): + before = self.info['effectivecaps'] or [] + after = before[:] + if self.module.params['cap_drop'] is not None: + for c in ["cap_" + i.lower() for i in self.module.params['cap_drop']]: + if c in after: + after.remove(c) + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('cap_drop', before, after) + + def diffparam_cgroup_parent(self): + before = self.info['hostconfig']['cgroupparent'] + after = self.params['cgroup_parent'] + return self._diff_update_and_compare('cgroup_parent', before, after) + + def diffparam_cgroups(self): + # Cgroups output is not supported in all versions + if 'cgroups' in self.info['hostconfig']: + before = self.info['hostconfig']['cgroups'] + after = self.params['cgroups'] + return self._diff_update_and_compare('cgroups', before, after) + return False + + def diffparam_cidfile(self): + before = self.info['hostconfig']['containeridfile'] + after = self.params['cidfile'] + return self._diff_update_and_compare('cidfile', before, after) + + def diffparam_command(self): + before = self.info['config']['cmd'] + after = self.params['command'] + if isinstance(after, str): + after = [i.lower() for i in after.split()] + elif isinstance(after, list): + after = [i.lower() for i in after] + return self._diff_update_and_compare('command', before, after) + + def diffparam_conmon_pidfile(self): + before = self.info['conmonpidfile'] + if self.module.params['conmon_pidfile'] is None: + after = before + else: + after = self.params['conmon_pidfile'] + return self._diff_update_and_compare('conmon_pidfile', before, after) + + def diffparam_cpu_period(self): + before = self.info['hostconfig']['cpuperiod'] + after = self.params['cpu_period'] + return self._diff_update_and_compare('cpu_period', before, after) + + def diffparam_cpu_rt_period(self): + before = self.info['hostconfig']['cpurealtimeperiod'] + after = self.params['cpu_rt_period'] + return self._diff_update_and_compare('cpu_rt_period', before, after) + + def diffparam_cpu_rt_runtime(self): + before = self.info['hostconfig']['cpurealtimeruntime'] + after = self.params['cpu_rt_runtime'] + return self._diff_update_and_compare('cpu_rt_runtime', before, after) + + def diffparam_cpu_shares(self): + before = self.info['hostconfig']['cpushares'] + after = self.params['cpu_shares'] + return self._diff_update_and_compare('cpu_shares', before, after) + + def diffparam_cpus(self): + before = int(self.info['hostconfig']['nanocpus']) / 1000000000 + after = self.params['cpus'] + return self._diff_update_and_compare('cpus', before, after) + + def diffparam_cpuset_cpus(self): + before = self.info['hostconfig']['cpusetcpus'] + after = self.params['cpuset_cpus'] + return self._diff_update_and_compare('cpuset_cpus', before, after) + + def diffparam_cpuset_mems(self): + before = self.info['hostconfig']['cpusetmems'] + after = self.params['cpuset_mems'] + return self._diff_update_and_compare('cpuset_mems', before, after) + + def diffparam_device(self): + before = [":".join([i['pathonhost'], i['pathincontainer']]) + for i in self.info['hostconfig']['devices']] + after = [":".join(i.split(":")[:2]) for i in self.params['device']] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('devices', before, after) + + def diffparam_device_read_bps(self): + before = self.info['hostconfig']['blkiodevicereadbps'] or [] + before = ["%s:%s" % (i['path'], i['rate']) for i in before] + after = self.params['device_read_bps'] or [] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('device_read_bps', before, after) + + def diffparam_device_read_iops(self): + before = self.info['hostconfig']['blkiodevicereadiops'] or [] + before = ["%s:%s" % (i['path'], i['rate']) for i in before] + after = self.params['device_read_iops'] or [] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('device_read_iops', before, after) + + def diffparam_device_write_bps(self): + before = self.info['hostconfig']['blkiodevicewritebps'] or [] + before = ["%s:%s" % (i['path'], i['rate']) for i in before] + after = self.params['device_write_bps'] or [] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('device_write_bps', before, after) + + def diffparam_device_write_iops(self): + before = self.info['hostconfig']['blkiodevicewriteiops'] or [] + before = ["%s:%s" % (i['path'], i['rate']) for i in before] + after = self.params['device_write_iops'] or [] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('device_write_iops', before, after) + + # Limited idempotency, it can't guess default values + def diffparam_env(self): + env_before = self.info['config']['env'] or {} + before = {i.split("=")[0]: i.split("=")[1] for i in env_before} + after = before.copy() + if self.params['env']: + after.update({ + str(k).lower(): str(v).lower() + for k, v in self.params['env'].items() + }) + return self._diff_update_and_compare('env', before, after) + + def diffparam_etc_hosts(self): + if self.info['hostconfig']['extrahosts']: + before = dict([i.split(":") for i in self.info['hostconfig']['extrahosts']]) + else: + before = {} + after = self.params['etc_hosts'] + return self._diff_update_and_compare('etc_hosts', before, after) + + def diffparam_group_add(self): + before = self.info['hostconfig']['groupadd'] + after = self.params['group_add'] + return self._diff_update_and_compare('group_add', before, after) + + # Because of hostname is random generated, this parameter has partial idempotency only. + def diffparam_hostname(self): + before = self.info['config']['hostname'] + after = self.params['hostname'] or before + return self._diff_update_and_compare('hostname', before, after) + + def diffparam_image(self): + before = self.info['config']['image'].replace( + "docker.io/library/", "").replace( + "docker.io/", "").replace( + ":latest", "") + after = self.params['image'].replace( + "docker.io/library/", "").replace( + "docker.io/", "").replace( + ":latest", "") + return self._diff_update_and_compare('image', before, after) + + def diffparam_ipc(self): + before = self.info['hostconfig']['ipcmode'] + after = self.params['ipc'] + return self._diff_update_and_compare('ipc', before, after) + + def diffparam_label(self): + before = self.info['config']['labels'] or {} + after = before.copy() + if self.params['label']: + after.update({ + str(k).lower(): str(v).lower() + for k, v in self.params['label'].items() + }) + return self._diff_update_and_compare('label', before, after) + + def diffparam_log_driver(self): + before = self.info['hostconfig']['logconfig']['type'] + after = self.params['log_driver'] + return self._diff_update_and_compare('log_driver', before, after) + + # Parameter has limited idempotency, unable to guess the default log_path + def diffparam_log_opt(self): + before = self.info['logpath'] + if self.module.params['log_opt'] in [None, '']: + after = before + else: + after = self.params['log_opt'].split("=")[1] + return self._diff_update_and_compare('log_opt', before, after) + + def diffparam_memory(self): + before = str(self.info['hostconfig']['memory']) + after = self.params['memory'] + return self._diff_update_and_compare('memory', before, after) + + def diffparam_memory_swap(self): + # By default it's twice memory parameter + before = str(self.info['hostconfig']['memoryswap']) + after = self.params['memory_swap'] + if (self.module.params['memory_swap'] is None + and self.params['memory'] != 0 + and self.params['memory'].isdigit()): + after = str(int(self.params['memory']) * 2) + return self._diff_update_and_compare('memory_swap', before, after) + + def diffparam_memory_reservation(self): + before = str(self.info['hostconfig']['memoryreservation']) + after = self.params['memory_reservation'] + return self._diff_update_and_compare('memory_reservation', before, after) + + def diffparam_network(self): + before = [self.info['hostconfig']['networkmode']] + after = self.params['network'] + return self._diff_update_and_compare('network', before, after) + + def diffparam_no_hosts(self): + before = not bool(self.info['hostspath']) + after = self.params['no_hosts'] + if self.params['network'] == ['none']: + after = True + return self._diff_update_and_compare('no_hosts', before, after) + + def diffparam_oom_score_adj(self): + before = self.info['hostconfig']['oomscoreadj'] + after = self.params['oom_score_adj'] + return self._diff_update_and_compare('oom_score_adj', before, after) + + def diffparam_privileged(self): + before = self.info['hostconfig']['privileged'] + after = self.params['privileged'] + return self._diff_update_and_compare('privileged', before, after) + + def diffparam_pid(self): + before = self.info['hostconfig']['pidmode'] + after = self.params['pid'] + return self._diff_update_and_compare('pid', before, after) + + def diffparam_rm(self): + before = self.info['hostconfig']['autoremove'] + after = self.params['rm'] + return self._diff_update_and_compare('rm', before, after) + + def diffparam_security_opt(self): + before = self.info['hostconfig']['securityopt'] + after = self.params['security_opt'] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('security_opt', before, after) + + def diffparam_stop_signal(self): + before = self.info['config']['stopsignal'] + after = self.params['stop_signal'] + return self._diff_update_and_compare('stop_signal', before, after) + + def diffparam_tty(self): + before = self.info['config']['tty'] + after = self.params['tty'] + return self._diff_update_and_compare('tty', before, after) + + def diffparam_user(self): + before = self.info['config']['user'] + if self.module.params['user'] is None and before: + after = before + else: + after = self.params['user'] + return self._diff_update_and_compare('user', before, after) + + def diffparam_uts(self): + before = self.info['hostconfig']['utsmode'] + after = self.params['uts'] + return self._diff_update_and_compare('uts', before, after) + + def diffparam_volume(self): + before = self.info['mounts'] + if before: + volumes = [] + for m in before: + if m['type'] == 'volume': + volumes.append([m['name'], m['destination']]) + else: + volumes.append([m['source'], m['destination']]) + before = [":".join(v) for v in volumes] + # Ignore volumes option for idempotency + after = [":".join(v.split(":")[:2]) for v in self.params['volume']] + before, after = sorted(list(set(before))), sorted(list(set(after))) + return self._diff_update_and_compare('volume', before, after) + + def diffparam_volumes_from(self): + before = self.info['hostconfig']['volumesfrom'] or [] + after = self.params['volumes_from'] or [] + return self._diff_update_and_compare('volumes_from', before, after) + + def diffparam_workdir(self): + before = self.info['config']['workingdir'] + after = self.params['workdir'] + return self._diff_update_and_compare('workdir', before, after) + + def is_different(self): + diff_func_list = [func for func in dir(self) + if callable(getattr(self, func)) and func.startswith( + "diffparam")] + fail_fast = not bool(self.module._diff) + different = False + for func_name in diff_func_list: + dff_func = getattr(self, func_name) + if dff_func(): + if fail_fast: + return True + else: + different = True + # Check non idempotent parameters + for p in self.non_idempotent: + if self.module.params[p] is not None and self.module.params[p] not in [{}, [], '']: + different = True + return different + + def ensure_image_exists(module, image): """If image is passed, ensure it exists, if not - pull it or fail. @@ -1205,6 +1724,9 @@ class PodmanContainer: self.name = name self.stdout, self.stderr = '', '' self.info = self.get_info() + self.version = self._get_podman_version() + self.diff = {} + self.actions = [] @property def exists(self): @@ -1214,9 +1736,17 @@ class PodmanContainer: @property def different(self): """Check if container is different.""" - # TODO(sshnaidm): implement difference calculation between input vars - # and current container to understand if we need to recreate it - return True + diffcheck = PodmanContainerDiff(self.module, self.info, self.version) + is_different = diffcheck.is_different() + diffs = diffcheck.diff + if self.module._diff and is_different and diffs['before'] and diffs['after']: + self.diff['before'] = "\n".join( + ["%s - %s" % (k, v) for k, v in sorted( + diffs['before'].items())]) + "\n" + self.diff['after'] = "\n".join( + ["%s - %s" % (k, v) for k, v in sorted( + diffs['after'].items())]) + "\n" + return is_different @property def running(self): @@ -1234,6 +1764,13 @@ class PodmanContainer: [self.module.params['executable'], b'container', b'inspect', self.name]) return json.loads(out)[0] if rc == 0 else {} + def _get_podman_version(self): + rc, out, err = self.module.run_command( + [self.module.params['executable'], b'--version']) + if rc != 0 or not out or "version" not in out: + self.module.fail_json(msg="%s run failed!" % self.module.params['executable']) + return out.split("version")[1].strip() + def _perform_action(self, action): """Perform action with container. @@ -1241,19 +1778,25 @@ class PodmanContainer: action {str} -- action to perform - start, create, stop, run, delete """ - b_command = PodmanModuleParams(action, self.module.params + b_command = PodmanModuleParams(action, + self.module.params, + self.version, + self.module, ).construct_command_from_params() - self.module.log("PODMAN-CONTAINER-DEBUG: " + - "%s" % " ".join([to_native(i) for i in b_command])) - rc, out, err = self.module.run_command( - [self.module.params['executable'], b'container'] + b_command, - expand_user_and_vars=False) - self.stdout = out - self.stderr = err - if rc != 0: - self.module.fail_json( - msg="Can't %s container %s" % (action, self.name), - stdout=out, stderr=err) + full_cmd = " ".join([self.module.params['executable']] + + [to_native(i) for i in b_command]) + self.module.log("PODMAN-CONTAINER-DEBUG: %s" % full_cmd) + self.actions.append(full_cmd) + if not self.module.check_mode: + rc, out, err = self.module.run_command( + [self.module.params['executable'], b'container'] + b_command, + expand_user_and_vars=False) + self.stdout = out + self.stderr = err + if rc != 0: + self.module.fail_json( + msg="Can't %s container %s" % (action, self.name), + stdout=out, stderr=err) def run(self): """Run the container.""" @@ -1326,11 +1869,15 @@ class PodmanManager: changed {bool} -- whether any action was performed (default: {True}) """ - facts = self.container.get_info() + facts = self.container.get_info() if changed else self.container.info out, err = self.container.stdout, self.container.stderr self.results.update({'changed': changed, 'container': facts, - 'ansible_facts': {'podman_container': facts}}, - stdout=out, stderr=err) + 'podman_actions': self.container.actions}, + stdout=out, stderr=err) + if self.container.diff: + self.results.update({'diff': self.container.diff}) + if self.module.params['debug']: + self.results.update({'podman_version': self.container.version}) self.module.exit_json(**self.results) def make_started(self): @@ -1347,7 +1894,7 @@ class PodmanManager: self.results['actions'].append('restarted %s' % self.container.name) self.update_container_result() - self.module.exit_json(**self.results) + self.update_container_result(changed=False) elif not self.container.exists: self.container.run() self.results['actions'].append('started %s' % self.container.name) @@ -1365,7 +1912,7 @@ class PodmanManager: def make_stopped(self): """Run actions if desired state is 'stopped'.""" if not self.container.exists and not self.image: - self.module.fail_json(msg='Cannot create container when image' + + self.module.fail_json(msg='Cannot create container when image' ' is not specified!') if not self.container.exists: self.container.create() @@ -1387,7 +1934,7 @@ class PodmanManager: self.results['actions'].append('deleted %s' % self.container.name) self.results.update({'changed': True}) self.results.update({'container': {}, - 'ansible_facts': {'podman_container': {}}}) + 'podman_actions': self.container.actions}) self.module.exit_json(**self.results) def execute(self): @@ -1400,7 +1947,7 @@ class PodmanManager: } process_action = states_map[self.state] process_action() - self.module.fail_json(msg="Unexpected logic error happened, " + + self.module.fail_json(msg="Unexpected logic error happened, " "please contact maintainers ASAP!") @@ -1411,6 +1958,7 @@ def main(): ['no_hosts', 'etc_hosts'], ), + supports_check_mode=True, ) # work on input vars if module.params['state'] in ['started', 'present'] and \ diff --git a/tripleo_ansible/ansible_plugins/modules/podman_container_info.py b/tripleo_ansible/ansible_plugins/modules/podman_container_info.py index 257844a02..0e63e15af 100644 --- a/tripleo_ansible/ansible_plugins/modules/podman_container_info.py +++ b/tripleo_ansible/ansible_plugins/modules/podman_container_info.py @@ -13,54 +13,43 @@ # License for the specific language governing permissions and limitations # under the License. -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function +__metaclass__ = type -import json -import yaml - -from ansible.module_utils.basic import AnsibleModule -import ansible.module_utils.six as six - -six.add_metaclass(type) ANSIBLE_METADATA = { - 'metadata_version': '1.0', + 'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community' } DOCUMENTATION = """ ---- module: podman_container_info author: - Sagi Shnaidman (@sshnaidm) - Emilien Macchi (@EmilienM) -version_added: '2.9' +version_added: '2.10' short_description: Gather facts about containers using podman notes: - - Podman may required elevated privileges in order to run properly. + - Podman may require elevated privileges in order to run properly. description: - Gather facts about containers using C(podman) requirements: - "Podman installed on host" options: - executable: - description: - - Path to C(podman) executable if it is not in the C($PATH) on the - machine running C(podman) - default: 'podman' - type: str - name: - description: - - List of tags or UID to gather facts about. If no name is given - return facts about all containers. - type: list - elements: str - + name: + description: + - List of container names to gather facts about. If no name is given + return facts about all containers. + type: list + elements: str + executable: + description: + - Path to C(podman) executable if it is not in the C($PATH) on the + machine running C(podman) + default: 'podman' + type: str """ - EXAMPLES = """ - name: Gather facts for all containers podman_container_info: @@ -75,273 +64,274 @@ EXAMPLES = """ - redis - web1 """ - RETURN = """ containers: description: Facts from all or specificed containers returned: always - type: dict + type: list sample: [ - { - "Id": "c5c39f9b80a6ea2ad665aa9946435934e478a0c5322da835f3883872f", - "Created": "2019-10-01T12:51:00.233106443Z", - "Path": "dumb-init", - "Args": [ - "--single-child", - "--", - "kolla_start" - ], - "State": { - "OciVersion": "1.0.1-dev", - "Status": "configured", - "Running": false, - "Paused": false, - "Restarting": false, - "OOMKilled": false, - "Dead": false, - "Pid": 0, - "ExitCode": 0, - "Error": "", - "StartedAt": "0001-01-01T00:00:00Z", - "FinishedAt": "0001-01-01T00:00:00Z", - "Healthcheck": { - "Status": "", - "FailingStreak": 0, - "Log": null - } - }, - "Image": "0e267acda67d0ebd643e900d820a91b961d859743039e620191ca1", - "ImageName": "docker.io/tripleomaster/centos-haproxy:latest", - "Rootfs": "", - "Pod": "", - "ResolvConfPath": "", - "HostnamePath": "", - "HostsPath": "", - "OCIRuntime": "runc", - "Name": "haproxy", - "RestartCount": 0, - "Driver": "overlay", - "MountLabel": "system_u:object_r:svirt_sandbox_file_t:s0:c78,c866", - "ProcessLabel": "system_u:system_r:svirt_lxc_net_t:s0:c785,c866", - "AppArmorProfile": "", - "EffectiveCaps": [ - "CAP_CHOWN", - "CAP_DAC_OVERRIDE", - "CAP_FSETID", - "CAP_FOWNER", - "CAP_MKNOD", - "CAP_NET_RAW", - "CAP_SETGID", - "CAP_SETUID", - "CAP_SETFCAP", - "CAP_SETPCAP", - "CAP_NET_BIND_SERVICE", - "CAP_SYS_CHROOT", - "CAP_KILL", - "CAP_AUDIT_WRITE" - ], - "BoundingCaps": [ - "CAP_CHOWN", - "CAP_DAC_OVERRIDE", - "CAP_FSETID", - "CAP_FOWNER", - "CAP_MKNOD", - "CAP_NET_RAW", - "CAP_SETGID", - "CAP_SETUID", - "CAP_SETFCAP", - "CAP_SETPCAP", - "CAP_NET_BIND_SERVICE", - "CAP_SYS_CHROOT", - "CAP_KILL", - "CAP_AUDIT_WRITE" - ], - "ExecIDs": [], - "GraphDriver": { - "Name": "overlay", - } - }, - "Mounts": [], - "Dependencies": [], - "NetworkSettings": { - "Bridge": "", - "SandboxID": "", - "HairpinMode": false, - "LinkLocalIPv6Address": "", - "LinkLocalIPv6PrefixLen": 0, - "Ports": [], - "SandboxKey": "", - "SecondaryIPAddresses": null, - "SecondaryIPv6Addresses": null, - "EndpointID": "", - "Gateway": "", - "GlobalIPv6Address": "", - "GlobalIPv6PrefixLen": 0, - "IPAddress": "", - "IPPrefixLen": 0, - "IPv6Gateway": "", - "MacAddress": "" - }, - "ExitCommand": [ - "/usr/bin/podman", - "--root", - "/var/lib/containers/storage", - "--runroot", - "/var/run/containers/storage", - "--log-level", - "error", - "--cgroup-manager", - "systemd", - "--tmpdir", - "/var/run/libpod", - "--runtime", - "runc", - "--storage-driver", - "overlay", - "--events-backend", - "journald", - "container", - "cleanup", - "c9e813703f9b80a6ea2ad665aa9946435934e478a0c5322da835f3883872f" - ], - "Namespace": "", - "IsInfra": false, - "Config": { - "Hostname": "c5c39e813703", - "Domainname": "", - "User": "", - "AttachStdin": false, - "AttachStdout": false, - "AttachStderr": false, - "Tty": false, - "OpenStdin": false, - "StdinOnce": false, - "Env": [ - "PATH=/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", - "TERM=xterm", - "HOSTNAME=", - "container=oci", - "KOLLA_INSTALL_METATYPE=rdo", - "KOLLA_BASE_DISTRO=centos", - "KOLLA_INSTALL_TYPE=binary", - "KOLLA_DISTRO_PYTHON_VERSION=2.7", - "KOLLA_BASE_ARCH=x86_64" - ], - "Cmd": [ + { + "Id": "c5c39f9b80a6ea2ad665aa9946435934e478a0c5322da835f3883872f", + "Created": "2019-10-01T12:51:00.233106443Z", + "Path": "dumb-init", + "Args": [ + "--single-child", + "--", "kolla_start" ], - "Image": "docker.io/tripleomaster/centos-haproxy:latest", - "Volumes": null, - "WorkingDir": "/", - "Entrypoint": "dumb-init --single-child --", - "OnBuild": null, - "Labels": { - "build-date": "20190919", - "kolla_version": "8.1.0", - "name": "haproxy", - "org.label-schema.build-date": "20190801", - "org.label-schema.license": "GPLv2", - "org.label-schema.name": "CentOS Base Image", - "org.label-schema.schema-version": "1.0", - "org.label-schema.vendor": "CentOS" - }, - "Annotations": { - "io.kubernetes.cri-o.ContainerType": "sandbox", - "io.kubernetes.cri-o.TTY": "false", - "io.podman.annotations.autoremove": "FALSE", - "io.podman.annotations.init": "FALSE", - "io.podman.annotations.privileged": "FALSE", - "io.podman.annotations.publish-all": "FALSE" - }, - "StopSignal": 15 - }, - "HostConfig": { - "Binds": [], - "ContainerIDFile": "", - "LogConfig": { - "Type": "k8s-file", - "Config": null - }, - "NetworkMode": "default", - "PortBindings": {}, - "RestartPolicy": { - "Name": "", - "MaximumRetryCount": 0 - }, - "AutoRemove": false, - "VolumeDriver": "", - "VolumesFrom": null, - "CapAdd": [], - "CapDrop": [], - "Dns": [], - "DnsOptions": [], - "DnsSearch": [], - "ExtraHosts": [], - "GroupAdd": [], - "IpcMode": "", - "Cgroup": "", - "Links": null, - "OomScoreAdj": 0, - "PidMode": "", - "Privileged": false, - "PublishAllPorts": false, - "ReadonlyRootfs": false, - "SecurityOpt": [], - "Tmpfs": {}, - "UTSMode": "", - "UsernsMode": "", - "ShmSize": 65536000, - "Runtime": "oci", - "ConsoleSize": [ - 0, - 0 - ], - "Isolation": "", - "CpuShares": 0, - "Memory": 0, - "NanoCpus": 0, - "CgroupParent": "", - "BlkioWeight": 0, - "BlkioWeightDevice": null, - "BlkioDeviceReadBps": null, - "BlkioDeviceWriteBps": null, - "BlkioDeviceReadIOps": null, - "BlkioDeviceWriteIOps": null, - "CpuPeriod": 0, - "CpuQuota": 0, - "CpuRealtimePeriod": 0, - "CpuRealtimeRuntime": 0, - "CpusetCpus": "", - "CpusetMems": "", - "Devices": [], - "DiskQuota": 0, - "KernelMemory": 0, - "MemoryReservation": 0, - "MemorySwap": 0, - "MemorySwappiness": -1, - "OomKillDisable": false, - "PidsLimit": 0, - "Ulimits": [ - { - "Name": "RLIMIT_NOFILE", - "Soft": 1048576, - "Hard": 1048576 - }, - { - "Name": "RLIMIT_NPROC", - "Soft": 1048576, - "Hard": 1048576 + "State": { + "OciVersion": "1.0.1-dev", + "Status": "configured", + "Running": false, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 0, + "ExitCode": 0, + "Error": "", + "StartedAt": "0001-01-01T00:00:00Z", + "FinishedAt": "0001-01-01T00:00:00Z", + "Healthcheck": { + "Status": "", + "FailingStreak": 0, + "Log": null } + }, + "Image": "0e267acda67d0ebd643e900d820a91b961d859743039e620191ca1", + "ImageName": "docker.io/tripleomaster/centos-haproxy:latest", + "Rootfs": "", + "Pod": "", + "ResolvConfPath": "", + "HostnamePath": "", + "HostsPath": "", + "OCIRuntime": "runc", + "Name": "haproxy", + "RestartCount": 0, + "Driver": "overlay", + "MountLabel": "system_u:object_r:svirt_sandbox_file_t:s0:c78,c866", + "ProcessLabel": "system_u:system_r:svirt_lxc_net_t:s0:c785,c866", + "AppArmorProfile": "", + "EffectiveCaps": [ + "CAP_CHOWN", + "CAP_DAC_OVERRIDE", + "CAP_FSETID", + "CAP_FOWNER", + "CAP_MKNOD", + "CAP_NET_RAW", + "CAP_SETGID", + "CAP_SETUID", + "CAP_SETFCAP", + "CAP_SETPCAP", + "CAP_NET_BIND_SERVICE", + "CAP_SYS_CHROOT", + "CAP_KILL", + "CAP_AUDIT_WRITE" ], - "CpuCount": 0, - "CpuPercent": 0, - "IOMaximumIOps": 0, - "IOMaximumBandwidth": 0 - } - } - ] + "BoundingCaps": [ + "CAP_CHOWN", + "CAP_DAC_OVERRIDE", + "CAP_FSETID", + "CAP_FOWNER", + "CAP_MKNOD", + "CAP_NET_RAW", + "CAP_SETGID", + "CAP_SETUID", + "CAP_SETFCAP", + "CAP_SETPCAP", + "CAP_NET_BIND_SERVICE", + "CAP_SYS_CHROOT", + "CAP_KILL", + "CAP_AUDIT_WRITE" + ], + "ExecIDs": [], + "GraphDriver": { + "Name": "overlay" + }, + "Mounts": [], + "Dependencies": [], + "NetworkSettings": { + "Bridge": "", + "SandboxID": "", + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "Ports": [], + "SandboxKey": "", + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "", + "Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "MacAddress": "" + }, + "ExitCommand": [ + "/usr/bin/podman", + "--root", + "/var/lib/containers/storage", + "--runroot", + "/var/run/containers/storage", + "--log-level", + "error", + "--cgroup-manager", + "systemd", + "--tmpdir", + "/var/run/libpod", + "--runtime", + "runc", + "--storage-driver", + "overlay", + "--events-backend", + "journald", + "container", + "cleanup", + "c9e813703f9b80a6ea2ad665aa9946435934e478a0c5322da835f3883872f" + ], + "Namespace": "", + "IsInfra": false, + "Config": { + "Hostname": "c5c39e813703", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "TERM=xterm", + "HOSTNAME=", + "container=oci", + "KOLLA_INSTALL_METATYPE=rdo", + "KOLLA_BASE_DISTRO=centos", + "KOLLA_INSTALL_TYPE=binary", + "KOLLA_DISTRO_PYTHON_VERSION=2.7", + "KOLLA_BASE_ARCH=x86_64" + ], + "Cmd": [ + "kolla_start" + ], + "Image": "docker.io/tripleomaster/centos-haproxy:latest", + "Volumes": null, + "WorkingDir": "/", + "Entrypoint": "dumb-init --single-child --", + "OnBuild": null, + "Labels": { + "build-date": "20190919", + "kolla_version": "8.1.0", + "name": "haproxy", + "org.label-schema.build-date": "20190801", + "org.label-schema.license": "GPLv2", + "org.label-schema.name": "CentOS Base Image", + "org.label-schema.schema-version": "1.0", + "org.label-schema.vendor": "CentOS" + }, + "Annotations": { + "io.kubernetes.cri-o.ContainerType": "sandbox", + "io.kubernetes.cri-o.TTY": "false", + "io.podman.annotations.autoremove": "FALSE", + "io.podman.annotations.init": "FALSE", + "io.podman.annotations.privileged": "FALSE", + "io.podman.annotations.publish-all": "FALSE" + }, + "StopSignal": 15 + }, + "HostConfig": { + "Binds": [], + "ContainerIDFile": "", + "LogConfig": { + "Type": "k8s-file", + "Config": null + }, + "NetworkMode": "default", + "PortBindings": {}, + "RestartPolicy": { + "Name": "", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "CapAdd": [], + "CapDrop": [], + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": [], + "GroupAdd": [], + "IpcMode": "", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 0, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": [], + "Tmpfs": {}, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 65536000, + "Runtime": "oci", + "ConsoleSize": [ + 0, + 0 + ], + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": null, + "BlkioDeviceReadBps": null, + "BlkioDeviceWriteBps": null, + "BlkioDeviceReadIOps": null, + "BlkioDeviceWriteIOps": null, + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": [], + "DiskQuota": 0, + "KernelMemory": 0, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": -1, + "OomKillDisable": false, + "PidsLimit": 0, + "Ulimits": [ + { + "Name": "RLIMIT_NOFILE", + "Soft": 1048576, + "Hard": 1048576 + }, + { + "Name": "RLIMIT_NPROC", + "Soft": 1048576, + "Hard": 1048576 + } + ], + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0 + } + } + ] """ +import json +from ansible.module_utils.basic import AnsibleModule + def get_containers_facts(module, executable, name): if not name: @@ -353,30 +343,32 @@ def get_containers_facts(module, executable, name): command = [executable, 'container', 'inspect'] command.extend(name) rc, out, err = module.run_command(command) - if not out or rc != 0: + if rc != 0: + module.fail_json(msg="Unable to gather info for %s: %s" % (name or 'all containers', err)) + if not out or json.loads(out) is None: return [], out, err return json.loads(out), out, err def main(): module = AnsibleModule( - argument_spec=yaml.safe_load(DOCUMENTATION)['options'], + argument_spec=dict( + executable=dict(type='str', default='podman'), + name=dict(type='list', elements='str') + ), supports_check_mode=True, ) - executable = module.params['executable'] - name = module.params.get('name') - executable = module.get_bin_path(executable, required=True) + name = module.params['name'] + executable = module.get_bin_path(module.params['executable'], required=True) inspect_results, out, err = get_containers_facts(module, executable, name) - results = dict( - changed=False, - containers=inspect_results, - ansible_facts=dict(podman_containers=inspect_results), - stdout=out, - stderr=err - ) + results = { + "changed": False, + "containers": inspect_results, + "stderr": err + } module.exit_json(**results) diff --git a/tripleo_ansible/ansible_plugins/modules/podman_volume_info.py b/tripleo_ansible/ansible_plugins/modules/podman_volume_info.py index 943043b4c..71176dd30 100644 --- a/tripleo_ansible/ansible_plugins/modules/podman_volume_info.py +++ b/tripleo_ansible/ansible_plugins/modules/podman_volume_info.py @@ -15,17 +15,11 @@ # License for the specific language governing permissions and limitations # under the License. - from __future__ import absolute_import, division, print_function - -import json -import yaml - -from ansible.module_utils.basic import AnsibleModule - +__metaclass__ = type ANSIBLE_METADATA = { - 'metadata_version': '1.0', + 'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community' } @@ -34,7 +28,7 @@ DOCUMENTATION = """ module: podman_volume_info author: - "Sagi Shnaidman (@sshnaidm)" -version_added: '2.9' +version_added: '2.10' short_description: Gather info about podman volumes notes: [] description: @@ -65,21 +59,22 @@ RETURN = """ volumes: description: Facts from all or specified volumes returned: always - type: dict - sample: - [ - { - "name": "testvolume", - "labels": {}, - "mountPoint": "/home/ansible/.local/share/testvolume/_data", - "driver": "local", - "options": {}, - "scope": "local" - } - ] - + type: list + sample: [ + { + "name": "testvolume", + "labels": {}, + "mountPoint": "/home/ansible/.local/share/testvolume/_data", + "driver": "local", + "options": {}, + "scope": "local" + } + ] """ +import json +from ansible.module_utils.basic import AnsibleModule + def get_volume_info(module, executable, name): command = [executable, 'volume', 'inspect'] @@ -88,31 +83,32 @@ def get_volume_info(module, executable, name): else: command.append("--all") rc, out, err = module.run_command(command) - if not out or rc != 0: + if rc != 0 or 'no such volume' in err: + module.fail_json(msg="Unable to gather info for %s: %s" % (name or 'all volumes', err)) + if not out or json.loads(out) is None: return [], out, err return json.loads(out), out, err def main(): module = AnsibleModule( - argument_spec=yaml.safe_load(DOCUMENTATION)['options'], + argument_spec=dict( + executable=dict(type='str', default='podman'), + name=dict(type='str') + ), supports_check_mode=True, ) - executable = module.params['executable'] name = module.params['name'] - executable = module.get_bin_path(executable, required=True) + executable = module.get_bin_path(module.params['executable'], required=True) inspect_results, out, err = get_volume_info(module, executable, name) - results = dict( - changed=False, - volume=inspect_results, - stdout=out, - stderr=err - ) - if name: - results.update({"exists": bool(inspect_results)}) + results = { + "changed": False, + "volumes": inspect_results, + "stderr": err + } module.exit_json(**results) diff --git a/tripleo_ansible/ansible_plugins/tests/.yamllint b/tripleo_ansible/ansible_plugins/tests/.yamllint new file mode 100644 index 000000000..882767605 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/tests/.yamllint @@ -0,0 +1,33 @@ +--- +# Based on ansible-lint config +extends: default + +rules: + braces: + max-spaces-inside: 1 + level: error + brackets: + max-spaces-inside: 1 + level: error + colons: + max-spaces-after: -1 + level: error + commas: + max-spaces-after: -1 + level: error + comments: disable + comments-indentation: disable + document-start: disable + empty-lines: + max: 3 + level: error + hyphens: + level: error + indentation: disable + key-duplicates: enable + line-length: disable + new-line-at-end-of-file: disable + new-lines: + type: unix + trailing-spaces: disable + truthy: disable diff --git a/tripleo_ansible/ansible_plugins/tests/molecule/podman_container/molecule.yml b/tripleo_ansible/ansible_plugins/tests/molecule/podman_container/molecule.yml new file mode 100644 index 000000000..bea14b986 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/tests/molecule/podman_container/molecule.yml @@ -0,0 +1,56 @@ +--- +driver: + name: delegated + options: + managed: false + login_cmd_template: >- + ssh + -o UserKnownHostsFile=/dev/null + -o StrictHostKeyChecking=no + -o Compression=no + -o TCPKeepAlive=yes + -o VerifyHostKeyDNS=no + -o ForwardX11=no + -o ForwardAgent=no + {instance} + ansible_connection_options: + ansible_connection: ssh + +log: true + +platforms: + - name: instance + +provisioner: + name: ansible + config_options: + defaults: + fact_caching: jsonfile + fact_caching_connection: /tmp/molecule/facts + inventory: + hosts: + all: + hosts: + instance: + ansible_host: localhost + log: true + env: + ANSIBLE_STDOUT_CALLBACK: yaml + ANSIBLE_ROLES_PATH: "${ANSIBLE_ROLES_PATH:-/usr/share/ansible/roles}:${HOME}/zuul-jobs/roles" + ANSIBLE_LIBRARY: "${ANSIBLE_LIBRARY:-/usr/share/ansible/plugins/modules}" + ANSIBLE_FILTER_PLUGINS: "${ANSIBLE_FILTER_PLUGINS:-/usr/share/ansible/plugins/filter}" + +scenario: + name: podman_container + test_sequence: + - prepare + - converge + - verify + +lint: + enabled: false + +verifier: + name: testinfra + lint: + name: flake8 diff --git a/tripleo_ansible/ansible_plugins/tests/molecule/podman_container/playbook.yml b/tripleo_ansible/ansible_plugins/tests/molecule/podman_container/playbook.yml new file mode 100644 index 000000000..50344dc65 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/tests/molecule/podman_container/playbook.yml @@ -0,0 +1,393 @@ +--- +- name: Converge + hosts: all + tasks: + - name: Test podman_container + become: true + block: + - name: Delete all container leftovers from tests + podman_container: + name: "{{ item }}" + state: absent + loop: + - "alpine:3.7" + - "container" + - "container2" + + - name: Test no image with default action + podman_container: + name: container + ignore_errors: true + register: no_image + + - name: Test no image with state 'started' + podman_container: + name: container + state: started + ignore_errors: true + register: no_image1 + + - name: Test no image with state 'present' + podman_container: + name: container + state: present + ignore_errors: true + register: no_image2 + + - name: Check no image + assert: + that: + - no_image is failed + - no_image1 is failed + - no_image2 is failed + - no_image.msg == "State 'started' required image to be configured!" + - no_image1.msg == "State 'started' required image to be configured!" + - no_image2.msg == "State 'present' required image to be configured!" + fail_msg: No image test failed! + success_msg: No image test passed! + + - name: Ensure image doesn't exist + podman_image: + name: alpine:3.7 + state: absent + + - name: Check pulling image + podman_container: + name: container + image: alpine:3.7 + state: present + command: sleep 1d + register: image + + - name: Check using already pulled image + podman_container: + name: container2 + image: alpine:3.7 + state: present + command: sleep 1d + register: image2 + + - name: Check output is correct + assert: + that: + - image is changed + - image.container is defined + - image.container['State']['Running'] + - "'pulled image alpine:3.7' in image.actions" + - "'started container' in image.actions" + - image2 is changed + - image2.container is defined + - image2.container['State']['Running'] + - "'pulled image alpine:3.7' not in image2.actions" + - "'started container2' in image2.actions" + fail_msg: Pulling image test failed! + success_msg: Pulling image test passed! + + - name: Check failed image pull + podman_container: + name: container + image: ineverneverneverexist + state: present + command: sleep 1d + register: imagefail + ignore_errors: true + + - name: Check output is correct + assert: + that: + - imagefail is failed + - imagefail.msg == "Can't pull image ineverneverneverexist" + + + - name: Force container recreate + podman_container: + name: container + image: alpine + state: present + command: sleep 1d + recreate: true + register: recreated + + - name: Check output is correct + assert: + that: + - recreated is changed + - recreated.container is defined + - recreated.container['State']['Running'] + - "'recreated container' in recreated.actions" + fail_msg: Force recreate test failed! + success_msg: Force recreate test passed! + + - name: Stop container + podman_container: + name: container + state: stopped + register: stopped + + - name: Stop the same container again (idempotency) + podman_container: + name: container + state: stopped + register: stopped_again + + - name: Check output is correct + assert: + that: + - stopped is changed + - stopped.container is defined + - not stopped.container['State']['Running'] + - "'stopped container' in stopped.actions" + - stopped_again is not changed + - stopped_again.container is defined + - not stopped_again.container['State']['Running'] + - stopped_again.actions == [] + fail_msg: Stopping container test failed! + success_msg: Stopping container test passed! + + - name: Delete stopped container + podman_container: + name: container + state: absent + register: deleted + + - name: Delete again container (idempotency) + podman_container: + name: container + state: absent + register: deleted_again + + - name: Check output is correct + assert: + that: + - deleted is changed + - deleted.container is defined + - deleted.container == {} + - "'deleted container' in deleted.actions" + - deleted_again is not changed + - deleted_again.container is defined + - deleted_again.container == {} + - deleted_again.actions == [] + fail_msg: Deleting stopped container test failed! + success_msg: Deleting stopped container test passed! + + - name: Create container, but don't run + podman_container: + name: container + image: alpine:3.7 + state: stopped + command: sleep 1d + register: created + + - name: Check output is correct + assert: + that: + - created is changed + - created.container is defined + - created.container != {} + - not created.container['State']['Running'] + - "'created container' in created.actions" + fail_msg: "Creating stopped container test failed!" + success_msg: "Creating stopped container test passed!" + + - name: Delete created container + podman_container: + name: container + state: absent + + - name: Start container that was deleted + podman_container: + name: container + image: alpine:3.7 + state: started + command: sleep 1d + register: started + + - name: Check output is correct + assert: + that: + - started is changed + - started.container is defined + - started.container['State']['Running'] + - "'pulled image alpine:3.7' not in started.actions" + + - name: Delete started container + podman_container: + name: container + state: absent + register: deleted + + - name: Delete again container (idempotency) + podman_container: + name: container + state: absent + register: deleted_again + + - name: Check output is correct + assert: + that: + - deleted is changed + - deleted.container is defined + - deleted.container == {} + - "'deleted container' in deleted.actions" + - deleted_again is not changed + - deleted_again.container is defined + - deleted_again.container == {} + - deleted_again.actions == [] + fail_msg: Deleting started container test failed! + success_msg: Deleting started container test passed! + + - name: Recreate container with parameters + podman_container: + name: container + image: docker.io/alpine:3.7 + state: started + command: sleep 1d + recreate: true + etc_hosts: + host1: 127.0.0.1 + host2: 127.0.0.1 + annotation: + this: "annotation_value" + dns: + - 1.1.1.1 + - 8.8.4.4 + dns_search: example.com + cap_add: + - SYS_TIME + - NET_ADMIN + publish: + - "9000:80" + - "9001:8000" + workdir: "/bin" + env: + FOO: bar + BAR: foo + TEST: 1 + BOOL: false + group_add: "somegroup" + label: + somelabel: labelvalue + otheralbe: othervalue + volumes: + - /tmp:/data + register: test + + - name: Check output is correct + assert: + that: + - test is changed + - test.container is defined + - test.container != {} + - test.container['State']['Running'] + # test capabilities + - "'CAP_SYS_TIME' in test.container['BoundingCaps']" + - "'CAP_NET_ADMIN' in test.container['BoundingCaps']" + # test annotations + - test.container['Config']['Annotations']['this'] is defined + - test.container['Config']['Annotations']['this'] == "annotation_value" + # test DNS + - >- + (test.container['HostConfig']['Dns'] is defined and + test.container['HostConfig']['Dns'] == ['1.1.1.1', '8.8.4.4']) or + (test.container['HostConfig']['DNS'] is defined and + test.container['HostConfig']['DNS'] == ['1.1.1.1', '8.8.4.4']) + # test ports + - test.container['NetworkSettings']['Ports']|length == 2 + # test working dir + - test.container['Config']['WorkingDir'] == "/bin" + # test dns search + - >- + (test.container['HostConfig']['DnsSearch'] is defined and + test.container['HostConfig']['DnsSearch'] == ['example.com']) or + (test.container['HostConfig']['DNSSearch'] is defined and + test.container['HostConfig']['DNSSearch'] == ['example.com']) + # test environment variables + - "'FOO=bar' in test.container['Config']['Env']" + - "'BAR=foo' in test.container['Config']['Env']" + - "'TEST=1' in test.container['Config']['Env']" + - "'BOOL=False' in test.container['Config']['Env']" + # test labels + - test.container['Config']['Labels'] | length == 2 + - test.container['Config']['Labels']['somelabel'] == "labelvalue" + - test.container['Config']['Labels']['otheralbe'] == "othervalue" + # test mounts + - >- + (test.container['Mounts'][0]['Destination'] is defined and + '/data' in test.container['Mounts'] | map(attribute='Destination') | list) or + (test.container['Mounts'][0]['destination'] is defined and + '/data' in test.container['Mounts'] | map(attribute='destination') | list) + - >- + (test.container['Mounts'][0]['Source'] is defined and + '/tmp' in test.container['Mounts'] | map(attribute='Source') | list) or + (test.container['Mounts'][0]['source'] is defined and + '/tmp' in test.container['Mounts'] | map(attribute='source') | list) + fail_msg: Parameters container test failed! + success_msg: Parameters container test passed! + + - name: Check basic idempotency of running container + podman_container: + name: testidem + image: alpine + state: present + command: sleep 20m + + - name: Check basic idempotency of running container - run it again + podman_container: + name: testidem + image: alpine + state: present + command: sleep 20m + register: idem + + - name: Check that nothing was changed + assert: + that: + - not idem.changed + + - name: Run changed container (with tty enabled) + podman_container: + name: testidem + image: alpine + state: present + command: sleep 20m + tty: true + register: idem1 + + - name: Check that container is recreated when changed + assert: + that: + - idem1 is changed + + - name: Run changed container without specifying an option, use defaults + podman_container: + name: testidem + image: alpine + state: present + command: sleep 20m + register: idem2 + + - name: Check that container is recreated when changed to default value + assert: + that: + - idem2 is changed + + - name: Remove container + podman_container: + name: testidem + state: absent + register: remove + + - name: Check podman_actions + assert: + that: + - "'podman rm -f testidem' in remove.podman_actions" + + always: + - name: Delete all container leftovers from tests + podman_container: + name: "{{ item }}" + state: absent + loop: + - "alpine:3.7" + - "container" + - "container2" diff --git a/tripleo_ansible/ansible_plugins/tests/molecule/podman_container/prepare.yml b/tripleo_ansible/ansible_plugins/tests/molecule/podman_container/prepare.yml new file mode 100644 index 000000000..9bbae21a9 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/tests/molecule/podman_container/prepare.yml @@ -0,0 +1,39 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Prepare + hosts: all + gather_facts: true + roles: + - role: test_deps + test_deps_extra_packages: + - podman + + post_tasks: + - name: Check podman version + command: podman version + register: p_ver + changed_when: false + + - name: Print podman version + debug: + msg: | + podman version: + {{ p_ver.stdout }} + + Testing with ansible {{ ansible_version.full }} + with python {{ ansible_python_version }} + on host {{ ansible_distribution }} {{ ansible_distribution_version }} diff --git a/tripleo_ansible/ansible_plugins/tests/molecule/podman_container_info/molecule.yml b/tripleo_ansible/ansible_plugins/tests/molecule/podman_container_info/molecule.yml new file mode 100644 index 000000000..34a29b082 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/tests/molecule/podman_container_info/molecule.yml @@ -0,0 +1,56 @@ +--- +driver: + name: delegated + options: + managed: false + login_cmd_template: >- + ssh + -o UserKnownHostsFile=/dev/null + -o StrictHostKeyChecking=no + -o Compression=no + -o TCPKeepAlive=yes + -o VerifyHostKeyDNS=no + -o ForwardX11=no + -o ForwardAgent=no + {instance} + ansible_connection_options: + ansible_connection: ssh + +log: true + +platforms: + - name: instance + +provisioner: + name: ansible + config_options: + defaults: + fact_caching: jsonfile + fact_caching_connection: /tmp/molecule/facts + inventory: + hosts: + all: + hosts: + instance: + ansible_host: localhost + log: true + env: + ANSIBLE_STDOUT_CALLBACK: yaml + ANSIBLE_ROLES_PATH: "${ANSIBLE_ROLES_PATH:-/usr/share/ansible/roles}:${HOME}/zuul-jobs/roles" + ANSIBLE_LIBRARY: "${ANSIBLE_LIBRARY:-/usr/share/ansible/plugins/modules}" + ANSIBLE_FILTER_PLUGINS: "${ANSIBLE_FILTER_PLUGINS:-/usr/share/ansible/plugins/filter}" + +scenario: + name: podman_container_info + test_sequence: + - prepare + - converge + - verify + +lint: + enabled: false + +verifier: + name: testinfra + lint: + name: flake8 diff --git a/tripleo_ansible/ansible_plugins/tests/molecule/podman_container_info/playbook.yml b/tripleo_ansible/ansible_plugins/tests/molecule/podman_container_info/playbook.yml new file mode 100644 index 000000000..aaebfdb14 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/tests/molecule/podman_container_info/playbook.yml @@ -0,0 +1,70 @@ +--- +- name: Converge + hosts: all + tasks: + - name: Test podman_container_info + become: true + block: + - name: Generate random value for container name + set_fact: + container_name: "{{ 'ansible-test-podman-%0x' % ((2**32) | random) }}" + + - name: Make sure container doesn't exist + command: podman container rm -f {{ container_name }} + ignore_errors: true + + - name: Get missing container info + podman_container_info: + name: "{{ container_name }}" + register: nonexist + ignore_errors: true + + - name: Check results + assert: + that: + - "'containers' not in nonexist" + - nonexist is failed + + - name: Make sure container exists + command: podman container run -d --name {{ container_name }} alpine sleep 15m + + - name: Get existing container info + podman_container_info: + name: "{{ container_name }}" + register: existing_container + + - name: Get all containers info + podman_container_info: + register: all_containers + + - name: Dump podman container inspect result + debug: var=existing_container + + - name: Comparison with 'podman container inspect' + command: podman container inspect "{{ container_name }}" + register: podman_inspect + + - name: Convert podman inspect output to JSON + set_fact: + podman_inspect_result: "{{ podman_inspect.stdout | from_json }}" + + - name: Cleanup + command: podman container rm -f {{ container_name }} + + - name: Make checks + assert: + that: + - "'containers' in existing_container" + - existing_container.containers + - "existing_container.containers == podman_inspect_result" + - all_containers.containers == existing_container.containers + + always: + - name: Delete all container leftovers from tests + podman_container: + name: "{{ item }}" + state: absent + loop: + - "alpine:3.7" + - "container" + - "container2" diff --git a/tripleo_ansible/ansible_plugins/tests/molecule/podman_container_info/prepare.yml b/tripleo_ansible/ansible_plugins/tests/molecule/podman_container_info/prepare.yml new file mode 100644 index 000000000..9c4724137 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/tests/molecule/podman_container_info/prepare.yml @@ -0,0 +1,22 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Prepare + hosts: all + roles: + - role: test_deps + test_deps_extra_packages: + - podman diff --git a/tripleo_ansible/ansible_plugins/tests/molecule/podman_volume_info/molecule.yml b/tripleo_ansible/ansible_plugins/tests/molecule/podman_volume_info/molecule.yml new file mode 100644 index 000000000..64204d4ca --- /dev/null +++ b/tripleo_ansible/ansible_plugins/tests/molecule/podman_volume_info/molecule.yml @@ -0,0 +1,56 @@ +--- +driver: + name: delegated + options: + managed: false + login_cmd_template: >- + ssh + -o UserKnownHostsFile=/dev/null + -o StrictHostKeyChecking=no + -o Compression=no + -o TCPKeepAlive=yes + -o VerifyHostKeyDNS=no + -o ForwardX11=no + -o ForwardAgent=no + {instance} + ansible_connection_options: + ansible_connection: ssh + +log: true + +platforms: + - name: instance + +provisioner: + name: ansible + config_options: + defaults: + fact_caching: jsonfile + fact_caching_connection: /tmp/molecule/facts + inventory: + hosts: + all: + hosts: + instance: + ansible_host: localhost + log: true + env: + ANSIBLE_STDOUT_CALLBACK: yaml + ANSIBLE_ROLES_PATH: "${ANSIBLE_ROLES_PATH:-/usr/share/ansible/roles}:${HOME}/zuul-jobs/roles" + ANSIBLE_LIBRARY: "${ANSIBLE_LIBRARY:-/usr/share/ansible/plugins/modules}" + ANSIBLE_FILTER_PLUGINS: "${ANSIBLE_FILTER_PLUGINS:-/usr/share/ansible/plugins/filter}" + +scenario: + name: podman_volume_info + test_sequence: + - prepare + - converge + - verify + +lint: + enabled: false + +verifier: + name: testinfra + lint: + name: flake8 diff --git a/tripleo_ansible/ansible_plugins/tests/molecule/podman_volume_info/playbook.yml b/tripleo_ansible/ansible_plugins/tests/molecule/podman_volume_info/playbook.yml new file mode 100644 index 000000000..d955222c3 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/tests/molecule/podman_volume_info/playbook.yml @@ -0,0 +1,63 @@ +--- +- name: Converge + hosts: all + tasks: + - name: Test podman_volume_info + become: true + block: + - name: Print podman version + command: podman version + + - name: Generate random value for volume name + set_fact: + volume_name: "{{ 'ansible-test-podman-%0x' % ((2**32) | random) }}" + + - name: Make sure volume doesn't exist + command: podman volume rm {{ volume_name }} + ignore_errors: true + + - name: Get missing volume info + podman_volume_info: + name: "{{ volume_name }}" + register: nonexist + ignore_errors: true + + - name: Check results + assert: + that: + - "'volumes' not in nonexist" + - nonexist is failed + + - name: Make sure volume exists + command: podman volume create {{ volume_name }} + + - name: Get existing volume info + podman_volume_info: + name: "{{ volume_name }}" + register: existing_volume + + - name: Dump podman volume inspect result + debug: var=existing_volume + + - name: Comparison with 'podman volume inspect' + command: podman volume inspect "{{ volume_name }}" + register: podman_inspect + + - name: Convert podman inspect output to JSON + set_fact: + podman_inspect_result: "{{ podman_inspect.stdout | from_json }}" + + - name: Cleanup + command: podman volume rm {{ volume_name }} + + - name: Make checks + assert: + that: + - "'volumes' in existing_volume" + - existing_volume.volumes + - "existing_volume.volumes == podman_inspect_result" + always: + + - name: Cleanup + command: podman volume rm {{ volume_name }} + ignore_errors: true diff --git a/tripleo_ansible/ansible_plugins/tests/molecule/podman_volume_info/prepare.yml b/tripleo_ansible/ansible_plugins/tests/molecule/podman_volume_info/prepare.yml new file mode 100644 index 000000000..9c4724137 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/tests/molecule/podman_volume_info/prepare.yml @@ -0,0 +1,22 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Prepare + hosts: all + roles: + - role: test_deps + test_deps_extra_packages: + - podman diff --git a/tripleo_ansible/playbooks/podman_tests.yml b/tripleo_ansible/playbooks/podman_tests.yml deleted file mode 100644 index cc34fe18b..000000000 --- a/tripleo_ansible/playbooks/podman_tests.yml +++ /dev/null @@ -1,351 +0,0 @@ ---- -- name: Test podman_image - when: - - ansible_facts.virtualization_type != 'docker' - - ansible_facts.distribution in ['RedHat', 'Fedora', 'CentOS'] - block: - - name: Delete all container leftovers from tests - podman_container: - name: "{{ item }}" - state: absent - loop: - - "alpine:3.7" - - "container" - - "container2" - - - name: Test no image with default action - podman_container: - name: container - ignore_errors: true - register: no_image - - - name: Test no image with state 'started' - podman_container: - name: container - state: started - ignore_errors: true - register: no_image1 - - - name: Test no image with state 'present' - podman_container: - name: container - state: present - ignore_errors: true - register: no_image2 - - - name: Check no image - assert: - that: - - no_image is failed - - no_image1 is failed - - no_image2 is failed - - no_image.msg == "State 'started' required image to be configured!" - - no_image1.msg == "State 'started' required image to be configured!" - - no_image2.msg == "State 'present' required image to be configured!" - fail_msg: No image test failed! - success_msg: No image test passed! - - - name: Ensure image doesn't exist - podman_image: - name: alpine:3.7 - state: absent - - - name: Check pulling image - podman_container: - name: container - image: alpine:3.7 - state: present - command: sleep 1d - register: image - - - name: Check using already pulled image - podman_container: - name: container2 - image: alpine:3.7 - state: present - command: sleep 1d - register: image2 - - - name: Check output is correct - assert: - that: - - image is changed - - image.ansible_facts is defined - - image.ansible_facts.podman_container is defined - - image.ansible_facts.podman_container['State']['Running'] - - image.container is defined - - image.container['State']['Running'] - - "'pulled image alpine:3.7' in image.actions" - - "'started container' in image.actions" - - image2 is changed - - image2.ansible_facts is defined - - image2.ansible_facts.podman_container is defined - - image2.ansible_facts.podman_container['State']['Running'] - - image2.container is defined - - image2.container['State']['Running'] - - "'pulled image alpine:3.7' not in image2.actions" - - "'started container2' in image2.actions" - fail_msg: Pulling image test failed! - success_msg: Pulling image test passed! - - - name: Check failed image pull - podman_container: - name: container - image: ineverneverneverexist - state: present - command: sleep 1d - register: imagefail - ignore_errors: true - - - name: Check output is correct - assert: - that: - - imagefail is failed - - imagefail.msg == "Can't pull image ineverneverneverexist" - - - - name: Force container recreate - podman_container: - name: container - image: alpine - state: present - command: sleep 1d - recreate: true - register: recreated - - - name: Check output is correct - assert: - that: - - recreated is changed - - recreated.ansible_facts is defined - - recreated.ansible_facts.podman_container is defined - - recreated.ansible_facts.podman_container['State']['Running'] - - "'recreated container' in recreated.actions" - fail_msg: Force recreate test failed! - success_msg: Force recreate test passed! - - - name: Stop container - podman_container: - name: container - state: stopped - register: stopped - - - name: Stop the same container again (idempotency) - podman_container: - name: container - state: stopped - register: stopped_again - - - name: Check output is correct - assert: - that: - - stopped is changed - - stopped.ansible_facts is defined - - stopped.ansible_facts.podman_container is defined - - not stopped.ansible_facts.podman_container['State']['Running'] - - "'stopped container' in stopped.actions" - - stopped_again is not changed - - stopped_again.ansible_facts is defined - - stopped_again.ansible_facts.podman_container is defined - - not stopped_again.ansible_facts.podman_container['State']['Running'] - - stopped_again.actions == [] - fail_msg: Stopping container test failed! - success_msg: Stopping container test passed! - - - name: Delete stopped container - podman_container: - name: container - state: absent - register: deleted - - - name: Delete again container (idempotency) - podman_container: - name: container - state: absent - register: deleted_again - - - name: Check output is correct - assert: - that: - - deleted is changed - - deleted.ansible_facts is defined - - deleted.ansible_facts.podman_container is defined - - deleted.ansible_facts.podman_container == {} - - "'deleted container' in deleted.actions" - - deleted_again is not changed - - deleted_again.ansible_facts is defined - - deleted_again.ansible_facts.podman_container is defined - - deleted_again.ansible_facts.podman_container == {} - - deleted_again.actions == [] - fail_msg: Deleting stopped container test failed! - success_msg: Deleting stopped container test passed! - - - name: Create container, but don't run - podman_container: - name: container - image: alpine:3.7 - state: stopped - command: sleep 1d - register: created - - - name: Check output is correct - assert: - that: - - created is changed - - created.ansible_facts is defined - - created.ansible_facts.podman_container is defined - - created.ansible_facts.podman_container != {} - - not created.ansible_facts.podman_container['State']['Running'] - - "'created container' in created.actions" - fail_msg: "Creating stopped container test failed!" - success_msg: "Creating stopped container test passed!" - - - name: Delete created container - podman_container: - name: container - state: absent - - - name: Start container that was deleted - podman_container: - name: container - image: alpine:3.7 - state: started - command: sleep 1d - register: started - - - name: Check output is correct - assert: - that: - - started is changed - - started.ansible_facts is defined - - started.ansible_facts.podman_container is defined - - started.ansible_facts.podman_container['State']['Running'] - - started.container is defined - - started.container['State']['Running'] - - "'pulled image alpine:3.7' not in started.actions" - - - name: Delete started container - podman_container: - name: container - state: absent - register: deleted - - - name: Delete again container (idempotency) - podman_container: - name: container - state: absent - register: deleted_again - - - name: Check output is correct - assert: - that: - - deleted is changed - - deleted.ansible_facts is defined - - deleted.ansible_facts.podman_container is defined - - deleted.ansible_facts.podman_container == {} - - "'deleted container' in deleted.actions" - - deleted_again is not changed - - deleted_again.ansible_facts is defined - - deleted_again.ansible_facts.podman_container is defined - - deleted_again.ansible_facts.podman_container == {} - - deleted_again.actions == [] - fail_msg: Deleting started container test failed! - success_msg: Deleting started container test passed! - - - name: Recreate container with parameters - podman_container: - name: container - image: alpine:3.7 - state: started - command: sleep 1d - recreate: true - etc_hosts: - host1: 127.0.0.1 - host2: 127.0.0.1 - annotation: - this: "annotation_value" - dns: - - 1.1.1.1 - - 8.8.4.4 - dns_search: example.com - cap_add: - - SYS_TIME - - NET_ADMIN - publish: - - "9000:80" - - "9001:8000" - workdir: "/bin" - env: - FOO: bar - BAR: foo - TEST: 1 - BOOL: false - group_add: "somegroup" - label: - somelabel: labelvalue - otheralbe: othervalue - volumes: - - /tmp:/data - register: test - - - name: Check output is correct - assert: - that: - - test is changed - - test.ansible_facts is defined - - test.ansible_facts.podman_container is defined - - test.ansible_facts.podman_container != {} - - test.ansible_facts.podman_container['State']['Running'] - # test capabilities - - "'CAP_SYS_TIME' in test.ansible_facts.podman_container['BoundingCaps']" - - "'CAP_NET_ADMIN' in test.ansible_facts.podman_container['BoundingCaps']" - # test annotations - - test.ansible_facts.podman_container['Config']['Annotations']['this'] is defined - - test.ansible_facts.podman_container['Config']['Annotations']['this'] == "annotation_value" - # test DNS - - >- - (test.ansible_facts.podman_container['HostConfig']['Dns'] is defined and - test.ansible_facts.podman_container['HostConfig']['Dns'] == ['1.1.1.1', '8.8.4.4']) or - (test.ansible_facts.podman_container['HostConfig']['DNS'] is defined and - test.ansible_facts.podman_container['HostConfig']['DNS'] == ['1.1.1.1', '8.8.4.4']) - # test ports - - test.ansible_facts.podman_container['NetworkSettings']['Ports']|length == 2 - # test working dir - - test.ansible_facts.podman_container['Config']['WorkingDir'] == "/bin" - # test dns search - - >- - (test.ansible_facts.podman_container['HostConfig']['DnsSearch'] is defined and - test.ansible_facts.podman_container['HostConfig']['DnsSearch'] == ['example.com']) or - (test.ansible_facts.podman_container['HostConfig']['DNSSearch'] is defined and - test.ansible_facts.podman_container['HostConfig']['DNSSearch'] == ['example.com']) - # test environment variables - - "'FOO=bar' in test.ansible_facts.podman_container['Config']['Env']" - - "'BAR=foo' in test.ansible_facts.podman_container['Config']['Env']" - - "'TEST=1' in test.ansible_facts.podman_container['Config']['Env']" - - "'BOOL=False' in test.ansible_facts.podman_container['Config']['Env']" - # test labels - - test.ansible_facts.podman_container['Config']['Labels'] | length == 2 - - test.ansible_facts.podman_container['Config']['Labels']['somelabel'] == "labelvalue" - - test.ansible_facts.podman_container['Config']['Labels']['otheralbe'] == "othervalue" - # test mounts - - >- - (test.ansible_facts.podman_container['Mounts'][0]['Destination'] is defined and - '/data' in test.ansible_facts.podman_container['Mounts'] | map(attribute='Destination') | list) or - (test.ansible_facts.podman_container['Mounts'][0]['destination'] is defined and - '/data' in test.ansible_facts.podman_container['Mounts'] | map(attribute='destination') | list) - - >- - (test.ansible_facts.podman_container['Mounts'][0]['Source'] is defined and - '/tmp' in test.ansible_facts.podman_container['Mounts'] | map(attribute='Source') | list) or - (test.ansible_facts.podman_container['Mounts'][0]['source'] is defined and - '/tmp' in test.ansible_facts.podman_container['Mounts'] | map(attribute='source') | list) - fail_msg: Parameters container test failed! - success_msg: Parameters container test passed! - always: - - name: Delete all container leftovers from tests - podman_container: - name: "{{ item }}" - state: absent - loop: - - "alpine:3.7" - - "container" - - "container2" diff --git a/tripleo_ansible/roles/tripleo-container-manage/defaults/main.yml b/tripleo_ansible/roles/tripleo-container-manage/defaults/main.yml new file mode 100644 index 000000000..151be69ca --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/defaults/main.yml @@ -0,0 +1,29 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +# All variables intended for modification should place placed in this file. + +# All variables within this role should have a prefix of "tripleo_container_manage" +tripleo_container_manage_cli: podman +tripleo_container_manage_concurrency: 1 +tripleo_container_manage_config: "/var/lib/tripleo-config/" +tripleo_container_manage_config_id: tripleo +tripleo_container_manage_config_patterns: 'hashed-*.json' +tripleo_container_manage_debug: false +tripleo_container_manage_healthcheck_disabled: false +tripleo_container_manage_log_path: '/var/log/containers/stdouts' +tripleo_container_manage_systemd_order: false diff --git a/tripleo_ansible/roles/tripleo-container-manage/files/91-netns-placeholder-preset b/tripleo_ansible/roles/tripleo-container-manage/files/91-netns-placeholder-preset new file mode 100644 index 000000000..8bf2ba2b0 --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/files/91-netns-placeholder-preset @@ -0,0 +1 @@ +enable netns-placeholder.service diff --git a/tripleo_ansible/roles/tripleo-container-manage/files/91-tripleo-container-shutdown-preset b/tripleo_ansible/roles/tripleo-container-manage/files/91-tripleo-container-shutdown-preset new file mode 100644 index 000000000..675a9c7cb --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/files/91-tripleo-container-shutdown-preset @@ -0,0 +1 @@ +enable tripleo-container-shutdown.service diff --git a/tripleo_ansible/roles/tripleo-container-manage/files/netns-placeholder-service b/tripleo_ansible/roles/tripleo-container-manage/files/netns-placeholder-service new file mode 100644 index 000000000..57326edaa --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/files/netns-placeholder-service @@ -0,0 +1,11 @@ +[Unit] +Description=Create netns directory +Before=tripleo-container-shutdown.service +Wants=network.target +[Service] +Type=oneshot +ExecStart=/sbin/ip netns add placeholder +ExecStop=/sbin/ip netns delete placeholder +KillMode=process +[Install] +WantedBy=multi-user.target diff --git a/tripleo_ansible/roles/tripleo-container-manage/files/tripleo-container-shutdown b/tripleo_ansible/roles/tripleo-container-manage/files/tripleo-container-shutdown new file mode 100644 index 000000000..cba9ecaec --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/files/tripleo-container-shutdown @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +TIMEOUT=${1:-90} +PARALLEL=${2:-10} + +if command -v dnf >/dev/null;then + if command -v podman >/dev/null; then + containers=$(podman ps --filter label=managed_by=tripleo_ansible --format {{.Names}}) + for c in $containers; do + logger -p warning "WARNING ($c) Container $c managed by tripleo-ansible is not stopped yet" + logger -p warning "WARNING ($c) Check systemd logs: journalctl -u tripleo_$c" + done + fi +else + if command -v docker >/dev/null; then + /usr/bin/docker ps --format \"{{.Names}}\" --filter "label=managed_by=tripleo_ansible" | \ + /usr/bin/xargs -n 1 -P $PARALLEL /usr/bin/docker stop --time=$TIMEOUT + fi +fi diff --git a/tripleo_ansible/roles/tripleo-container-manage/files/tripleo-container-shutdown-service b/tripleo_ansible/roles/tripleo-container-manage/files/tripleo-container-shutdown-service new file mode 100644 index 000000000..e30c9b1a1 --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/files/tripleo-container-shutdown-service @@ -0,0 +1,21 @@ +[Unit] +Description=TripleO Container Shutdown +Documentation=https://docs.openstack.org/tripleo-docs/ +# Note: docker.service will be removed once CentOS8 / RHEL8 will be the default +# platform, but for now we keep it for Pacemaker testing. +# pacemaker.service is needed here, to make sure that all non-Pacemaker managed +# containers are stopped before Pacemaker. +After=pacemaker.service docker.service network-online.target iptables.service ip6tables.service +Before=shutdown.target +RefuseManualStop=yes + +[Service] +Type=oneshot +ExecStart=/bin/true +RemainAfterExit=yes +ExecStop=/usr/libexec/tripleo-container-shutdown +# Wait at most 900 seconds for all containers to shutdown +TimeoutStopSec=900 + +[Install] +WantedBy=multi-user.target diff --git a/tripleo_ansible/roles/tripleo-container-manage/files/tripleo-start-podman-container b/tripleo_ansible/roles/tripleo-container-manage/files/tripleo-start-podman-container new file mode 100644 index 000000000..3423b5df6 --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/files/tripleo-start-podman-container @@ -0,0 +1,83 @@ +#!/usr/bin/env bash + +PODMAN=/usr/bin/podman + +NAME=$1 + +if [ -z "$NAME" ]; then + echo "No name provided, cannot start container. Aborting" >&2 + exit 1 +fi + +# Start container. Podman does not fail if container is already started +$PODMAN start $NAME +rc=$? + +if [ $rc -ne 0 ]; then + echo "Error starting podman container $NAME: $rc" >&2 + exit $rc +fi + +# The environment can ben configured to create additional drop-in +# dependencies for the scopes associated with the container. This is +# done to prevent systemd from stopping the scopes early and break the +# configured dependencies in tripleo_*.services +# Stop here otherwise. +if [ ! -f "/etc/sysconfig/podman_drop_in" ]; then + exit 0 +fi + +# Retrieve the container's ID +# Note: currently the only API to retrieve the CID is either +# 1) via "podman inspect" but we don't want to use it because it can be +# very slow under IO load. +# 2) by running "podman start $NAME" but that command only returns the CID +# if the container is already running. Otherwise it returns the container +# name, which would break us. +# The only other means is via "podman ps". ps option "--filter" cannot +# enforce full name matches, so use grep instead and stop at first match. +CID=$($PODMAN ps --no-trunc --format '{{.ID}} {{.Names}}' | grep -F -w -m1 "$NAME" | cut -d' ' -f1) + +if [ -z "$CID" ]; then + echo "Container ID not found for \"$NAME\". Not creating drop-in dependency" 2>&1 + exit 1 +else + echo "Creating additional drop-in dependency for \"$NAME\" ($CID)" +fi + +# Note: a tripleo-ansible container has three systemd files associated with it: +# 1. tripleo_*.service - the regular systemd service generated by tripleo-ansible +# 2. libpod-conmon*.scope - created dynamically by podman. runs a conmon +# process that creates a pidfile for tripleo_*.service and monitor it. +# 3. libpod-*.scope - created dynamically by runc. for cgroups accounting +# +# tripleo-ansible can only set start/stop dependencies on 1., not 2. and 3. +# On reboot, systemd is allowed to stop 2. or 3. at any time, which can +# cause 1. to stop before its deps as set up by tripleo-ansible. +# +# To prevent an unexpected stop of 1. from happening, inject a dependency +# in 2. and 3. so that systemd is forbidden to stop those scopes +# automatically until tripleo-container-shutdown.service is stopped. +# That way, when systemd stops 1., the two scopes 2. and 3. will +# finish in sequence and tripleo-ansible dependencies will be respected. + +for scope in "libpod-$CID.scope.d" "libpod-conmon-$CID.scope.d"; do + if [ $rc -eq 0 ] && [ ! -d /run/systemd/transient/"$scope" ]; then + mkdir -p /run/systemd/transient/"$scope" && \ + echo -e "[Unit]\nBefore=tripleo-container-shutdown.service" > /run/systemd/transient/"$scope"/dep.conf && \ + chmod ago+r /run/systemd/transient/"$scope" /run/systemd/transient/"$scope"/dep.conf + rc=$? + fi +done + +if [ $rc -ne 0 ]; then + echo "Could not create drop-in dependency for \"$NAME\" ($CID)" >&2 + exit 1 +fi + +systemctl daemon-reload +rc=$? +if [ $rc -ne 0 ]; then + echo "Could not refresh service definition after creating drop-in for \"$NAME\": $rc" >&2 + exit 1 +fi diff --git a/tripleo_ansible/roles/tripleo-container-manage/meta/main.yml b/tripleo_ansible/roles/tripleo-container-manage/meta/main.yml new file mode 100644 index 000000000..787b78ece --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/meta/main.yml @@ -0,0 +1,44 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +galaxy_info: + author: OpenStack + description: TripleO OpenStack Role -- tripleo-container-manage + company: Red Hat + license: Apache-2.0 + min_ansible_version: 2.7 + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + platforms: + - name: Fedora + versions: + - 28 + - name: CentOS + versions: + - 7 + + galaxy_tags: + - tripleo + + +# List your role dependencies here, one per line. Be sure to remove the '[]' above, +# if you add dependencies to this list. +dependencies: [] diff --git a/tripleo_ansible/roles/tripleo-container-manage/molecule/default/molecule.yml b/tripleo_ansible/roles/tripleo-container-manage/molecule/default/molecule.yml new file mode 100644 index 000000000..052c5d4bf --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/molecule/default/molecule.yml @@ -0,0 +1,56 @@ +--- +driver: + name: delegated + options: + managed: false + login_cmd_template: >- + ssh + -o UserKnownHostsFile=/dev/null + -o StrictHostKeyChecking=no + -o Compression=no + -o TCPKeepAlive=yes + -o VerifyHostKeyDNS=no + -o ForwardX11=no + -o ForwardAgent=no + {instance} + ansible_connection_options: + ansible_connection: ssh + +log: true + +platforms: + - name: instance + +provisioner: + name: ansible + config_options: + defaults: + fact_caching: jsonfile + fact_caching_connection: /tmp/molecule/facts + inventory: + hosts: + all: + hosts: + instance: + ansible_host: localhost + log: true + env: + ANSIBLE_STDOUT_CALLBACK: yaml + ANSIBLE_ROLES_PATH: "${ANSIBLE_ROLES_PATH:-/usr/share/ansible/roles}:${HOME}/zuul-jobs/roles" + ANSIBLE_LIBRARY: "${ANSIBLE_LIBRARY:-/usr/share/ansible/plugins/modules}" + ANSIBLE_FILTER_PLUGINS: "${ANSIBLE_FILTER_PLUGINS:-/usr/share/ansible/plugins/filter}" + +scenario: + name: default + test_sequence: + - prepare + - converge + - verify + +lint: + enabled: false + +verifier: + name: testinfra + lint: + name: flake8 diff --git a/tripleo_ansible/roles/tripleo-container-manage/molecule/default/playbook.yml b/tripleo_ansible/roles/tripleo-container-manage/molecule/default/playbook.yml new file mode 100644 index 000000000..4c1c4802b --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/molecule/default/playbook.yml @@ -0,0 +1,62 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +- name: Converge + hosts: all + gather_facts: false + vars: + tripleo_container_manage_config: '/tmp/container-configs' + tripleo_container_manage_debug: true + tripleo_container_manage_config_patterns: '*.json' + tripleo_container_manage_systemd_order: true + tasks: + - include_role: + name: tripleo-container-manage + post_tasks: + - name: Verify that Fedora container was created correctly + become: true + block: + - name: Check for fedora container + command: podman container exists fedora + - name: Gather facts about fedora container + podman_container_info: + name: fedora + register: fedora_infos + - name: Assert that fedora container has the right image + assert: + that: + - "'fedora:latest' in fedora_infos.containers.0.ImageName" + fail_msg: 'fedora container has wrong image' + success_msg: 'fedora container has the right image' + - name: Check if tripleo_fedora systemd service is active + command: systemctl is-active --quiet tripleo_fedora + register: tripleo_fedora_active_result + - name: Assert that tripleo_fedora systemd service is active + assert: + that: + - tripleo_fedora_active_result.rc == 0 + fail_msg: 'tripleo_fedora systemd service is not active' + success_msg: 'tripleo_fedora systemd service is active' + - name: Check if tripleo_fedora systemd healthcheck service is active + command: systemctl is-active --quiet tripleo_fedora_healthcheck.timer + register: tripleo_fedora_healthcheck_active_result + - name: Assert that tripleo_fedora systemd healthcheck service is active + assert: + that: + - tripleo_fedora_healthcheck_active_result.rc == 0 + fail_msg: 'tripleo_fedora systemd healthcheck service is not active' + success_msg: 'tripleo_fedora systemd healthcheck service is active' diff --git a/tripleo_ansible/roles/tripleo-container-manage/molecule/default/prepare.yml b/tripleo_ansible/roles/tripleo-container-manage/molecule/default/prepare.yml new file mode 100644 index 000000000..25656e00a --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/molecule/default/prepare.yml @@ -0,0 +1,40 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +- name: Prepare + hosts: all + roles: + - role: test_deps + test_deps_extra_packages: + - podman + tasks: + - name: Prepare the container configs directory + file: + path: '/tmp/container-configs' + state: directory + - name: Create a configuration file for a fedora container + copy: + content: | + { + "image": "fedora:latest", + "net": "host", + "command": "sleep 3600", + "restart": "always", + "net": "host", + "healthcheck": { "test": "echo test" } + } + dest: '/tmp/container-configs/fedora.json' diff --git a/tripleo_ansible/roles/tripleo-container-manage/tasks/container_running.yml b/tripleo_ansible/roles/tripleo-container-manage/tasks/container_running.yml new file mode 100644 index 000000000..7d51b5056 --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/tasks/container_running.yml @@ -0,0 +1,29 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +- name: "Get {{ lookup('dict', container_exists_data).value.command.0 }} container status" + set_fact: + container_running: >- + {{ podman_containers.containers | selectattr('Name', 'equalto', lookup('dict', container_exists_data).value.command.0) | + map(attribute='State.Running') | first | default(false) }} + +- name: "Fail if {{ lookup('dict', container_exists_data).value.command.0 }} is not running" + fail: + msg: >- + Can't run container exec for {{ lookup('dict', container_exists_data).key }}, + {{ lookup('dict', container_exists_data).value.command.0 }} is not running + when: + - not container_running|default(false)|bool diff --git a/tripleo_ansible/roles/tripleo-container-manage/tasks/create.yml b/tripleo_ansible/roles/tripleo-container-manage/tasks/create.yml new file mode 100644 index 000000000..7116fd678 --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/tasks/create.yml @@ -0,0 +1,21 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: "Create containers managed by Podman for {{ tripleo_container_manage_config }}" + when: + - tripleo_container_manage_cli == 'podman' + include: podman/start_order.yml order="{{ item.key }}" data="{{ item.value }}" + loop: "{{ all_containers_hash | subsort(attribute='start_order', null_value=0) | dict2items | list }}" diff --git a/tripleo_ansible/roles/tripleo-container-manage/tasks/delete.yml b/tripleo_ansible/roles/tripleo-container-manage/tasks/delete.yml new file mode 100644 index 000000000..3c4d2a902 --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/tasks/delete.yml @@ -0,0 +1,27 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Gather podman infos + podman_container_info: {} + register: podman_containers + when: + - tripleo_container_manage_cli == 'podman' + +- name: "Delete containers managed by Podman for {{ tripleo_container_manage_config }}" + when: + - tripleo_container_manage_cli == 'podman' + include_tasks: podman/delete.yml + loop: "{{ podman_containers.containers | needs_delete(config=all_containers_hash, config_id=tripleo_container_manage_config_id) }}" diff --git a/tripleo_ansible/roles/tripleo-container-manage/tasks/main.yml b/tripleo_ansible/roles/tripleo-container-manage/tasks/main.yml new file mode 100644 index 000000000..2f9ab4606 --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/tasks/main.yml @@ -0,0 +1,95 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +# "tripleo-container-manage" will search for and load any operating system variable file + +# found within the "vars/" path. If no OS files are found the task will skip. +- name: Gather variables for each operating system + include_vars: "{{ item }}" + with_first_found: + - skip: true + files: + - "{{ ansible_distribution | lower }}-{{ ansible_distribution_version | lower }}.yml" + - "{{ ansible_distribution | lower }}-{{ ansible_distribution_major_version | lower }}.yml" + - "{{ ansible_os_family | lower }}-{{ ansible_distribution_major_version | lower }}.yml" + - "{{ ansible_distribution | lower }}.yml" + - "{{ ansible_os_family | lower }}-{{ ansible_distribution_version.split('.')[0] }}.yml" + - "{{ ansible_os_family | lower }}.yml" + tags: + - always + +- name: Create container logs path + file: + path: "{{ tripleo_container_manage_log_path }}" + state: directory + owner: root + group: root + become: true + +- name: Create ansible-managed dropin file + copy: + dest: "{{ tripleo_container_manage_config | dirname }}/.ansible-managed" + content: | + Containers are managed by the tripleo-container-manage role in + tripleo-ansible project. This file is used by paunch to show a warning + if paunch is used against a deployment done with tripleo-container-manage. + +- name: Generate containers configs data + no_log: "{{ false if tripleo_container_manage_debug else true }}" + block: + - name: "Find all matching configs configs for in {{ tripleo_container_manage_config }}" + find: + paths: "{{ tripleo_container_manage_config }}" + patterns: "{{ tripleo_container_manage_config_patterns }}" + recurse: true + register: matched_files + - name: "Read config for each container in {{ tripleo_container_manage_config }}" + slurp: + src: "{{ item.path }}" + register: containers_data + loop: "{{ matched_files.files }}" + - name: Prepare container hashes from config + set_fact: + container_hash: "{'{{ item.source|basename|regex_replace('^hashed-','')|regex_replace('.json$','') }}': {{ item.content|b64decode|from_json }} }" + register: container_hashes + loop: "{{ containers_data['results'] }}" + - name: Compile container hashes from results + set_fact: + container_hash: "{{ item.ansible_facts.container_hash | combine(item.ansible_facts.container_hash) }}" + register: container_hashes + loop: "{{ container_hashes.results }}" + - name: Finalise hashes for all containers + set_fact: + all_containers_hash: "{{ container_hashes.results | map(attribute='ansible_facts.container_hash') | list | singledict() }}" + +- name: "Manage systemd shutdown files" + become: true + when: + - tripleo_container_manage_systemd_order + block: + - name: Include tasks for systemd shutdown service + include_tasks: shutdown.yml + +- name: "Manage containers from {{ tripleo_container_manage_config }}" + when: + - tripleo_container_manage_cli == 'podman' + become: true + block: + - name: "Delete containers from {{ tripleo_container_manage_config }}" + include_tasks: delete.yml + - name: "Create containers from {{ tripleo_container_manage_config }}" + include_tasks: create.yml diff --git a/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/create.yml b/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/create.yml new file mode 100644 index 000000000..7040f57c0 --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/create.yml @@ -0,0 +1,93 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: "Async container create/run" + async: 300 + poll: 0 + register: create_async_results + # async and check mode don't work together + when: + - not ansible_check_mode|bool + loop: "{{ batched_container_data | haskey(attribute='action', reverse=True) }}" + loop_control: + loop_var: container_data + podman_container: + cap_add: "{{ lookup('dict', container_data).value.cap_add | default(omit) }}" + cap_drop: "{{ lookup('dict', container_data).value.cap_drop | default(omit) }}" + command: "{{ lookup('dict', container_data).value.command | default(omit) }}" + conmon_pidfile: "/var/run/{{ lookup('dict', container_data).key }}.pid" + cpu_shares: "{{ lookup('dict', container_data).value.cpu_shares | default(omit) | int }}" + # cpuset_cpus: "{{ lookup('dict', container_data).value.cpuset_cpus | default(omit) }}" + debug: true + detach: "{{ lookup('dict', container_data).value.detach | default(true) }}" + env: "{{ lookup('dict', container_data).value.environment | default(omit) }}" + env_file: "{{ lookup('dict', container_data).value.env_file | default(omit) }}" + etc_hosts: "{{ lookup('dict', container_data).value.extra_hosts | default({}) }}" + group_add: "{{ lookup('dict', container_data).value.group_add | default(omit) }}" + hostname: "{{ lookup('dict', container_data).value.hostname | default(omit) }}" + image: "{{ lookup('dict', container_data).value.image }}" + interactive: "{{ lookup('dict', container_data).value.interactive | default(false) }}" + ipc: "{{ lookup('dict', container_data).value.ipc | default(omit) }}" + label: + config_id: "{{ tripleo_container_manage_config_id }}" + container_name: "{{ lookup('dict', container_data).key }}" + managed_by: tripleo_ansible + config_data: "{{ lookup('dict', container_data).value }}" + log_driver: 'k8s-file' + log_opt: "path={{ tripleo_container_manage_log_path }}/{{ lookup('dict', container_data).key }}.log" + memory: "{{ lookup('dict', container_data).value.mem_limit | default(omit) }}" + memory_swap: "{{ lookup('dict', container_data).value.mem_swappiness | default(omit) }}" + name: "{{ lookup('dict', container_data).key }}" + net: "{{ lookup('dict', container_data).value.net | default('none') }}" + pid: "{{ lookup('dict', container_data).value.pid | default(omit) }}" + privileged: "{{ lookup('dict', container_data).value.privileged | default(false) }}" + rm: "{{ lookup('dict', container_data).value.remove | default(false) }}" + security_opt: "{{ lookup('dict', container_data).value.security_opt | default(omit) }}" + state: present + stop_signal: "{{ lookup('dict', container_data).value.stop_signal | default(omit) }}" + stop_timeout: "{{ lookup('dict', container_data).value.stop_grace_period | default(omit) | int }}" + tty: "{{ lookup('dict', container_data).value.tty | default(false) }}" + ulimit: "{{ lookup('dict', container_data).value.ulimit | default(omit) }}" + user: "{{ lookup('dict', container_data).value.user | default(omit) }}" + uts: "{{ lookup('dict', container_data).value.uts | default(omit) }}" + volume: "{{ lookup('dict', container_data).value.volumes | default(omit) }}" + volumes_from: "{{ lookup('dict', container_data).value.volumes_from | default([]) }}" + +- name: "Check podman create status" + async_status: + jid: "{{ create_async_result_item.ansible_job_id }}" + loop: "{{ create_async_results.results }}" + loop_control: + loop_var: "create_async_result_item" + register: create_async_poll_results + until: create_async_poll_results.finished + retries: 30 + # async and check mode don't work together + when: + - not ansible_check_mode|bool + +# This fact will be used in systemd playbook to figure out if whether or not +# a container managed by systemd needs to be restarted +- name: "Create a list of containers which changed" + set_fact: + containers_changed: >- + {{ create_async_results.results | selectattr('changed', 'equalto', true) | + map(attribute='container_data') | list | list_of_keys }} + +- name: "Print the list of containers which changed" + debug: + var: containers_changed + when: tripleo_container_manage_debug | bool diff --git a/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/delete.yml b/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/delete.yml new file mode 100644 index 000000000..b02509a7a --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/delete.yml @@ -0,0 +1,77 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# This playbook is a "best effort" way to remove a container from a host. +# It'll try to remove the healthcheck, service and then container without +# much validation in case things failed in the middle. + +- name: "Remove systemd healthcheck for {{ item }}" + block: + - name: "Stop and disable systemd timer for {{ item }}" + systemd: + state: stopped + name: "tripleo_{{ item }}_healthcheck.timer" + enabled: false + ignore_errors: true + - name: "Delete systemd timer file for {{ item }}" + file: + path: "/etc/systemd/system/tripleo_{{ item }}_healthcheck.timer" + state: absent + register: systemd_timer_deleted + - name: "Stop and disable systemd healthcheck for {{ item }}" + systemd: + state: stopped + name: "tripleo_{{ item }}_healthcheck.service" + enabled: false + ignore_errors: true + - name: "Delete systemd healthcheck file for {{ item }}" + file: + path: "/etc/systemd/system/tripleo_{{ item }}_healthcheck.service" + state: absent + register: systemd_healthcheck_deleted + - name: Force systemd to reread configs + systemd: + daemon_reload: true + when: systemd_timer_deleted.changed or systemd_healthcheck_deleted.changed + +- name: "Stop and disable systemd service for {{ item }}" + systemd: + state: stopped + name: "tripleo_{{ item }}.service" + enabled: false + ignore_errors: true + +- name: "Delete systemd unit file for {{ item }}" + file: + path: "/etc/systemd/system/tripleo_{{ item }}.service" + state: absent + register: systemd_file_deleted + +- name: "Remove trailing .requires for {{ item }}" + file: + path: "/etc/systemd/system/tripleo_{{ item }}.requires" + state: absent + register: systemd_requires_deleted + +- name: Force systemd to reread configs + systemd: + daemon_reload: true + when: systemd_file_deleted.changed or systemd_requires_deleted.changed + +- name: "Remove container {{ item }}" + podman_container: + name: "{{ item }}" + state: absent diff --git a/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/exec.yml b/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/exec.yml new file mode 100644 index 000000000..39346fd8b --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/exec.yml @@ -0,0 +1,44 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: "Check if containers are running before doing exec" + include_tasks: container_running.yml + loop: "{{ batched_container_data | haskey(attribute='action', value='exec') }}" + loop_control: + loop_var: container_exists_data + when: not ansible_check_mode|bool + +- name: "Async container exec" + command: + argv: "{{ lookup('dict', container_exec_data).value | container_exec_cmd(cli=tripleo_container_manage_cli) }}" + async: 60 + poll: 0 + register: exec_async_results + loop: "{{ batched_container_data | haskey(attribute='action', value='exec') }}" + loop_control: + loop_var: container_exec_data + when: not ansible_check_mode|bool + +- name: "Check podman exec status" + async_status: + jid: "{{ exec_async_result_item.ansible_job_id }}" + loop: "{{ exec_async_results.results }}" + loop_control: + loop_var: "exec_async_result_item" + register: exec_async_poll_results + until: exec_async_poll_results.finished + retries: 30 + when: not ansible_check_mode|bool diff --git a/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/manage.yml b/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/manage.yml new file mode 100644 index 000000000..8427f8043 --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/manage.yml @@ -0,0 +1,33 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Run containers execs asynchronously + include_tasks: podman/exec.yml + +- name: Manage containers asynchronously + include_tasks: podman/create.yml + +# We don't want to use async for the systemd tasks or we can have startup +# errors when systemd has to deal with multiple services trying to start +# at the same time. It is more reliable to start them in serial. +- name: Manage container systemd services and healthchecks in serial + include_tasks: podman/systemd.yml + # systemd doesn't have the equivalent of docker unless-stopped. + # Let's force 'always' so containers aren't restarted when stopped by + # systemd, but restarted when in failure. + loop: "{{ batched_container_data | haskey(attribute='restart', value=['always','unless-stopped'], any=True) }}" + loop_control: + loop_var: container_config diff --git a/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/start_order.yml b/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/start_order.yml new file mode 100644 index 000000000..7bf328f0e --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/start_order.yml @@ -0,0 +1,27 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Gather podman infos + podman_container_info: {} + register: podman_containers + when: + - tripleo_container_manage_cli == 'podman' + +- name: "Batching items for start_order {{ order }}" + include_tasks: podman/manage.yml + loop: "{{ data | batch(tripleo_container_manage_concurrency) | list }}" + loop_control: + loop_var: batched_container_data diff --git a/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/systemd.yml b/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/systemd.yml new file mode 100644 index 000000000..ce5645aa1 --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/tasks/podman/systemd.yml @@ -0,0 +1,74 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Set container_name and container_sysd facts + set_fact: + container_sysd_name: "{{ lookup('dict', container_config).key }}" + container_sysd_data: "{{ lookup('dict', container_config).value }}" + +- name: "Start systemd service for {{ container_sysd_name }}" + block: + - name: "Remove trailing .requires for {{ container_sysd_name }}" + file: + path: "/etc/systemd/system/tripleo_{{ container_sysd_name }}.requires" + state: absent + - name: "Create systemd unit file for {{ container_sysd_name }} service" + template: + src: systemd-service.j2 + dest: "/etc/systemd/system/tripleo_{{ container_sysd_name }}.service" + mode: '0644' + owner: root + group: root + register: systemd_file + - name: "Enable and start systemd service for {{ container_sysd_name }}" + systemd: + # Restart the service if it was already running + state: restarted + name: "tripleo_{{ container_sysd_name }}.service" + enabled: true + daemon_reload: true + when: + - systemd_file is changed or container_sysd_name in containers_changed + - name: "Manage systemd healthcheck for {{ container_sysd_name }}" + when: + - not tripleo_container_manage_healthcheck_disabled + - container_sysd_data.healthcheck is defined + block: + - name: "Create systemd unit file for {{ container_sysd_name }} healthcheck" + template: + src: systemd-healthcheck.j2 + dest: "/etc/systemd/system/tripleo_{{ container_sysd_name }}_healthcheck.service" + mode: '0644' + owner: root + group: root + register: systemd_healthcheck + - name: "Create systemd timer for {{ container_sysd_name }} healthcheck" + template: + src: systemd-timer.j2 + dest: "/etc/systemd/system/tripleo_{{ container_sysd_name }}_healthcheck.timer" + mode: '0644' + owner: root + group: root + register: systemd_timer + - name: "Enable and start systemd timer for {{ container_sysd_name }}" + systemd: + # Restart the timer if it was already running + state: restarted + name: "tripleo_{{ container_sysd_name }}_healthcheck.timer" + enabled: true + daemon_reload: true + when: + - systemd_healthcheck.changed or systemd_timer.changed diff --git a/tripleo_ansible/roles/tripleo-container-manage/tasks/shutdown.yml b/tripleo_ansible/roles/tripleo-container-manage/tasks/shutdown.yml new file mode 100644 index 000000000..2ae1547af --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/tasks/shutdown.yml @@ -0,0 +1,114 @@ +--- +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Check if /etc/sysconfig/podman_drop_in exists + stat: + path: /etc/sysconfig/podman_drop_in + register: podman_drop_in + +- name: Set podman_drop_in fact + set_fact: + podman_drop_in: true + when: + - podman_drop_in.stat.exists + +- name: Cleanup Paunch services and files + block: + - name: Check if /usr/lib/systemd/system/paunch-container-shutdown.service exists + stat: + path: /usr/lib/systemd/system/paunch-container-shutdown.service + register: paunch_shutdown + - name: Tear-down paunch-container-shutdown + when: + - paunch_shutdown.stat.exists + block: + - name: Allow paunch-container-shutdown to be stopped + lineinfile: + path: /usr/lib/systemd/system/paunch-container-shutdown.service + regexp: '^RefuseManualStop' + line: 'RefuseManualStop=no' + - name: Force systemd to reread configs + systemd: + daemon_reload: true + - name: Disable and stop paunch-container-shutdown service + systemd: + name: paunch-container-shutdown + state: stopped + enabled: false + # TODO(emilien): this task can be removed later when paunch isn't a + # dependency of python-tripleoclient. It'll be replaced by an rpm removal. + - name: "Remove paunch files for systemd" + file: + path: "{{ item }}" + state: absent + loop: + - /usr/libexec/paunch-container-shutdown + - /usr/libexec/paunch-start-podman-container + - /usr/lib/systemd/system/paunch-container-shutdown.service + - /usr/lib/systemd/system-preset/91-paunch-container-shutdown.preset + +- name: Create TripleO Container systemd service + block: + - name: "Deploy tripleo-container-shutdown and tripleo-start-podman-container" + copy: + src: "{{ role_path }}/files/{{ item }}" + dest: "/usr/libexec/{{ item }}" + mode: '0700' + owner: root + group: root + loop: + - 'tripleo-container-shutdown' + - 'tripleo-start-podman-container' + - name: "Create /usr/lib/systemd/system/tripleo-container-shutdown.service" + copy: + src: "{{ role_path }}/files/tripleo-container-shutdown-service" + dest: "/usr/lib/systemd/system/tripleo-container-shutdown.service" + mode: '0700' + owner: root + group: root + - name: "Create /usr/lib/systemd/system-preset/91-tripleo-container-shutdown.preset" + copy: + src: "{{ role_path }}/files/91-tripleo-container-shutdown-preset" + dest: "/usr/lib/systemd/system-preset/91-tripleo-container-shutdown.preset" + mode: '0700' + owner: root + group: root + - name: Enable and start tripleo-container-shutdown + systemd: + name: tripleo-container-shutdown + state: started + enabled: true + daemon_reload: true + - name: "Create /usr/lib/systemd/system/netns-placeholder.service" + copy: + src: "{{ role_path }}/files/netns-placeholder-service" + dest: "/usr/lib/systemd/system/netns-placeholder.service" + mode: '0700' + owner: root + group: root + - name: "Create /usr/lib/systemd/system-preset/91-netns-placeholder.preset" + copy: + src: "{{ role_path }}/files/91-netns-placeholder-preset" + dest: "/usr/lib/systemd/system-preset/91-netns-placeholder.preset" + mode: '0700' + owner: root + group: root + - name: Enable and start netns-placeholder + systemd: + name: netns-placeholder + state: started + enabled: true + daemon_reload: true diff --git a/tripleo_ansible/roles/tripleo-container-manage/templates/systemd-healthcheck.j2 b/tripleo_ansible/roles/tripleo-container-manage/templates/systemd-healthcheck.j2 new file mode 100644 index 000000000..21330a7f9 --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/templates/systemd-healthcheck.j2 @@ -0,0 +1,10 @@ +[Unit] +Description=tripleo_{{ container_sysd_name }} healthcheck +After=tripleo-container-shutdown.service tripleo_{{ container_sysd_name }}.service +Requisite=tripleo_{{ container_sysd_name }}.service +[Service] +Type=oneshot +ExecStart=/usr/bin/podman exec {{ container_sysd_name }} {{ container_sysd_data.healthcheck.test }} +SyslogIdentifier=healthcheck_{{ container_sysd_name }} +[Install] +WantedBy=multi-user.target diff --git a/tripleo_ansible/roles/tripleo-container-manage/templates/systemd-service.j2 b/tripleo_ansible/roles/tripleo-container-manage/templates/systemd-service.j2 new file mode 100644 index 000000000..d27283cd5 --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/templates/systemd-service.j2 @@ -0,0 +1,23 @@ +[Unit] +Description={{ container_sysd_name }} container +After=tripleo-container-shutdown.service +Wants={{ container_sysd_data.depends_on | default([]) | join(',') }} +[Service] +Restart=always +{% if container_sysd_data.depends_on is defined and (container_sysd_data.depends_on | length > 0) and podman_drop_in | default('false') %} +ExecStart=/usr/libexec/tripleo-start-podman-container {{ container_sysd_name }} +{% else %} +ExecStart=/usr/bin/podman start {{ container_sysd_name }} +{% endif %} +ExecReload=/usr/bin/podman kill --signal HUP {{ container_sysd_name }} +ExecStop=/usr/bin/podman stop -t {{ container_sysd_data.stop_grace_period | default(10) | int }} {{ container_sysd_name }} +KillMode=none +Type=forking +PIDFile=/var/run/{{ container_sysd_name }}.pid +{% if container_sysd_data.systemd_exec_flags is defined %} +{% for s_flag, s_value in container_sysd_data.systemd_exec_flags.items() %} +{{ s_flag }}={{ s_value }} +{% endfor %} +{% endif %} +[Install] +WantedBy=multi-user.target diff --git a/tripleo_ansible/roles/tripleo-container-manage/templates/systemd-timer.j2 b/tripleo_ansible/roles/tripleo-container-manage/templates/systemd-timer.j2 new file mode 100644 index 000000000..6b8818f31 --- /dev/null +++ b/tripleo_ansible/roles/tripleo-container-manage/templates/systemd-timer.j2 @@ -0,0 +1,9 @@ +[Unit] +Description=tripleo_{{ container_sysd_name }} container healthcheck +PartOf=tripleo_{{ container_sysd_name }}.service +[Timer] +OnActiveSec=120 +OnUnitActiveSec={{ container_sysd_data.check_interval | default(60) }} +RandomizedDelaySec={{ 45 if container_sysd_data.check_interval is not defined else (container_sysd_data.check_interval * 3 / 4) | int | abs }} +[Install] +WantedBy=timers.target diff --git a/tripleo_ansible/tests/__init__.py b/tripleo_ansible/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tripleo_ansible/tests/base.py b/tripleo_ansible/tests/base.py new file mode 100644 index 000000000..f052aba9b --- /dev/null +++ b/tripleo_ansible/tests/base.py @@ -0,0 +1,21 @@ +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslotest import base + + +class TestCase(base.BaseTestCase): + + """Test case base class for all unit tests.""" diff --git a/tripleo_ansible/tests/plugins/__init__.py b/tripleo_ansible/tests/plugins/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tripleo_ansible/tests/plugins/filter/__init__.py b/tripleo_ansible/tests/plugins/filter/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tripleo_ansible/tests/plugins/filter/test_helpers.py b/tripleo_ansible/tests/plugins/filter/test_helpers.py new file mode 100644 index 000000000..17fa05a35 --- /dev/null +++ b/tripleo_ansible/tests/plugins/filter/test_helpers.py @@ -0,0 +1,376 @@ +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tripleo_ansible.ansible_plugins.filter import helpers +from tripleo_ansible.tests import base as tests_base + + +class TestHelperFilters(tests_base.TestCase): + + def setUp(self): + super(TestHelperFilters, self).setUp() + self.filters = helpers.FilterModule() + + def test_subsort(self): + dict = { + 'keystone': { + 'start_order': 1, + 'image': 'quay.io/tripleo/keystone' + }, + 'haproxy': { + 'image': 'quay.io/tripleo/haproxy' + }, + 'mysql': { + 'start_order': 0, + 'image': 'quay.io/tripleo/mysql' + } + } + expected_ordered_dict = { + 0: [ + {'haproxy': { + 'image': 'quay.io/tripleo/haproxy', + 'start_order': 0 + }}, + {'mysql': { + 'image': 'quay.io/tripleo/mysql', + 'start_order': 0 + }} + ], + 1: [ + {'keystone': { + 'image': 'quay.io/tripleo/keystone', + 'start_order': 1 + }} + ] + } + result = self.filters.subsort(dict_to_sort=dict, + attribute='start_order') + self.assertEqual(result, expected_ordered_dict) + + def test_subsort_with_null_value(self): + dict = { + 'keystone': { + 'start_order': 1, + 'image': 'quay.io/tripleo/keystone' + }, + 'haproxy': { + 'image': 'quay.io/tripleo/haproxy' + }, + 'mysql': { + 'start_order': 0, + 'image': 'quay.io/tripleo/mysql' + } + } + expected_ordered_dict = { + 0: [ + {'mysql': { + 'image': 'quay.io/tripleo/mysql', + 'start_order': 0 + }} + ], + 1: [ + {'keystone': { + 'image': 'quay.io/tripleo/keystone', + 'start_order': 1 + }} + ], + 5: [ + {'haproxy': { + 'image': 'quay.io/tripleo/haproxy', + 'start_order': 5 + }} + ] + } + result = self.filters.subsort(dict_to_sort=dict, + attribute='start_order', null_value=5) + self.assertEqual(result, expected_ordered_dict) + + def test_singledict(self): + list = [ + { + 'keystone': { + 'start_order': 1, + 'image': 'quay.io/tripleo/keystone' + }, + }, + { + 'mysql': { + 'start_order': 0, + 'image': 'quay.io/tripleo/mysql' + } + } + ] + expected_dict = { + 'keystone': { + 'start_order': 1, + 'image': 'quay.io/tripleo/keystone' + }, + 'mysql': { + 'start_order': 0, + 'image': 'quay.io/tripleo/mysql' + } + } + result = self.filters.singledict(list) + self.assertEqual(result, expected_dict) + + def test_list_of_keys(self): + keys = [ + { + 'foo1': 'bar1' + }, + { + 'foo2': 'bar2' + }, + ] + expected_list = ['foo1', 'foo2'] + result = self.filters.list_of_keys(keys) + self.assertEqual(result, expected_list) + + def test_haskey(self): + data = [ + { + 'keystone': { + 'start_order': 1, + 'image': 'quay.io/tripleo/keystone', + 'restart': 'always' + }, + }, + { + 'mysql': { + 'start_order': 0, + 'image': 'quay.io/tripleo/mysql' + } + } + ] + expected_list = [ + { + 'keystone': { + 'start_order': 1, + 'image': 'quay.io/tripleo/keystone', + 'restart': 'always' + }, + } + ] + result = self.filters.haskey(batched_container_data=data, + attribute='restart', value='always') + self.assertEqual(result, expected_list) + + def test_haskey_reverse(self): + data = [ + { + 'keystone': { + 'start_order': 1, + 'image': 'quay.io/tripleo/keystone', + 'restart': 'always' + }, + }, + { + 'mysql': { + 'start_order': 0, + 'image': 'quay.io/tripleo/mysql' + } + } + ] + expected_list = [ + { + 'mysql': { + 'start_order': 0, + 'image': 'quay.io/tripleo/mysql' + }, + } + ] + result = self.filters.haskey(batched_container_data=data, + attribute='restart', + value='always', + reverse=True) + self.assertEqual(result, expected_list) + + def test_haskey_any(self): + data = [ + { + 'keystone': { + 'start_order': 1, + 'image': 'quay.io/tripleo/keystone', + 'restart': 'always' + }, + }, + { + 'mysql': { + 'start_order': 0, + 'image': 'quay.io/tripleo/mysql' + } + } + ] + expected_list = [ + { + 'keystone': { + 'start_order': 1, + 'image': 'quay.io/tripleo/keystone', + 'restart': 'always' + }, + } + ] + result = self.filters.haskey(batched_container_data=data, + attribute='restart', + any=True) + self.assertEqual(result, expected_list) + + def test_haskey_any_reverse(self): + data = [ + { + 'keystone': { + 'start_order': 1, + 'image': 'quay.io/tripleo/keystone', + 'restart': 'always' + }, + }, + { + 'mysql': { + 'start_order': 0, + 'image': 'quay.io/tripleo/mysql' + } + } + ] + expected_list = [ + { + 'mysql': { + 'start_order': 0, + 'image': 'quay.io/tripleo/mysql' + }, + } + ] + result = self.filters.haskey(batched_container_data=data, + attribute='restart', + reverse=True, + any=True) + self.assertEqual(result, expected_list) + + def test_needs_delete(self): + data = [ + { + 'Name': 'mysql', + 'Config': { + 'Labels': { + 'config_id': 'dontdeleteme', + 'managed_by': 'triple_ansible', + } + } + }, + { + 'Name': 'rabbitmq', + 'Config': { + 'Labels': { + 'managed_by': 'tripleo_ansible', + 'config_id': 'tripleo_step1', + 'container_name': 'rabbitmq', + 'name': 'rabbitmq', + } + } + }, + { + 'Name': 'swift', + 'Config': { + 'Labels': { + 'managed_by': 'tripleo_ansible', + 'config_id': 'tripleo_step1', + 'container_name': 'swift', + 'name': 'swift', + 'config_data': "{'foo': 'bar'}", + } + } + }, + { + 'Name': 'heat', + 'Config': { + 'Labels': { + 'managed_by': 'tripleo_ansible', + 'config_id': 'tripleo_step1', + 'container_name': 'heat', + 'name': 'heat', + 'config_data': "{'start_order': 0}", + } + } + }, + { + 'Name': 'haproxy', + 'Config': { + 'Labels': { + 'managed_by': 'tripleo_ansible', + 'config_id': 'tripleo_step1', + } + } + }, + { + 'Name': 'tripleo', + 'Config': { + 'Labels': { + 'foo': 'bar' + } + } + }, + { + 'Name': 'none_tripleo', + 'Config': { + 'Labels': None, + } + }, + ] + config = { + # we don't want that container to be touched: no restart + 'mysql': '', + # container has no Config, therefore no Labels: restart needed + 'rabbitmq': '', + # container has no config_data: restart needed + 'haproxy': '', + # container isn't part of config_id: no restart + 'tripleo': '', + # container isn't in container_infos but not part of config_id: + # no restart. + 'doesnt_exist': '', + # config_data didn't change: no restart + 'swift': {'foo': 'bar'}, + # config_data changed: restart needed + 'heat': {'start_order': 1}, + } + expected_list = ['rabbitmq', 'haproxy', 'heat'] + result = self.filters.needs_delete(container_infos=data, + config=config, + config_id='tripleo_step1') + self.assertEqual(result, expected_list) + + def test_container_exec_cmd(self): + data = { + "action": "exec", + "environment": { + "OS_BOOTSTRAP_PASSWORD": "IH7PdaZc5DozbmunSTjMa7", + "KOLLA_BOOTSTRAP": True + }, + "start_order": 3, + "command": [ + "keystone", + "/usr/bin/bootstrap_host_exec", + "keystone", + "keystone-manage", + "bootstrap" + ], + "user": "root" + } + expected_cmd = ['podman', 'exec', '--user=root', + '--env=KOLLA_BOOTSTRAP=True', + '--env=OS_BOOTSTRAP_PASSWORD=IH7PdaZc5DozbmunSTjMa7', + 'keystone', '/usr/bin/bootstrap_host_exec', + 'keystone', 'keystone-manage', 'bootstrap'] + result = self.filters.container_exec_cmd(data=data) + self.assertEqual(result, expected_cmd) diff --git a/zuul.d/layout.yaml b/zuul.d/layout.yaml index 999003392..f9f6ca4e0 100644 --- a/zuul.d/layout.yaml +++ b/zuul.d/layout.yaml @@ -3,6 +3,7 @@ templates: - tripleo-ansible-molecule-jobs - release-notes-jobs-python3 + - openstack-python3-ussuri-jobs check: jobs: - openstack-tox-linters diff --git a/zuul.d/molecule.yaml b/zuul.d/molecule.yaml index 8419eff19..7c6f5a782 100644 --- a/zuul.d/molecule.yaml +++ b/zuul.d/molecule.yaml @@ -38,6 +38,8 @@ - tripleo-ansible-centos-7-molecule-backup-and-restore - tripleo-ansible-centos-7-molecule-tripleo-packages - tripleo-ansible-centos-7-molecule-tripleo-hosts-entries + - tripleo-ansible-centos-7-molecule-tripleo-container-manage + - tripleo-ansible-centos-7-molecule-tripleo-modules gate: jobs: - tripleo-ansible-centos-7-molecule-aide @@ -76,6 +78,8 @@ - tripleo-ansible-centos-7-molecule-backup-and-restore - tripleo-ansible-centos-7-molecule-tripleo-packages - tripleo-ansible-centos-7-molecule-tripleo-hosts-entries + - tripleo-ansible-centos-7-molecule-tripleo-container-manage + - tripleo-ansible-centos-7-molecule-tripleo-modules name: tripleo-ansible-molecule-jobs - job: files: @@ -338,3 +342,18 @@ parent: tripleo-ansible-centos-7-base vars: tripleo_role_name: tripleo-hosts-entries +- job: + files: + - ^tripleo_ansible/roles/tripleo-container-manage/.* + name: tripleo-ansible-centos-7-molecule-tripleo-container-manage + parent: tripleo-ansible-centos-7-base + vars: + tripleo_role_name: tripleo-container-manage + +- job: + files: + - ^tripleo_ansible/ansible_plugins/.*$ + - ^tox.ini + - ^molecule-requirements.txt + name: tripleo-ansible-centos-7-molecule-tripleo-modules + parent: tripleo-ansible-centos-7-base diff --git a/zuul.d/playbooks/run.yml b/zuul.d/playbooks/run.yml index 1ced4073e..69d395906 100644 --- a/zuul.d/playbooks/run.yml +++ b/zuul.d/playbooks/run.yml @@ -4,10 +4,21 @@ environment: ANSIBLE_LOG_PATH: "{{ ansible_user_dir }}/zuul-output/logs/ansible-execution.log" pre_tasks: + - name: Set project path fact set_fact: tripleo_ansible_project_path: "{{ ansible_user_dir }}/{{ zuul.project.src_dir }}" + - name: Set role or plugin path fact + set_fact: + triple_ansible_testdir: "{{ tripleo_ansible_project_path }}/tripleo_ansible/roles/{{ tripleo_role_name }}" + when: tripleo_role_name is defined and tripleo_role_name + + - name: Set role or plugin path fact + set_fact: + triple_ansible_testdir: "{{ tripleo_ansible_project_path }}/tripleo_ansible/ansible_plugins/tests" + when: tripleo_role_name is not defined + - name: Set action plugin path fact set_fact: tripleo_action_plugins_paths: @@ -25,7 +36,7 @@ --ansible-args='{{ tripleo_job_ansible_args | default('') }}' \ {{ tripleo_ansible_project_path }}/tests/test_molecule.py args: - chdir: "{{ tripleo_ansible_project_path }}/tripleo_ansible/roles/{{ tripleo_role_name }}" + chdir: "{{ triple_ansible_testdir }}" executable: /bin/bash environment: ANSIBLE_ACTION_PLUGINS: "{{ tripleo_action_plugins_paths | join(':') }}"