Docker common consolidation (#49707)

* [docker] Consolidating Python Boolean conversion for Docker API (#49563)

* [docker] Consolidating docker option min version checks (#49564)

* [docker] Moving option min version checks out of docker_swarm (#49564)

Also renaming Boolean cleanup function and fixing docker_container minimum
version check for network interfaces.

* Cleanup from PR feedback
This commit is contained in:
Dave Bendit 2018-12-12 03:05:12 -06:00 committed by Toshio Kuratomi
parent 5a7f7f1183
commit a11e0c184a
4 changed files with 99 additions and 131 deletions

View file

@ -163,7 +163,8 @@ class AnsibleDockerClient(Client):
def __init__(self, argument_spec=None, supports_check_mode=False, mutually_exclusive=None, 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, 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 = dict()
merged_arg_spec.update(DOCKER_COMMON_ARGS) merged_arg_spec.update(DOCKER_COMMON_ARGS)
@ -235,6 +236,9 @@ class AnsibleDockerClient(Client):
if self.docker_api_version < LooseVersion(min_docker_api_version): 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)) 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): def log(self, msg, pretty_print=False):
pass pass
# if self.debug: # if self.debug:
@ -416,6 +420,58 @@ class AnsibleDockerClient(Client):
% (self.auth_params['tls_hostname'], match.group(1), match.group(1))) % (self.auth_params['tls_hostname'], match.group(1), match.group(1)))
self.fail("SSL Exception: %s" % (error)) 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): def get_container(self, name=None):
''' '''
Lookup a container and return the inspection results. Lookup a container and return the inspection results.
@ -637,3 +693,23 @@ def compare_generic(a, b, method, type):
if not found: if not found:
return False return False
return True 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

View file

@ -2223,27 +2223,24 @@ class AnsibleDockerClientContainer(AnsibleDockerClient):
comparisons['expected_ports'] = dict(type='dict', comparison=comparisons['published_ports']['comparison'], name='expected_ports') comparisons['expected_ports'] = dict(type='dict', comparison=comparisons['published_ports']['comparison'], name='expected_ports')
self.comparisons = comparisons self.comparisons = comparisons
def _get_minimal_versions(self): def __init__(self, **kwargs):
# Helper function to detect whether any specified network uses ipv4_address or ipv6_address
def detect_ipvX_address_usage(): 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 []: 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: if network.get('ipv4_address') is not None or network.get('ipv6_address') is not None:
return True return True
return False return False
self.option_minimal_versions = dict( option_minimal_versions = dict(
# internal options # internal options
log_config=dict(), log_config=dict(),
publish_all_ports=dict(), publish_all_ports=dict(),
ports=dict(), ports=dict(),
volume_binds=dict(), volume_binds=dict(),
name=dict(), name=dict(),
) # normal options
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(
dns_opts=dict(docker_api_version='1.21', docker_py_version='1.10.0'), dns_opts=dict(docker_api_version='1.21', docker_py_version='1.10.0'),
ipc_mode=dict(docker_api_version='1.25'), ipc_mode=dict(docker_api_version='1.25'),
mac_address=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'), uts=dict(docker_py_version='3.5.0', docker_api_version='1.25'),
# specials # specials
ipvX_address_supported=dict(docker_py_version='1.9.0', detect_usage=detect_ipvX_address_usage, 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(): super(AnsibleDockerClientContainer, self).__init__(
# Test whether option is supported, and store result option_minimal_versions=option_minimal_versions,
support_docker_py = True option_minimal_versions_ignore_params=self.__NON_CONTAINER_PROPERTY_OPTIONS,
support_docker_api = True **kwargs
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()
self._setup_comparisons() self._setup_comparisons()

View file

@ -157,7 +157,7 @@ facts:
sample: {} 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: try:
from docker import utils 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 [] 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): class DockerNetworkManager(object):
def __init__(self, client): def __init__(self, client):
@ -225,7 +210,7 @@ class DockerNetworkManager(object):
self.parameters.connected = container_names_in_network(self.existing_network) self.parameters.connected = container_names_in_network(self.existing_network)
if self.parameters.driver_options: 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 state = self.parameters.state
if state == 'present': if state == 'present':

View file

@ -288,60 +288,6 @@ class TaskParameters(DockerBaseClass):
class SwarmManager(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): def __init__(self, client, results):
super(SwarmManager, self).__init__() super(SwarmManager, self).__init__()
@ -350,8 +296,6 @@ class SwarmManager(DockerBaseClass):
self.results = results self.results = results
self.check_mode = self.client.check_mode self.check_mode = self.client.check_mode
self._get_minimal_versions()
self.parameters = TaskParameters(client) self.parameters = TaskParameters(client)
def __call__(self): def __call__(self):
@ -562,12 +506,19 @@ def main():
('state', 'remove', ['node_id']) ('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( client = AnsibleDockerClient(
argument_spec=argument_spec, argument_spec=argument_spec,
supports_check_mode=True, supports_check_mode=True,
required_if=required_if, required_if=required_if,
min_docker_version='2.6.0', min_docker_version='2.6.0',
min_docker_api_version='1.25', min_docker_api_version='1.25',
option_minimal_versions=option_minimal_versions,
) )
results = dict( results = dict(