From ae99e8860e31f204f2eb7f035cb39d7dc8df50fa Mon Sep 17 00:00:00 2001 From: Pavel Antonov Date: Wed, 22 Jan 2014 16:04:19 +0400 Subject: [PATCH] Docker supports links and port binding. Added docker_image module --- cloud/docker | 72 +++++++++-- cloud/docker_image | 315 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 376 insertions(+), 11 deletions(-) create mode 100644 cloud/docker_image diff --git a/cloud/docker b/cloud/docker index c1727c7524c..8efccdb9f26 100644 --- a/cloud/docker +++ b/cloud/docker @@ -1,6 +1,7 @@ #!/usr/bin/python # (c) 2013, Cove Schneider +# (c) 2014, Pavel Antonov # # This file is part of Ansible, # @@ -45,12 +46,24 @@ options: 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 @@ -232,7 +245,9 @@ class DockerManager: if self.module.params.get('volumes'): self.binds = {} self.volumes = {} - vols = self.module.params.get('volumes').split(" ") + 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) @@ -253,11 +268,37 @@ class DockerManager: self.ports = None if self.module.params.get('ports'): - self.ports = self.module.params.get('ports').split(",") + 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'): - self.env = dict(map(lambda x: x.split("="), self.module.params.get('env').split(","))) + 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')) @@ -301,22 +342,27 @@ class DockerManager: 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') + containers = self.client.containers(all=True) + image = self.module.params.get('image') + command = self.module.params.get('command') if command: command = command.strip() - deployed = [] + 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 running_image == image and (not tag or tag == running_tag) and (not command or running_command == command): + 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) @@ -334,6 +380,7 @@ class DockerManager: 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'), @@ -365,7 +412,8 @@ class DockerManager: def start_containers(self, containers): for i in containers: - self.client.start(i['Id'], lxc_conf=self.lxc_conf, binds=self.binds) + 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): @@ -396,7 +444,10 @@ def main(): 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), @@ -434,7 +485,6 @@ def main(): # start/stop containers if state == "present": - # start more containers if we don't have enough if delta > 0: containers = manager.create_containers(delta) diff --git a/cloud/docker_image b/cloud/docker_image new file mode 100644 index 00000000000..4121babed10 --- /dev/null +++ b/cloud/docker_image @@ -0,0 +1,315 @@ +#!/usr/bin/env python +# + +# (c) 2014, Pavel Antonov +# +# This module 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, +# 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 . + +###################################################################### + +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: [] + 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" ] +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 DockerImageManager: + + counters = {'created':0, 'started':0, 'stopped':0, 'killed':0, 'removed':0, 'restarted':0, 'pull':0} + + def __init__(self, module): + self.module = module + self.path = self.module.params.get('path') + self.name = self.module.params.get('name') + self.tag = self.module.params.get('tag') + self.nocache = self.module.params.get('nocache') + docker_url = urlparse(module.params.get('docker_url')) + self.client = docker.Client(base_url=docker_url.geturl(), timeout=module.params.get('timeout')) + self.changed = False + + def build(self): + res = self.client.build(self.path, tag=":".join([self.name, self.tag]), nocache=self.nocache, rm=True) + self.changed = True + return res + + + def has_changed(self): + return self.changed + + def get_images(self): + filtered_images = [] + images = self.client.images() + for i in images: + if (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 docker.APIError as e: + # image can be removed by docker if not used + pass + + +def main(): + module = AnsibleModule( + argument_spec = dict( + path = dict(required=False, default=None), + name = dict(required=True), + #id = dict(required=False, default=None), + tag = dict(required=False, default=""), + nocache = dict(default=False, type='bool'), + state = dict(default='present', choices=['absent', 'present', 'build']), + docker_url = dict(default='unix://var/run/docker.sock'), + timeout = dict(default=600, type='int'), + ) + ) + + try: + manager = DockerImageManager(module) + state = module.params.get('state') + failed = False + image_id = None + msg = '' + + # build image if not exists + if state == "present": + images = manager.get_images() + if len(images) == 0: + image_id, msg = manager.build() + if image_id is None: + failed = True + + + # remove image or images + elif state == "absent": + manager.remove_images() + + # build image + elif state == "build": + image_id, msg = manager.build() + if image_id is None: + failed = True + + module.exit_json(failed=failed, changed=manager.has_changed(), msg=msg, image_id=image_id) + + except docker.client.APIError 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)) + +# import module snippets +from ansible.module_utils.basic import * + +main()