diff --git a/lib/ansible/module_utils/docker_common.py b/lib/ansible/module_utils/docker_common.py index 0b9e05706a..51afbba187 100644 --- a/lib/ansible/module_utils/docker_common.py +++ b/lib/ansible/module_utils/docker_common.py @@ -163,7 +163,8 @@ class AnsibleDockerClient(Client): def __init__(self, argument_spec=None, supports_check_mode=False, mutually_exclusive=None, required_together=None, required_if=None, min_docker_version=MIN_DOCKER_VERSION, - min_docker_api_version=None): + min_docker_api_version=None, option_minimal_versions=None, + option_minimal_versions_ignore_params=None): merged_arg_spec = dict() merged_arg_spec.update(DOCKER_COMMON_ARGS) @@ -235,6 +236,9 @@ class AnsibleDockerClient(Client): if self.docker_api_version < LooseVersion(min_docker_api_version): self.fail('docker API version is %s. Minimum version required is %s.' % (self.docker_api_version_str, min_docker_api_version)) + if option_minimal_versions is not None: + self._get_minimal_versions(option_minimal_versions, option_minimal_versions_ignore_params) + def log(self, msg, pretty_print=False): pass # if self.debug: @@ -416,6 +420,58 @@ class AnsibleDockerClient(Client): % (self.auth_params['tls_hostname'], match.group(1), match.group(1))) self.fail("SSL Exception: %s" % (error)) + def _get_minimal_versions(self, option_minimal_versions, ignore_params=None): + self.option_minimal_versions = dict() + for option in self.module.argument_spec: + if ignore_params is not None: + if option in ignore_params: + continue + self.option_minimal_versions[option] = dict() + self.option_minimal_versions.update(option_minimal_versions) + + for option, data in self.option_minimal_versions.items(): + # Test whether option is supported, and store result + support_docker_py = True + support_docker_api = True + if 'docker_py_version' in data: + support_docker_py = self.docker_py_version >= LooseVersion(data['docker_py_version']) + if 'docker_api_version' in data: + support_docker_api = self.docker_api_version >= LooseVersion(data['docker_api_version']) + data['supported'] = support_docker_py and support_docker_api + # Fail if option is not supported but used + if not data['supported']: + # Test whether option is specified + if 'detect_usage' in data: + used = data['detect_usage']() + else: + used = self.module.params.get(option) is not None + if used and 'default' in self.module.argument_spec[option]: + used = self.module.params[option] != self.module.argument_spec[option]['default'] + if used: + # If the option is used, compose error message. + if 'usage_msg' in data: + usg = data['usage_msg'] + else: + usg = 'set %s option' % (option, ) + if not support_docker_api: + msg = 'docker API version is %s. Minimum version required is %s to %s.' + msg = msg % (self.docker_api_version_str, data['docker_api_version'], usg) + elif not support_docker_py: + if LooseVersion(data['docker_py_version']) < LooseVersion('2.0.0'): + msg = ("docker-py version is %s. Minimum version required is %s to %s. " + "Consider switching to the 'docker' package if you do not require Python 2.6 support.") + elif self.docker_py_version < LooseVersion('2.0.0'): + msg = ("docker-py version is %s. Minimum version required is %s to %s. " + "You have to switch to the Python 'docker' package. First uninstall 'docker-py' before " + "installing 'docker' to avoid a broken installation.") + else: + msg = "docker version is %s. Minimum version required is %s to %s." + msg = msg % (docker_version, data['docker_py_version'], usg) + else: + # should not happen + msg = 'Cannot %s with your configuration.' % (usg, ) + self.fail(msg) + def get_container(self, name=None): ''' Lookup a container and return the inspection results. @@ -637,3 +693,23 @@ def compare_generic(a, b, method, type): if not found: return False return True + + +def clean_dict_booleans_for_docker_api(data): + ''' + 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). + ''' + result = dict() + if data is not None: + for k, v in data.items(): + if v is True: + v = 'true' + elif v is False: + v = 'false' + else: + v = str(v) + result[str(k)] = v + return result diff --git a/lib/ansible/modules/cloud/docker/docker_container.py b/lib/ansible/modules/cloud/docker/docker_container.py index a500674103..522a3d5045 100644 --- a/lib/ansible/modules/cloud/docker/docker_container.py +++ b/lib/ansible/modules/cloud/docker/docker_container.py @@ -2223,27 +2223,24 @@ class AnsibleDockerClientContainer(AnsibleDockerClient): comparisons['expected_ports'] = dict(type='dict', comparison=comparisons['published_ports']['comparison'], name='expected_ports') self.comparisons = comparisons - def _get_minimal_versions(self): - # Helper function to detect whether any specified network uses ipv4_address or ipv6_address + def __init__(self, **kwargs): def detect_ipvX_address_usage(): + ''' + Helper function to detect whether any specified network uses ipv4_address or ipv6_address + ''' for network in self.module.params.get("networks") or []: if network.get('ipv4_address') is not None or network.get('ipv6_address') is not None: return True return False - self.option_minimal_versions = dict( + option_minimal_versions = dict( # internal options log_config=dict(), publish_all_ports=dict(), ports=dict(), volume_binds=dict(), name=dict(), - ) - for option, data in self.module.argument_spec.items(): - if option in self.__NON_CONTAINER_PROPERTY_OPTIONS: - continue - self.option_minimal_versions[option] = dict() - self.option_minimal_versions.update(dict( + # normal options dns_opts=dict(docker_api_version='1.21', docker_py_version='1.10.0'), ipc_mode=dict(docker_api_version='1.25'), mac_address=dict(docker_api_version='1.25'), @@ -2262,55 +2259,14 @@ class AnsibleDockerClientContainer(AnsibleDockerClient): uts=dict(docker_py_version='3.5.0', docker_api_version='1.25'), # specials ipvX_address_supported=dict(docker_py_version='1.9.0', detect_usage=detect_ipvX_address_usage, - usage_msg='ipv4_address or ipv6_address in networks'), - )) + usage_msg='ipv4_address or ipv6_address in networks'), # see above + ) - for option, data in self.option_minimal_versions.items(): - # Test whether option is supported, and store result - support_docker_py = True - support_docker_api = True - if 'docker_py_version' in data: - support_docker_py = self.docker_py_version >= LooseVersion(data['docker_py_version']) - if 'docker_api_version' in data: - support_docker_api = self.docker_api_version >= LooseVersion(data['docker_api_version']) - data['supported'] = support_docker_py and support_docker_api - # Fail if option is not supported but used - if not data['supported']: - # Test whether option is specified - if 'detect_usage' in data: - used = data['detect_usage']() - else: - used = self.module.params.get(option) is not None - if used and 'default' in self.module.argument_spec[option]: - used = self.module.params[option] != self.module.argument_spec[option]['default'] - if used: - # If the option is used, compose error message. - if 'usage_msg' in data: - usg = data['usage_msg'] - else: - usg = 'set %s option' % (option, ) - if not support_docker_api: - msg = 'docker API version is %s. Minimum version required is %s to %s.' - msg = msg % (self.docker_api_version_str, data['docker_api_version'], usg) - elif not support_docker_py: - if LooseVersion(data['docker_py_version']) < LooseVersion('2.0.0'): - msg = ("docker-py version is %s. Minimum version required is %s to %s. " - "Consider switching to the 'docker' package if you do not require Python 2.6 support.") - elif self.docker_py_version < LooseVersion('2.0.0'): - msg = ("docker-py version is %s. Minimum version required is %s to %s. " - "You have to switch to the Python 'docker' package. First uninstall 'docker-py' before " - "installing 'docker' to avoid a broken installation.") - else: - msg = "docker version is %s. Minimum version required is %s to %s." - msg = msg % (docker_version, data['docker_py_version'], usg) - else: - # should not happen - msg = 'Cannot %s with your configuration.' % (usg, ) - self.fail(msg) - - def __init__(self, **kwargs): - super(AnsibleDockerClientContainer, self).__init__(**kwargs) - self._get_minimal_versions() + super(AnsibleDockerClientContainer, self).__init__( + option_minimal_versions=option_minimal_versions, + option_minimal_versions_ignore_params=self.__NON_CONTAINER_PROPERTY_OPTIONS, + **kwargs + ) self._setup_comparisons() diff --git a/lib/ansible/modules/cloud/docker/docker_network.py b/lib/ansible/modules/cloud/docker/docker_network.py index e48da0bfe0..abd5496c4e 100644 --- a/lib/ansible/modules/cloud/docker/docker_network.py +++ b/lib/ansible/modules/cloud/docker/docker_network.py @@ -157,7 +157,7 @@ facts: sample: {} ''' -from ansible.module_utils.docker_common import AnsibleDockerClient, DockerBaseClass, HAS_DOCKER_PY_2, HAS_DOCKER_PY_3 +from ansible.module_utils.docker_common import AnsibleDockerClient, DockerBaseClass, HAS_DOCKER_PY_2, HAS_DOCKER_PY_3, clean_dict_booleans_for_docker_api try: from docker import utils @@ -192,21 +192,6 @@ def container_names_in_network(network): return [c['Name'] for c in network['Containers'].values()] if network['Containers'] else [] -def get_driver_options(driver_options): - result = dict() - if driver_options is not None: - for k, v in driver_options.items(): - # Go doesn't like 'True' or 'False' - if v is True: - v = 'true' - elif v is False: - v = 'false' - else: - v = str(v) - result[str(k)] = v - return result - - class DockerNetworkManager(object): def __init__(self, client): @@ -225,7 +210,7 @@ class DockerNetworkManager(object): self.parameters.connected = container_names_in_network(self.existing_network) if self.parameters.driver_options: - self.parameters.driver_options = get_driver_options(self.parameters.driver_options) + self.parameters.driver_options = clean_dict_booleans_for_docker_api(self.parameters.driver_options) state = self.parameters.state if state == 'present': diff --git a/lib/ansible/modules/cloud/docker/docker_swarm.py b/lib/ansible/modules/cloud/docker/docker_swarm.py index 7abad9cdda..e98b8837a9 100644 --- a/lib/ansible/modules/cloud/docker/docker_swarm.py +++ b/lib/ansible/modules/cloud/docker/docker_swarm.py @@ -288,60 +288,6 @@ class TaskParameters(DockerBaseClass): class SwarmManager(DockerBaseClass): - def _get_minimal_versions(self): - # TODO: Move this and the same from docker_container.py to docker_common.py - self.option_minimal_versions = dict() - for option, data in self.client.module.argument_spec.items(): - self.option_minimal_versions[option] = dict() - self.option_minimal_versions.update(dict( - signing_ca_cert=dict(docker_api_version='1.30'), - signing_ca_key=dict(docker_api_version='1.30'), - ca_force_rotate=dict(docker_api_version='1.30'), - )) - - for option, data in self.option_minimal_versions.items(): - # Test whether option is supported, and store result - support_docker_py = True - support_docker_api = True - if 'docker_py_version' in data: - support_docker_py = self.client.docker_py_version >= LooseVersion(data['docker_py_version']) - if 'docker_api_version' in data: - support_docker_api = self.client.docker_api_version >= LooseVersion(data['docker_api_version']) - data['supported'] = support_docker_py and support_docker_api - # Fail if option is not supported but used - if not data['supported']: - # Test whether option is specified - if 'detect_usage' in data: - used = data['detect_usage']() - else: - used = self.client.module.params.get(option) is not None - if used and 'default' in self.client.module.argument_spec[option]: - used = self.client.module.params[option] != self.client.module.argument_spec[option]['default'] - if used: - # If the option is used, compose error message. - if 'usage_msg' in data: - usg = data['usage_msg'] - else: - usg = 'set %s option' % (option, ) - if not support_docker_api: - msg = 'docker API version is %s. Minimum version required is %s to %s.' - msg = msg % (self.client.docker_api_version_str, data['docker_api_version'], usg) - elif not support_docker_py: - if LooseVersion(data['docker_py_version']) < LooseVersion('2.0.0'): - msg = ("docker-py version is %s. Minimum version required is %s to %s. " - "Consider switching to the 'docker' package if you do not require Python 2.6 support.") - elif self.client.docker_py_version < LooseVersion('2.0.0'): - msg = ("docker-py version is %s. Minimum version required is %s to %s. " - "You have to switch to the Python 'docker' package. First uninstall 'docker-py' before " - "installing 'docker' to avoid a broken installation.") - else: - msg = "docker version is %s. Minimum version required is %s to %s." - msg = msg % (docker_version, data['docker_py_version'], usg) - else: - # should not happen - msg = 'Cannot %s with your configuration.' % (usg, ) - self.client.fail(msg) - def __init__(self, client, results): super(SwarmManager, self).__init__() @@ -350,8 +296,6 @@ class SwarmManager(DockerBaseClass): self.results = results self.check_mode = self.client.check_mode - self._get_minimal_versions() - self.parameters = TaskParameters(client) def __call__(self): @@ -562,12 +506,19 @@ def main(): ('state', 'remove', ['node_id']) ] + option_minimal_versions = dict( + signing_ca_cert=dict(docker_api_version='1.30'), + signing_ca_key=dict(docker_api_version='1.30'), + ca_force_rotate=dict(docker_api_version='1.30'), + ) + client = AnsibleDockerClient( argument_spec=argument_spec, supports_check_mode=True, required_if=required_if, min_docker_version='2.6.0', min_docker_api_version='1.25', + option_minimal_versions=option_minimal_versions, ) results = dict(