#!/usr/bin/python # (c) 2013, Cove Schneider # (c) 2014, Pavel Antonov # # 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 version_added: "1.4" 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: [] name: description: - Set name for container (used to find single container or to provide links) 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: [] links: description: - Add links to another container (e.g. links=redis,postgresql:db) 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" ] notes: - Currently supports Docker version <= 0.6.4 only. ''' 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') if not isinstance(vols, list): vols = vols.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'): ports = self.module.params.get('ports') self.ports = ports if isinstance(ports, list) else ports.split(",") self.port_bindings = None if self.module.params.get('port_bindings'): self.port_bindings = {} bindings = self.module.params.get('port_bindings') bindings = bindings if isinstance(bindings, list) else bindings.split(",") for binding in bindings: parts = binding.split(":") if len(parts) == 3: self.port_bindings[parts[0]] = (parts[1], parts[2]) elif len(parts) == 2: self.port_bindings[parts[0]] = parts[1] else: self.port_bindings[parts[0]] = None self.links = None if self.module.params.get('links'): self.links = {} links = self.module.params.get('links') if not isinstance(links, list): links = links.split(",") for link in links: parts = link.split(":") self.links[parts[0]] = parts[1] if len(parts) == 2 else parts[0] self.env = None if self.module.params.get('env'): env = self.module.params.get('env') self.env = dict(map(lambda x: x.split("="), env if isinstance(env, list) else 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(all=True) image = self.module.params.get('image') command = self.module.params.get('command') if command: command = command.strip() name = self.module.params.get('name') if name and not name.startswith('/'): name = '/' + name 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 (name and name in i['Names']) or \ (not name and 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'), 'name': self.module.params.get('name'), '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, port_bindings = self.port_bindings, links=self.links) 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), name = dict(required=False, default=None), ports = dict(required=False, default=None), port_bindings = dict(required=False, default=None), links = 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, e: changed = manager.has_changed() module.exit_json(failed=True, changed=changed, msg="Docker API error: " + e.explanation) except RequestException, e: changed = manager.has_changed() module.exit_json(failed=True, changed=changed, msg=repr(e)) # import module snippets from ansible.module_utils.basic import * main()