docker: improve TLS config (#53906)

* Stop repeating names of common config arguments in docker_container.

* Prefer tls_verify over tls for docker modules and docker_swarm inventory plugin.

* tls and tls_verify are no longer mutually exclusive.

* Share setup code between docker_* modules and docker_swarm inventory plugin.

* Add support for more parameters.

* PEP8.

* Fix typo.

* Rename host -> docker_host.
This commit is contained in:
Felix Fontein 2019-03-17 22:10:40 +01:00 committed by ansibot
parent 410a1d2161
commit 29d6418822
4 changed files with 152 additions and 163 deletions

View file

@ -93,9 +93,7 @@ DOCKER_COMMON_ARGS = dict(
debug=dict(type='bool', default=False) debug=dict(type='bool', default=False)
) )
DOCKER_MUTUALLY_EXCLUSIVE = [ DOCKER_MUTUALLY_EXCLUSIVE = []
['tls', 'tls_verify']
]
DOCKER_REQUIRED_TOGETHER = [ DOCKER_REQUIRED_TOGETHER = [
['cert_path', 'key_path'] ['cert_path', 'key_path']
@ -163,6 +161,99 @@ class DockerBaseClass(object):
# log_file.write(msg + u'\n') # log_file.write(msg + u'\n')
def update_tls_hostname(result):
if result['tls_hostname'] is None:
# 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)
return tls_config
except TLSParameterError as exc:
fail_function("TLS config error: %s" % exc)
def get_connect_params(auth, fail_function):
if auth['tls'] or auth['tls_verify']:
auth['docker_host'] = auth['docker_host'].replace('tcp://', 'https://')
if auth['tls_verify'] and auth['cert_path'] and auth['key_path']:
# TLS with certs and host verification
if auth['cacert_path']:
tls_config = _get_tls_config(client_cert=(auth['cert_path'], auth['key_path']),
ca_cert=auth['cacert_path'],
verify=True,
assert_hostname=auth['tls_hostname'],
ssl_version=auth['ssl_version'],
fail_function=fail_function)
else:
tls_config = _get_tls_config(client_cert=(auth['cert_path'], auth['key_path']),
verify=True,
assert_hostname=auth['tls_hostname'],
ssl_version=auth['ssl_version'],
fail_function=fail_function)
return dict(base_url=auth['docker_host'],
tls=tls_config,
version=auth['api_version'],
timeout=auth['timeout'])
if auth['tls_verify'] and auth['cacert_path']:
# TLS with cacert only
tls_config = _get_tls_config(ca_cert=auth['cacert_path'],
assert_hostname=auth['tls_hostname'],
verify=True,
ssl_version=auth['ssl_version'],
fail_function=fail_function)
return dict(base_url=auth['docker_host'],
tls=tls_config,
version=auth['api_version'],
timeout=auth['timeout'])
if auth['tls_verify']:
# TLS with verify and no certs
tls_config = _get_tls_config(verify=True,
assert_hostname=auth['tls_hostname'],
ssl_version=auth['ssl_version'],
fail_function=fail_function)
return dict(base_url=auth['docker_host'],
tls=tls_config,
version=auth['api_version'],
timeout=auth['timeout'])
if auth['tls'] and auth['cert_path'] and auth['key_path']:
# TLS with certs and no host verification
tls_config = _get_tls_config(client_cert=(auth['cert_path'], auth['key_path']),
verify=False,
ssl_version=auth['ssl_version'],
fail_function=fail_function)
return dict(base_url=auth['docker_host'],
tls=tls_config,
version=auth['api_version'],
timeout=auth['timeout'])
if auth['tls']:
# TLS with no certs and not host verification
tls_config = _get_tls_config(verify=False,
ssl_version=auth['ssl_version'],
fail_function=fail_function)
return dict(base_url=auth['docker_host'],
tls=tls_config,
version=auth['api_version'],
timeout=auth['timeout'])
# No TLS
return dict(base_url=auth['docker_host'],
version=auth['api_version'],
timeout=auth['timeout'])
class AnsibleDockerClient(Client): 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,
@ -229,7 +320,7 @@ class AnsibleDockerClient(Client):
self.debug = self.module.params.get('debug') self.debug = self.module.params.get('debug')
self.check_mode = self.module.check_mode self.check_mode = self.module.check_mode
self._connect_params = self._get_connect_params() self._connect_params = get_connect_params(self.auth_params, fail_function=self.fail)
try: try:
super(AnsibleDockerClient, self).__init__(**self._connect_params) super(AnsibleDockerClient, self).__init__(**self._connect_params)
@ -327,99 +418,10 @@ class AnsibleDockerClient(Client):
DEFAULT_TIMEOUT_SECONDS), DEFAULT_TIMEOUT_SECONDS),
) )
if result['tls_hostname'] is None: update_tls_hostname(result)
# 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
return result return result
def _get_tls_config(self, **kwargs):
self.log("get_tls_config:")
for key in kwargs:
self.log(" %s: %s" % (key, kwargs[key]))
try:
tls_config = TLSConfig(**kwargs)
return tls_config
except TLSParameterError as exc:
self.fail("TLS config error: %s" % exc)
def _get_connect_params(self):
auth = self.auth_params
self.log("connection params:")
for key in auth:
self.log(" %s: %s" % (key, auth[key]))
if auth['tls'] or auth['tls_verify']:
auth['docker_host'] = auth['docker_host'].replace('tcp://', 'https://')
if auth['tls'] and auth['cert_path'] and auth['key_path']:
# TLS with certs and no host verification
tls_config = self._get_tls_config(client_cert=(auth['cert_path'], auth['key_path']),
verify=False,
ssl_version=auth['ssl_version'])
return dict(base_url=auth['docker_host'],
tls=tls_config,
version=auth['api_version'],
timeout=auth['timeout'])
if auth['tls']:
# TLS with no certs and not host verification
tls_config = self._get_tls_config(verify=False,
ssl_version=auth['ssl_version'])
return dict(base_url=auth['docker_host'],
tls=tls_config,
version=auth['api_version'],
timeout=auth['timeout'])
if auth['tls_verify'] and auth['cert_path'] and auth['key_path']:
# TLS with certs and host verification
if auth['cacert_path']:
tls_config = self._get_tls_config(client_cert=(auth['cert_path'], auth['key_path']),
ca_cert=auth['cacert_path'],
verify=True,
assert_hostname=auth['tls_hostname'],
ssl_version=auth['ssl_version'])
else:
tls_config = self._get_tls_config(client_cert=(auth['cert_path'], auth['key_path']),
verify=True,
assert_hostname=auth['tls_hostname'],
ssl_version=auth['ssl_version'])
return dict(base_url=auth['docker_host'],
tls=tls_config,
version=auth['api_version'],
timeout=auth['timeout'])
if auth['tls_verify'] and auth['cacert_path']:
# TLS with cacert only
tls_config = self._get_tls_config(ca_cert=auth['cacert_path'],
assert_hostname=auth['tls_hostname'],
verify=True,
ssl_version=auth['ssl_version'])
return dict(base_url=auth['docker_host'],
tls=tls_config,
version=auth['api_version'],
timeout=auth['timeout'])
if auth['tls_verify']:
# TLS with verify and no certs
tls_config = self._get_tls_config(verify=True,
assert_hostname=auth['tls_hostname'],
ssl_version=auth['ssl_version'])
return dict(base_url=auth['docker_host'],
tls=tls_config,
version=auth['api_version'],
timeout=auth['timeout'])
# No TLS
return dict(base_url=auth['docker_host'],
version=auth['api_version'],
timeout=auth['timeout'])
def _handle_ssl_error(self, error): def _handle_ssl_error(self, error):
match = re.match(r"hostname.*doesn\'t match (\'.*\')", str(error)) match = re.match(r"hostname.*doesn\'t match (\'.*\')", str(error))
if match: if match:

View file

@ -931,7 +931,8 @@ from ansible.module_utils.docker.common import (
compare_generic, compare_generic,
is_image_name_id, is_image_name_id,
sanitize_result, sanitize_result,
parse_healthcheck parse_healthcheck,
DOCKER_COMMON_ARGS,
) )
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
@ -2642,13 +2643,11 @@ def detect_ipvX_address_usage(client):
class AnsibleDockerClientContainer(AnsibleDockerClient): class AnsibleDockerClientContainer(AnsibleDockerClient):
# A list of module options which are not docker container properties # A list of module options which are not docker container properties
__NON_CONTAINER_PROPERTY_OPTIONS = ( __NON_CONTAINER_PROPERTY_OPTIONS = tuple([
'docker_host', 'tls_hostname', 'api_version', 'timeout', 'cacert_path', 'cert_path', 'env_file', 'force_kill', 'keep_volumes', 'ignore_image', 'name', 'pull', 'purge_networks',
'key_path', 'ssl_version', 'tls', 'tls_verify', 'debug', 'env_file', 'force_kill', 'recreate', 'restart', 'state', 'trust_image_content', 'networks', 'cleanup', 'kill_signal',
'keep_volumes', 'ignore_image', 'name', 'pull', 'purge_networks', 'recreate',
'restart', 'state', 'trust_image_content', 'networks', 'cleanup', 'kill_signal',
'output_logs', 'paused' 'output_logs', 'paused'
) ] + list(DOCKER_COMMON_ARGS.keys()))
def _parse_comparisons(self): def _parse_comparisons(self):
comparisons = {} comparisons = {}

View file

@ -72,7 +72,7 @@ options:
tls: tls:
description: description:
- Secure the connection to the API by using TLS without verifying the authenticity of the Docker host - Secure the connection to the API by using TLS without verifying the authenticity of the Docker host
server. server. Note that if C(tls_verify) is set to C(yes) as well, it will take precedence.
- If the value is not specified in the task, the value of environment variable C(DOCKER_TLS) will be used - If the value is not specified in the task, the value of environment variable C(DOCKER_TLS) will be used
instead. If the environment variable is not set, the default value will be used. instead. If the environment variable is not set, the default value will be used.
type: bool type: bool

View file

@ -28,8 +28,10 @@ DOCUMENTATION = '''
type: str type: str
required: true required: true
choices: docker_swarm choices: docker_swarm
host: docker_host:
description: Socket of a Docker swarm manager node (tcp,unix). description:
- Socket of a Docker swarm manager node (tcp,unix).
- "Use C(unix://var/run/docker.sock) to connect via local socket."
type: str type: str
required: true required: true
verbose_output: verbose_output:
@ -56,6 +58,21 @@ DOCUMENTATION = '''
tls_hostname: tls_hostname:
description: When verifying the authenticity of the Docker Host server, provide the expected name of the server. description: When verifying the authenticity of the Docker Host server, provide the expected name of the server.
type: str type: str
ssl_version:
description: Provide a valid SSL version number. Default value determined by ssl.py module.
type: str
api_version:
description:
- The version of the Docker API running on the Docker Host.
- Defaults to the latest version of the API supported by docker-py.
type: str
timeout:
description:
- The maximum amount of time in seconds to wait on a response from the API.
- If the value is not specified in the task, the value of environment variable C(DOCKER_TIMEOUT) will be used
instead. If the environment variable is not set, the default value will be used.
type: int
default: 60
include_host_uri: include_host_uri:
description: Toggle to return the additional attribute I(ansible_host_uri) which contains the URI of the description: Toggle to return the additional attribute I(ansible_host_uri) which contains the URI of the
swarm leader in format of M(tcp://172.16.0.1:2376). This value may be used without additional swarm leader in format of M(tcp://172.16.0.1:2376). This value may be used without additional
@ -71,20 +88,20 @@ DOCUMENTATION = '''
EXAMPLES = ''' EXAMPLES = '''
# Minimal example using local docker # Minimal example using local docker
plugin: docker_swarm plugin: docker_swarm
host: unix://var/run/docker.sock docker_host: unix://var/run/docker.sock
# Minimal example using remote docker # Minimal example using remote docker
plugin: docker_swarm plugin: docker_swarm
host: tcp://my-docker-host:2375 docker_host: tcp://my-docker-host:2375
# Example using remote docker with unverified TLS # Example using remote docker with unverified TLS
plugin: docker_swarm plugin: docker_swarm
host: tcp://my-docker-host:2376 docker_host: tcp://my-docker-host:2376
tls: yes tls: yes
# Example using remote docker with verified TLS and client certificate verification # Example using remote docker with verified TLS and client certificate verification
plugin: docker_swarm plugin: docker_swarm
host: tcp://my-docker-host:2376 docker_host: tcp://my-docker-host:2376
tls_verify: yes tls_verify: yes
cacert_path: /somewhere/ca.pem cacert_path: /somewhere/ca.pem
key_path: /somewhere/key.pem key_path: /somewhere/key.pem
@ -92,7 +109,7 @@ cert_path: /somewhere/cert.pem
# Example using constructed features to create groups and set ansible_host # Example using constructed features to create groups and set ansible_host
plugin: docker_swarm plugin: docker_swarm
host: tcp://my-docker-host:2375 docker_host: tcp://my-docker-host:2375
strict: False strict: False
keyed_groups: keyed_groups:
# add e.g. x86_64 hosts to an arch_x86_64 group # add e.g. x86_64 hosts to an arch_x86_64 group
@ -110,6 +127,7 @@ keyed_groups:
from ansible.errors import AnsibleError from ansible.errors import AnsibleError
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
from ansible.module_utils.docker.common import update_tls_hostname, get_connect_params
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
from ansible.parsing.utils.addresses import parse_address from ansible.parsing.utils.addresses import parse_address
@ -125,61 +143,31 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
NAME = 'docker_swarm' NAME = 'docker_swarm'
def _get_tls_config(self, **kwargs): def _fail(self, msg):
try: raise AnsibleError(msg)
tls_config = docker.tls.TLSConfig(**kwargs)
return tls_config
except Exception as e:
raise AnsibleError('Unable to setup TLS, this was the original exception: %s' % to_native(e))
def _get_tls_connect_params(self):
if self.get_option('tls') and self.get_option('cert_path') and self.get_option('key_path'):
# TLS with certs and no host verification
tls_config = self._get_tls_config(client_cert=(self.get_option('cert_path'),
self.get_option('key_path')),
verify=False)
return tls_config
if self.get_option('tls'):
# TLS with no certs and not host verification
tls_config = self._get_tls_config(verify=False)
return tls_config
if self.get_option('tls_verify') and self.get_option('cert_path') and self.get_option('key_path'):
# TLS with certs and host verification
if self.get_option('cacert_path'):
tls_config = self._get_tls_config(client_cert=(self.get_option('cert_path'),
self.get_option('key_path')),
ca_cert=self.get_option('cacert_path'),
verify=True,
assert_hostname=self.get_option('tls_hostname'))
else:
tls_config = self._get_tls_config(client_cert=(self.get_option('cert_path'),
self.get_option('key_path')),
verify=True,
assert_hostname=self.get_option('tls_hostname'))
return tls_config
if self.get_option('tls_verify') and self.get_option('cacert_path'):
# TLS with cacert only
tls_config = self._get_tls_config(ca_cert=self.get_option('cacert_path'),
assert_hostname=self.get_option('tls_hostname'),
verify=True)
return tls_config
if self.get_option('tls_verify'):
# TLS with verify and no certs
tls_config = self._get_tls_config(verify=True,
assert_hostname=self.get_option('tls_hostname'))
return tls_config
# No TLS
return None
def _populate(self): def _populate(self):
self.client = docker.DockerClient(base_url=self.get_option('host'), raw_params = dict(
tls=self._get_tls_connect_params()) docker_host=self.get_option('docker_host'),
tls=self.get_option('tls'),
tls_verify=self.get_option('tls_verify'),
key_path=self.get_option('key_path'),
cacert_path=self.get_option('cacert_path'),
cert_path=self.get_option('cert_path'),
tls_hostname=self.get_option('tls_hostname'),
api_version=self.get_option('api_version'),
timeout=self.get_option('timeout') or 60,
ssl_version=self.get_option('ssl_version'),
debug=None,
)
if raw_params['timeout'] is not None:
try:
raw_params['timeout'] = int(raw_params['timeout'])
except Exception as dummy:
raise AnsibleError('Argument to timeout function must be an integer')
update_tls_hostname(raw_params)
connect_params = get_connect_params(raw_params, fail_function=self._fail)
self.client = docker.DockerClient(**connect_params)
self.inventory.add_group('all') self.inventory.add_group('all')
self.inventory.add_group('manager') self.inventory.add_group('manager')
self.inventory.add_group('worker') self.inventory.add_group('worker')