9344bc175d
When using repositories other than the main one at docker.io, the image name contains the repo name (which itself contains ":" as a separator between domain and port). We don't really care about it here, so just get rid of it before looking at the image name.
484 lines
16 KiB
Python
484 lines
16 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
|
|
# (c) 2013, Cove Schneider
|
|
#
|
|
# 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/>.
|
|
|
|
######################################################################
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
module: docker
|
|
short_description: manage docker containers
|
|
description:
|
|
- Manage the life cycle of docker containers.
|
|
options:
|
|
count:
|
|
description:
|
|
- Set number of containers to run
|
|
required: False
|
|
default: 1
|
|
aliases: []
|
|
image:
|
|
description:
|
|
- Set container image to use
|
|
required: true
|
|
default: null
|
|
aliases: []
|
|
command:
|
|
description:
|
|
- Set command to run in a container on startup
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
ports:
|
|
description:
|
|
- Set private to public port mapping specification (e.g. ports=22,80 or ports=:8080 maps 8080 directly to host)
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
volumes:
|
|
description:
|
|
- Set volume(s) to mount on the container
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
volumes_from:
|
|
description:
|
|
- Set shared volume(s) from another container
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
memory_limit:
|
|
description:
|
|
- Set RAM allocated to container
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
default: 256MB
|
|
docker_url:
|
|
description:
|
|
- URL of docker host to issue commands to
|
|
required: false
|
|
default: unix://var/run/docker.sock
|
|
aliases: []
|
|
username:
|
|
description:
|
|
- Set remote API username
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
password:
|
|
description:
|
|
- Set remote API password
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
hostname:
|
|
description:
|
|
- Set container hostname
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
env:
|
|
description:
|
|
- Set environment variables (e.g. env="PASSWORD=sEcRe7,WORKERS=4")
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
dns:
|
|
description:
|
|
- Set custom DNS servers for the container
|
|
required: false
|
|
default: null
|
|
aliases: []
|
|
detach:
|
|
description:
|
|
- Enable detached mode on start up, leaves container running in background
|
|
required: false
|
|
default: true
|
|
aliases: []
|
|
state:
|
|
description:
|
|
- Set the state of the container
|
|
required: false
|
|
default: present
|
|
choices: [ "present", "stopped", "absent", "killed", "restarted" ]
|
|
aliases: []
|
|
privileged:
|
|
description:
|
|
- Set whether the container should run in privileged mode
|
|
required: false
|
|
default: false
|
|
aliases: []
|
|
lxc_conf:
|
|
description:
|
|
- LXC config parameters, e.g. lxc.aa_profile:unconfined
|
|
required: false
|
|
default:
|
|
aliases: []
|
|
author: Cove Schneider
|
|
requirements: [ "docker-py" ]
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
Start one docker container running tomcat in each host of the web group and bind tomcat's listening port to 8080
|
|
on the host:
|
|
|
|
- hosts: web
|
|
sudo: yes
|
|
tasks:
|
|
- name: run tomcat servers
|
|
docker: image=centos command="service tomcat6 start" ports=:8080
|
|
|
|
The tomcat server's port is NAT'ed to a dynamic port on the host, but you can determine which port the server was
|
|
mapped to using docker_containers:
|
|
|
|
- hosts: web
|
|
sudo: yes
|
|
tasks:
|
|
- name: run tomcat servers
|
|
docker: image=centos command="service tomcat6 start" ports=8080 count=5
|
|
- name: Display IP address and port mappings for containers
|
|
debug: msg={{inventory_hostname}}:{{item.NetworkSettings.Ports['8080/tcp'][0].HostPort}}
|
|
with_items: docker_containers
|
|
|
|
Just as in the previous example, but iterates over the list of docker containers with a sequence:
|
|
|
|
- hosts: web
|
|
sudo: yes
|
|
vars:
|
|
start_containers_count: 5
|
|
tasks:
|
|
- name: run tomcat servers
|
|
docker: image=centos command="service tomcat6 start" ports=8080 count={{start_containers_count}}
|
|
- name: Display IP address and port mappings for containers
|
|
debug: msg={{inventory_hostname}}:{{docker_containers[{{item}}].NetworkSettings.Ports['8080/tcp'][0].HostPort}}"
|
|
with_sequence: start=0 end={{start_containers_count - 1}}
|
|
|
|
Stop, remove all of the running tomcat containers and list the exit code from the stopped containers:
|
|
|
|
- hosts: web
|
|
sudo: yes
|
|
tasks:
|
|
- name: stop tomcat servers
|
|
docker: image=centos command="service tomcat6 start" state=absent
|
|
- name: Display return codes from stopped containers
|
|
debug: msg="Returned {{inventory_hostname}}:{{item}}"
|
|
with_items: docker_containers
|
|
'''
|
|
|
|
try:
|
|
import sys
|
|
import docker.client
|
|
from requests.exceptions import *
|
|
from urlparse import urlparse
|
|
except ImportError, e:
|
|
print "failed=True msg='failed to import python module: %s'" % e
|
|
sys.exit(1)
|
|
|
|
def _human_to_bytes(number):
|
|
suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
|
|
|
if isinstance(number, int):
|
|
return number
|
|
if number[-1] == suffixes[0] and number[-2].isdigit():
|
|
return number[:-1]
|
|
|
|
i = 1
|
|
for each in suffixes[1:]:
|
|
if number[-len(each):] == suffixes[i]:
|
|
return int(number[:-len(each)]) * (1024 ** i)
|
|
i = i + 1
|
|
|
|
print "failed=True msg='Could not convert %s to integer'" % (number)
|
|
sys.exit(1)
|
|
|
|
def _ansible_facts(container_list):
|
|
return {"docker_containers": container_list}
|
|
|
|
def _docker_id_quirk(inspect):
|
|
# XXX: some quirk in docker
|
|
if 'ID' in inspect:
|
|
inspect['Id'] = inspect['ID']
|
|
del inspect['ID']
|
|
return inspect
|
|
|
|
class DockerManager:
|
|
|
|
counters = {'created':0, 'started':0, 'stopped':0, 'killed':0, 'removed':0, 'restarted':0, 'pull':0}
|
|
|
|
def __init__(self, module):
|
|
self.module = module
|
|
|
|
self.binds = None
|
|
self.volumes = None
|
|
if self.module.params.get('volumes'):
|
|
self.binds = {}
|
|
self.volumes = {}
|
|
vols = self.module.params.get('volumes').split(" ")
|
|
for vol in vols:
|
|
parts = vol.split(":")
|
|
# host mount (e.g. /mnt:/tmp, bind mounts host's /tmp to /mnt in the container)
|
|
if len(parts) == 2:
|
|
self.volumes[parts[1]] = {}
|
|
self.binds[parts[0]] = parts[1]
|
|
# docker mount (e.g. /www, mounts a docker volume /www on the container at the same location)
|
|
else:
|
|
self.volumes[parts[0]] = {}
|
|
|
|
self.lxc_conf = None
|
|
if self.module.params.get('lxc_conf'):
|
|
self.lxc_conf = []
|
|
options = self.module.params.get('lxc_conf').split(" ")
|
|
for option in options:
|
|
parts = option.split(':')
|
|
self.lxc_conf.append({"Key": parts[0], "Value": parts[1]})
|
|
|
|
self.ports = None
|
|
if self.module.params.get('ports'):
|
|
self.ports = self.module.params.get('ports').split(",")
|
|
|
|
self.env = None
|
|
if self.module.params.get('env'):
|
|
self.env = dict(map(lambda x: x.split("="), self.module.params.get('env').split(",")))
|
|
|
|
# connect to docker server
|
|
docker_url = urlparse(module.params.get('docker_url'))
|
|
self.client = docker.Client(base_url=docker_url.geturl())
|
|
|
|
|
|
def get_split_image_tag(self, image):
|
|
if '/' in image:
|
|
image = image.split('/')[1]
|
|
tag = None
|
|
if image.find(':') > 0:
|
|
return image.split(':')
|
|
else:
|
|
return image, tag
|
|
|
|
def get_summary_counters_msg(self):
|
|
msg = ""
|
|
for k, v in self.counters.iteritems():
|
|
msg = msg + "%s %d " % (k, v)
|
|
|
|
return msg
|
|
|
|
def increment_counter(self, name):
|
|
self.counters[name] = self.counters[name] + 1
|
|
|
|
def has_changed(self):
|
|
for k, v in self.counters.iteritems():
|
|
if v > 0:
|
|
return True
|
|
|
|
return False
|
|
|
|
def get_inspect_containers(self, containers):
|
|
inspect = []
|
|
for i in containers:
|
|
details = self.client.inspect_container(i['Id'])
|
|
details = _docker_id_quirk(details)
|
|
inspect.append(details)
|
|
|
|
return inspect
|
|
|
|
def get_deployed_containers(self):
|
|
# determine which images/commands are running already
|
|
containers = self.client.containers()
|
|
image = self.module.params.get('image')
|
|
command = self.module.params.get('command')
|
|
if command:
|
|
command = command.strip()
|
|
deployed = []
|
|
|
|
# if we weren't given a tag with the image, we need to only compare on the image name, as that
|
|
# docker will give us back the full image name including a tag in the container list if one exists.
|
|
image, tag = self.get_split_image_tag(image)
|
|
|
|
for i in containers:
|
|
running_image, running_tag = self.get_split_image_tag(i['Image'])
|
|
running_command = i['Command'].strip()
|
|
|
|
if running_image == image and (not tag or tag == running_tag) and (not command or running_command == command):
|
|
details = self.client.inspect_container(i['Id'])
|
|
details = _docker_id_quirk(details)
|
|
deployed.append(details)
|
|
|
|
return deployed
|
|
|
|
def get_running_containers(self):
|
|
running = []
|
|
for i in self.get_deployed_containers():
|
|
if i['State']['Running'] == True and i['State']['Ghost'] == False:
|
|
running.append(i)
|
|
|
|
return running
|
|
|
|
def create_containers(self, count=1):
|
|
params = {'image': self.module.params.get('image'),
|
|
'command': self.module.params.get('command'),
|
|
'ports': self.ports,
|
|
'volumes': self.volumes,
|
|
'volumes_from': self.module.params.get('volumes_from'),
|
|
'mem_limit': _human_to_bytes(self.module.params.get('memory_limit')),
|
|
'environment': self.env,
|
|
'dns': self.module.params.get('dns'),
|
|
'hostname': self.module.params.get('hostname'),
|
|
'detach': self.module.params.get('detach'),
|
|
'privileged': self.module.params.get('privileged'),
|
|
}
|
|
|
|
def do_create(count, params):
|
|
results = []
|
|
for _ in range(count):
|
|
result = self.client.create_container(**params)
|
|
self.increment_counter('created')
|
|
results.append(result)
|
|
|
|
return results
|
|
|
|
try:
|
|
containers = do_create(count, params)
|
|
except:
|
|
self.client.pull(params['image'])
|
|
self.increment_counter('pull')
|
|
containers = do_create(count, params)
|
|
|
|
return containers
|
|
|
|
def start_containers(self, containers):
|
|
for i in containers:
|
|
self.client.start(i['Id'], lxc_conf=self.lxc_conf, binds=self.binds)
|
|
self.increment_counter('started')
|
|
|
|
def stop_containers(self, containers):
|
|
for i in containers:
|
|
self.client.stop(i['Id'])
|
|
self.increment_counter('stopped')
|
|
|
|
return [self.client.wait(i['Id']) for i in containers]
|
|
|
|
def remove_containers(self, containers):
|
|
for i in containers:
|
|
self.client.remove_container(i['Id'])
|
|
self.increment_counter('removed')
|
|
|
|
def kill_containers(self, containers):
|
|
for i in containers:
|
|
self.client.kill(i['Id'])
|
|
self.increment_counter('killed')
|
|
|
|
def restart_containers(self, containers):
|
|
for i in containers:
|
|
self.client.restart(i['Id'])
|
|
self.increment_counter('restarted')
|
|
|
|
def main():
|
|
module = AnsibleModule(
|
|
argument_spec = dict(
|
|
count = dict(default=1),
|
|
image = dict(required=True),
|
|
command = dict(required=False, default=None),
|
|
ports = dict(required=False, default=None),
|
|
volumes = dict(default=None),
|
|
volumes_from = dict(default=None),
|
|
memory_limit = dict(default=0),
|
|
memory_swap = dict(default=0),
|
|
docker_url = dict(default='unix://var/run/docker.sock'),
|
|
user = dict(default=None),
|
|
password = dict(),
|
|
email = dict(),
|
|
hostname = dict(default=None),
|
|
env = dict(),
|
|
dns = dict(),
|
|
detach = dict(default=True, type='bool'),
|
|
state = dict(default='present', choices=['absent', 'present', 'stopped', 'killed', 'restarted']),
|
|
debug = dict(default=False, type='bool'),
|
|
privileged = dict(default=False, type='bool'),
|
|
lxc_conf = dict(default=None)
|
|
)
|
|
)
|
|
|
|
try:
|
|
manager = DockerManager(module)
|
|
state = module.params.get('state')
|
|
count = int(module.params.get('count'))
|
|
|
|
if count < 1:
|
|
module.fail_json(msg="Count must be positive number")
|
|
|
|
running_containers = manager.get_running_containers()
|
|
running_count = len(running_containers)
|
|
delta = count - running_count
|
|
deployed_containers = manager.get_deployed_containers()
|
|
facts = None
|
|
failed = False
|
|
changed = False
|
|
|
|
# start/stop containers
|
|
if state == "present":
|
|
|
|
# start more containers if we don't have enough
|
|
if delta > 0:
|
|
containers = manager.create_containers(delta)
|
|
manager.start_containers(containers)
|
|
|
|
# stop containers if we have too many
|
|
elif delta < 0:
|
|
containers_to_stop = running_containers[0:abs(delta)]
|
|
containers = manager.stop_containers(containers_to_stop)
|
|
manager.remove_containers(containers_to_stop)
|
|
|
|
facts = manager.get_running_containers()
|
|
|
|
# stop and remove containers
|
|
elif state == "absent":
|
|
facts = manager.stop_containers(deployed_containers)
|
|
manager.remove_containers(deployed_containers)
|
|
|
|
# stop containers
|
|
elif state == "stopped":
|
|
facts = manager.stop_containers(running_containers)
|
|
|
|
# kill containers
|
|
elif state == "killed":
|
|
manager.kill_containers(running_containers)
|
|
|
|
# restart containers
|
|
elif state == "restarted":
|
|
manager.restart_containers(running_containers)
|
|
facts = manager.get_inspect_containers(running_containers)
|
|
|
|
msg = "%s container(s) running image %s with command %s" % \
|
|
(manager.get_summary_counters_msg(), module.params.get('image'), module.params.get('command'))
|
|
changed = manager.has_changed()
|
|
|
|
module.exit_json(failed=failed, changed=changed, msg=msg, ansible_facts=_ansible_facts(facts))
|
|
|
|
except docker.client.APIError as e:
|
|
changed = manager.has_changed()
|
|
module.exit_json(failed=True, changed=changed, msg="Docker API error: " + e.explanation)
|
|
|
|
except RequestException as e:
|
|
changed = manager.has_changed()
|
|
module.exit_json(failed=True, changed=changed, msg=repr(e))
|
|
|
|
# this is magic, see lib/ansible/module_common.py
|
|
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
|
|
|
main()
|