ansible/cloud/docker/docker_image.py
Michael Schuett bf5929d32a docker_image TLS
Check commit enables using tls when using the docker_image module. It
also removes the default for docker_url which doesn't allow us to check
for DOCKER_HOST which is a more sane default. This allows you to use
docker_image on OSX but more documentation is needed.
2015-11-02 10:49:24 -08:00

383 lines
12 KiB
Python

#!/usr/bin/python
#
# (c) 2014, Pavel Antonov <antonov@adwz.ru>
#
# This file is part of Ansible
#
# This module 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.
#
# This software 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 this software. If not, see <http://www.gnu.org/licenses/>.
######################################################################
DOCUMENTATION = '''
---
module: docker_image
author: "Pavel Antonov (@softzilla)"
version_added: "1.5"
short_description: manage docker images
description:
- Create, check and remove docker images
options:
path:
description:
- Path to directory with Dockerfile
required: false
default: null
aliases: []
dockerfile:
description:
- Dockerfile to use
required: false
default: Dockerfile
version_added: "2.0"
name:
description:
- Image name to work with
required: true
default: null
aliases: []
tag:
description:
- Image tag to work with
required: false
default: "latest"
aliases: []
nocache:
description:
- Do not use cache with building
required: false
default: false
aliases: []
docker_url:
description:
- URL of docker host to issue commands to
required: false
default: unix://var/run/docker.sock
aliases: []
docker_api_version:
description:
- Remote API version to use. This defaults to the current default as
specified by docker-py.
default: docker-py default remote API version
version_added: "2.0"
state:
description:
- Set the state of the image
required: false
default: present
choices: [ "present", "absent", "build" ]
aliases: []
timeout:
description:
- Set image operation timeout
required: false
default: 600
aliases: []
requirements:
- "python >= 2.6"
- "docker-py"
- "requests"
'''
EXAMPLES = '''
Build docker image if required. Path should contains Dockerfile to build image:
- hosts: web
sudo: yes
tasks:
- name: check or build image
docker_image: path="/path/to/build/dir" name="my/app" state=present
Build new version of image:
- hosts: web
sudo: yes
tasks:
- name: check or build image
docker_image: path="/path/to/build/dir" name="my/app" state=build
Remove image from local docker storage:
- hosts: web
sudo: yes
tasks:
- name: remove image
docker_image: name="my/app" state=absent
'''
import re
import os
from urlparse import urlparse
try:
import json
except ImportError:
import simplejson as json
try:
from requests.exceptions import *
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
try:
import docker.client
HAS_DOCKER_CLIENT = True
except ImportError:
HAS_DOCKER_CLIENT = False
DEFAULT_DOCKER_API_VERSION = None
if HAS_DOCKER_CLIENT:
try:
from docker.errors import APIError as DockerAPIError
except ImportError:
from docker.client import APIError as DockerAPIError
try:
# docker-py 1.2+
import docker.constants
DEFAULT_DOCKER_API_VERSION = docker.constants.DEFAULT_DOCKER_API_VERSION
except (ImportError, AttributeError):
# docker-py less than 1.2
DEFAULT_DOCKER_API_VERSION = docker.client.DEFAULT_DOCKER_API_VERSION
class DockerImageManager:
def __init__(self, module):
self.module = module
self.path = self.module.params.get('path')
self.dockerfile = self.module.params.get('dockerfile')
self.name = self.module.params.get('name')
self.tag = self.module.params.get('tag')
self.nocache = self.module.params.get('nocache')
# Connect to the docker server using any configured host and TLS settings.
env_host = os.getenv('DOCKER_HOST')
env_docker_verify = os.getenv('DOCKER_TLS_VERIFY')
env_cert_path = os.getenv('DOCKER_CERT_PATH')
env_docker_hostname = os.getenv('DOCKER_TLS_HOSTNAME')
docker_url = module.params.get('docker_url')
if not docker_url:
if env_host:
docker_url = env_host
else:
docker_url = 'unix://var/run/docker.sock'
docker_api_version = module.params.get('docker_api_version')
tls_client_cert = module.params.get('tls_client_cert', None)
if not tls_client_cert and env_cert_path:
tls_client_cert = os.path.join(env_cert_path, 'cert.pem')
tls_client_key = module.params.get('tls_client_key', None)
if not tls_client_key and env_cert_path:
tls_client_key = os.path.join(env_cert_path, 'key.pem')
tls_ca_cert = module.params.get('tls_ca_cert')
if not tls_ca_cert and env_cert_path:
tls_ca_cert = os.path.join(env_cert_path, 'ca.pem')
tls_hostname = module.params.get('tls_hostname')
if tls_hostname is None:
if env_docker_hostname:
tls_hostname = env_docker_hostname
else:
parsed_url = urlparse(docker_url)
if ':' in parsed_url.netloc:
tls_hostname = parsed_url.netloc[:parsed_url.netloc.rindex(':')]
else:
tls_hostname = parsed_url
if not tls_hostname:
tls_hostname = True
# use_tls can be one of four values:
# no: Do not use tls
# encrypt: Use tls. We may do client auth. We will not verify the server
# verify: Use tls. We may do client auth. We will verify the server
# None: Only use tls if the parameters for client auth were specified
# or tls_ca_cert (which requests verifying the server with
# a specific ca certificate)
use_tls = module.params.get('use_tls')
if use_tls is None and env_docker_verify is not None:
use_tls = 'verify'
tls_config = None
if use_tls != 'no':
params = {}
# Setup client auth
if tls_client_cert and tls_client_key:
params['client_cert'] = (tls_client_cert, tls_client_key)
# We're allowed to verify the connection to the server
if use_tls == 'verify' or (use_tls is None and tls_ca_cert):
if tls_ca_cert:
params['ca_cert'] = tls_ca_cert
params['verify'] = True
params['assert_hostname'] = tls_hostname
else:
params['verify'] = True
params['assert_hostname'] = tls_hostname
elif use_tls == 'encrypt':
params['verify'] = False
if params:
# See https://github.com/docker/docker-py/blob/d39da11/docker/utils/utils.py#L279-L296
docker_url = docker_url.replace('tcp://', 'https://')
tls_config = docker.tls.TLSConfig(**params)
self.client = docker.Client(
base_url=docker_url.geturl(),
version=module.params.get('docker_api_version'),
timeout=module.params.get('timeout'),
tls=tls_config)
self.changed = False
self.log = []
self.error_msg = None
def get_log(self, as_string=True):
return "".join(self.log) if as_string else self.log
def build(self):
stream = self.client.build(self.path, dockerfile=self.dockerfile, tag=':'.join([self.name, self.tag]), nocache=self.nocache, rm=True, stream=True)
success_search = r'Successfully built ([0-9a-f]+)'
image_id = None
self.changed = True
for chunk in stream:
if not chunk:
continue
try:
chunk_json = json.loads(chunk)
except ValueError:
continue
if 'error' in chunk_json:
self.error_msg = chunk_json['error']
return None
if 'stream' in chunk_json:
output = chunk_json['stream']
self.log.append(output)
match = re.search(success_search, output)
if match:
image_id = match.group(1)
# Just in case we skipped evaluating the JSON returned from build
# during every iteration, add an error if the image_id was never
# populated
if not image_id:
self.error_msg = 'Unknown error encountered'
return image_id
def has_changed(self):
return self.changed
def get_images(self):
filtered_images = []
images = self.client.images()
for i in images:
# Docker-py version >= 0.3 (Docker API >= 1.8)
if 'RepoTags' in i:
repotag = ':'.join([self.name, self.tag])
if not self.name or repotag in i['RepoTags']:
filtered_images.append(i)
# Docker-py version < 0.3 (Docker API < 1.8)
elif (not self.name or self.name == i['Repository']) and (not self.tag or self.tag == i['Tag']):
filtered_images.append(i)
return filtered_images
def remove_images(self):
images = self.get_images()
for i in images:
try:
self.client.remove_image(i['Id'])
self.changed = True
except DockerAPIError as e:
# image can be removed by docker if not used
pass
def main():
module = AnsibleModule(
argument_spec = dict(
path = dict(required=False, default=None),
dockerfile = dict(required=False, default="Dockerfile"),
name = dict(required=True),
tag = dict(required=False, default="latest"),
nocache = dict(default=False, type='bool'),
state = dict(default='present', choices=['absent', 'present', 'build']),
use_tls = dict(default=None, choices=['no', 'encrypt', 'verify']),
tls_client_cert = dict(required=False, default=None, type='str'),
tls_client_key = dict(required=False, default=None, type='str'),
tls_ca_cert = dict(required=False, default=None, type='str'),
tls_hostname = dict(required=False, type='str', default=None),
docker_url = dict(),
docker_api_version = dict(required=False,
default=DEFAULT_DOCKER_API_VERSION,
type='str'),
timeout = dict(default=600, type='int'),
)
)
if not HAS_DOCKER_CLIENT:
module.fail_json(msg='docker-py is needed for this module')
if not HAS_REQUESTS:
module.fail_json(msg='requests is needed for this module')
try:
manager = DockerImageManager(module)
state = module.params.get('state')
failed = False
image_id = None
msg = ''
do_build = False
# build image if not exists
if state == "present":
images = manager.get_images()
if len(images) == 0:
do_build = True
# build image
elif state == "build":
do_build = True
# remove image or images
elif state == "absent":
manager.remove_images()
if do_build:
image_id = manager.build()
if image_id:
msg = "Image built: %s" % image_id
else:
failed = True
msg = "Error: %s\nLog:%s" % (manager.error_msg, manager.get_log())
module.exit_json(failed=failed, changed=manager.has_changed(), msg=msg, image_id=image_id)
except DockerAPIError as e:
module.exit_json(failed=True, changed=manager.has_changed(), msg="Docker API error: " + e.explanation)
except RequestException as e:
module.exit_json(failed=True, changed=manager.has_changed(), msg=repr(e))
# import module snippets
from ansible.module_utils.basic import *
if __name__ == '__main__':
main()