mirror of https://github.com/ansible/ansible.git
Merge pull request #3505 from chouseknecht/docker_image
Adding refactored docker_image module.reviewable/pr18780/r1
commit
b68114f838
@ -1,457 +1,516 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
|
||||
# (c) 2014, Pavel Antonov <antonov@adwz.ru>
|
||||
# Copyright 2016 Red Hat | Ansible
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# 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.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# 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 this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
######################################################################
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: docker_image
|
||||
author: "Pavel Antonov (@softzilla)"
|
||||
|
||||
short_description: Manage docker images.
|
||||
|
||||
version_added: "1.5"
|
||||
short_description: manage docker images
|
||||
|
||||
description:
|
||||
- Create, check and remove docker images
|
||||
- Build, load or pull an image, making the image available for creating containers. Also supports tagging an
|
||||
image into a repository and archiving an image to a .tar file.
|
||||
|
||||
options:
|
||||
path:
|
||||
archive_path:
|
||||
description:
|
||||
- Path to directory with Dockerfile
|
||||
- Use with state 'present' to archive an image to a .tar file.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "2.1"
|
||||
dockerfile:
|
||||
description:
|
||||
- Dockerfile to use
|
||||
required: false
|
||||
- Use with state 'present' to provide an alternate name for the Dockerfile to use when building an image.
|
||||
default: Dockerfile
|
||||
version_added: "2.0"
|
||||
name:
|
||||
force:
|
||||
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
|
||||
- Use with absent state to un-tag and remove all images matching the specified name. Use with states 'present'
|
||||
and 'tagged' to take action even when an image already exists.
|
||||
default: false
|
||||
aliases: []
|
||||
docker_url:
|
||||
version_added: "2.1"
|
||||
http_timeout:
|
||||
description:
|
||||
- URL of docker host to issue commands to
|
||||
- Timeout for HTTP requests during the image build operation. Provide a positive integer value for the number of
|
||||
seconds.
|
||||
default: null
|
||||
required: false
|
||||
default: ${DOCKER_HOST} or unix://var/run/docker.sock
|
||||
aliases: []
|
||||
use_tls:
|
||||
version_added: "2.1"
|
||||
name:
|
||||
description:
|
||||
- Whether to use tls to connect to the docker server. "no" means not to
|
||||
use tls (and ignore any other tls related parameters). "encrypt" means
|
||||
to use tls to encrypt the connection to the server. "verify" means to
|
||||
also verify that the server's certificate is valid for the server
|
||||
(this both verifies the certificate against the CA and that the
|
||||
certificate was issued for that host. If this is unspecified, tls will
|
||||
only be used if one of the other tls options require it.
|
||||
choices: [ "no", "encrypt", "verify" ]
|
||||
version_added: "2.0"
|
||||
tls_client_cert:
|
||||
- "Image name. Name format will be one of: name, repository/name, registry_server:port/name.
|
||||
When pushing or pulling an image the name can optionally include the tag by appending ':tag_name'."
|
||||
required: true
|
||||
path:
|
||||
description:
|
||||
- Path to the PEM-encoded certificate used to authenticate docker client.
|
||||
If specified tls_client_key must be valid
|
||||
default: ${DOCKER_CERT_PATH}/cert.pem
|
||||
version_added: "2.0"
|
||||
tls_client_key:
|
||||
- Use with state 'present' to build an image. Will be the path to a directory containing the context and
|
||||
Dockerfile for building an image.
|
||||
aliases:
|
||||
- build_path
|
||||
default: null
|
||||
required: false
|
||||
pull:
|
||||
description:
|
||||
- Path to the PEM-encoded key used to authenticate docker client. If
|
||||
specified tls_client_cert must be valid
|
||||
default: ${DOCKER_CERT_PATH}/key.pem
|
||||
version_added: "2.0"
|
||||
tls_ca_cert:
|
||||
- When building an image downloads any updates to the FROM image in Dockerfile.
|
||||
default: true
|
||||
version_added: "2.1"
|
||||
rm:
|
||||
description:
|
||||
- Path to a PEM-encoded certificate authority to secure the Docker connection.
|
||||
This has no effect if use_tls is encrypt.
|
||||
default: ${DOCKER_CERT_PATH}/ca.pem
|
||||
version_added: "2.0"
|
||||
tls_hostname:
|
||||
- Remove intermediate containers after build.
|
||||
default: true
|
||||
version_added: "2.1"
|
||||
nocache:
|
||||
description:
|
||||
- A hostname to check matches what's supplied in the docker server's
|
||||
certificate. If unspecified, the hostname is taken from the docker_url.
|
||||
default: Taken from docker_url
|
||||
version_added: "2.0"
|
||||
docker_api_version:
|
||||
- Do not use cache when building an image.
|
||||
default: false
|
||||
repository:
|
||||
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"
|
||||
- Full path to a repository. Use with state 'present' to tag the image into the repository.
|
||||
required: false
|
||||
default: null
|
||||
version_added: "2.1"
|
||||
state:
|
||||
description:
|
||||
- Set the state of the image
|
||||
required: false
|
||||
- Make assertions about the state of an image.
|
||||
- When 'absent' an image will be removed. Use the force option to un-tag and remove all images
|
||||
matching the provided name.
|
||||
- When 'present' check if an image exists using the provided name and tag. If the image is not found or the
|
||||
force option is used, the image will either be pulled, built or loaded. By default the image will be pulled
|
||||
from Docker Hub. To build the image, provide a path value set to a directory containing a context and
|
||||
Dockerfile. To load an image, specify load_path to provide a path to an archive file. To tag an image to a
|
||||
repository, provide a repository path. If the name contains a repository path, it will be pushed.
|
||||
- "NOTE: 'build' is DEPRECATED. Specifying 'build' will behave the same as 'present'."
|
||||
default: present
|
||||
choices: [ "present", "absent", "build" ]
|
||||
aliases: []
|
||||
timeout:
|
||||
choices:
|
||||
- absent
|
||||
- present
|
||||
- build (DEPRECATED)
|
||||
tag:
|
||||
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:
|
||||
- Used to select an image when pulling. Will be added to the image when pushing, tagging or building. Defaults to
|
||||
'latest' when pulling an image.
|
||||
default: latest
|
||||
use_tls:
|
||||
description:
|
||||
- "DEPRECATED. Whether to use tls to connect to the docker server. Set to 'no' when TLS will not be used. Set to
|
||||
'encrypt' to use TLS. And set to 'verify' to use TLS and verify that the server's certificate is valid for the
|
||||
server. NOTE: If you specify this option, it will set the value of the tls or tls_verify parameters."
|
||||
choices:
|
||||
- no
|
||||
- encrypt
|
||||
- verify
|
||||
default: no
|
||||
version_added: "2.0"
|
||||
|
||||
- hosts: web
|
||||
become: yes
|
||||
tasks:
|
||||
- name: check or build image
|
||||
docker_image: path="/path/to/build/dir" name="my/app" state=present
|
||||
extends_documentation_fragment:
|
||||
- docker
|
||||
|
||||
Build new version of image:
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "docker-py >= 1.7.0"
|
||||
- "Docker API >= 1.20"
|
||||
|
||||
- hosts: web
|
||||
become: yes
|
||||
tasks:
|
||||
- name: check or build image
|
||||
docker_image: path="/path/to/build/dir" name="my/app" state=build
|
||||
authors:
|
||||
- Pavel Antonov (@softzilla)
|
||||
- Chris Houseknecht (@chouseknecht)
|
||||
- James Tanner (@jctanner)
|
||||
|
||||
Remove image from local docker storage:
|
||||
'''
|
||||
|
||||
- hosts: web
|
||||
become: yes
|
||||
tasks:
|
||||
- name: remove image
|
||||
docker_image: name="my/app" state=absent
|
||||
EXAMPLES = '''
|
||||
|
||||
- name: pull an image
|
||||
docker_image:
|
||||
name: pacur/centos-7
|
||||
|
||||
- name: Tag to repository to a private registry and push it
|
||||
docker_image:
|
||||
name: pacur/centos-7
|
||||
repository: registry.ansible.com/chouseknecht/centos_images
|
||||
tag: 7.0
|
||||
|
||||
- name: Remove image
|
||||
docker_image:
|
||||
state: absent
|
||||
name: registry.ansible.com/chouseknecht/sinatra
|
||||
tag: v1
|
||||
|
||||
- name: Build an image ad push it to a private repo
|
||||
docker_image:
|
||||
path: ./sinatra
|
||||
name: registry.ansible.com/chouseknecht/sinatra
|
||||
tag: v1
|
||||
|
||||
- name: Archive image
|
||||
docker_image:
|
||||
name: registry.ansible.com/chouseknecht/sinatra
|
||||
tag: v1
|
||||
archive_path: my_sinatra.tar
|
||||
|
||||
- name: Load image from archive and push it to a private registry
|
||||
docker_image:
|
||||
name: registry.ansible.com/chouseknecht/sinatra
|
||||
tag: v1
|
||||
load_path: my_sinatra.tar
|
||||
'''
|
||||
|
||||
import re
|
||||
import os
|
||||
from urlparse import urlparse
|
||||
RETURN = '''
|
||||
actions:
|
||||
description: List of actions taken by the module.
|
||||
returned: always
|
||||
type: list
|
||||
sample: [
|
||||
"Removed image myimage"
|
||||
]
|
||||
image:
|
||||
description: Facts representing the current state of the image.
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {
|
||||
|
||||
}
|
||||
'''
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
from ansible.module_utils.docker_common import *
|
||||
|
||||
try:
|
||||
from requests.exceptions import *
|
||||
HAS_REQUESTS = True
|
||||
from docker import auth
|
||||
from docker import utils
|
||||
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
|
||||
# missing docker-py handled in docker_common
|
||||
pass
|
||||
|
||||
|
||||
class ImageManager(DockerBaseClass):
|
||||
|
||||
def __init__(self, client, results):
|
||||
|
||||
super(ImageManager, self).__init__()
|
||||
|
||||
self.client = client
|
||||
self.results = results
|
||||
parameters = self.client.module.params
|
||||
self.check_mode = self.client.check_mode
|
||||
|
||||
self.archive_path = parameters.get('archive_path')
|
||||
self.container_limits = parameters.get('container_limits')
|
||||
self.dockerfile = parameters.get('dockerfile')
|
||||
self.force = parameters.get('force')
|
||||
self.load_path = parameters.get('load_path')
|
||||
self.name = parameters.get('name')
|
||||
self.nocache = parameters.get('nocache')
|
||||
self.path = parameters.get('path')
|
||||
self.pull = parameters.get('pull')
|
||||
self.repository = parameters.get('repository')
|
||||
self.rm = parameters.get('rm')
|
||||
self.state = parameters.get('state')
|
||||
self.tag = parameters.get('tag')
|
||||
self.http_timeout = parameters.get('http_timeout')
|
||||
self.debug = parameters.get('debug')
|
||||
self.push = False
|
||||
|
||||
if self.state in ['present', 'build']:
|
||||
self.present()
|
||||
elif self.state == 'absent':
|
||||
self.absent()
|
||||
|
||||
def fail(self, msg):
|
||||
self.client.fail(msg)
|
||||
|
||||
def present(self):
|
||||
'''
|
||||
Handles state = 'present', which includes building, loading or pulling an image,
|
||||
depending on user provided parameters.
|
||||
|
||||
:returns None
|
||||
'''
|
||||
image = self.client.find_image(name=self.name, tag=self.tag)
|
||||
|
||||
if not image or self.force:
|
||||
if self.path:
|
||||
# Build the image
|
||||
image_name = self.name
|
||||
if self.tag:
|
||||
image_name = "%s:%s" % (self.name, self.tag)
|
||||
self.log("Building image %s" % image_name)
|
||||
self.results['actions'].append("Built image %s from %s" % (image_name, self.path))
|
||||
self.results['changed'] = True
|
||||
self.push = True
|
||||
if not self.check_mode:
|
||||
self.results['image'] = self.build_image()
|
||||
elif self.load_path:
|
||||
# Load the image from an archive
|
||||
if not os.path.isfile(self.load_path):
|
||||
self.fail("Error loading image %s. Specified path %s does not exist." % (self.name,
|
||||
self.load_path))
|
||||
self.push = True
|
||||
image_name = self.name
|
||||
if self.tag:
|
||||
image_name = "%s:%s" % (self.name, self.tag)
|
||||
self.results['actions'].append("Loaded image %s from %s" % (image_name, self.load_path))
|
||||
self.results['changed'] = True
|
||||
if not self.check_mode:
|
||||
self.results['image'] = self.load_image()
|
||||
else:
|
||||
docker_url = 'unix://var/run/docker.sock'
|
||||
|
||||
docker_api_version = module.params.get('docker_api_version')
|
||||
# pull the image
|
||||
self.results['actions'].append('Pulled image %s:%s' % (self.name, self.tag))
|
||||
self.results['changed'] = True
|
||||
if not self.check_mode:
|
||||
self.results['image'] = self.client.pull_image(self.name, tag=self.tag)
|
||||
|
||||
if self.archive_path:
|
||||
self.archive_image(self.name, self.tag)
|
||||
|
||||
if self.push and not self.repository:
|
||||
self.push_image(self.name, self.tag)
|
||||
elif self.repository:
|
||||
self.tag_image(self.name, self.tag, self.repository, force=self.force)
|
||||
|
||||
def absent(self):
|
||||
'''
|
||||
Handles state = 'absent', which removes an image.
|
||||
|
||||
:return None
|
||||
'''
|
||||
image = self.client.find_image(self.name, self.tag)
|
||||
if image:
|
||||
name = self.name
|
||||
if self.tag:
|
||||
name = "%s:%s" % (self.name, self.tag)
|
||||
if not self.check_mode:
|
||||
try:
|
||||
self.client.remove_image(name, force=self.force)
|
||||
except Exception as exc:
|
||||
self.fail("Error removing image %s - %s" % (name, str(exc)))
|
||||
|
||||
self.results['changed'] = True
|
||||
self.results['actions'].append("Removed image %s" % (name))
|
||||
self.results['image']['state'] = 'Deleted'
|
||||
|
||||
def archive_image(self, name, tag):
|
||||
'''
|
||||
Archive an image to a .tar file. Called when archive_path is passed.
|
||||
|
||||
:param name - name of the image. Type: str
|
||||
:return None
|
||||
'''
|
||||
|
||||
if not tag:
|
||||
tag = "latest"
|
||||
|
||||
image = self.client.find_image(name=name, tag=tag)
|
||||
if not image:
|
||||
self.log("archive image: image %s:%s not found" % (name, tag))
|
||||
return
|
||||
|
||||
image_name = "%s:%s" % (name, tag)
|
||||
self.results['actions'].append('Archived image %s to %s' % (image_name, self.archive_path))
|
||||
self.results['changed'] = True
|
||||
if not self.check_mode:
|
||||
self.log("Getting archive of image %s" % image_name)
|
||||
try:
|
||||
image = self.client.get_image(image_name)
|
||||
except Exception as exc:
|
||||
self.fail("Error getting image %s - %s" % (image_name, str(exc)))
|
||||
|
||||
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')
|
||||
try:
|
||||
image_tar = open(self.archive_path, 'w')
|
||||
image_tar.write(image.data)
|
||||
image_tar.close()
|
||||
except Exception as exc:
|
||||
self.fail("Error writing image archive %s - %s" % (self.archive_path, str(exc)))
|
||||
|
||||
image = self.client.find_image(name=name, tag=tag)
|
||||
if image:
|
||||
self.results['image'] = image
|
||||
|
||||
def push_image(self, name, tag=None):
|
||||
'''
|
||||
If the name of the image contains a repository path, then push the image.
|
||||
|
||||
:param name Name of the image to push.
|
||||
:param tag Use a specific tag.
|
||||
:return: None
|
||||
'''
|
||||
|
||||
repository = name
|
||||
if not tag:
|
||||
repository, tag = utils.parse_repository_tag(name)
|
||||
registry, repo_name = auth.resolve_repository_name(repository)
|
||||
|
||||
if re.search('/', repository):
|
||||
if registry:
|
||||
config = auth.load_config()
|
||||
if not auth.resolve_authconfig(config, registry):
|
||||
self.fail("Error: configuration for %s not found. Try logging into %s first." % registry)
|
||||
|
||||
self.log("pushing image %s" % repository)
|
||||
self.results['actions'].append("Pushed image %s to %s:%s" % (self.name, self.repository, self.tag))
|
||||
self.results['changed'] = True
|
||||
if not self.check_mode:
|
||||
status = None
|
||||
try:
|
||||
for line in self.client.push(repository, tag=tag, stream=True):
|
||||
line = json.loads(line)
|
||||
self.log(line, pretty_print=True)
|
||||
if line.get('errorDetail'):
|
||||
raise Exception(line['errorDetail']['message'])
|
||||
status = line.get('status')
|
||||
except Exception as exc:
|
||||
if re.search('unauthorized', str(exc)):
|
||||
self.fail("Error pushing image %s: %s. Does the repository exist?" % (repository, str(exc)))
|
||||
self.fail("Error pushing image %s: %s" % (repository, str(exc)))
|
||||
self.results['image'] = self.client.find_image(name=repository, tag=tag)
|
||||
if not self.results['image']:
|
||||
self.results['image'] = dict()
|
||||
self.results['image']['push_status'] = status
|
||||
|
||||
def tag_image(self, name, tag, repository, force=False):
|
||||
'''
|
||||
Tag an image into a repository.
|
||||
|
||||
:param name: name of the image. required.
|
||||
:param tag: image tag.
|
||||
:param repository: path to the repository. required.
|
||||
:param force: bool. force tagging, even it image already exists with the repository path.
|
||||
:param push: bool. push the image once it's tagged.
|
||||
:return: None
|
||||
'''
|
||||
repo, repo_tag = utils.parse_repository_tag(repository)
|
||||
image = self.client.find_image(name=repo, tag=repo_tag)
|
||||
found = 'found' if image else 'not found'
|
||||
self.log("image %s was %s" % (repo, found))
|
||||
if not image or force:
|
||||
self.log("tagging %s:%s to %s" % (name, tag, repository))
|
||||
self.results['changed'] = True
|
||||
self.results['actions'].append("Tagged image %s:%s to %s" % (name, tag, repository))
|
||||
if not self.check_mode:
|
||||
try:
|
||||
# Finding the image does not always work, especially running a localhost registry. In those
|
||||
# cases, if we don't set force=True, it errors.
|
||||
image_name = name
|
||||
if tag and not re.search(tag, name):
|
||||
image_name = "%s:%s" % (name, tag)
|
||||
tag_status = self.client.tag(image_name, repository, tag=tag, force=True)
|
||||
if not tag_status:
|
||||
raise Exception("Tag operation failed.")
|
||||
except Exception as exc:
|
||||
self.fail("Error: failed to tag image %s - %s" % (name, str(exc)))
|
||||
self.results['image'] = self.client.find_image(name=repository, tag=tag)
|
||||
self.push_image(repository, tag)
|
||||
|
||||
def build_image(self):
|
||||
'''
|
||||
Build an image
|
||||
|
||||
:return: image dict
|
||||
'''
|
||||
params = dict(
|
||||
path=self.path,
|
||||
tag=self.name,
|
||||
rm=self.rm,
|
||||
nocache=self.nocache,
|
||||
stream=True,
|
||||
timeout=self.http_timeout,
|
||||
pull=self.pull,
|
||||
forcerm=self.rm,
|
||||
dockerfile=self.dockerfile,
|
||||
decode=True
|
||||
)
|
||||
if self.tag:
|
||||
params['tag'] = "%s:%s" % (self.name, self.tag)
|
||||
if self.container_limits:
|
||||
params['container_limits'] = self.container_limits,
|
||||
for line in self.client.build(**params):
|
||||
# line = json.loads(line)
|
||||
self.log(line, pretty_print=True)
|
||||
if line.get('error'):
|
||||
if line.get('errorDetail'):
|
||||
errorDetail = line.get('errorDetail')
|
||||
self.fail("Error building %s - code: %s message: %s" % (self.name,
|
||||
errorDetail.get('code'),
|
||||
errorDetail.get('message')))
|
||||
else:
|
||||
self.fail("Error building %s - %s" % (self.name, line.get('error')))
|
||||
return self.client.find_image(name=self.name, tag=self.tag)
|
||||
|
||||
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')
|
||||
def load_image(self):
|
||||
'''
|
||||
Load an image from a .tar archive
|
||||
|
||||
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')
|
||||
:return: image dict
|
||||
'''
|
||||
try:
|
||||
self.log("Reading image data from %s" % self.load_path)
|
||||
image_tar = open(self.load_path, 'r')
|
||||
image_data = image_tar.read()
|
||||
image_tar.close()
|
||||
except Exception as exc:
|
||||
self.fail("Error reading image data %s - %s" % (self.load_path, str(exc)))
|
||||
|
||||
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,
|
||||
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:
|
||||
self.log("Loading image from %s" % self.load_path)
|
||||
self.client.load_image(image_data)
|
||||
except Exception as exc:
|
||||
self.fail("Error loading image %s - %s" % (self.name, str(exc)))
|
||||
|
||||
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
|
||||
return self.client.find_image(self.name, self.tag)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
path = dict(required=False, default=None, type='path'),
|
||||
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'),
|
||||
)
|
||||
argument_spec = dict(
|
||||
archive_path=dict(type='str'),
|
||||
container_limits=dict(type='dict'),
|
||||
dockerfile=dict(type='str'),
|
||||
force=dict(type='bool', default=False),
|
||||
http_timeout=dict(type='int'),
|
||||
load_path=dict(type='str'),
|
||||
name=dict(type='str', required=True),
|
||||
nocache=dict(type='str', default=False),
|
||||
path=dict(type='str', aliases=['build_path']),
|
||||
pull=dict(type='bool', default=True),
|
||||
repository=dict(type='str'),
|
||||
rm=dict(type='bool', default=True),
|
||||
state=dict(type='str', choices=['absent', 'present'], default='present'),
|
||||
tag=dict(type='str', default='latest'),
|
||||
use_tls=dict(type='str', default='no', choices=['no', 'encrypt', 'verify'])
|
||||
)
|
||||
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":
|
||||
# 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, changed=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 setting
|
||||
tls_hostname=boot2docker in your role. If this does not
|
||||
resolve the issue please open an issue at
|
||||
ansible/ansible-modules-core and ping michaeljs1990
|
||||
'''
|
||||
module.exit_json(failed=True, changed=manager.has_changed(), msg="SSLError: " + str(e) + environment_error)
|
||||
# General error for non darwin users
|
||||
else:
|
||||
module.exit_json(failed=True, changed=manager.has_changed(), msg="SSLError: " + str(e))
|
||||
|
||||
except ConnectionError as e:
|
||||
if get_platform() == "Darwin" and "DOCKER_HOST" not in os.environ:
|
||||
# Ensure that the environment variables has been set
|
||||
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, changed=manager.has_changed(), msg="ConnectionError: " + str(e) + environment_error)
|
||||
|
||||
module.exit_json(failed=True, changed=manager.has_changed(), msg="ConnectionError: " + 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))
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
results = dict(
|
||||
changed=False,
|
||||
actions=[],
|
||||
image={}
|
||||
)
|
||||
|
||||
ImageManager(client, results)
|
||||
client.module.exit_json(**results)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
Loading…
Reference in New Issue