ansible/lib/ansible/module_utils/docker_common.py
2016-04-23 12:46:47 -04:00

498 lines
18 KiB
Python

#
# 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 logging
import re
import json
import sys
import copy
from requests.exceptions import SSLError
from urlparse import urlparse
from logging import Handler, NOTSET
from ansible.module_utils.basic import *
HAS_DOCKER_PY = True
HAS_DOCKER_ERROR = None
try:
from docker import Client
from docker import __version__ as docker_version
from docker.errors import APIError, TLSParameterError, NotFound
from docker.tls import TLSConfig
from docker.constants import DEFAULT_TIMEOUT_SECONDS, DEFAULT_DOCKER_API_VERSION
from docker.utils.types import Ulimit, LogConfig
except ImportError, exc:
HAS_DOCKER_ERROR = str(exc)
HAS_DOCKER_PY = False
DEFAULT_DOCKER_HOST = 'unix://var/run/docker.sock'
DEFAULT_TLS = False
DEFAULT_TLS_VERIFY = False
MIN_DOCKER_VERSION = "1.7.0"
DOCKER_COMMON_ARGS = dict(
docker_host=dict(type='str', aliases=['docker_url']),
tls_hostname=dict(type='str'),
api_version=dict(type='str', aliases=['docker_api_version']),
timeout=dict(type='int'),
cacert_path=dict(type='str', aliases=['tls_ca_cert']),
cert_path=dict(type='str', aliases=['tls_client_cert']),
key_path=dict(type='str', aliases=['tls_client_key']),
ssl_version=dict(type='str'),
tls=dict(type='bool'),
tls_verify=dict(type='bool'),
debug=dict(type='bool', default=False),
filter_logger=dict(type='bool', default=False),
log_path=dict(type='str', default='docker.log'),
log_mode=dict(type='str', choices=['stderr', 'file', 'syslog'], default='syslog'),
)
DOCKER_MUTUALLY_EXCLUSIVE = [
['tls', 'tls_verify']
]
DOCKER_REQUIRED_TOGETHER = [
['cert_path', 'key_path']
]
DEFAULT_DOCKER_REGISTRY = 'https://index.docker.io/v1/'
EMAIL_REGEX = '[^@]+@[^@]+\.[^@]+'
BYTE_SUFFIXES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
if not HAS_DOCKER_PY:
# No docker-py. Create a place holder client to allow
# instantiation of AnsibleModule and proper error handing
class Client(object):
def __init__(self, **kwargs):
pass
def human_to_bytes(number):
if number is None:
return 0
if isinstance(number, int):
return number
if number[-1].isdigit():
return int(number)
if number[-1] == BYTE_SUFFIXES[0] and number[-2].isdigit():
return int(number[:-1])
i = 1
for each in BYTE_SUFFIXES[1:]:
if number[-len(each):] == BYTE_SUFFIXES[i]:
return int(number[:-len(each)]) * (1024 ** i)
i += 1
raise ValueError("Failed to convert %s. The suffix must be one of %s" % (number, ','.join(BYTE_SUFFIXES)))
def log_msg(data, pretty_print=False):
'''
Sanitize data to be logged, and if requested, attempt to pretty print JSON.
:param data: string to dumped to the log
:param pretty_print: bool
:return: None
'''
out = None
if data:
if isinstance(data, dict):
sanitized = copy.deepcopy(data)
sanitize_dict(sanitized)
elif isinstance(data, list):
sanitized = copy.deepcopy(data)
sanitize_list(sanitized)
else:
sanitized = heuristic_log_sanitize(data)
out = sanitized
if pretty_print:
# Attempt formatting JSON. The sanitizing may have broken things.
try:
out = json.dumps(sanitized, sort_keys=True, indent=4, separators=(',', ': '))
except:
out = sanitized
return out
def sanitize_dict(data):
for key, value in data.items():
if isinstance(value, basestring):
data[key] = heuristic_log_sanitize(value)
if isinstance(value, dict):
sanitize_dict(value)
if isinstance(value, list):
sanitize_list(value)
def sanitize_list(data):
for item in data:
if isinstance(item, basestring):
item = heuristic_log_sanitize(item)
if isinstance(item, list):
sanitize_list(item)
if isinstance(item, dict):
sanitize_dict(item)
class DockerBaseClass(object):
def __init__(self):
self.logger = logging.getLogger(self.__class__.__name__)
def log(self, msg, pretty_print=False):
self.logger.debug(log_msg(msg, pretty_print=pretty_print))
class DockerSysLogHandler(Handler):
def __init__(self, level=NOTSET, module=None):
self.module = module
super(DockerSysLogHandler, self).__init__(level)
def emit(self, record):
log_entry = self.format_record
self.module.debug(log_entry)
class AnsibleDockerClient(Client):
def __init__(self, argument_spec=None, supports_check_mode=False, mutually_exclusive=None,
required_together=None, required_if=None):
self.logger = logging.getLogger(self.__class__.__name__)
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)
if not HAS_DOCKER_PY:
self.fail("Failed to import docker-py - %s. Try `pip install docker-py`" % HAS_DOCKER_ERROR)
if docker_version < MIN_DOCKER_VERSION:
self.fail("Error: docker-py version is %s. Minimum version required is %s." % (docker_version,
MIN_DOCKER_VERSION))
debug = self.module.params.get('debug')
log_mode = self.module.params.get('log_mode')
filter_logger = self.module.params.get('filter_logger')
if debug and log_mode == 'syslog':
handler = DockerSysLogHandler(level=logging.DEBUG, module=self.module)
self.logger.addHandler(handler)
logging.basicConfig(level=logging.DEBUG)
elif debug and log_mode == 'file':
log_path = self.module.params.get('log_path')
logging.basicConfig(level=logging.DEBUG, filename=log_path)
elif debug and log_mode == 'stderr':
logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
if filter_logger:
for h in logging.root.handlers:
h.addFilter(logging.Filter(name=self.logger.name))
self.check_mode = self.module.check_mode
self._connect_params = self._get_connect_params()
try:
super(AnsibleDockerClient, self).__init__(**self._connect_params)
except APIError, exc:
self.fail("Docker API error: %s" % exc)
except Exception, exc:
self.fail("Error connecting: %s" % exc)
def log(self, msg, pretty_print=False):
self.logger.debug(log_msg(msg, pretty_print=pretty_print))
def fail(self, msg):
self.module.fail_json(msg=msg)
@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'],
'DOCKER_TLS_HOSTNAME', 'localhost'),
api_version=self._get_value('api_version', params['api_version'], 'DOCKER_API_VERSION',
DEFAULT_DOCKER_API_VERSION),
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:
self.log(" %s: %s" % (key, kwargs[key]))
try:
tls_config = TLSConfig(**kwargs)
return tls_config
except TLSParameterError, 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):
match = re.match(r"hostname.*doesn\'t match (\'.*\')", str(error))
if match:
msg = "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))
self.fail(msg)
self.fail("SSL Exception: %s" % (error))
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']))
if search_name in container['Names']:
result = container
break
if container['Id'].startswith(name):
result = container
break
if container['Id'] == name:
result = container
break
except SSLError, exc:
self._handle_ssl_error(exc)
except Exception, exc:
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")
except Exception, exc:
self.fail("Error inspecting container: %s" % exc)
return result
def find_image(self, name, tag):
'''
Lookup an image and return the inspection results.
'''
if not name:
return None
lookup = name
if tag:
lookup = "%s:%s" % (name, tag)
self.log("Find image %s" % lookup)
try:
images = self.images(name=lookup)
except Exception, exc:
self.fail("Error getting image: %s" % str(exc))
if len(images) > 1:
self.fail("Registry returned more than one result for %s" % lookup)
if len(images) == 1:
try:
inspection = self.inspect_image(images[0]['Id'])
except Exception, exc:
self.fail("Error inspecting image %s - %s" % (lookup, str(exc)))
inspection['Name'] = lookup
return inspection
self.log("Image %s not found." % lookup)
return None
def pull_image(self, name, tag="latest"):
'''
Pull an image
'''
try:
self.log("Pulling image %s:%s" % (name, tag))
for line in self.pull(name, tag=tag, stream=True):
response = json.loads(line)
self.log(response, pretty_print=True)
return self.find_image(name, tag)
except Exception, exc:
self.fail("Error pulling image %s:%s - %s" % (name, tag, str(exc)))