#!/usr/bin/python # # (c) 2014, Pavel Antonov # # 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 . ###################################################################### 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 SSLError as e: if get_platform() == "Darwin" and "DOCKER_HOST" in os.environ: # Ensure that the environment variables has been set if "DOCKER_HOST" not in os.environ: environment_error = ''' It looks like you have not set your docker environment variables. Please ensure that you have set the requested variables as instructed when running boot2docker up. If they are set in .bash_profile you will need to symlink it to .bashrc. ''' module.exit_json(failed=True, chaged=manager.has_changed(), msg="SSLError: " + str(e) + environment_error) # If the above is true it's likely the hostname does not match else: environment_error = ''' You may need to ignore hostname missmatches by passing -e 'host_key_checking=False' through the command line. If this does not resolve the issue please open an issue at ansible/ansible-modules-core and ping michaeljs1990 ''' module.exit_json(failed=True, chaged=manager.has_changed(), msg="SSLError: " + str(e) + environment_error) # General error for non darwin users else: module.exit_json(failed=True, chaged=manager.has_changed(), msg="SSLError: " + str(e)) 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()