mirror of
https://github.com/ansible-collections/community.docker.git
synced 2025-12-15 19:42:06 +00:00
* Move licenses to LICENSES/, use SPDX-License-Identifier, mention all licenses in galaxy.yml. * ignore.txt lines cannot be empty or contain only a comment. * Cleanup. * This particular __init__.py seems to be crucial. * Try extra newline. * Markdown comments are a real mess. I hope this won't break Galaxy... * More licenses. * Add sanity test. * Skip some files, lint. * Make sure there is a copyright line everywhere. * Also check for copyright line in sanity tests. * Remove colon after 'Copyright'. * Normalize lint script. * Avoid colon after 'Copyright' in lint script. * Improve license checker. * Update README.md Co-authored-by: Maxwell G <9920591+gotmax23@users.noreply.github.com> * Remove superfluous space. * Referencing target instead of symlink Co-authored-by: Maxwell G <9920591+gotmax23@users.noreply.github.com>
1199 lines
38 KiB
Python
1199 lines
38 KiB
Python
# Copyright (c) 2022 Felix Fontein <felix@fontein.de>
|
|
# 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,
|
|
normalize_healthcheck,
|
|
omit_none_from_dict,
|
|
)
|
|
|
|
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,
|
|
):
|
|
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
|
|
|
|
|
|
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.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)
|
|
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):
|
|
pass
|
|
|
|
@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):
|
|
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
|
|
|
|
|
|
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_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 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):
|
|
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
|
|
|
|
|
|
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')
|
|
)
|
|
|
|
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,
|
|
]
|