2016-04-20 22:19:10 +00:00
|
|
|
#
|
|
|
|
# Copyright 2016 Red Hat | Ansible
|
|
|
|
#
|
|
|
|
# This file is part of Ansible
|
|
|
|
#
|
|
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
import os
|
|
|
|
import re
|
2019-02-18 09:46:14 +00:00
|
|
|
from datetime import timedelta
|
2016-09-10 07:02:50 +00:00
|
|
|
from distutils.version import LooseVersion
|
2017-01-30 17:03:04 +00:00
|
|
|
|
2019-02-18 09:46:14 +00:00
|
|
|
|
2019-02-13 19:10:23 +00:00
|
|
|
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
2017-01-30 17:03:04 +00:00
|
|
|
from ansible.module_utils.six.moves.urllib.parse import urlparse
|
2017-07-14 23:44:58 +00:00
|
|
|
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE, BOOLEANS_FALSE
|
2016-04-20 22:19:10 +00:00
|
|
|
|
|
|
|
HAS_DOCKER_PY = True
|
2016-12-20 13:55:03 +00:00
|
|
|
HAS_DOCKER_PY_2 = False
|
2018-03-06 12:14:31 +00:00
|
|
|
HAS_DOCKER_PY_3 = False
|
2016-04-20 22:19:10 +00:00
|
|
|
HAS_DOCKER_ERROR = None
|
|
|
|
|
|
|
|
try:
|
2016-04-23 16:47:16 +00:00
|
|
|
from requests.exceptions import SSLError
|
2016-04-20 22:19:10 +00:00
|
|
|
from docker import __version__ as docker_version
|
2018-10-29 08:22:52 +00:00
|
|
|
from docker.errors import APIError, NotFound, TLSParameterError
|
2016-04-20 22:19:10 +00:00
|
|
|
from docker.tls import TLSConfig
|
2016-04-25 20:07:24 +00:00
|
|
|
from docker import auth
|
2018-03-06 12:14:31 +00:00
|
|
|
|
|
|
|
if LooseVersion(docker_version) >= LooseVersion('3.0.0'):
|
|
|
|
HAS_DOCKER_PY_3 = True
|
|
|
|
from docker import APIClient as Client
|
|
|
|
elif LooseVersion(docker_version) >= LooseVersion('2.0.0'):
|
2016-12-20 13:55:03 +00:00
|
|
|
HAS_DOCKER_PY_2 = True
|
|
|
|
from docker import APIClient as Client
|
|
|
|
else:
|
|
|
|
from docker import Client
|
|
|
|
|
2016-05-16 12:10:35 +00:00
|
|
|
except ImportError as exc:
|
2016-04-20 22:19:10 +00:00
|
|
|
HAS_DOCKER_ERROR = str(exc)
|
|
|
|
HAS_DOCKER_PY = False
|
|
|
|
|
2018-04-17 15:29:24 +00:00
|
|
|
|
|
|
|
# The next 2 imports ``docker.models`` and ``docker.ssladapter`` are used
|
|
|
|
# to ensure the user does not have both ``docker`` and ``docker-py`` modules
|
|
|
|
# installed, as they utilize the same namespace are are incompatible
|
|
|
|
try:
|
|
|
|
# docker
|
2018-10-06 13:50:31 +00:00
|
|
|
import docker.models # noqa: F401
|
2018-04-17 15:29:24 +00:00
|
|
|
HAS_DOCKER_MODELS = True
|
|
|
|
except ImportError:
|
|
|
|
HAS_DOCKER_MODELS = False
|
|
|
|
|
|
|
|
try:
|
|
|
|
# docker-py
|
2018-10-06 13:50:31 +00:00
|
|
|
import docker.ssladapter # noqa: F401
|
2018-04-17 15:29:24 +00:00
|
|
|
HAS_DOCKER_SSLADAPTER = True
|
|
|
|
except ImportError:
|
|
|
|
HAS_DOCKER_SSLADAPTER = False
|
|
|
|
|
|
|
|
|
2016-04-20 22:19:10 +00:00
|
|
|
DEFAULT_DOCKER_HOST = 'unix://var/run/docker.sock'
|
|
|
|
DEFAULT_TLS = False
|
|
|
|
DEFAULT_TLS_VERIFY = False
|
2018-04-17 15:51:56 +00:00
|
|
|
DEFAULT_TLS_HOSTNAME = 'localhost'
|
2018-09-26 06:28:32 +00:00
|
|
|
MIN_DOCKER_VERSION = "1.8.0"
|
2018-04-17 15:51:56 +00:00
|
|
|
DEFAULT_TIMEOUT_SECONDS = 60
|
2016-04-20 22:19:10 +00:00
|
|
|
|
|
|
|
DOCKER_COMMON_ARGS = dict(
|
2019-02-18 20:40:52 +00:00
|
|
|
docker_host=dict(type='str', default=DEFAULT_DOCKER_HOST, fallback=(env_fallback, ['DOCKER_HOST']), aliases=['docker_url']),
|
2018-08-29 09:49:27 +00:00
|
|
|
tls_hostname=dict(type='str', default=DEFAULT_TLS_HOSTNAME, fallback=(env_fallback, ['DOCKER_TLS_HOSTNAME'])),
|
2019-02-18 20:40:52 +00:00
|
|
|
api_version=dict(type='str', default='auto', fallback=(env_fallback, ['DOCKER_API_VERSION']), aliases=['docker_api_version']),
|
2018-08-29 09:49:27 +00:00
|
|
|
timeout=dict(type='int', default=DEFAULT_TIMEOUT_SECONDS, fallback=(env_fallback, ['DOCKER_TIMEOUT'])),
|
2019-02-16 15:14:29 +00:00
|
|
|
cacert_path=dict(type='path', aliases=['tls_ca_cert']),
|
|
|
|
cert_path=dict(type='path', aliases=['tls_client_cert']),
|
|
|
|
key_path=dict(type='path', aliases=['tls_client_key']),
|
2018-08-29 09:49:27 +00:00
|
|
|
ssl_version=dict(type='str', fallback=(env_fallback, ['DOCKER_SSL_VERSION'])),
|
|
|
|
tls=dict(type='bool', default=DEFAULT_TLS, fallback=(env_fallback, ['DOCKER_TLS'])),
|
|
|
|
tls_verify=dict(type='bool', default=DEFAULT_TLS_VERIFY, fallback=(env_fallback, ['DOCKER_TLS_VERIFY'])),
|
2018-04-17 15:51:56 +00:00
|
|
|
debug=dict(type='bool', default=False)
|
2016-04-20 22:19:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
DOCKER_MUTUALLY_EXCLUSIVE = [
|
|
|
|
['tls', 'tls_verify']
|
|
|
|
]
|
|
|
|
|
|
|
|
DOCKER_REQUIRED_TOGETHER = [
|
|
|
|
['cert_path', 'key_path']
|
|
|
|
]
|
|
|
|
|
|
|
|
DEFAULT_DOCKER_REGISTRY = 'https://index.docker.io/v1/'
|
2017-11-21 03:08:30 +00:00
|
|
|
EMAIL_REGEX = r'[^@]+@[^@]+\.[^@]+'
|
2016-04-20 22:19:10 +00:00
|
|
|
BYTE_SUFFIXES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
|
|
|
|
|
|
|
|
|
|
|
if not HAS_DOCKER_PY:
|
2018-11-07 13:31:12 +00:00
|
|
|
docker_version = None
|
|
|
|
|
2016-04-20 22:19:10 +00:00
|
|
|
# No docker-py. Create a place holder client to allow
|
|
|
|
# instantiation of AnsibleModule and proper error handing
|
2018-10-06 13:50:31 +00:00
|
|
|
class Client(object): # noqa: F811
|
2016-04-20 22:19:10 +00:00
|
|
|
def __init__(self, **kwargs):
|
|
|
|
pass
|
|
|
|
|
2018-10-06 13:50:31 +00:00
|
|
|
class APIError(Exception): # noqa: F811
|
2017-07-14 12:40:44 +00:00
|
|
|
pass
|
|
|
|
|
2018-10-29 08:22:52 +00:00
|
|
|
class NotFound(Exception): # noqa: F811
|
|
|
|
pass
|
|
|
|
|
2016-04-20 22:19:10 +00:00
|
|
|
|
2018-10-06 13:50:31 +00:00
|
|
|
def is_image_name_id(name):
|
|
|
|
"""Checks 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
|
|
|
|
|
|
|
|
|
2018-08-28 11:03:33 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2016-04-20 22:19:10 +00:00
|
|
|
class DockerBaseClass(object):
|
|
|
|
|
|
|
|
def __init__(self):
|
2016-04-23 16:47:16 +00:00
|
|
|
self.debug = False
|
2016-04-20 22:19:10 +00:00
|
|
|
|
|
|
|
def log(self, msg, pretty_print=False):
|
2016-04-23 16:47:16 +00:00
|
|
|
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=(',', ': ')))
|
2016-04-25 20:07:24 +00:00
|
|
|
# log_file.write(u'\n')
|
2016-04-23 16:47:16 +00:00
|
|
|
# else:
|
|
|
|
# log_file.write(msg + u'\n')
|
2016-04-20 22:19:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
class AnsibleDockerClient(Client):
|
|
|
|
|
|
|
|
def __init__(self, argument_spec=None, supports_check_mode=False, mutually_exclusive=None,
|
2018-10-18 09:51:58 +00:00
|
|
|
required_together=None, required_if=None, min_docker_version=MIN_DOCKER_VERSION,
|
2018-12-12 09:05:12 +00:00
|
|
|
min_docker_api_version=None, option_minimal_versions=None,
|
2019-02-13 19:10:23 +00:00
|
|
|
option_minimal_versions_ignore_params=None, fail_results=None):
|
|
|
|
|
|
|
|
# Modules can put information in here which will always be returned
|
|
|
|
# in case client.fail() is called.
|
|
|
|
self.fail_results = fail_results or {}
|
2016-04-20 22:19:10 +00:00
|
|
|
|
|
|
|
merged_arg_spec = dict()
|
|
|
|
merged_arg_spec.update(DOCKER_COMMON_ARGS)
|
|
|
|
if argument_spec:
|
|
|
|
merged_arg_spec.update(argument_spec)
|
|
|
|
self.arg_spec = merged_arg_spec
|
|
|
|
|
|
|
|
mutually_exclusive_params = []
|
|
|
|
mutually_exclusive_params += DOCKER_MUTUALLY_EXCLUSIVE
|
|
|
|
if mutually_exclusive:
|
|
|
|
mutually_exclusive_params += mutually_exclusive
|
|
|
|
|
|
|
|
required_together_params = []
|
|
|
|
required_together_params += DOCKER_REQUIRED_TOGETHER
|
|
|
|
if required_together:
|
|
|
|
required_together_params += required_together
|
|
|
|
|
|
|
|
self.module = AnsibleModule(
|
|
|
|
argument_spec=merged_arg_spec,
|
|
|
|
supports_check_mode=supports_check_mode,
|
|
|
|
mutually_exclusive=mutually_exclusive_params,
|
|
|
|
required_together=required_together_params,
|
|
|
|
required_if=required_if)
|
|
|
|
|
2018-10-18 13:50:26 +00:00
|
|
|
NEEDS_DOCKER_PY2 = (LooseVersion(min_docker_version) >= LooseVersion('2.0.0'))
|
2018-10-18 09:51:58 +00:00
|
|
|
|
2018-11-05 00:25:11 +00:00
|
|
|
self.docker_py_version = LooseVersion(docker_version)
|
|
|
|
|
2018-04-17 15:29:24 +00:00
|
|
|
if HAS_DOCKER_MODELS and HAS_DOCKER_SSLADAPTER:
|
|
|
|
self.fail("Cannot have both the docker-py and docker python modules installed together as they use the same namespace and "
|
2018-07-11 21:57:39 +00:00
|
|
|
"cause a corrupt installation. Please uninstall both packages, and re-install only the docker-py or docker python "
|
2018-08-28 16:53:43 +00:00
|
|
|
"module. It is recommended to install the docker module if no support for Python 2.6 is required. "
|
|
|
|
"Please note that simply uninstalling one of the modules can leave the other module in a broken state.")
|
2018-04-17 15:29:24 +00:00
|
|
|
|
2016-04-20 22:19:10 +00:00
|
|
|
if not HAS_DOCKER_PY:
|
2018-10-18 09:51:58 +00:00
|
|
|
if NEEDS_DOCKER_PY2:
|
|
|
|
msg = "Failed to import docker - %s. Try `pip install docker`"
|
|
|
|
else:
|
|
|
|
msg = "Failed to import docker or docker-py - %s. Try `pip install docker` or `pip install docker-py` (Python 2.6)"
|
|
|
|
self.fail(msg % HAS_DOCKER_ERROR)
|
|
|
|
|
2018-11-05 00:25:11 +00:00
|
|
|
if self.docker_py_version < LooseVersion(min_docker_version):
|
2018-10-18 09:51:58 +00:00
|
|
|
if NEEDS_DOCKER_PY2:
|
|
|
|
if docker_version < LooseVersion('2.0'):
|
|
|
|
msg = "Error: docker-py version is %s, while this module requires docker %s. Try `pip uninstall docker-py` and then `pip install docker`"
|
|
|
|
else:
|
|
|
|
msg = "Error: docker version is %s. Minimum version required is %s. Use `pip install --upgrade docker` to upgrade."
|
|
|
|
else:
|
|
|
|
# The minimal required version is < 2.0 (and the current version as well).
|
|
|
|
# Advertise docker (instead of docker-py) for non-Python-2.6 users.
|
|
|
|
msg = ("Error: docker / docker-py version is %s. Minimum version required is %s. "
|
|
|
|
"Hint: if you do not need Python 2.6 support, try `pip uninstall docker-py` followed by `pip install docker`")
|
|
|
|
self.fail(msg % (docker_version, min_docker_version))
|
2016-04-20 22:19:10 +00:00
|
|
|
|
2016-04-23 16:47:16 +00:00
|
|
|
self.debug = self.module.params.get('debug')
|
2016-12-20 13:55:03 +00:00
|
|
|
self.check_mode = self.module.check_mode
|
2016-04-20 22:19:10 +00:00
|
|
|
self._connect_params = self._get_connect_params()
|
|
|
|
|
|
|
|
try:
|
|
|
|
super(AnsibleDockerClient, self).__init__(**self._connect_params)
|
2016-05-16 12:10:35 +00:00
|
|
|
except APIError as exc:
|
2016-04-20 22:19:10 +00:00
|
|
|
self.fail("Docker API error: %s" % exc)
|
2016-05-16 12:10:35 +00:00
|
|
|
except Exception as exc:
|
2016-04-20 22:19:10 +00:00
|
|
|
self.fail("Error connecting: %s" % exc)
|
|
|
|
|
2018-10-18 09:51:58 +00:00
|
|
|
if min_docker_api_version is not None:
|
2018-11-05 00:25:11 +00:00
|
|
|
self.docker_api_version_str = self.version()['ApiVersion']
|
|
|
|
self.docker_api_version = LooseVersion(self.docker_api_version_str)
|
|
|
|
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))
|
2018-10-18 09:51:58 +00:00
|
|
|
|
2018-12-12 09:05:12 +00:00
|
|
|
if option_minimal_versions is not None:
|
|
|
|
self._get_minimal_versions(option_minimal_versions, option_minimal_versions_ignore_params)
|
|
|
|
|
2016-04-20 22:19:10 +00:00
|
|
|
def log(self, msg, pretty_print=False):
|
2016-04-23 16:47:16 +00:00
|
|
|
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=(',', ': ')))
|
2016-04-25 20:07:24 +00:00
|
|
|
# log_file.write(u'\n')
|
2016-04-23 16:47:16 +00:00
|
|
|
# else:
|
|
|
|
# log_file.write(msg + u'\n')
|
|
|
|
|
2019-02-13 19:10:23 +00:00
|
|
|
def fail(self, msg, **kwargs):
|
|
|
|
self.fail_results.update(kwargs)
|
|
|
|
self.module.fail_json(msg=msg, **sanitize_result(self.fail_results))
|
2016-04-20 22:19:10 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _get_value(param_name, param_value, env_variable, default_value):
|
|
|
|
if param_value is not None:
|
|
|
|
# take module parameter value
|
|
|
|
if param_value in BOOLEANS_TRUE:
|
|
|
|
return True
|
|
|
|
if param_value in BOOLEANS_FALSE:
|
|
|
|
return False
|
|
|
|
return param_value
|
|
|
|
|
|
|
|
if env_variable is not None:
|
|
|
|
env_value = os.environ.get(env_variable)
|
|
|
|
if env_value is not None:
|
|
|
|
# take the env variable value
|
|
|
|
if param_name == 'cert_path':
|
|
|
|
return os.path.join(env_value, 'cert.pem')
|
|
|
|
if param_name == 'cacert_path':
|
|
|
|
return os.path.join(env_value, 'ca.pem')
|
|
|
|
if param_name == 'key_path':
|
|
|
|
return os.path.join(env_value, 'key.pem')
|
|
|
|
if env_value in BOOLEANS_TRUE:
|
|
|
|
return True
|
|
|
|
if env_value in BOOLEANS_FALSE:
|
|
|
|
return False
|
|
|
|
return env_value
|
|
|
|
|
|
|
|
# take the default
|
|
|
|
return default_value
|
|
|
|
|
|
|
|
@property
|
|
|
|
def auth_params(self):
|
|
|
|
# Get authentication credentials.
|
|
|
|
# Precedence: module parameters-> environment variables-> defaults.
|
|
|
|
|
|
|
|
self.log('Getting credentials')
|
|
|
|
|
|
|
|
params = dict()
|
|
|
|
for key in DOCKER_COMMON_ARGS:
|
|
|
|
params[key] = self.module.params.get(key)
|
|
|
|
|
|
|
|
if self.module.params.get('use_tls'):
|
|
|
|
# support use_tls option in docker_image.py. This will be deprecated.
|
|
|
|
use_tls = self.module.params.get('use_tls')
|
|
|
|
if use_tls == 'encrypt':
|
|
|
|
params['tls'] = True
|
|
|
|
if use_tls == 'verify':
|
|
|
|
params['tls_verify'] = True
|
|
|
|
|
|
|
|
result = dict(
|
|
|
|
docker_host=self._get_value('docker_host', params['docker_host'], 'DOCKER_HOST',
|
|
|
|
DEFAULT_DOCKER_HOST),
|
|
|
|
tls_hostname=self._get_value('tls_hostname', params['tls_hostname'],
|
2018-08-29 09:49:27 +00:00
|
|
|
'DOCKER_TLS_HOSTNAME', DEFAULT_TLS_HOSTNAME),
|
2016-04-20 22:19:10 +00:00
|
|
|
api_version=self._get_value('api_version', params['api_version'], 'DOCKER_API_VERSION',
|
2016-09-10 07:02:50 +00:00
|
|
|
'auto'),
|
2016-04-20 22:19:10 +00:00
|
|
|
cacert_path=self._get_value('cacert_path', params['cacert_path'], 'DOCKER_CERT_PATH', None),
|
|
|
|
cert_path=self._get_value('cert_path', params['cert_path'], 'DOCKER_CERT_PATH', None),
|
|
|
|
key_path=self._get_value('key_path', params['key_path'], 'DOCKER_CERT_PATH', None),
|
|
|
|
ssl_version=self._get_value('ssl_version', params['ssl_version'], 'DOCKER_SSL_VERSION', None),
|
|
|
|
tls=self._get_value('tls', params['tls'], 'DOCKER_TLS', DEFAULT_TLS),
|
|
|
|
tls_verify=self._get_value('tls_verfy', params['tls_verify'], 'DOCKER_TLS_VERIFY',
|
|
|
|
DEFAULT_TLS_VERIFY),
|
|
|
|
timeout=self._get_value('timeout', params['timeout'], 'DOCKER_TIMEOUT',
|
|
|
|
DEFAULT_TIMEOUT_SECONDS),
|
|
|
|
)
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
def _get_tls_config(self, **kwargs):
|
|
|
|
self.log("get_tls_config:")
|
|
|
|
for key in kwargs:
|
2017-06-02 11:14:11 +00:00
|
|
|
self.log(" %s: %s" % (key, kwargs[key]))
|
2016-04-20 22:19:10 +00:00
|
|
|
try:
|
|
|
|
tls_config = TLSConfig(**kwargs)
|
|
|
|
return tls_config
|
2016-05-16 12:10:35 +00:00
|
|
|
except TLSParameterError as exc:
|
2017-01-30 23:01:47 +00:00
|
|
|
self.fail("TLS config error: %s" % exc)
|
2016-04-20 22:19:10 +00:00
|
|
|
|
|
|
|
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):
|
|
|
|
match = re.match(r"hostname.*doesn\'t match (\'.*\')", str(error))
|
|
|
|
if match:
|
2017-06-02 11:14:11 +00:00
|
|
|
self.fail("You asked for verification that Docker host name matches %s. The actual hostname is %s. "
|
|
|
|
"Most likely you need to set DOCKER_TLS_HOSTNAME or pass tls_hostname with a value of %s. "
|
|
|
|
"You may also use TLS without verification by setting the tls parameter to true."
|
|
|
|
% (self.auth_params['tls_hostname'], match.group(1), match.group(1)))
|
2016-04-20 22:19:10 +00:00
|
|
|
self.fail("SSL Exception: %s" % (error))
|
|
|
|
|
2018-12-12 09:05:12 +00:00
|
|
|
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:
|
2019-01-21 18:45:47 +00:00
|
|
|
used = data['detect_usage'](self)
|
2018-12-12 09:05:12 +00:00
|
|
|
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)
|
|
|
|
|
2016-04-20 22:19:10 +00:00
|
|
|
def get_container(self, name=None):
|
|
|
|
'''
|
|
|
|
Lookup a container and return the inspection results.
|
|
|
|
'''
|
|
|
|
if name is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
search_name = name
|
|
|
|
if not name.startswith('/'):
|
|
|
|
search_name = '/' + name
|
|
|
|
|
|
|
|
result = None
|
|
|
|
try:
|
|
|
|
for container in self.containers(all=True):
|
|
|
|
self.log("testing container: %s" % (container['Names']))
|
2017-01-27 22:42:10 +00:00
|
|
|
if isinstance(container['Names'], list) and search_name in container['Names']:
|
2016-04-20 22:19:10 +00:00
|
|
|
result = container
|
|
|
|
break
|
|
|
|
if container['Id'].startswith(name):
|
|
|
|
result = container
|
|
|
|
break
|
|
|
|
if container['Id'] == name:
|
|
|
|
result = container
|
|
|
|
break
|
2016-05-16 12:10:35 +00:00
|
|
|
except SSLError as exc:
|
2016-04-20 22:19:10 +00:00
|
|
|
self._handle_ssl_error(exc)
|
2016-05-16 12:10:35 +00:00
|
|
|
except Exception as exc:
|
2016-04-20 22:19:10 +00:00
|
|
|
self.fail("Error retrieving container list: %s" % exc)
|
|
|
|
|
|
|
|
if result is not None:
|
|
|
|
try:
|
|
|
|
self.log("Inspecting container Id %s" % result['Id'])
|
|
|
|
result = self.inspect_container(container=result['Id'])
|
|
|
|
self.log("Completed container inspection")
|
2019-02-13 19:10:23 +00:00
|
|
|
except NotFound as dummy:
|
2018-10-29 08:22:52 +00:00
|
|
|
return None
|
2016-05-16 12:10:35 +00:00
|
|
|
except Exception as exc:
|
2016-04-20 22:19:10 +00:00
|
|
|
self.fail("Error inspecting container: %s" % exc)
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
2018-12-10 05:24:05 +00:00
|
|
|
def get_network(self, name=None, id=None):
|
|
|
|
'''
|
|
|
|
Lookup a network and return the inspection results.
|
|
|
|
'''
|
|
|
|
if name is None and id is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
result = None
|
|
|
|
|
|
|
|
if id is None:
|
|
|
|
try:
|
|
|
|
for network in self.networks():
|
|
|
|
self.log("testing network: %s" % (network['Name']))
|
|
|
|
if name == network['Name']:
|
|
|
|
result = network
|
|
|
|
break
|
|
|
|
if network['Id'].startswith(name):
|
|
|
|
result = network
|
|
|
|
break
|
|
|
|
except SSLError as exc:
|
|
|
|
self._handle_ssl_error(exc)
|
|
|
|
except Exception as exc:
|
|
|
|
self.fail("Error retrieving network list: %s" % exc)
|
|
|
|
|
|
|
|
if result is not None:
|
|
|
|
id = result['Id']
|
|
|
|
|
|
|
|
if id is not None:
|
|
|
|
try:
|
|
|
|
self.log("Inspecting network Id %s" % id)
|
|
|
|
result = self.inspect_network(id)
|
|
|
|
self.log("Completed network inspection")
|
2019-02-13 19:10:23 +00:00
|
|
|
except NotFound as dummy:
|
2018-12-10 05:24:05 +00:00
|
|
|
return None
|
|
|
|
except Exception as exc:
|
|
|
|
self.fail("Error inspecting network: %s" % exc)
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
2016-04-20 22:19:10 +00:00
|
|
|
def find_image(self, name, tag):
|
|
|
|
'''
|
2018-10-06 13:50:31 +00:00
|
|
|
Lookup an image (by name and tag) and return the inspection results.
|
2016-04-20 22:19:10 +00:00
|
|
|
'''
|
|
|
|
if not name:
|
|
|
|
return None
|
|
|
|
|
2016-04-25 20:07:24 +00:00
|
|
|
self.log("Find image %s:%s" % (name, tag))
|
|
|
|
images = self._image_lookup(name, tag)
|
|
|
|
if len(images) == 0:
|
|
|
|
# In API <= 1.20 seeing 'docker.io/<name>' as the name of images pulled from docker hub
|
2016-12-20 13:55:03 +00:00
|
|
|
registry, repo_name = auth.resolve_repository_name(name)
|
2016-04-25 20:07:24 +00:00
|
|
|
if registry == 'docker.io':
|
2019-02-17 19:28:40 +00:00
|
|
|
# If docker.io is explicitly there in name, the image
|
|
|
|
# isn't found in some cases (#41509)
|
|
|
|
self.log("Check for docker.io image: %s" % repo_name)
|
|
|
|
images = self._image_lookup(repo_name, tag)
|
|
|
|
if len(images) == 0 and repo_name.startswith('library/'):
|
|
|
|
# Sometimes library/xxx images are not found
|
|
|
|
lookup = repo_name[len('library/'):]
|
|
|
|
self.log("Check for docker.io image: %s" % lookup)
|
|
|
|
images = self._image_lookup(lookup, tag)
|
|
|
|
if len(images) == 0:
|
|
|
|
# Last case: if docker.io wasn't there, it can be that
|
|
|
|
# the image wasn't found either (#15586)
|
|
|
|
lookup = "%s/%s" % (registry, repo_name)
|
|
|
|
self.log("Check for docker.io image: %s" % lookup)
|
|
|
|
images = self._image_lookup(lookup, tag)
|
2016-04-20 22:19:10 +00:00
|
|
|
|
|
|
|
if len(images) > 1:
|
2016-04-25 20:07:24 +00:00
|
|
|
self.fail("Registry returned more than one result for %s:%s" % (name, tag))
|
2016-04-20 22:19:10 +00:00
|
|
|
|
|
|
|
if len(images) == 1:
|
|
|
|
try:
|
|
|
|
inspection = self.inspect_image(images[0]['Id'])
|
2016-05-16 12:10:35 +00:00
|
|
|
except Exception as exc:
|
2016-04-25 20:07:24 +00:00
|
|
|
self.fail("Error inspecting image %s:%s - %s" % (name, tag, str(exc)))
|
2016-04-20 22:19:10 +00:00
|
|
|
return inspection
|
2016-04-25 20:07:24 +00:00
|
|
|
|
|
|
|
self.log("Image %s:%s not found." % (name, tag))
|
2016-04-20 22:19:10 +00:00
|
|
|
return None
|
|
|
|
|
2018-10-06 13:50:31 +00:00
|
|
|
def find_image_by_id(self, id):
|
|
|
|
'''
|
|
|
|
Lookup an image (by ID) and return the inspection results.
|
|
|
|
'''
|
|
|
|
if not id:
|
|
|
|
return None
|
|
|
|
|
|
|
|
self.log("Find image %s (by ID)" % id)
|
|
|
|
try:
|
|
|
|
inspection = self.inspect_image(id)
|
|
|
|
except Exception as exc:
|
|
|
|
self.fail("Error inspecting image ID %s - %s" % (id, str(exc)))
|
|
|
|
return inspection
|
|
|
|
|
2016-12-20 13:55:03 +00:00
|
|
|
def _image_lookup(self, name, tag):
|
2016-04-25 20:07:24 +00:00
|
|
|
'''
|
2016-12-20 13:55:03 +00:00
|
|
|
Including a tag in the name parameter sent to the docker-py images method does not
|
2016-04-25 20:07:24 +00:00
|
|
|
work consistently. Instead, get the result set for name and manually check if the tag
|
|
|
|
exists.
|
|
|
|
'''
|
|
|
|
try:
|
|
|
|
response = self.images(name=name)
|
|
|
|
except Exception as exc:
|
|
|
|
self.fail("Error searching for image %s - %s" % (name, str(exc)))
|
|
|
|
images = response
|
2016-12-20 13:55:03 +00:00
|
|
|
if tag:
|
2016-04-25 20:07:24 +00:00
|
|
|
lookup = "%s:%s" % (name, tag)
|
2016-07-11 21:10:57 +00:00
|
|
|
images = []
|
2016-04-25 20:07:24 +00:00
|
|
|
for image in response:
|
2016-07-11 21:10:57 +00:00
|
|
|
tags = image.get('RepoTags')
|
|
|
|
if tags and lookup in tags:
|
2016-04-25 20:07:24 +00:00
|
|
|
images = [image]
|
|
|
|
break
|
|
|
|
return images
|
|
|
|
|
2016-04-20 22:19:10 +00:00
|
|
|
def pull_image(self, name, tag="latest"):
|
|
|
|
'''
|
|
|
|
Pull an image
|
|
|
|
'''
|
2016-04-25 20:07:24 +00:00
|
|
|
self.log("Pulling image %s:%s" % (name, tag))
|
2017-04-03 19:35:49 +00:00
|
|
|
old_tag = self.find_image(name, tag)
|
2016-04-20 22:19:10 +00:00
|
|
|
try:
|
2016-10-03 15:38:12 +00:00
|
|
|
for line in self.pull(name, tag=tag, stream=True, decode=True):
|
2016-04-25 20:07:24 +00:00
|
|
|
self.log(line, pretty_print=True)
|
|
|
|
if line.get('error'):
|
|
|
|
if line.get('errorDetail'):
|
|
|
|
error_detail = line.get('errorDetail')
|
|
|
|
self.fail("Error pulling %s - code: %s message: %s" % (name,
|
|
|
|
error_detail.get('code'),
|
|
|
|
error_detail.get('message')))
|
|
|
|
else:
|
|
|
|
self.fail("Error pulling %s - %s" % (name, line.get('error')))
|
2016-05-16 12:10:35 +00:00
|
|
|
except Exception as exc:
|
2016-04-20 22:19:10 +00:00
|
|
|
self.fail("Error pulling image %s:%s - %s" % (name, tag, str(exc)))
|
|
|
|
|
2017-04-03 19:35:49 +00:00
|
|
|
new_tag = self.find_image(name, tag)
|
|
|
|
|
|
|
|
return new_tag, old_tag == new_tag
|
2018-10-30 08:50:34 +00:00
|
|
|
|
|
|
|
|
|
|
|
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, type):
|
|
|
|
'''
|
|
|
|
Compare values a and b as described by method and type.
|
|
|
|
|
|
|
|
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
|
|
|
|
``type == '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 ``type`` 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 type == '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 type == 'value':
|
|
|
|
return a == b
|
|
|
|
elif type == '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 type == 'dict':
|
|
|
|
if method == 'strict':
|
|
|
|
return a == b
|
|
|
|
else:
|
|
|
|
return compare_dict_allow_more_present(a, b)
|
|
|
|
elif type == 'set':
|
|
|
|
set_a = set(a)
|
|
|
|
set_b = set(b)
|
|
|
|
if method == 'strict':
|
|
|
|
return set_a == set_b
|
|
|
|
else:
|
|
|
|
return set_b >= set_a
|
|
|
|
elif type == '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
|
2018-11-19 09:59:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
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']
|
2018-11-26 13:52:21 +00:00
|
|
|
return before, after
|
2018-11-19 09:59:54 +00:00
|
|
|
|
|
|
|
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
|
2018-12-12 09:05:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
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
|
2019-02-18 09:46:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
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<hours>\d+)h)?'
|
|
|
|
r'((?P<minutes>\d+)m(?!s))?'
|
|
|
|
r'((?P<seconds>\d+)s)?'
|
|
|
|
r'((?P<milliseconds>\d+)ms)?'
|
|
|
|
r'((?P<microseconds>\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
|