# Copyright (c) 2022 Felix Fontein # Copyright 2016 Red Hat | Ansible # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import absolute_import, division, print_function __metaclass__ = type import abc import os import re import shlex from functools import partial from ansible.module_utils.common.text.converters import to_native, to_text from ansible.module_utils.common.text.formatters import human_to_bytes from ansible.module_utils.six import string_types from ansible_collections.community.docker.plugins.module_utils.util import ( clean_dict_booleans_for_docker_api, compare_generic, normalize_healthcheck, omit_none_from_dict, ) from ansible_collections.community.docker.plugins.module_utils._platform import ( compare_platform_strings, ) from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import ( parse_env_file, ) _DEFAULT_IP_REPLACEMENT_STRING = '[[DEFAULT_IP:iewahhaeB4Sae6Aen8IeShairoh4zeph7xaekoh8Geingunaesaeweiy3ooleiwi]]' _MOUNT_OPTION_TYPES = dict( volume_driver='volume', volume_options='volume', propagation='bind', no_copy='volume', labels='volume', tmpfs_size='tmpfs', tmpfs_mode='tmpfs', ) def _get_ansible_type(type): if type == 'set': return 'list' if type not in ('list', 'dict', 'bool', 'int', 'float', 'str'): raise Exception('Invalid type "%s"' % (type, )) return type class Option(object): def __init__( self, name, type, owner, ansible_type=None, elements=None, ansible_elements=None, ansible_suboptions=None, ansible_aliases=None, ansible_choices=None, needs_no_suboptions=False, default_comparison=None, not_a_container_option=False, not_an_ansible_option=False, copy_comparison_from=None, compare=None, ): self.name = name self.type = type self.ansible_type = ansible_type or _get_ansible_type(type) needs_elements = self.type in ('list', 'set') needs_ansible_elements = self.ansible_type in ('list', ) if elements is not None and not needs_elements: raise Exception('elements only allowed for lists/sets') if elements is None and needs_elements: raise Exception('elements required for lists/sets') if ansible_elements is not None and not needs_ansible_elements: raise Exception('Ansible elements only allowed for Ansible lists') if (elements is None and ansible_elements is None) and needs_ansible_elements: raise Exception('Ansible elements required for Ansible lists') self.elements = elements if needs_elements else None self.ansible_elements = (ansible_elements or _get_ansible_type(elements)) if needs_ansible_elements else None needs_suboptions = (self.ansible_type == 'list' and self.ansible_elements == 'dict') or (self.ansible_type == 'dict') if ansible_suboptions is not None and not needs_suboptions: raise Exception('suboptions only allowed for Ansible lists with dicts, or Ansible dicts') if ansible_suboptions is None and needs_suboptions and not needs_no_suboptions and not not_an_ansible_option: raise Exception('suboptions required for Ansible lists with dicts, or Ansible dicts') self.ansible_suboptions = ansible_suboptions if needs_suboptions else None self.ansible_aliases = ansible_aliases or [] self.ansible_choices = ansible_choices comparison_type = self.type if comparison_type == 'set' and self.elements == 'dict': comparison_type = 'set(dict)' elif comparison_type not in ('set', 'list', 'dict'): comparison_type = 'value' self.comparison_type = comparison_type if default_comparison is not None: self.comparison = default_comparison elif comparison_type in ('list', 'value'): self.comparison = 'strict' else: self.comparison = 'allow_more_present' self.not_a_container_option = not_a_container_option self.not_an_ansible_option = not_an_ansible_option self.copy_comparison_from = copy_comparison_from self.compare = ( lambda param_value, container_value: compare(self, param_value, container_value) ) if compare else ( lambda param_value, container_value: compare_generic(param_value, container_value, self.comparison, self.comparison_type) ) class OptionGroup(object): def __init__( self, preprocess=None, ansible_mutually_exclusive=None, ansible_required_together=None, ansible_required_one_of=None, ansible_required_if=None, ansible_required_by=None, ): if preprocess is None: def preprocess(module, values): return values self.preprocess = preprocess self.options = [] self.all_options = [] self.engines = {} self.ansible_mutually_exclusive = ansible_mutually_exclusive or [] self.ansible_required_together = ansible_required_together or [] self.ansible_required_one_of = ansible_required_one_of or [] self.ansible_required_if = ansible_required_if or [] self.ansible_required_by = ansible_required_by or {} self.argument_spec = {} def add_option(self, *args, **kwargs): option = Option(*args, owner=self, **kwargs) if not option.not_a_container_option: self.options.append(option) self.all_options.append(option) if not option.not_an_ansible_option: ansible_option = { 'type': option.ansible_type, } if option.ansible_elements is not None: ansible_option['elements'] = option.ansible_elements if option.ansible_suboptions is not None: ansible_option['options'] = option.ansible_suboptions if option.ansible_aliases: ansible_option['aliases'] = option.ansible_aliases if option.ansible_choices is not None: ansible_option['choices'] = option.ansible_choices self.argument_spec[option.name] = ansible_option return self def supports_engine(self, engine_name): return engine_name in self.engines def get_engine(self, engine_name): return self.engines[engine_name] def add_engine(self, engine_name, engine): self.engines[engine_name] = engine return self class Engine(object): min_api_version = None # string or None min_api_version_obj = None # LooseVersion object or None @abc.abstractmethod def get_value(self, module, container, api_version, options, image, host_info): pass def compare_value(self, option, param_value, container_value): return option.compare(param_value, container_value) @abc.abstractmethod def set_value(self, module, data, api_version, options, values): pass @abc.abstractmethod def get_expected_values(self, module, client, api_version, options, image, values, host_info): pass @abc.abstractmethod def ignore_mismatching_result(self, module, client, api_version, option, image, container_value, expected_value): pass @abc.abstractmethod def preprocess_value(self, module, client, api_version, options, values): pass @abc.abstractmethod def update_value(self, module, data, api_version, options, values): pass @abc.abstractmethod def can_set_value(self, api_version): pass @abc.abstractmethod def can_update_value(self, api_version): pass @abc.abstractmethod def needs_container_image(self, values): pass @abc.abstractmethod def needs_host_info(self, values): pass class EngineDriver(object): name = None # string @abc.abstractmethod def setup(self, argument_spec, mutually_exclusive=None, required_together=None, required_one_of=None, required_if=None, required_by=None): # Return (module, active_options, client) pass @abc.abstractmethod def get_host_info(self, client): pass @abc.abstractmethod def get_api_version(self, client): pass @abc.abstractmethod def get_container_id(self, container): pass @abc.abstractmethod def get_image_from_container(self, container): pass @abc.abstractmethod def get_image_name_from_container(self, container): pass @abc.abstractmethod def is_container_removing(self, container): pass @abc.abstractmethod def is_container_running(self, container): pass @abc.abstractmethod def is_container_paused(self, container): pass @abc.abstractmethod def inspect_container_by_name(self, client, container_name): pass @abc.abstractmethod def inspect_container_by_id(self, client, container_id): pass @abc.abstractmethod def inspect_image_by_id(self, client, image_id): pass @abc.abstractmethod def inspect_image_by_name(self, client, repository, tag): pass @abc.abstractmethod def pull_image(self, client, repository, tag, platform=None): pass @abc.abstractmethod def pause_container(self, client, container_id): pass @abc.abstractmethod def unpause_container(self, client, container_id): pass @abc.abstractmethod def disconnect_container_from_network(self, client, container_id, network_id): pass @abc.abstractmethod def connect_container_to_network(self, client, container_id, network_id, parameters=None): pass @abc.abstractmethod def create_container(self, client, container_name, create_parameters): pass @abc.abstractmethod def start_container(self, client, container_id): pass @abc.abstractmethod def wait_for_container(self, client, container_id, timeout=None): pass @abc.abstractmethod def get_container_output(self, client, container_id): pass @abc.abstractmethod def update_container(self, client, container_id, update_parameters): pass @abc.abstractmethod def restart_container(self, client, container_id, timeout=None): pass @abc.abstractmethod def kill_container(self, client, container_id, kill_signal=None): pass @abc.abstractmethod def stop_container(self, client, container_id, timeout=None): pass @abc.abstractmethod def remove_container(self, client, container_id, remove_volumes=False, link=False, force=False): pass @abc.abstractmethod def run(self, runner, client): pass def _is_volume_permissions(mode): for part in mode.split(','): if part not in ('rw', 'ro', 'z', 'Z', 'consistent', 'delegated', 'cached', 'rprivate', 'private', 'rshared', 'shared', 'rslave', 'slave', 'nocopy'): return False return True def _parse_port_range(range_or_port, module): ''' Parses a string containing either a single port or a range of ports. Returns a list of integers for each port in the list. ''' if '-' in range_or_port: try: start, end = [int(port) for port in range_or_port.split('-')] except Exception: module.fail_json(msg='Invalid port range: "{0}"'.format(range_or_port)) if end < start: module.fail_json(msg='Invalid port range: "{0}"'.format(range_or_port)) return list(range(start, end + 1)) else: try: return [int(range_or_port)] except Exception: module.fail_json(msg='Invalid port: "{0}"'.format(range_or_port)) def _split_colon_ipv6(text, module): ''' Split string by ':', while keeping IPv6 addresses in square brackets in one component. ''' if '[' not in text: return text.split(':') start = 0 result = [] while start < len(text): i = text.find('[', start) if i < 0: result.extend(text[start:].split(':')) break j = text.find(']', i) if j < 0: module.fail_json(msg='Cannot find closing "]" in input "{0}" for opening "[" at index {1}!'.format(text, i + 1)) result.extend(text[start:i].split(':')) k = text.find(':', j) if k < 0: result[-1] += text[i:] start = len(text) else: result[-1] += text[i:k] if k == len(text): result.append('') break start = k + 1 return result def _preprocess_command(module, values): if 'command' not in values: return values value = values['command'] if module.params['command_handling'] == 'correct': if value is not None: if not isinstance(value, list): # convert from str to list value = shlex.split(to_text(value, errors='surrogate_or_strict')) value = [to_text(x, errors='surrogate_or_strict') for x in value] elif value: # convert from list to str if isinstance(value, list): value = shlex.split(' '.join([to_text(x, errors='surrogate_or_strict') for x in value])) value = [to_text(x, errors='surrogate_or_strict') for x in value] else: value = shlex.split(to_text(value, errors='surrogate_or_strict')) value = [to_text(x, errors='surrogate_or_strict') for x in value] else: return {} return { 'command': value, } def _preprocess_entrypoint(module, values): if 'entrypoint' not in values: return values value = values['entrypoint'] if module.params['command_handling'] == 'correct': if value is not None: value = [to_text(x, errors='surrogate_or_strict') for x in value] elif value: # convert from list to str. value = shlex.split(' '.join([to_text(x, errors='surrogate_or_strict') for x in value])) value = [to_text(x, errors='surrogate_or_strict') for x in value] else: return {} return { 'entrypoint': value, } def _preprocess_env(module, values): if not values: return {} final_env = {} if 'env_file' in values: parsed_env_file = parse_env_file(values['env_file']) for name, value in parsed_env_file.items(): final_env[name] = to_text(value, errors='surrogate_or_strict') if 'env' in values: for name, value in values['env'].items(): if not isinstance(value, string_types): module.fail_json(msg='Non-string value found for env option. Ambiguous env options must be ' 'wrapped in quotes to avoid them being interpreted. Key: %s' % (name, )) final_env[name] = to_text(value, errors='surrogate_or_strict') formatted_env = [] for key, value in final_env.items(): formatted_env.append('%s=%s' % (key, value)) return { 'env': formatted_env, } def _preprocess_healthcheck(module, values): if not values: return {} return { 'healthcheck': normalize_healthcheck(values['healthcheck'], normalize_test=False), } def _preprocess_convert_to_bytes(module, values, name, unlimited_value=None): if name not in values: return values try: value = values[name] if unlimited_value is not None and value in ('unlimited', str(unlimited_value)): value = unlimited_value else: value = human_to_bytes(value) values[name] = value return values except ValueError as exc: module.fail_json(msg='Failed to convert %s to bytes: %s' % (name, to_native(exc))) def _preprocess_mac_address(module, values): if 'mac_address' not in values: return values return { 'mac_address': values['mac_address'].replace('-', ':'), } def _preprocess_networks(module, values): if module.params['networks_cli_compatible'] is True and values.get('networks') and 'network_mode' not in values: # Same behavior as Docker CLI: if networks are specified, use the name of the first network as the value for network_mode # (assuming no explicit value is specified for network_mode) values['network_mode'] = values['networks'][0]['name'] if 'networks' in values: for network in values['networks']: if network['links']: parsed_links = [] for link in network['links']: parsed_link = link.split(':', 1) if len(parsed_link) == 1: parsed_link = (link, link) parsed_links.append(tuple(parsed_link)) network['links'] = parsed_links return values def _preprocess_sysctls(module, values): if 'sysctls' in values: for key, value in values['sysctls'].items(): values['sysctls'][key] = to_text(value, errors='surrogate_or_strict') return values def _preprocess_tmpfs(module, values): if 'tmpfs' not in values: return values result = {} for tmpfs_spec in values['tmpfs']: split_spec = tmpfs_spec.split(":", 1) if len(split_spec) > 1: result[split_spec[0]] = split_spec[1] else: result[split_spec[0]] = "" return { 'tmpfs': result } def _preprocess_ulimits(module, values): if 'ulimits' not in values: return values result = [] for limit in values['ulimits']: limits = dict() pieces = limit.split(':') if len(pieces) >= 2: limits['Name'] = pieces[0] limits['Soft'] = int(pieces[1]) limits['Hard'] = int(pieces[1]) if len(pieces) == 3: limits['Hard'] = int(pieces[2]) result.append(limits) return { 'ulimits': result, } def _preprocess_mounts(module, values): last = dict() def check_collision(t, name): if t in last: if name == last[t]: module.fail_json(msg='The mount point "{0}" appears twice in the {1} option'.format(t, name)) else: module.fail_json(msg='The mount point "{0}" appears both in the {1} and {2} option'.format(t, name, last[t])) last[t] = name if 'mounts' in values: mounts = [] for mount in values['mounts']: target = mount['target'] mount_type = mount['type'] check_collision(target, 'mounts') mount_dict = dict(mount) # Sanity checks if mount['source'] is None and mount_type not in ('tmpfs', 'volume'): module.fail_json(msg='source must be specified for mount "{0}" of type "{1}"'.format(target, mount_type)) for option, req_mount_type in _MOUNT_OPTION_TYPES.items(): if mount[option] is not None and mount_type != req_mount_type: module.fail_json( msg='{0} cannot be specified for mount "{1}" of type "{2}" (needs type "{3}")'.format(option, target, mount_type, req_mount_type) ) # Streamline options volume_options = mount_dict.pop('volume_options') if mount_dict['volume_driver'] and volume_options: mount_dict['volume_options'] = clean_dict_booleans_for_docker_api(volume_options) if mount_dict['labels']: mount_dict['labels'] = clean_dict_booleans_for_docker_api(mount_dict['labels']) if mount_dict['tmpfs_size'] is not None: try: mount_dict['tmpfs_size'] = human_to_bytes(mount_dict['tmpfs_size']) except ValueError as exc: module.fail_json(msg='Failed to convert tmpfs_size of mount "{0}" to bytes: {1}'.format(target, to_native(exc))) if mount_dict['tmpfs_mode'] is not None: try: mount_dict['tmpfs_mode'] = int(mount_dict['tmpfs_mode'], 8) except Exception as dummy: module.fail_json(msg='tmp_fs mode of mount "{0}" is not an octal string!'.format(target)) # Add result to list mounts.append(omit_none_from_dict(mount_dict)) values['mounts'] = mounts if 'volumes' in values: new_vols = [] for vol in values['volumes']: parts = vol.split(':') if ':' in vol: if len(parts) == 3: host, container, mode = parts if not _is_volume_permissions(mode): module.fail_json(msg='Found invalid volumes mode: {0}'.format(mode)) if re.match(r'[.~]', host): host = os.path.abspath(os.path.expanduser(host)) check_collision(container, 'volumes') new_vols.append("%s:%s:%s" % (host, container, mode)) continue elif len(parts) == 2: if not _is_volume_permissions(parts[1]) and re.match(r'[.~]', parts[0]): host = os.path.abspath(os.path.expanduser(parts[0])) check_collision(parts[1], 'volumes') new_vols.append("%s:%s:rw" % (host, parts[1])) continue check_collision(parts[min(1, len(parts) - 1)], 'volumes') new_vols.append(vol) values['volumes'] = new_vols new_binds = [] for vol in new_vols: host = None if ':' in vol: parts = vol.split(':') if len(parts) == 3: host, container, mode = parts if not _is_volume_permissions(mode): module.fail_json(msg='Found invalid volumes mode: {0}'.format(mode)) elif len(parts) == 2: if not _is_volume_permissions(parts[1]): host, container, mode = (parts + ['rw']) if host is not None: new_binds.append('%s:%s:%s' % (host, container, mode)) values['volume_binds'] = new_binds return values def _preprocess_log(module, values): result = {} if 'log_driver' not in values: return result result['log_driver'] = values['log_driver'] if 'log_options' in values: options = {} for k, v in values['log_options'].items(): if not isinstance(v, string_types): module.warn( "Non-string value found for log_options option '%s'. The value is automatically converted to '%s'. " "If this is not correct, or you want to avoid such warnings, please quote the value." % ( k, to_text(v, errors='surrogate_or_strict')) ) v = to_text(v, errors='surrogate_or_strict') options[k] = v result['log_options'] = options return result def _preprocess_ports(module, values): if 'published_ports' in values: if 'all' in values['published_ports']: module.fail_json( msg='Specifying "all" in published_ports is no longer allowed. Set publish_all_ports to "true" instead ' 'to randomly assign port mappings for those not specified by published_ports.') binds = {} for port in values['published_ports']: parts = _split_colon_ipv6(to_text(port, errors='surrogate_or_strict'), module) container_port = parts[-1] protocol = '' if '/' in container_port: container_port, protocol = parts[-1].split('/') container_ports = _parse_port_range(container_port, module) p_len = len(parts) if p_len == 1: port_binds = len(container_ports) * [(_DEFAULT_IP_REPLACEMENT_STRING, )] elif p_len == 2: if len(container_ports) == 1: port_binds = [(_DEFAULT_IP_REPLACEMENT_STRING, parts[0])] else: port_binds = [(_DEFAULT_IP_REPLACEMENT_STRING, port) for port in _parse_port_range(parts[0], module)] elif p_len == 3: # We only allow IPv4 and IPv6 addresses for the bind address ipaddr = parts[0] if not re.match(r'^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$', parts[0]) and not re.match(r'^\[[0-9a-fA-F:]+(?:|%[^\]/]+)\]$', ipaddr): module.fail_json( msg='Bind addresses for published ports must be IPv4 or IPv6 addresses, not hostnames. ' 'Use the dig lookup to resolve hostnames. (Found hostname: {0})'.format(ipaddr) ) if re.match(r'^\[[0-9a-fA-F:]+\]$', ipaddr): ipaddr = ipaddr[1:-1] if parts[1]: if len(container_ports) == 1: port_binds = [(ipaddr, parts[1])] else: port_binds = [(ipaddr, port) for port in _parse_port_range(parts[1], module)] else: port_binds = len(container_ports) * [(ipaddr,)] else: module.fail_json( msg='Invalid port description "%s" - expected 1 to 3 colon-separated parts, but got %d. ' 'Maybe you forgot to use square brackets ([...]) around an IPv6 address?' % (port, p_len) ) for bind, container_port in zip(port_binds, container_ports): idx = '{0}/{1}'.format(container_port, protocol) if protocol else container_port if idx in binds: old_bind = binds[idx] if isinstance(old_bind, list): old_bind.append(bind) else: binds[idx] = [old_bind, bind] else: binds[idx] = bind values['published_ports'] = binds exposed = [] if 'exposed_ports' in values: for port in values['exposed_ports']: port = to_text(port, errors='surrogate_or_strict').strip() protocol = 'tcp' match = re.search(r'(/.+$)', port) if match: protocol = match.group(1).replace('/', '') port = re.sub(r'/.+$', '', port) exposed.append((port, protocol)) if 'published_ports' in values: # Any published port should also be exposed for publish_port in values['published_ports']: match = False if isinstance(publish_port, string_types) and '/' in publish_port: port, protocol = publish_port.split('/') port = int(port) else: protocol = 'tcp' port = int(publish_port) for exposed_port in exposed: if exposed_port[1] != protocol: continue if isinstance(exposed_port[0], string_types) and '-' in exposed_port[0]: start_port, end_port = exposed_port[0].split('-') if int(start_port) <= port <= int(end_port): match = True elif exposed_port[0] == port: match = True if not match: exposed.append((port, protocol)) values['ports'] = exposed return values def _compare_platform(option, param_value, container_value): if option.comparison == 'ignore': return True try: return compare_platform_strings(param_value, container_value) except ValueError: return param_value == container_value OPTION_AUTO_REMOVE = ( OptionGroup() .add_option('auto_remove', type='bool') ) OPTION_BLKIO_WEIGHT = ( OptionGroup() .add_option('blkio_weight', type='int') ) OPTION_CAPABILITIES = ( OptionGroup() .add_option('capabilities', type='set', elements='str') ) OPTION_CAP_DROP = ( OptionGroup() .add_option('cap_drop', type='set', elements='str') ) OPTION_CGROUP_NS_MODE = ( OptionGroup() .add_option('cgroupns_mode', type='str', ansible_choices=['private', 'host']) ) OPTION_CGROUP_PARENT = ( OptionGroup() .add_option('cgroup_parent', type='str') ) OPTION_COMMAND = ( OptionGroup(preprocess=_preprocess_command) .add_option('command', type='list', elements='str', ansible_type='raw') ) OPTION_CPU_PERIOD = ( OptionGroup() .add_option('cpu_period', type='int') ) OPTION_CPU_QUOTA = ( OptionGroup() .add_option('cpu_quota', type='int') ) OPTION_CPUSET_CPUS = ( OptionGroup() .add_option('cpuset_cpus', type='str') ) OPTION_CPUSET_MEMS = ( OptionGroup() .add_option('cpuset_mems', type='str') ) OPTION_CPU_SHARES = ( OptionGroup() .add_option('cpu_shares', type='int') ) OPTION_ENTRYPOINT = ( OptionGroup(preprocess=_preprocess_entrypoint) .add_option('entrypoint', type='list', elements='str') ) OPTION_CPUS = ( OptionGroup() .add_option('cpus', type='int', ansible_type='float') ) OPTION_DETACH_INTERACTIVE = ( OptionGroup() .add_option('detach', type='bool') .add_option('interactive', type='bool') ) OPTION_DEVICES = ( OptionGroup() .add_option('devices', type='set', elements='dict', ansible_elements='str') ) OPTION_DEVICE_READ_BPS = ( OptionGroup() .add_option('device_read_bps', type='set', elements='dict', ansible_suboptions=dict( path=dict(required=True, type='str'), rate=dict(required=True, type='str'), )) ) OPTION_DEVICE_WRITE_BPS = ( OptionGroup() .add_option('device_write_bps', type='set', elements='dict', ansible_suboptions=dict( path=dict(required=True, type='str'), rate=dict(required=True, type='str'), )) ) OPTION_DEVICE_READ_IOPS = ( OptionGroup() .add_option('device_read_iops', type='set', elements='dict', ansible_suboptions=dict( path=dict(required=True, type='str'), rate=dict(required=True, type='int'), )) ) OPTION_DEVICE_WRITE_IOPS = ( OptionGroup() .add_option('device_write_iops', type='set', elements='dict', ansible_suboptions=dict( path=dict(required=True, type='str'), rate=dict(required=True, type='int'), )) ) OPTION_DEVICE_REQUESTS = ( OptionGroup() .add_option('device_requests', type='set', elements='dict', ansible_suboptions=dict( capabilities=dict(type='list', elements='list'), count=dict(type='int'), device_ids=dict(type='list', elements='str'), driver=dict(type='str'), options=dict(type='dict'), )) ) OPTION_DNS_SERVERS = ( OptionGroup() .add_option('dns_servers', type='list', elements='str') ) OPTION_DNS_OPTS = ( OptionGroup() .add_option('dns_opts', type='set', elements='str') ) OPTION_DNS_SEARCH_DOMAINS = ( OptionGroup() .add_option('dns_search_domains', type='list', elements='str') ) OPTION_DOMAINNAME = ( OptionGroup() .add_option('domainname', type='str') ) OPTION_ENVIRONMENT = ( OptionGroup(preprocess=_preprocess_env) .add_option('env', type='set', ansible_type='dict', elements='str', needs_no_suboptions=True) .add_option('env_file', type='set', ansible_type='path', elements='str', not_a_container_option=True) ) OPTION_ETC_HOSTS = ( OptionGroup() .add_option('etc_hosts', type='set', ansible_type='dict', elements='str', needs_no_suboptions=True) ) OPTION_GROUPS = ( OptionGroup() .add_option('groups', type='set', elements='str') ) OPTION_HEALTHCHECK = ( OptionGroup(preprocess=_preprocess_healthcheck) .add_option('healthcheck', type='dict', ansible_suboptions=dict( test=dict(type='raw'), interval=dict(type='str'), timeout=dict(type='str'), start_period=dict(type='str'), retries=dict(type='int'), )) ) OPTION_HOSTNAME = ( OptionGroup() .add_option('hostname', type='str') ) OPTION_IMAGE = ( OptionGroup(preprocess=_preprocess_networks) .add_option('image', type='str') ) OPTION_INIT = ( OptionGroup() .add_option('init', type='bool') ) OPTION_IPC_MODE = ( OptionGroup() .add_option('ipc_mode', type='str') ) OPTION_KERNEL_MEMORY = ( OptionGroup(preprocess=partial(_preprocess_convert_to_bytes, name='kernel_memory')) .add_option('kernel_memory', type='int', ansible_type='str') ) OPTION_LABELS = ( OptionGroup() .add_option('labels', type='dict', needs_no_suboptions=True) ) OPTION_LINKS = ( OptionGroup() .add_option('links', type='set', elements='list', ansible_elements='str') ) OPTION_LOG_DRIVER_OPTIONS = ( OptionGroup(preprocess=_preprocess_log, ansible_required_by={'log_options': ['log_driver']}) .add_option('log_driver', type='str') .add_option('log_options', type='dict', ansible_aliases=['log_opt'], needs_no_suboptions=True) ) OPTION_MAC_ADDRESS = ( OptionGroup(preprocess=_preprocess_mac_address) .add_option('mac_address', type='str') ) OPTION_MEMORY = ( OptionGroup(preprocess=partial(_preprocess_convert_to_bytes, name='memory')) .add_option('memory', type='int', ansible_type='str') ) OPTION_MEMORY_RESERVATION = ( OptionGroup(preprocess=partial(_preprocess_convert_to_bytes, name='memory_reservation')) .add_option('memory_reservation', type='int', ansible_type='str') ) OPTION_MEMORY_SWAP = ( OptionGroup(preprocess=partial(_preprocess_convert_to_bytes, name='memory_swap', unlimited_value=-1)) .add_option('memory_swap', type='int', ansible_type='str') ) OPTION_MEMORY_SWAPPINESS = ( OptionGroup() .add_option('memory_swappiness', type='int') ) OPTION_STOP_TIMEOUT = ( OptionGroup() .add_option('stop_timeout', type='int', default_comparison='ignore') ) OPTION_NETWORK = ( OptionGroup(preprocess=_preprocess_networks) .add_option('network_mode', type='str') .add_option('networks', type='set', elements='dict', ansible_suboptions=dict( name=dict(type='str', required=True), ipv4_address=dict(type='str'), ipv6_address=dict(type='str'), aliases=dict(type='list', elements='str'), links=dict(type='list', elements='str'), )) ) OPTION_OOM_KILLER = ( OptionGroup() .add_option('oom_killer', type='bool') ) OPTION_OOM_SCORE_ADJ = ( OptionGroup() .add_option('oom_score_adj', type='int') ) OPTION_PID_MODE = ( OptionGroup() .add_option('pid_mode', type='str') ) OPTION_PIDS_LIMIT = ( OptionGroup() .add_option('pids_limit', type='int') ) OPTION_PLATFORM = ( OptionGroup() .add_option('platform', type='str', compare=_compare_platform) ) OPTION_PRIVILEGED = ( OptionGroup() .add_option('privileged', type='bool') ) OPTION_READ_ONLY = ( OptionGroup() .add_option('read_only', type='bool') ) OPTION_RESTART_POLICY = ( OptionGroup(ansible_required_by={'restart_retries': ['restart_policy']}) .add_option('restart_policy', type='str', ansible_choices=['no', 'on-failure', 'always', 'unless-stopped']) .add_option('restart_retries', type='int') ) OPTION_RUNTIME = ( OptionGroup() .add_option('runtime', type='str') ) OPTION_SECURITY_OPTS = ( OptionGroup() .add_option('security_opts', type='set', elements='str') ) OPTION_SHM_SIZE = ( OptionGroup(preprocess=partial(_preprocess_convert_to_bytes, name='shm_size')) .add_option('shm_size', type='int', ansible_type='str') ) OPTION_STOP_SIGNAL = ( OptionGroup() .add_option('stop_signal', type='str') ) OPTION_STORAGE_OPTS = ( OptionGroup() .add_option('storage_opts', type='dict', needs_no_suboptions=True) ) OPTION_SYSCTLS = ( OptionGroup(preprocess=_preprocess_sysctls) .add_option('sysctls', type='dict', needs_no_suboptions=True) ) OPTION_TMPFS = ( OptionGroup(preprocess=_preprocess_tmpfs) .add_option('tmpfs', type='dict', ansible_type='list', ansible_elements='str') ) OPTION_TTY = ( OptionGroup() .add_option('tty', type='bool') ) OPTION_ULIMITS = ( OptionGroup(preprocess=_preprocess_ulimits) .add_option('ulimits', type='set', elements='dict', ansible_elements='str') ) OPTION_USER = ( OptionGroup() .add_option('user', type='str') ) OPTION_USERNS_MODE = ( OptionGroup() .add_option('userns_mode', type='str') ) OPTION_UTS = ( OptionGroup() .add_option('uts', type='str') ) OPTION_VOLUME_DRIVER = ( OptionGroup() .add_option('volume_driver', type='str') ) OPTION_VOLUMES_FROM = ( OptionGroup() .add_option('volumes_from', type='set', elements='str') ) OPTION_WORKING_DIR = ( OptionGroup() .add_option('working_dir', type='str') ) OPTION_MOUNTS_VOLUMES = ( OptionGroup(preprocess=_preprocess_mounts) .add_option('mounts', type='set', elements='dict', ansible_suboptions=dict( target=dict(type='str', required=True), source=dict(type='str'), type=dict(type='str', choices=['bind', 'volume', 'tmpfs', 'npipe'], default='volume'), read_only=dict(type='bool'), consistency=dict(type='str', choices=['default', 'consistent', 'cached', 'delegated']), propagation=dict(type='str', choices=['private', 'rprivate', 'shared', 'rshared', 'slave', 'rslave']), no_copy=dict(type='bool'), labels=dict(type='dict'), volume_driver=dict(type='str'), volume_options=dict(type='dict'), tmpfs_size=dict(type='str'), tmpfs_mode=dict(type='str'), )) .add_option('volumes', type='set', elements='str') .add_option('volume_binds', type='set', elements='str', not_an_ansible_option=True, copy_comparison_from='volumes') ) OPTION_PORTS = ( OptionGroup(preprocess=_preprocess_ports) .add_option('exposed_ports', type='set', elements='str', ansible_aliases=['exposed', 'expose']) .add_option('publish_all_ports', type='bool') .add_option('published_ports', type='dict', ansible_type='list', ansible_elements='str', ansible_aliases=['ports']) .add_option('ports', type='set', elements='str', not_an_ansible_option=True, default_comparison='ignore') ) OPTIONS = [ OPTION_AUTO_REMOVE, OPTION_BLKIO_WEIGHT, OPTION_CAPABILITIES, OPTION_CAP_DROP, OPTION_CGROUP_NS_MODE, OPTION_CGROUP_PARENT, OPTION_COMMAND, OPTION_CPU_PERIOD, OPTION_CPU_QUOTA, OPTION_CPUSET_CPUS, OPTION_CPUSET_MEMS, OPTION_CPU_SHARES, OPTION_ENTRYPOINT, OPTION_CPUS, OPTION_DETACH_INTERACTIVE, OPTION_DEVICES, OPTION_DEVICE_READ_BPS, OPTION_DEVICE_WRITE_BPS, OPTION_DEVICE_READ_IOPS, OPTION_DEVICE_WRITE_IOPS, OPTION_DEVICE_REQUESTS, OPTION_DNS_SERVERS, OPTION_DNS_OPTS, OPTION_DNS_SEARCH_DOMAINS, OPTION_DOMAINNAME, OPTION_ENVIRONMENT, OPTION_ETC_HOSTS, OPTION_GROUPS, OPTION_HEALTHCHECK, OPTION_HOSTNAME, OPTION_IMAGE, OPTION_INIT, OPTION_IPC_MODE, OPTION_KERNEL_MEMORY, OPTION_LABELS, OPTION_LINKS, OPTION_LOG_DRIVER_OPTIONS, OPTION_MAC_ADDRESS, OPTION_MEMORY, OPTION_MEMORY_RESERVATION, OPTION_MEMORY_SWAP, OPTION_MEMORY_SWAPPINESS, OPTION_STOP_TIMEOUT, OPTION_NETWORK, OPTION_OOM_KILLER, OPTION_OOM_SCORE_ADJ, OPTION_PID_MODE, OPTION_PIDS_LIMIT, OPTION_PLATFORM, OPTION_PRIVILEGED, OPTION_READ_ONLY, OPTION_RESTART_POLICY, OPTION_RUNTIME, OPTION_SECURITY_OPTS, OPTION_SHM_SIZE, OPTION_STOP_SIGNAL, OPTION_STORAGE_OPTS, OPTION_SYSCTLS, OPTION_TMPFS, OPTION_TTY, OPTION_ULIMITS, OPTION_USER, OPTION_USERNS_MODE, OPTION_UTS, OPTION_VOLUME_DRIVER, OPTION_VOLUMES_FROM, OPTION_WORKING_DIR, OPTION_MOUNTS_VOLUMES, OPTION_PORTS, ]