diff --git a/changelogs/fragments/390-util.yml b/changelogs/fragments/390-util.yml new file mode 100644 index 000000000..92d214115 --- /dev/null +++ b/changelogs/fragments/390-util.yml @@ -0,0 +1,2 @@ +minor_changes: + - "Move common utility functions from the ``common`` module_util to a new module_util called ``util``. This should not have any user-visible effect (https://github.com/ansible-collections/community.docker/pull/390)." diff --git a/plugins/inventory/docker_containers.py b/plugins/inventory/docker_containers.py index 23574624e..eef49a018 100644 --- a/plugins/inventory/docker_containers.py +++ b/plugins/inventory/docker_containers.py @@ -156,6 +156,8 @@ from ansible_collections.community.docker.plugins.module_utils.common import ( RequestException, +) +from ansible_collections.community.docker.plugins.module_utils.util import ( DOCKER_COMMON_ARGS_VARS, ) from ansible_collections.community.docker.plugins.plugin_utils.common import ( diff --git a/plugins/inventory/docker_swarm.py b/plugins/inventory/docker_swarm.py index d20ab4771..cad41f7a5 100644 --- a/plugins/inventory/docker_swarm.py +++ b/plugins/inventory/docker_swarm.py @@ -147,7 +147,8 @@ from ansible.errors import AnsibleError from ansible.module_utils.common.text.converters import to_native -from ansible_collections.community.docker.plugins.module_utils.common import update_tls_hostname, get_connect_params +from ansible_collections.community.docker.plugins.module_utils.common import get_connect_params +from ansible_collections.community.docker.plugins.module_utils.util import update_tls_hostname from ansible.plugins.inventory import BaseInventoryPlugin, Constructable from ansible.parsing.utils.addresses import parse_address diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index c02a6d99a..dd0f6984a 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -77,43 +77,34 @@ class RequestException(Exception): pass - -DEFAULT_DOCKER_HOST = 'unix://var/run/docker.sock' -DEFAULT_TLS = False -DEFAULT_TLS_VERIFY = False -DEFAULT_TLS_HOSTNAME = 'localhost' # deprecated -MIN_DOCKER_VERSION = "1.8.0" -DEFAULT_TIMEOUT_SECONDS = 60 - -DOCKER_COMMON_ARGS = dict( - docker_host=dict(type='str', default=DEFAULT_DOCKER_HOST, fallback=(env_fallback, ['DOCKER_HOST']), aliases=['docker_url']), - tls_hostname=dict(type='str', fallback=(env_fallback, ['DOCKER_TLS_HOSTNAME'])), - api_version=dict(type='str', default='auto', fallback=(env_fallback, ['DOCKER_API_VERSION']), aliases=['docker_api_version']), - timeout=dict(type='int', default=DEFAULT_TIMEOUT_SECONDS, fallback=(env_fallback, ['DOCKER_TIMEOUT'])), - ca_cert=dict(type='path', aliases=['tls_ca_cert', 'cacert_path']), - client_cert=dict(type='path', aliases=['tls_client_cert', 'cert_path']), - client_key=dict(type='path', aliases=['tls_client_key', 'key_path']), - ssl_version=dict(type='str', fallback=(env_fallback, ['DOCKER_SSL_VERSION'])), - tls=dict(type='bool', default=DEFAULT_TLS, fallback=(env_fallback, ['DOCKER_TLS'])), - use_ssh_client=dict(type='bool', default=False), - validate_certs=dict(type='bool', default=DEFAULT_TLS_VERIFY, fallback=(env_fallback, ['DOCKER_TLS_VERIFY']), aliases=['tls_verify']), - debug=dict(type='bool', default=False) +from ansible_collections.community.docker.plugins.module_utils.util import ( + DEFAULT_DOCKER_HOST, + DEFAULT_TLS, + DEFAULT_TLS_VERIFY, + DEFAULT_TLS_HOSTNAME, + DEFAULT_TIMEOUT_SECONDS, + DOCKER_COMMON_ARGS, + DOCKER_COMMON_ARGS_VARS, + DOCKER_MUTUALLY_EXCLUSIVE, + DOCKER_REQUIRED_TOGETHER, + DEFAULT_DOCKER_REGISTRY, + BYTE_SUFFIXES, + is_image_name_id, + is_valid_tag, + sanitize_result, + DockerBaseClass, + update_tls_hostname, + compare_dict_allow_more_present, + compare_generic, + DifferenceTracker, + clean_dict_booleans_for_docker_api, + convert_duration_to_nanosecond, + parse_healthcheck, + omit_none_from_dict, ) -DOCKER_COMMON_ARGS_VARS = dict([ - [option_name, 'ansible_docker_%s' % option_name] - for option_name in DOCKER_COMMON_ARGS - if option_name != 'debug' -]) - -DOCKER_MUTUALLY_EXCLUSIVE = [] - -DOCKER_REQUIRED_TOGETHER = [ - ['client_cert', 'client_key'] -] -DEFAULT_DOCKER_REGISTRY = 'https://index.docker.io/v1/' -BYTE_SUFFIXES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] +MIN_DOCKER_VERSION = "1.8.0" if not HAS_DOCKER_PY: @@ -132,75 +123,6 @@ class NotFound(Exception): # noqa: F811 pass -def is_image_name_id(name): - """Check whether the given image name is in fact an image ID (hash).""" - if re.match('^sha256:[0-9a-fA-F]{64}$', name): - return True - return False - - -def is_valid_tag(tag, allow_empty=False): - """Check whether the given string is a valid docker tag name.""" - if not tag: - return allow_empty - # See here ("Extended description") for a definition what tags can be: - # https://docs.docker.com/engine/reference/commandline/tag/ - return bool(re.match('^[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127}$', tag)) - - -def sanitize_result(data): - """Sanitize data object for return to Ansible. - - When the data object contains types such as docker.types.containers.HostConfig, - Ansible will fail when these are returned via exit_json or fail_json. - HostConfig is derived from dict, but its constructor requires additional - arguments. This function sanitizes data structures by recursively converting - everything derived from dict to dict and everything derived from list (and tuple) - to a list. - """ - if isinstance(data, dict): - return dict((k, sanitize_result(v)) for k, v in data.items()) - elif isinstance(data, (list, tuple)): - return [sanitize_result(v) for v in data] - else: - return data - - -class DockerBaseClass(object): - def __init__(self): - self.debug = False - - def log(self, msg, pretty_print=False): - pass - # if self.debug: - # log_file = open('docker.log', 'a') - # if pretty_print: - # log_file.write(json.dumps(msg, sort_keys=True, indent=4, separators=(',', ': '))) - # log_file.write(u'\n') - # else: - # log_file.write(msg + u'\n') - - -def update_tls_hostname(result, old_behavior=False, deprecate_function=None, uses_tls=True): - if result['tls_hostname'] is None: - if old_behavior: - result['tls_hostname'] = DEFAULT_TLS_HOSTNAME - if uses_tls and deprecate_function is not None: - deprecate_function( - 'The default value "localhost" for tls_hostname is deprecated and will be removed in community.docker 3.0.0.' - ' From then on, docker_host will be used to compute tls_hostname. If you want to keep using "localhost",' - ' please set that value explicitly.', - version='3.0.0', collection_name='community.docker') - return - - # get default machine name from the url - parsed_url = urlparse(result['docker_host']) - if ':' in parsed_url.netloc: - result['tls_hostname'] = parsed_url.netloc[:parsed_url.netloc.rindex(':')] - else: - result['tls_hostname'] = parsed_url - - def _get_tls_config(fail_function, **kwargs): try: tls_config = TLSConfig(**kwargs) @@ -773,284 +695,3 @@ def report_warnings(self, result, warnings_key=None): self.module.warn('Docker warning: {0}'.format(warning)) elif isinstance(result, string_types) and result: self.module.warn('Docker warning: {0}'.format(result)) - - -def compare_dict_allow_more_present(av, bv): - ''' - Compare two dictionaries for whether every entry of the first is in the second. - ''' - for key, value in av.items(): - if key not in bv: - return False - if bv[key] != value: - return False - return True - - -def compare_generic(a, b, method, datatype): - ''' - Compare values a and b as described by method and datatype. - - Returns ``True`` if the values compare equal, and ``False`` if not. - - ``a`` is usually the module's parameter, while ``b`` is a property - of the current object. ``a`` must not be ``None`` (except for - ``datatype == 'value'``). - - Valid values for ``method`` are: - - ``ignore`` (always compare as equal); - - ``strict`` (only compare if really equal) - - ``allow_more_present`` (allow b to have elements which a does not have). - - Valid values for ``datatype`` are: - - ``value``: for simple values (strings, numbers, ...); - - ``list``: for ``list``s or ``tuple``s where order matters; - - ``set``: for ``list``s, ``tuple``s or ``set``s where order does not - matter; - - ``set(dict)``: for ``list``s, ``tuple``s or ``sets`` where order does - not matter and which contain ``dict``s; ``allow_more_present`` is used - for the ``dict``s, and these are assumed to be dictionaries of values; - - ``dict``: for dictionaries of values. - ''' - if method == 'ignore': - return True - # If a or b is None: - if a is None or b is None: - # If both are None: equality - if a == b: - return True - # Otherwise, not equal for values, and equal - # if the other is empty for set/list/dict - if datatype == 'value': - return False - # For allow_more_present, allow a to be None - if method == 'allow_more_present' and a is None: - return True - # Otherwise, the iterable object which is not None must have length 0 - return len(b if a is None else a) == 0 - # Do proper comparison (both objects not None) - if datatype == 'value': - return a == b - elif datatype == 'list': - if method == 'strict': - return a == b - else: - i = 0 - for v in a: - while i < len(b) and b[i] != v: - i += 1 - if i == len(b): - return False - i += 1 - return True - elif datatype == 'dict': - if method == 'strict': - return a == b - else: - return compare_dict_allow_more_present(a, b) - elif datatype == 'set': - set_a = set(a) - set_b = set(b) - if method == 'strict': - return set_a == set_b - else: - return set_b >= set_a - elif datatype == 'set(dict)': - for av in a: - found = False - for bv in b: - if compare_dict_allow_more_present(av, bv): - found = True - break - if not found: - return False - if method == 'strict': - # If we would know that both a and b do not contain duplicates, - # we could simply compare len(a) to len(b) to finish this test. - # We can assume that b has no duplicates (as it is returned by - # docker), but we don't know for a. - for bv in b: - found = False - for av in a: - if compare_dict_allow_more_present(av, bv): - found = True - break - if not found: - return False - return True - - -class DifferenceTracker(object): - def __init__(self): - self._diff = [] - - def add(self, name, parameter=None, active=None): - self._diff.append(dict( - name=name, - parameter=parameter, - active=active, - )) - - def merge(self, other_tracker): - self._diff.extend(other_tracker._diff) - - @property - def empty(self): - return len(self._diff) == 0 - - def get_before_after(self): - ''' - Return texts ``before`` and ``after``. - ''' - before = dict() - after = dict() - for item in self._diff: - before[item['name']] = item['active'] - after[item['name']] = item['parameter'] - return before, after - - def has_difference_for(self, name): - ''' - Returns a boolean if a difference exists for name - ''' - return any(diff for diff in self._diff if diff['name'] == name) - - def get_legacy_docker_container_diffs(self): - ''' - Return differences in the docker_container legacy format. - ''' - result = [] - for entry in self._diff: - item = dict() - item[entry['name']] = dict( - parameter=entry['parameter'], - container=entry['active'], - ) - result.append(item) - return result - - def get_legacy_docker_diffs(self): - ''' - Return differences in the docker_container legacy format. - ''' - result = [entry['name'] for entry in self._diff] - return result - - -def clean_dict_booleans_for_docker_api(data, allow_sequences=False): - ''' - Go doesn't like Python booleans 'True' or 'False', while Ansible is just - fine with them in YAML. As such, they need to be converted in cases where - we pass dictionaries to the Docker API (e.g. docker_network's - driver_options and docker_prune's filters). When `allow_sequences=True` - YAML sequences (lists, tuples) are converted to [str] instead of str([...]) - which is the expected format of filters which accept lists such as labels. - ''' - def sanitize(value): - if value is True: - return 'true' - elif value is False: - return 'false' - else: - return str(value) - - result = dict() - if data is not None: - for k, v in data.items(): - result[str(k)] = [sanitize(e) for e in v] if allow_sequences and is_sequence(v) else sanitize(v) - return result - - -def convert_duration_to_nanosecond(time_str): - """ - Return time duration in nanosecond. - """ - if not isinstance(time_str, str): - raise ValueError('Missing unit in duration - %s' % time_str) - - regex = re.compile( - r'^(((?P\d+)h)?' - r'((?P\d+)m(?!s))?' - r'((?P\d+)s)?' - r'((?P\d+)ms)?' - r'((?P\d+)us)?)$' - ) - parts = regex.match(time_str) - - if not parts: - raise ValueError('Invalid time duration - %s' % time_str) - - parts = parts.groupdict() - time_params = {} - for (name, value) in parts.items(): - if value: - time_params[name] = int(value) - - delta = timedelta(**time_params) - time_in_nanoseconds = ( - delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10 ** 6 - ) * 10 ** 3 - - return time_in_nanoseconds - - -def parse_healthcheck(healthcheck): - """ - Return dictionary of healthcheck parameters and boolean if - healthcheck defined in image was requested to be disabled. - """ - if (not healthcheck) or (not healthcheck.get('test')): - return None, None - - result = dict() - - # All supported healthcheck parameters - options = dict( - test='test', - interval='interval', - timeout='timeout', - start_period='start_period', - retries='retries' - ) - - duration_options = ['interval', 'timeout', 'start_period'] - - for (key, value) in options.items(): - if value in healthcheck: - if healthcheck.get(value) is None: - # due to recursive argument_spec, all keys are always present - # (but have default value None if not specified) - continue - if value in duration_options: - time = convert_duration_to_nanosecond(healthcheck.get(value)) - if time: - result[key] = time - elif healthcheck.get(value): - result[key] = healthcheck.get(value) - if key == 'test': - if isinstance(result[key], (tuple, list)): - result[key] = [str(e) for e in result[key]] - else: - result[key] = ['CMD-SHELL', str(result[key])] - elif key == 'retries': - try: - result[key] = int(result[key]) - except ValueError: - raise ValueError( - 'Cannot parse number of retries for healthcheck. ' - 'Expected an integer, got "{0}".'.format(result[key]) - ) - - if result['test'] == ['NONE']: - # If the user explicitly disables the healthcheck, return None - # as the healthcheck object, and set disable_healthcheck to True - return None, True - - return result, False - - -def omit_none_from_dict(d): - """ - Return a copy of the dictionary with all keys with value None omitted. - """ - return dict((k, v) for (k, v) in d.items() if v is not None) diff --git a/plugins/module_utils/util.py b/plugins/module_utils/util.py new file mode 100644 index 000000000..44424d0cf --- /dev/null +++ b/plugins/module_utils/util.py @@ -0,0 +1,406 @@ +# Copyright 2016 Red Hat | Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +import abc +import os +import platform +import re +import sys +import traceback +from datetime import timedelta + +from ansible.module_utils.basic import env_fallback +from ansible.module_utils.common.collections import is_sequence +from ansible.module_utils.common._collections_compat import Sequence +from ansible.module_utils.six.moves.urllib.parse import urlparse + + +DEFAULT_DOCKER_HOST = 'unix://var/run/docker.sock' +DEFAULT_TLS = False +DEFAULT_TLS_VERIFY = False +DEFAULT_TLS_HOSTNAME = 'localhost' # deprecated +DEFAULT_TIMEOUT_SECONDS = 60 + +DOCKER_COMMON_ARGS = dict( + docker_host=dict(type='str', default=DEFAULT_DOCKER_HOST, fallback=(env_fallback, ['DOCKER_HOST']), aliases=['docker_url']), + tls_hostname=dict(type='str', fallback=(env_fallback, ['DOCKER_TLS_HOSTNAME'])), + api_version=dict(type='str', default='auto', fallback=(env_fallback, ['DOCKER_API_VERSION']), aliases=['docker_api_version']), + timeout=dict(type='int', default=DEFAULT_TIMEOUT_SECONDS, fallback=(env_fallback, ['DOCKER_TIMEOUT'])), + ca_cert=dict(type='path', aliases=['tls_ca_cert', 'cacert_path']), + client_cert=dict(type='path', aliases=['tls_client_cert', 'cert_path']), + client_key=dict(type='path', aliases=['tls_client_key', 'key_path']), + ssl_version=dict(type='str', fallback=(env_fallback, ['DOCKER_SSL_VERSION'])), + tls=dict(type='bool', default=DEFAULT_TLS, fallback=(env_fallback, ['DOCKER_TLS'])), + use_ssh_client=dict(type='bool', default=False), + validate_certs=dict(type='bool', default=DEFAULT_TLS_VERIFY, fallback=(env_fallback, ['DOCKER_TLS_VERIFY']), aliases=['tls_verify']), + debug=dict(type='bool', default=False) +) + +DOCKER_COMMON_ARGS_VARS = dict([ + [option_name, 'ansible_docker_%s' % option_name] + for option_name in DOCKER_COMMON_ARGS + if option_name != 'debug' +]) + +DOCKER_MUTUALLY_EXCLUSIVE = [] + +DOCKER_REQUIRED_TOGETHER = [ + ['client_cert', 'client_key'] +] + +DEFAULT_DOCKER_REGISTRY = 'https://index.docker.io/v1/' +BYTE_SUFFIXES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] + + +def is_image_name_id(name): + """Check whether the given image name is in fact an image ID (hash).""" + if re.match('^sha256:[0-9a-fA-F]{64}$', name): + return True + return False + + +def is_valid_tag(tag, allow_empty=False): + """Check whether the given string is a valid docker tag name.""" + if not tag: + return allow_empty + # See here ("Extended description") for a definition what tags can be: + # https://docs.docker.com/engine/reference/commandline/tag/ + return bool(re.match('^[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127}$', tag)) + + +def sanitize_result(data): + """Sanitize data object for return to Ansible. + + When the data object contains types such as docker.types.containers.HostConfig, + Ansible will fail when these are returned via exit_json or fail_json. + HostConfig is derived from dict, but its constructor requires additional + arguments. This function sanitizes data structures by recursively converting + everything derived from dict to dict and everything derived from list (and tuple) + to a list. + """ + if isinstance(data, dict): + return dict((k, sanitize_result(v)) for k, v in data.items()) + elif isinstance(data, (list, tuple)): + return [sanitize_result(v) for v in data] + else: + return data + + +class DockerBaseClass(object): + def __init__(self): + self.debug = False + + def log(self, msg, pretty_print=False): + pass + # if self.debug: + # log_file = open('docker.log', 'a') + # if pretty_print: + # log_file.write(json.dumps(msg, sort_keys=True, indent=4, separators=(',', ': '))) + # log_file.write(u'\n') + # else: + # log_file.write(msg + u'\n') + + +def update_tls_hostname(result, old_behavior=False, deprecate_function=None, uses_tls=True): + if result['tls_hostname'] is None: + if old_behavior: + result['tls_hostname'] = DEFAULT_TLS_HOSTNAME + if uses_tls and deprecate_function is not None: + deprecate_function( + 'The default value "localhost" for tls_hostname is deprecated and will be removed in community.docker 3.0.0.' + ' From then on, docker_host will be used to compute tls_hostname. If you want to keep using "localhost",' + ' please set that value explicitly.', + version='3.0.0', collection_name='community.docker') + return + + # get default machine name from the url + parsed_url = urlparse(result['docker_host']) + if ':' in parsed_url.netloc: + result['tls_hostname'] = parsed_url.netloc[:parsed_url.netloc.rindex(':')] + else: + result['tls_hostname'] = parsed_url + + +def compare_dict_allow_more_present(av, bv): + ''' + Compare two dictionaries for whether every entry of the first is in the second. + ''' + for key, value in av.items(): + if key not in bv: + return False + if bv[key] != value: + return False + return True + + +def compare_generic(a, b, method, datatype): + ''' + Compare values a and b as described by method and datatype. + + Returns ``True`` if the values compare equal, and ``False`` if not. + + ``a`` is usually the module's parameter, while ``b`` is a property + of the current object. ``a`` must not be ``None`` (except for + ``datatype == 'value'``). + + Valid values for ``method`` are: + - ``ignore`` (always compare as equal); + - ``strict`` (only compare if really equal) + - ``allow_more_present`` (allow b to have elements which a does not have). + + Valid values for ``datatype`` are: + - ``value``: for simple values (strings, numbers, ...); + - ``list``: for ``list``s or ``tuple``s where order matters; + - ``set``: for ``list``s, ``tuple``s or ``set``s where order does not + matter; + - ``set(dict)``: for ``list``s, ``tuple``s or ``sets`` where order does + not matter and which contain ``dict``s; ``allow_more_present`` is used + for the ``dict``s, and these are assumed to be dictionaries of values; + - ``dict``: for dictionaries of values. + ''' + if method == 'ignore': + return True + # If a or b is None: + if a is None or b is None: + # If both are None: equality + if a == b: + return True + # Otherwise, not equal for values, and equal + # if the other is empty for set/list/dict + if datatype == 'value': + return False + # For allow_more_present, allow a to be None + if method == 'allow_more_present' and a is None: + return True + # Otherwise, the iterable object which is not None must have length 0 + return len(b if a is None else a) == 0 + # Do proper comparison (both objects not None) + if datatype == 'value': + return a == b + elif datatype == 'list': + if method == 'strict': + return a == b + else: + i = 0 + for v in a: + while i < len(b) and b[i] != v: + i += 1 + if i == len(b): + return False + i += 1 + return True + elif datatype == 'dict': + if method == 'strict': + return a == b + else: + return compare_dict_allow_more_present(a, b) + elif datatype == 'set': + set_a = set(a) + set_b = set(b) + if method == 'strict': + return set_a == set_b + else: + return set_b >= set_a + elif datatype == 'set(dict)': + for av in a: + found = False + for bv in b: + if compare_dict_allow_more_present(av, bv): + found = True + break + if not found: + return False + if method == 'strict': + # If we would know that both a and b do not contain duplicates, + # we could simply compare len(a) to len(b) to finish this test. + # We can assume that b has no duplicates (as it is returned by + # docker), but we don't know for a. + for bv in b: + found = False + for av in a: + if compare_dict_allow_more_present(av, bv): + found = True + break + if not found: + return False + return True + + +class DifferenceTracker(object): + def __init__(self): + self._diff = [] + + def add(self, name, parameter=None, active=None): + self._diff.append(dict( + name=name, + parameter=parameter, + active=active, + )) + + def merge(self, other_tracker): + self._diff.extend(other_tracker._diff) + + @property + def empty(self): + return len(self._diff) == 0 + + def get_before_after(self): + ''' + Return texts ``before`` and ``after``. + ''' + before = dict() + after = dict() + for item in self._diff: + before[item['name']] = item['active'] + after[item['name']] = item['parameter'] + return before, after + + def has_difference_for(self, name): + ''' + Returns a boolean if a difference exists for name + ''' + return any(diff for diff in self._diff if diff['name'] == name) + + def get_legacy_docker_container_diffs(self): + ''' + Return differences in the docker_container legacy format. + ''' + result = [] + for entry in self._diff: + item = dict() + item[entry['name']] = dict( + parameter=entry['parameter'], + container=entry['active'], + ) + result.append(item) + return result + + def get_legacy_docker_diffs(self): + ''' + Return differences in the docker_container legacy format. + ''' + result = [entry['name'] for entry in self._diff] + return result + + +def clean_dict_booleans_for_docker_api(data, allow_sequences=False): + ''' + Go doesn't like Python booleans 'True' or 'False', while Ansible is just + fine with them in YAML. As such, they need to be converted in cases where + we pass dictionaries to the Docker API (e.g. docker_network's + driver_options and docker_prune's filters). When `allow_sequences=True` + YAML sequences (lists, tuples) are converted to [str] instead of str([...]) + which is the expected format of filters which accept lists such as labels. + ''' + def sanitize(value): + if value is True: + return 'true' + elif value is False: + return 'false' + else: + return str(value) + + result = dict() + if data is not None: + for k, v in data.items(): + result[str(k)] = [sanitize(e) for e in v] if allow_sequences and is_sequence(v) else sanitize(v) + return result + + +def convert_duration_to_nanosecond(time_str): + """ + Return time duration in nanosecond. + """ + if not isinstance(time_str, str): + raise ValueError('Missing unit in duration - %s' % time_str) + + regex = re.compile( + r'^(((?P\d+)h)?' + r'((?P\d+)m(?!s))?' + r'((?P\d+)s)?' + r'((?P\d+)ms)?' + r'((?P\d+)us)?)$' + ) + parts = regex.match(time_str) + + if not parts: + raise ValueError('Invalid time duration - %s' % time_str) + + parts = parts.groupdict() + time_params = {} + for (name, value) in parts.items(): + if value: + time_params[name] = int(value) + + delta = timedelta(**time_params) + time_in_nanoseconds = ( + delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10 ** 6 + ) * 10 ** 3 + + return time_in_nanoseconds + + +def parse_healthcheck(healthcheck): + """ + Return dictionary of healthcheck parameters and boolean if + healthcheck defined in image was requested to be disabled. + """ + if (not healthcheck) or (not healthcheck.get('test')): + return None, None + + result = dict() + + # All supported healthcheck parameters + options = dict( + test='test', + interval='interval', + timeout='timeout', + start_period='start_period', + retries='retries' + ) + + duration_options = ['interval', 'timeout', 'start_period'] + + for (key, value) in options.items(): + if value in healthcheck: + if healthcheck.get(value) is None: + # due to recursive argument_spec, all keys are always present + # (but have default value None if not specified) + continue + if value in duration_options: + time = convert_duration_to_nanosecond(healthcheck.get(value)) + if time: + result[key] = time + elif healthcheck.get(value): + result[key] = healthcheck.get(value) + if key == 'test': + if isinstance(result[key], (tuple, list)): + result[key] = [str(e) for e in result[key]] + else: + result[key] = ['CMD-SHELL', str(result[key])] + elif key == 'retries': + try: + result[key] = int(result[key]) + except ValueError: + raise ValueError( + 'Cannot parse number of retries for healthcheck. ' + 'Expected an integer, got "{0}".'.format(result[key]) + ) + + if result['test'] == ['NONE']: + # If the user explicitly disables the healthcheck, return None + # as the healthcheck object, and set disable_healthcheck to True + return None, True + + return result, False + + +def omit_none_from_dict(d): + """ + Return a copy of the dictionary with all keys with value None omitted. + """ + return dict((k, v) for (k, v) in d.items() if v is not None) diff --git a/plugins/modules/docker_compose.py b/plugins/modules/docker_compose.py index 3f80a2540..1ecf9e049 100644 --- a/plugins/modules/docker_compose.py +++ b/plugins/modules/docker_compose.py @@ -510,10 +510,13 @@ from ansible_collections.community.docker.plugins.module_utils.common import ( AnsibleDockerClient, - DockerBaseClass, RequestException, ) +from ansible_collections.community.docker.plugins.module_utils.util import ( + DockerBaseClass, +) + AUTH_PARAM_MAPPING = { u'docker_host': u'--host', diff --git a/plugins/modules/docker_config.py b/plugins/modules/docker_config.py index dcb9a62c4..71887a545 100644 --- a/plugins/modules/docker_config.py +++ b/plugins/modules/docker_config.py @@ -199,9 +199,11 @@ from ansible_collections.community.docker.plugins.module_utils.common import ( AnsibleDockerClient, + RequestException, +) +from ansible_collections.community.docker.plugins.module_utils.util import ( DockerBaseClass, compare_generic, - RequestException, ) from ansible.module_utils.common.text.converters import to_native, to_bytes diff --git a/plugins/modules/docker_container.py b/plugins/modules/docker_container.py index 5f47ccd98..f39bd8537 100644 --- a/plugins/modules/docker_container.py +++ b/plugins/modules/docker_container.py @@ -1217,6 +1217,9 @@ from ansible_collections.community.docker.plugins.module_utils.common import ( AnsibleDockerClient, + RequestException, +) +from ansible_collections.community.docker.plugins.module_utils.util import ( DifferenceTracker, DockerBaseClass, compare_generic, @@ -1226,7 +1229,6 @@ omit_none_from_dict, parse_healthcheck, DOCKER_COMMON_ARGS, - RequestException, ) try: diff --git a/plugins/modules/docker_host_info.py b/plugins/modules/docker_host_info.py index a3e71d893..570516cd1 100644 --- a/plugins/modules/docker_host_info.py +++ b/plugins/modules/docker_host_info.py @@ -203,12 +203,12 @@ import traceback +from ansible.module_utils.common.text.converters import to_native + from ansible_collections.community.docker.plugins.module_utils.common import ( AnsibleDockerClient, - DockerBaseClass, RequestException, ) -from ansible.module_utils.common.text.converters import to_native try: from docker.errors import DockerException, APIError @@ -216,7 +216,10 @@ # Missing Docker SDK for Python handled in ansible.module_utils.docker.common pass -from ansible_collections.community.docker.plugins.module_utils.common import clean_dict_booleans_for_docker_api +from ansible_collections.community.docker.plugins.module_utils.util import ( + DockerBaseClass, + clean_dict_booleans_for_docker_api, +) class DockerHostManager(DockerBaseClass): diff --git a/plugins/modules/docker_image.py b/plugins/modules/docker_image.py index 595d906f7..4b642dd45 100644 --- a/plugins/modules/docker_image.py +++ b/plugins/modules/docker_image.py @@ -329,13 +329,15 @@ import traceback from ansible_collections.community.docker.plugins.module_utils.common import ( - clean_dict_booleans_for_docker_api, - docker_version, AnsibleDockerClient, + RequestException, + docker_version, +) +from ansible_collections.community.docker.plugins.module_utils.util import ( + clean_dict_booleans_for_docker_api, DockerBaseClass, is_image_name_id, is_valid_tag, - RequestException, ) from ansible.module_utils.common.text.converters import to_native diff --git a/plugins/modules/docker_image_info.py b/plugins/modules/docker_image_info.py index 046fb0997..c44ebab9a 100644 --- a/plugins/modules/docker_image_info.py +++ b/plugins/modules/docker_image_info.py @@ -176,9 +176,11 @@ from ansible_collections.community.docker.plugins.module_utils.common import ( AnsibleDockerClient, + RequestException, +) +from ansible_collections.community.docker.plugins.module_utils.util import ( DockerBaseClass, is_image_name_id, - RequestException, ) diff --git a/plugins/modules/docker_image_load.py b/plugins/modules/docker_image_load.py index d0f009190..d8d2503e9 100644 --- a/plugins/modules/docker_image_load.py +++ b/plugins/modules/docker_image_load.py @@ -77,9 +77,11 @@ from ansible_collections.community.docker.plugins.module_utils.common import ( AnsibleDockerClient, + RequestException, +) +from ansible_collections.community.docker.plugins.module_utils.util import ( DockerBaseClass, is_image_name_id, - RequestException, ) try: diff --git a/plugins/modules/docker_login.py b/plugins/modules/docker_login.py index d3e9c7301..e79c2871f 100644 --- a/plugins/modules/docker_login.py +++ b/plugins/modules/docker_login.py @@ -138,11 +138,13 @@ pass from ansible_collections.community.docker.plugins.module_utils.common import ( - AnsibleDockerClient, HAS_DOCKER_PY, + AnsibleDockerClient, + RequestException, +) +from ansible_collections.community.docker.plugins.module_utils.util import ( DEFAULT_DOCKER_REGISTRY, DockerBaseClass, - RequestException, ) NEEDS_DOCKER_PYCREDS = False diff --git a/plugins/modules/docker_network.py b/plugins/modules/docker_network.py index 1d4048db4..b997e8690 100644 --- a/plugins/modules/docker_network.py +++ b/plugins/modules/docker_network.py @@ -258,11 +258,13 @@ from ansible_collections.community.docker.plugins.module_utils.common import ( AnsibleDockerClient, - DockerBaseClass, + RequestException, docker_version, +) +from ansible_collections.community.docker.plugins.module_utils.util import ( + DockerBaseClass, DifferenceTracker, clean_dict_booleans_for_docker_api, - RequestException, ) try: diff --git a/plugins/modules/docker_plugin.py b/plugins/modules/docker_plugin.py index c3a13fffd..f0e757b51 100644 --- a/plugins/modules/docker_plugin.py +++ b/plugins/modules/docker_plugin.py @@ -129,11 +129,13 @@ pass from ansible_collections.community.docker.plugins.module_utils.common import ( - DockerBaseClass, AnsibleDockerClient, - DifferenceTracker, RequestException ) +from ansible_collections.community.docker.plugins.module_utils.util import ( + DockerBaseClass, + DifferenceTracker, +) class TaskParameters(DockerBaseClass): diff --git a/plugins/modules/docker_secret.py b/plugins/modules/docker_secret.py index d5e535f5b..204af144e 100644 --- a/plugins/modules/docker_secret.py +++ b/plugins/modules/docker_secret.py @@ -191,9 +191,11 @@ from ansible_collections.community.docker.plugins.module_utils.common import ( AnsibleDockerClient, + RequestException, +) +from ansible_collections.community.docker.plugins.module_utils.util import ( DockerBaseClass, compare_generic, - RequestException, ) from ansible.module_utils.common.text.converters import to_native, to_bytes diff --git a/plugins/modules/docker_swarm.py b/plugins/modules/docker_swarm.py index 3903539cb..43088af08 100644 --- a/plugins/modules/docker_swarm.py +++ b/plugins/modules/docker_swarm.py @@ -291,9 +291,11 @@ from ansible_collections.community.docker.plugins.module_utils.common import ( DockerBaseClass, - DifferenceTracker, RequestException, ) +from ansible_collections.community.docker.plugins.module_utils.util import ( + DifferenceTracker, +) from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient diff --git a/plugins/modules/docker_swarm_info.py b/plugins/modules/docker_swarm_info.py index 408fb3ee6..3beeb14d7 100644 --- a/plugins/modules/docker_swarm_info.py +++ b/plugins/modules/docker_swarm_info.py @@ -202,10 +202,10 @@ from ansible.module_utils.common.text.converters import to_native from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient -from ansible_collections.community.docker.plugins.module_utils.common import ( +from ansible_collections.community.docker.plugins.module_utils.common import RequestException +from ansible_collections.community.docker.plugins.module_utils.util import ( DockerBaseClass, clean_dict_booleans_for_docker_api, - RequestException, ) diff --git a/plugins/modules/docker_swarm_service.py b/plugins/modules/docker_swarm_service.py index 1ee49ef04..064162a1e 100644 --- a/plugins/modules/docker_swarm_service.py +++ b/plugins/modules/docker_swarm_service.py @@ -941,12 +941,14 @@ from ansible_collections.community.docker.plugins.module_utils.common import ( AnsibleDockerClient, + RequestException, +) +from ansible_collections.community.docker.plugins.module_utils.util import ( DifferenceTracker, DockerBaseClass, convert_duration_to_nanosecond, parse_healthcheck, clean_dict_booleans_for_docker_api, - RequestException, ) from ansible.module_utils.basic import human_to_bytes diff --git a/plugins/modules/docker_volume.py b/plugins/modules/docker_volume.py index dcb359261..7e9c8bbe9 100644 --- a/plugins/modules/docker_volume.py +++ b/plugins/modules/docker_volume.py @@ -118,11 +118,13 @@ pass from ansible_collections.community.docker.plugins.module_utils.common import ( - DockerBaseClass, AnsibleDockerClient, - DifferenceTracker, RequestException, ) +from ansible_collections.community.docker.plugins.module_utils.util import ( + DockerBaseClass, + DifferenceTracker, +) from ansible.module_utils.six import iteritems, text_type diff --git a/plugins/plugin_utils/common.py b/plugins/plugin_utils/common.py index 120bb7918..2f48543a0 100644 --- a/plugins/plugin_utils/common.py +++ b/plugins/plugin_utils/common.py @@ -10,6 +10,9 @@ from ansible_collections.community.docker.plugins.module_utils.common import ( AnsibleDockerClientBase, +) + +from ansible_collections.community.docker.plugins.module_utils.util import ( DOCKER_COMMON_ARGS, ) diff --git a/tests/unit/plugins/module_utils/test_common.py b/tests/unit/plugins/module_utils/test_util.py similarity index 99% rename from tests/unit/plugins/module_utils/test_common.py rename to tests/unit/plugins/module_utils/test_util.py index b9f747dbe..1560dc3b3 100644 --- a/tests/unit/plugins/module_utils/test_common.py +++ b/tests/unit/plugins/module_utils/test_util.py @@ -3,7 +3,7 @@ import pytest -from ansible_collections.community.docker.plugins.module_utils.common import ( +from ansible_collections.community.docker.plugins.module_utils.util import ( compare_dict_allow_more_present, compare_generic, convert_duration_to_nanosecond,