diff --git a/cloud/docker b/cloud/docker
new file mode 100644
index 00000000000..b9afb093d37
--- /dev/null
+++ b/cloud/docker
@@ -0,0 +1,482 @@
+#!/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 .
+
+######################################################################
+
+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):
+ 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
+#<>
+
+main()