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()