Skip to content
Snippets Groups Projects
kolla_docker_worker.py 33.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • # Copyright 2015 Sam Yaple
    #
    # 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.
    
    # FIXME(yoctozepto): restart_policy is *not* checked in the container
    
    import docker
    import json
    import os
    import shlex
    
    
    from ansible.module_utils.kolla_systemd_worker import SystemdWorker
    
    from distutils.version import StrictVersion
    
    COMPARE_CONFIG_CMD = ['/usr/local/bin/kolla_set_configs', '--check']
    
    
    def get_docker_client():
        return docker.APIClient
    
    
    class DockerWorker(object):
    
        def __init__(self, module):
            self.module = module
            self.params = self.module.params
            self.changed = False
            # Use this to store arguments to pass to exit_json().
            self.result = {}
    
            # TLS not fully implemented
            # tls_config = self.generate_tls()
    
            options = {
                'version': self.params.get('api_version'),
                'timeout': self.params.get('client_timeout'),
            }
    
            self.dc = get_docker_client()(**options)
    
            self._cgroupns_mode_supported = (
                StrictVersion(self.dc._version) >= StrictVersion('1.41'))
    
            self._dimensions_kernel_memory_removed = (
                StrictVersion(self.dc._version) >= StrictVersion('1.42'))
    
            self.systemd = SystemdWorker(self.params)
    
    
        def generate_tls(self):
            tls = {'verify': self.params.get('tls_verify')}
            tls_cert = self.params.get('tls_cert'),
            tls_key = self.params.get('tls_key'),
            tls_cacert = self.params.get('tls_cacert')
    
            if tls['verify']:
                if tls_cert:
                    self.check_file(tls_cert)
                    self.check_file(tls_key)
                    tls['client_cert'] = (tls_cert, tls_key)
                if tls_cacert:
                    self.check_file(tls_cacert)
                    tls['verify'] = tls_cacert
    
            return docker.tls.TLSConfig(**tls)
    
        def check_file(self, path):
            if not os.path.isfile(path):
                self.module.fail_json(
                    failed=True,
                    msg='There is no file at "{}"'.format(path)
                )
            if not os.access(path, os.R_OK):
                self.module.fail_json(
                    failed=True,
                    msg='Permission denied for file at "{}"'.format(path)
                )
    
        def check_image(self):
            find_image = ':'.join(self.parse_image())
            for image in self.dc.images():
                repo_tags = image.get('RepoTags')
                if not repo_tags:
                    continue
                for image_name in repo_tags:
                    if image_name == find_image:
                        return image
    
        def check_volume(self):
            for vol in self.dc.volumes()['Volumes'] or list():
                if vol['Name'] == self.params.get('name'):
                    return vol
    
        def check_container(self):
            find_name = '/{}'.format(self.params.get('name'))
            for cont in self.dc.containers(all=True):
                if find_name in cont['Names']:
                    return cont
    
        def get_container_info(self):
            container = self.check_container()
            if not container:
                return None
            return self.dc.inspect_container(self.params.get('name'))
    
        def compare_container(self):
            container = self.check_container()
            if (not container or
                    self.check_container_differs() or
    
                    self.compare_config() or
                    self.systemd.check_unit_change()):
    
                self.changed = True
            return self.changed
    
        def check_container_differs(self):
            container_info = self.get_container_info()
            return (
                self.compare_cap_add(container_info) or
                self.compare_security_opt(container_info) or
                self.compare_image(container_info) or
                self.compare_ipc_mode(container_info) or
                self.compare_labels(container_info) or
                self.compare_privileged(container_info) or
                self.compare_pid_mode(container_info) or
                self.compare_cgroupns_mode(container_info) or
                self.compare_tmpfs(container_info) or
                self.compare_volumes(container_info) or
                self.compare_volumes_from(container_info) or
                self.compare_environment(container_info) or
                self.compare_container_state(container_info) or
                self.compare_dimensions(container_info) or
                self.compare_command(container_info) or
                self.compare_healthcheck(container_info)
            )
    
        def compare_ipc_mode(self, container_info):
            new_ipc_mode = self.params.get('ipc_mode')
            current_ipc_mode = container_info['HostConfig'].get('IpcMode')
            if not current_ipc_mode:
                current_ipc_mode = None
    
            # only check IPC mode if it is specified
            if new_ipc_mode is not None and new_ipc_mode != current_ipc_mode:
                return True
            return False
    
        def compare_cap_add(self, container_info):
            new_cap_add = self.params.get('cap_add', list())
            current_cap_add = container_info['HostConfig'].get('CapAdd',
                                                               list())
            if not current_cap_add:
                current_cap_add = list()
            if set(new_cap_add).symmetric_difference(set(current_cap_add)):
                return True
    
        def compare_security_opt(self, container_info):
            ipc_mode = self.params.get('ipc_mode')
            pid_mode = self.params.get('pid_mode')
            privileged = self.params.get('privileged', False)
            # NOTE(jeffrey4l) security opt is disabled when using host ipc mode or
            # host pid mode or privileged. So no need to compare security opts
            if ipc_mode == 'host' or pid_mode == 'host' or privileged:
                return False
            new_sec_opt = self.params.get('security_opt', list())
            current_sec_opt = container_info['HostConfig'].get('SecurityOpt',
                                                               list())
            if not current_sec_opt:
                current_sec_opt = list()
            if set(new_sec_opt).symmetric_difference(set(current_sec_opt)):
                return True
    
        def compare_pid_mode(self, container_info):
            new_pid_mode = self.params.get('pid_mode')
            current_pid_mode = container_info['HostConfig'].get('PidMode')
            if not current_pid_mode:
                current_pid_mode = None
    
            if new_pid_mode != current_pid_mode:
                return True
    
        def compare_cgroupns_mode(self, container_info):
            if not self._cgroupns_mode_supported:
                return False
            new_cgroupns_mode = self.params.get('cgroupns_mode')
            if new_cgroupns_mode is None:
                # means we don't care what it is
                return False
            current_cgroupns_mode = (container_info['HostConfig']
                                     .get('CgroupnsMode'))
            if current_cgroupns_mode == '':
                # means the container was created on Docker pre-20.10
                # it behaves like 'host'
                current_cgroupns_mode = 'host'
            return new_cgroupns_mode != current_cgroupns_mode
    
        def compare_privileged(self, container_info):
            new_privileged = self.params.get('privileged')
            current_privileged = container_info['HostConfig']['Privileged']
            if new_privileged != current_privileged:
                return True
    
        def compare_image(self, container_info=None):
            container_info = container_info or self.get_container_info()
            parse_repository_tag = docker.utils.parse_repository_tag
            if not container_info:
                return True
            new_image = self.check_image()
            current_image = container_info['Image']
            if not new_image:
                return True
            if new_image['Id'] != current_image:
                return True
            # NOTE(Jeffrey4l) when new image and the current image have
            # the same id, but the tag name different.
            elif (parse_repository_tag(container_info['Config']['Image']) !=
                  parse_repository_tag(self.params.get('image'))):
                return True
    
        def compare_labels(self, container_info):
            new_labels = self.params.get('labels')
            current_labels = container_info['Config'].get('Labels', dict())
            image_labels = self.check_image().get('Labels', dict())
            for k, v in image_labels.items():
                if k in new_labels:
                    if v != new_labels[k]:
                        return True
                else:
                    del current_labels[k]
    
            if new_labels != current_labels:
                return True
    
        def compare_tmpfs(self, container_info):
            new_tmpfs = self.generate_tmpfs()
            current_tmpfs = container_info['HostConfig'].get('Tmpfs')
            if not new_tmpfs:
                new_tmpfs = []
            if not current_tmpfs:
                current_tmpfs = []
    
            if set(current_tmpfs).symmetric_difference(set(new_tmpfs)):
                return True
    
        def compare_volumes_from(self, container_info):
            new_vols_from = self.params.get('volumes_from')
            current_vols_from = container_info['HostConfig'].get('VolumesFrom')
            if not new_vols_from:
                new_vols_from = list()
            if not current_vols_from:
                current_vols_from = list()
    
            if set(current_vols_from).symmetric_difference(set(new_vols_from)):
                return True
    
        def compare_volumes(self, container_info):
            volumes, binds = self.generate_volumes()
            current_vols = container_info['Config'].get('Volumes')
            current_binds = container_info['HostConfig'].get('Binds')
            if not volumes:
                volumes = list()
            if not current_vols:
                current_vols = list()
            if not current_binds:
                current_binds = list()
    
            if set(volumes).symmetric_difference(set(current_vols)):
                return True
    
            new_binds = list()
            if binds:
                for k, v in binds.items():
                    new_binds.append("{}:{}:{}".format(k, v['bind'], v['mode']))
    
            if set(new_binds).symmetric_difference(set(current_binds)):
                return True
    
        def compare_environment(self, container_info):
            if self.params.get('environment'):
                current_env = dict()
                for kv in container_info['Config'].get('Env', list()):
                    k, v = kv.split('=', 1)
                    current_env.update({k: v})
    
                for k, v in self.params.get('environment').items():
                    if k not in current_env:
                        return True
                    if current_env[k] != v:
                        return True
    
        def compare_container_state(self, container_info):
            new_state = self.params.get('state')
            current_state = container_info['State'].get('Status')
            if new_state != current_state:
                return True
    
        def compare_dimensions(self, container_info):
            new_dimensions = self.params.get('dimensions')
            # NOTE(mgoddard): The names used by Docker are inconsisent between
            # configuration of a container's resources and the resources in
            # container_info['HostConfig']. This provides a mapping between the
            # two.
            dimension_map = {
                'mem_limit': 'Memory', 'mem_reservation': 'MemoryReservation',
                'memswap_limit': 'MemorySwap', 'cpu_period': 'CpuPeriod',
                'cpu_quota': 'CpuQuota', 'cpu_shares': 'CpuShares',
                'cpuset_cpus': 'CpusetCpus', 'cpuset_mems': 'CpusetMems',
    
                'blkio_weight': 'BlkioWeight', 'ulimits': 'Ulimits'}
    
            if not self._dimensions_kernel_memory_removed:
                dimension_map['kernel_memory'] = 'KernelMemory'
    
    
            unsupported = set(new_dimensions.keys()) - \
                set(dimension_map.keys())
            if unsupported:
                self.module.exit_json(
                    failed=True, msg=repr("Unsupported dimensions"),
                    unsupported_dimensions=unsupported)
            current_dimensions = container_info['HostConfig']
            for key1, key2 in dimension_map.items():
                # NOTE(mgoddard): If a resource has been explicitly requested,
                # check for a match. Otherwise, ensure it is set to the default.
                if key1 in new_dimensions:
                    if key1 == 'ulimits':
                        if self.compare_ulimits(new_dimensions[key1],
                                                current_dimensions[key2]):
                            return True
                    elif new_dimensions[key1] != current_dimensions[key2]:
                        return True
                elif current_dimensions[key2]:
                    # The default values of all currently supported resources are
                    # '' or 0 - both falsey.
                    return True
    
        def compare_ulimits(self, new_ulimits, current_ulimits):
            # The new_ulimits is dict, we need make it to a list of Ulimit
            # instance.
            new_ulimits = self.build_ulimits(new_ulimits)
    
            def key(ulimit):
                return ulimit['Name']
    
            if current_ulimits is None:
                current_ulimits = []
            return sorted(new_ulimits, key=key) != sorted(current_ulimits, key=key)
    
        def compare_command(self, container_info):
            new_command = self.params.get('command')
            if new_command is not None:
                new_command_split = shlex.split(new_command)
                new_path = new_command_split[0]
                new_args = new_command_split[1:]
                if (new_path != container_info['Path'] or
                        new_args != container_info['Args']):
                    return True
    
        def compare_healthcheck(self, container_info):
            new_healthcheck = self.parse_healthcheck(
                self.params.get('healthcheck'))
            current_healthcheck = container_info['Config'].get('Healthcheck')
    
            healthcheck_map = {
                'test': 'Test',
                'retries': 'Retries',
                'interval': 'Interval',
                'start_period': 'StartPeriod',
                'timeout': 'Timeout'}
    
            if new_healthcheck:
                new_healthcheck = new_healthcheck['healthcheck']
                if current_healthcheck:
                    new_healthcheck = dict((healthcheck_map.get(k, k), v)
                                           for (k, v) in new_healthcheck.items())
                    return new_healthcheck != current_healthcheck
                else:
                    return True
            else:
                if current_healthcheck:
                    return True
    
        def compare_config(self):
            try:
                job = self.dc.exec_create(
                    self.params['name'],
                    COMPARE_CONFIG_CMD,
                    user='root',
                )
                output = self.dc.exec_start(job)
                exec_inspect = self.dc.exec_inspect(job)
            except docker.errors.APIError as e:
                # NOTE(yoctozepto): If we have a client error, then the container
                # cannot be used for config check (e.g., is restarting, or stopped
                # in the mean time) - assume config is stale = return True.
                # Else, propagate the server error back.
                if e.is_client_error():
                    return True
                else:
                    raise
            # Exit codes:
            # 0: not changed
            # 1: changed
            # 137: abrupt exit -> changed
            # else: error
            if exec_inspect['ExitCode'] == 0:
                return False
            elif exec_inspect['ExitCode'] == 1:
                return True
            elif exec_inspect['ExitCode'] == 137:
                # NOTE(yoctozepto): This is Docker's command exit due to container
                # exit. It means the container is unstable so we are better off
                # marking it as requiring a restart due to config update.
                return True
            else:
                raise Exception('Failed to compare container configuration: '
                                'ExitCode: %s Message: %s' %
                                (exec_inspect['ExitCode'], output))
    
        def parse_image(self):
            full_image = self.params.get('image')
    
            if '/' in full_image:
                registry, image = full_image.split('/', 1)
            else:
                image = full_image
    
            if ':' in image:
                return full_image.rsplit(':', 1)
            else:
                return full_image, 'latest'
    
        def get_image_id(self):
            full_image = self.params.get('image')
    
            image = self.dc.images(name=full_image, quiet=True)
            return image[0] if len(image) == 1 else None
    
        def pull_image(self):
            if self.params.get('auth_username'):
                self.dc.login(
                    username=self.params.get('auth_username'),
                    password=self.params.get('auth_password'),
                    registry=self.params.get('auth_registry'),
                    email=self.params.get('auth_email')
                )
    
            image, tag = self.parse_image()
            old_image_id = self.get_image_id()
    
            statuses = [
                json.loads(line.strip().decode('utf-8')) for line in self.dc.pull(
                    repository=image, tag=tag, stream=True
                )
            ]
    
            for status in reversed(statuses):
                if 'error' in status:
                    if status['error'].endswith('not found'):
                        self.module.fail_json(
                            msg="The requested image does not exist: {}:{}".format(
                                image, tag),
                            failed=True
                        )
                    else:
                        self.module.fail_json(
                            msg="Unknown error message: {}".format(
                                status['error']),
                            failed=True
                        )
    
            new_image_id = self.get_image_id()
            self.changed = old_image_id != new_image_id
    
        def remove_container(self):
    
            self.changed |= self.systemd.remove_unit_file()
    
            if self.check_container():
                self.changed = True
                # NOTE(jeffrey4l): in some case, docker failed to remove container
                # filesystem and raise error.  But the container info is
                # disappeared already. If this happens, assume the container is
                # removed.
                try:
                    self.dc.remove_container(
                        container=self.params.get('name'),
                        force=True
                    )
                except docker.errors.APIError:
                    if self.check_container():
                        raise
    
        def generate_tmpfs(self):
            tmpfs = self.params.get('tmpfs')
            if tmpfs:
                # NOTE(mgoddard): Filter out any empty strings.
                tmpfs = [t for t in tmpfs if t]
            return tmpfs
    
        def generate_volumes(self):
            volumes = self.params.get('volumes')
            if not volumes:
                return None, None
    
            vol_list = list()
            vol_dict = dict()
    
            for vol in volumes:
                if len(vol) == 0:
                    continue
    
                if ':' not in vol:
                    vol_list.append(vol)
                    continue
    
                split_vol = vol.split(':')
    
                if (len(split_vol) == 2 and
                   ('/' not in split_vol[0] or '/' in split_vol[1])):
                    split_vol.append('rw')
    
                vol_list.append(split_vol[1])
                vol_dict.update({
                    split_vol[0]: {
                        'bind': split_vol[1],
                        'mode': split_vol[2]
                    }
                })
    
            return vol_list, vol_dict
    
        def parse_dimensions(self, dimensions):
            # When the data object contains types such as
            # docker.types.Ulimit, Ansible will fail when these are
            # returned via exit_json or fail_json. HostConfig is derived from dict,
            # but its constructor requires additional arguments.
            # to avoid that, here do copy the dimensions and return a new one.
            dimensions = dimensions.copy()
    
            supported = {'cpu_period', 'cpu_quota', 'cpu_shares',
                         'cpuset_cpus', 'cpuset_mems', 'mem_limit',
                         'mem_reservation', 'memswap_limit',
                         'kernel_memory', 'blkio_weight', 'ulimits'}
            unsupported = set(dimensions) - supported
            if unsupported:
                self.module.exit_json(failed=True,
                                      msg=repr("Unsupported dimensions"),
                                      unsupported_dimensions=unsupported)
    
            ulimits = dimensions.get('ulimits')
            if ulimits:
                dimensions['ulimits'] = self.build_ulimits(ulimits)
    
            return dimensions
    
        def build_ulimits(self, ulimits):
            ulimits_opt = []
            for key, value in ulimits.items():
                soft = value.get('soft')
                hard = value.get('hard')
                ulimits_opt.append(docker.types.Ulimit(name=key,
                                                       soft=soft,
                                                       hard=hard))
            return ulimits_opt
    
        def build_host_config(self, binds):
            options = {
                'network_mode': 'host',
                'ipc_mode': self.params.get('ipc_mode'),
                'cap_add': self.params.get('cap_add'),
                'security_opt': self.params.get('security_opt'),
                'pid_mode': self.params.get('pid_mode'),
                'privileged': self.params.get('privileged'),
                'tmpfs': self.generate_tmpfs(),
                'volumes_from': self.params.get('volumes_from')
            }
    
            dimensions = self.params.get('dimensions')
    
            if dimensions:
                dimensions = self.parse_dimensions(dimensions)
                options.update(dimensions)
    
            if binds:
                options['binds'] = binds
    
            host_config = self.dc.create_host_config(**options)
    
            if self._cgroupns_mode_supported:
                # NOTE(yoctozepto): python-docker does not support CgroupnsMode
                # natively so we stuff it in manually.
                cgroupns_mode = self.params.get('cgroupns_mode')
                if cgroupns_mode is not None:
                    host_config['CgroupnsMode'] = cgroupns_mode
    
    
            # detached containers should only log to journald
            if self.params.get('detach'):
                options['log_config'] = docker.types.LogConfig(
                    type=docker.types.LogConfig.types.NONE)
    
    
            return host_config
    
        def _inject_env_var(self, environment_info):
            newenv = {
                'KOLLA_SERVICE_NAME': self.params.get('name').replace('_', '-')
            }
            environment_info.update(newenv)
            return environment_info
    
        def _format_env_vars(self):
            env = self._inject_env_var(self.params.get('environment'))
            return {k: "" if env[k] is None else env[k] for k in env}
    
        def build_container_options(self):
            volumes, binds = self.generate_volumes()
    
            options = {
                'command': self.params.get('command'),
                'detach': self.params.get('detach'),
                'environment': self._format_env_vars(),
                'host_config': self.build_host_config(binds),
                'labels': self.params.get('labels'),
                'image': self.params.get('image'),
                'name': self.params.get('name'),
                'volumes': volumes,
                'tty': self.params.get('tty'),
            }
    
            healthcheck = self.parse_healthcheck(self.params.get('healthcheck'))
            if healthcheck:
                options.update(healthcheck)
    
            return options
    
        def create_container(self):
            self.changed = True
            options = self.build_container_options()
            self.dc.create_container(**options)
    
            if self.params.get('restart_policy') != 'no':
                self.changed |= self.systemd.create_unit_file()
    
    
        def recreate_or_restart_container(self):
            self.changed = True
            container = self.check_container()
            # get config_strategy from env
            environment = self.params.get('environment')
            config_strategy = environment.get('KOLLA_CONFIG_STRATEGY')
    
            if not container:
                self.start_container()
                return
            # If config_strategy is COPY_ONCE or container's parameters are
            # changed, try to start a new one.
            if config_strategy == 'COPY_ONCE' or self.check_container_differs():
                # NOTE(mgoddard): Pull the image if necessary before stopping the
                # container, otherwise a failure to pull the image will leave the
                # container stopped.
                if not self.check_image():
                    self.pull_image()
                self.stop_container()
                self.remove_container()
                self.start_container()
            elif config_strategy == 'COPY_ALWAYS':
                self.restart_container()
    
        def start_container(self):
            if not self.check_image():
                self.pull_image()
    
            container = self.check_container()
            if container and self.check_container_differs():
                self.stop_container()
                self.remove_container()
                container = self.check_container()
    
            if not container:
                self.create_container()
                container = self.check_container()
    
            if not container['Status'].startswith('Up '):
                self.changed = True
    
                if self.params.get('restart_policy') == 'no':
                    self.dc.start(container=self.params.get('name'))
                else:
                    self.systemd.create_unit_file()
                    if not self.systemd.start():
                        self.module.fail_json(
                            changed=True,
                            msg="Container timed out",
                            **self.check_container())
    
    
            # We do not want to detach so we wait around for container to exit
            if not self.params.get('detach'):
                rc = self.dc.wait(self.params.get('name'))
                # NOTE(jeffrey4l): since python docker package 3.0, wait return a
                # dict all the time.
                if isinstance(rc, dict):
                    rc = rc['StatusCode']
                # Include container's return code, standard output and error in the
                # result.
                self.result['rc'] = rc
                self.result['stdout'] = self.dc.logs(self.params.get('name'),
                                                     stdout=True, stderr=False)
                self.result['stderr'] = self.dc.logs(self.params.get('name'),
                                                     stdout=False, stderr=True)
                if self.params.get('remove_on_exit'):
                    self.stop_container()
                    self.remove_container()
                if rc != 0:
                    self.module.fail_json(
                        changed=True,
                        msg="Container exited with non-zero return code %s" % rc,
                        **self.result
                    )
    
        def get_container_env(self):
            name = self.params.get('name')
            info = self.get_container_info()
            if not info:
                self.module.fail_json(msg="No such container: {}".format(name))
            else:
                envs = dict()
                for env in info['Config']['Env']:
                    if '=' in env:
                        key, value = env.split('=', 1)
                    else:
                        key, value = env, ''
                    envs[key] = value
    
                self.module.exit_json(**envs)
    
        def get_container_state(self):
            name = self.params.get('name')
            info = self.get_container_info()
            if not info:
                self.module.fail_json(msg="No such container: {}".format(name))
            else:
                self.module.exit_json(**info['State'])
    
        def parse_healthcheck(self, healthcheck):
            if not healthcheck:
                return None
    
            result = dict(healthcheck={})
    
            # All supported healthcheck parameters
            supported = set(['test', 'interval', 'timeout', 'start_period',
                             'retries'])
            unsupported = set(healthcheck) - supported
            missing = supported - set(healthcheck)
            duration_options = set(['interval', 'timeout', 'start_period'])
    
            if unsupported:
                self.module.exit_json(failed=True,
                                      msg=repr("Unsupported healthcheck options"),
                                      unsupported_healthcheck=unsupported)
    
            if missing:
                self.module.exit_json(failed=True,
                                      msg=repr("Missing healthcheck option"),
                                      missing_healthcheck=missing)
    
            for key in healthcheck:
                value = healthcheck.get(key)
                if key in duration_options:
                    try:
                        result['healthcheck'][key] = int(value) * 1000000000
                    except TypeError:
                        raise TypeError(
                            'Cannot parse healthcheck "{0}". '
                            'Expected an integer, got "{1}".'
                            .format(value, type(value).__name__)
                        )
                    except ValueError:
                        raise ValueError(
                            'Cannot parse healthcheck "{0}". '
                            'Expected an integer, got "{1}".'
                            .format(value, type(value).__name__)
                        )
                else:
                    if key == 'test':
                        # If the user explicitly disables the healthcheck,
                        # return None as the healthcheck object
                        if value in (['NONE'], 'NONE'):
                            return None
                        else:
                            if isinstance(value, (tuple, list)):
                                result['healthcheck'][key] = \
                                    [str(e) for e in value]
                            else:
                                result['healthcheck'][key] = \
                                    ['CMD-SHELL', str(value)]
                    elif key == 'retries':
                        try:
                            result['healthcheck'][key] = int(value)
                        except ValueError:
                            raise ValueError(
                                'Cannot parse healthcheck number of retries.'
                                'Expected an integer, got "{0}".'
                                .format(type(value))
                            )
    
            return result
    
        def stop_container(self):
            name = self.params.get('name')
            graceful_timeout = self.params.get('graceful_timeout')
            if not graceful_timeout:
                graceful_timeout = 10
            container = self.check_container()
            if not container:
                ignore_missing = self.params.get('ignore_missing')
                if not ignore_missing:
                    self.module.fail_json(
                        msg="No such container: {} to stop".format(name))
            elif not container['Status'].startswith('Exited '):
                self.changed = True
    
                if self.params.get('restart_policy') != 'no':
                    self.systemd.create_unit_file()
                    self.systemd.stop()
                else:
                    self.dc.stop(name, timeout=graceful_timeout)
    
    
        def stop_and_remove_container(self):
            container = self.check_container()
            if container:
                self.stop_container()
                self.remove_container()
    
        def restart_container(self):
            name = self.params.get('name')
            graceful_timeout = self.params.get('graceful_timeout')
            if not graceful_timeout:
                graceful_timeout = 10
            info = self.get_container_info()
            if not info:
                self.module.fail_json(
                    msg="No such container: {}".format(name))
            else:
                self.changed = True
    
                self.systemd.create_unit_file()
    
                if not self.systemd.restart():
                    self.module.fail_json(
                        changed=True,
                        msg="Container timed out",
                        **self.check_container())
    
    
        def create_volume(self):
            if not self.check_volume():
                self.changed = True
                self.dc.create_volume(name=self.params.get('name'), driver='local')
    
        def remove_volume(self):
            if self.check_volume():
                self.changed = True
                try:
                    self.dc.remove_volume(name=self.params.get('name'))
                except docker.errors.APIError as e:
                    if e.response.status_code == 409:
                        self.module.fail_json(
                            failed=True,
                            msg="Volume named '{}' is currently in-use".format(
                                self.params.get('name')
                            )
                        )
                    raise
    
        def remove_image(self):
            if self.check_image():
                self.changed = True
                try:
                    self.dc.remove_image(image=self.params.get('image'))
                except docker.errors.APIError as e:
                    if e.response.status_code == 409:
                        self.module.fail_json(
                            failed=True,
                            msg="Image '{}' is currently in-use".format(
                                self.params.get('image')
                            )
                        )
                    elif e.response.status_code == 500:
                        self.module.fail_json(
                            failed=True,
                            msg="Server error"
                        )
                    raise
    
        def ensure_image(self):
            if not self.check_image():
                self.pull_image()