diff --git a/base_system b/base_system new file mode 100644 index 0000000..c3962dc --- /dev/null +++ b/base_system @@ -0,0 +1,7 @@ +FROM ubuntu:14.04 +MAINTAINER Oleg Borisenko al@somestuff.ru + +RUN apt-get update && apt-get install -y openssh-server && sed -i "s/Port 22/Port {{ ssh_port_to_expose }}/g" /etc/ssh/sshd_config && sed -i "s/UsePAM yes/UsePam no/g" /etc/ssh/sshd_config && echo "UseDNS no" >> /etc/ssh/sshd_config && service ssh restart +RUN mkdir /root/.ssh && chmod 0700 /root/.ssh/ && echo "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" >> /root/.ssh/authorized_keys +EXPOSE {{ ssh_port_to_expose }} +CMD ["/usr/sbin/sshd", "-D"] diff --git a/docker_containers_tmpl b/docker_containers_tmpl new file mode 100644 index 0000000..b07ff93 --- /dev/null +++ b/docker_containers_tmpl @@ -0,0 +1,5 @@ +{% for host in groups['docker-containers'] %} + {{ host }} + {% endfor %} + +{{hostvars}} diff --git a/hosts b/hosts new file mode 100644 index 0000000..c730e5a --- /dev/null +++ b/hosts @@ -0,0 +1,26 @@ +[all-hosts:children] +docker +ansible-master + +[all-hosts:vars] +ssh_port_to_expose=2222 + +#n0x - real name of the host x; n0xhost - ansible alias for physical hosts +[docker-host-nodes] +#n01host ansible_ssh_port=22 ansible_ssh_host=n01 +#n02host ansible_ssh_port=22 ansible_ssh_host=n02 +#n03host ansible_ssh_port=22 ansible_ssh_host=n03 +#n04host ansible_ssh_port=22 ansible_ssh_host=n04 +#n05host ansible_ssh_port=22 ansible_ssh_host=n05 +#n06host ansible_ssh_port=22 ansible_ssh_host=n06 +lochost ansible_ssh_port=22 ansible_ssh_user=ars ansible_ssh_host=localhost + +[docker-containers] +#locdock ansible_ssh_port="{{ ssh_port_to_expose }}" ansible_ssh_user=root host_key_checking=False ansible_ssh_host=localhost + +[docker:children] +docker-host-nodes +docker-containers + +[ansible-master] +localhost ansible_ssh_port=22 diff --git a/library/cloud/docker b/library/cloud/docker new file mode 100644 index 0000000..255827e --- /dev/null +++ b/library/cloud/docker @@ -0,0 +1,785 @@ +#!/usr/bin/python + +# (c) 2013, Cove Schneider +# (c) 2014, Joshua Conner +# (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: [] + version_added: "1.5" + ports: + description: + - Set private to public port mapping specification using docker CLI-style syntax [([:[host_port]])|():][/udp] + required: false + default: null + aliases: [] + version_added: "1.5" + expose: + description: + - Set container ports to expose for port mappings or links. (If the port is already exposed using EXPOSE in a Dockerfile, you don't need to expose it again.) + required: false + default: null + aliases: [] + version_added: "1.5" + publish_all_ports: + description: + - Publish all exposed ports to the host interfaces + required: false + default: false + aliases: [] + version_added: "1.5" + 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: [] + links: + description: + - Link container(s) to other container(s) (e.g. links=redis,postgresql:db) + required: false + default: null + aliases: [] + version_added: "1.5" + 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", "running", "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: [] + name: + description: + - Set the name of the container (cannot use with count) + required: false + default: null + aliases: [] + version_added: "1.5" + stdin_open: + description: + - Keep stdin open + required: false + default: false + aliases: [] + version_added: "1.6" + tty: + description: + - Allocate a pseudo-tty + required: false + default: false + aliases: [] + version_added: "1.6" + net: + description: + - Specify network mode + required: false + default: bridge + aliases: [] + version_added: "1.6" +author: Cove Schneider, Joshua Conner, Pavel Antonov +requirements: [ "docker-py >= 0.3.0", "docker >= 0.10.0" ] +''' + +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['HostConfig']['PortBindings']['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}}]['HostConfig']['PortBindings']['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 + +Create a named container: + +- hosts: web + sudo: yes + tasks: + - name: run tomcat server + docker: image=centos name=tomcat command="service tomcat6 start" ports=8080 + +Create multiple named containers: + +- hosts: web + sudo: yes + tasks: + - name: run tomcat servers + docker: image=centos name={{item}} command="service tomcat6 start" ports=8080 + with_items: + - crookshank + - snowbell + - heathcliff + - felix + - sylvester + +Create containers named in a sequence: + +- hosts: web + sudo: yes + tasks: + - name: run tomcat servers + docker: image=centos name={{item}} command="service tomcat6 start" ports=8080 + with_sequence: start=1 end=5 format=tomcat_%d.example.com + +Create two linked containers: + +- hosts: web + sudo: yes + tasks: + - name: ensure redis container is running + docker: image=crosbymichael/redis name=redis + + - name: ensure redis_ambassador container is running + docker: image=svendowideit/ambassador ports=6379:6379 links=redis:redis name=redis_ambassador_ansible + +Create containers with options specified as key-value pairs and lists: + +- hosts: web + sudo: yes + tasks: + - docker: + image: namespace/image_name + links: + - postgresql:db + - redis:redis + + +Create containers with options specified as strings and lists as comma-separated strings: + +- hosts: web + sudo: yes + tasks: + docker: image=namespace/image_name links=postgresql:db,redis:redis + +Create and run a container using the hosts network stack: + +- hosts: web + sudo: yes + tasks: + docker: image=nginx net=host +''' + +HAS_DOCKER_PY = True + +import sys +from urlparse import urlparse +try: + import docker.client + import docker.utils + from requests.exceptions import * +except ImportError, e: + HAS_DOCKER_PY = False + +try: + from docker.errors import APIError as DockerAPIError +except ImportError: + from docker.client import APIError as DockerAPIError + + +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') + 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') + for option in options: + parts = option.split(':') + self.lxc_conf.append({"Key": parts[0], "Value": parts[1]}) + + self.exposed_ports = None + if self.module.params.get('expose'): + self.exposed_ports = self.get_exposed_ports(self.module.params.get('expose')) + + self.port_bindings = None + if self.module.params.get('ports'): + self.port_bindings = self.get_port_bindings(self.module.params.get('ports')) + + self.links = None + if self.module.params.get('links'): + self.links = self.get_links(self.module.params.get('links')) + + self.env = None + if self.module.params.get('env'): + self.env = dict(map(lambda x: x.split("=", 1), self.module.params.get('env'))) + + # connect to docker server + docker_url = urlparse(module.params.get('docker_url')) + self.client = docker.Client(base_url=docker_url.geturl()) + + + def get_links(self, links): + """ + Parse the links passed, if a link is specified without an alias then just create the alias of the same name as the link + """ + processed_links = {} + + for link in links: + parsed_link = link.split(':', 1) + if(len(parsed_link) == 2): + processed_links[parsed_link[0]] = parsed_link[1] + else: + processed_links[parsed_link[0]] = parsed_link[0] + + return processed_links + + + def get_exposed_ports(self, expose_list): + """ + Parse the ports and protocols (TCP/UDP) to expose in the docker-py `create_container` call from the docker CLI-style syntax. + """ + if expose_list: + exposed = [] + for port in expose_list: + if port.endswith('/tcp') or port.endswith('/udp'): + port_with_proto = tuple(port.split('/')) + else: + # assume tcp protocol if not specified + port_with_proto = (port, 'tcp') + exposed.append(port_with_proto) + return exposed + else: + return None + + + def get_port_bindings(self, ports): + """ + Parse the `ports` string into a port bindings dict for the `start_container` call. + """ + binds = {} + for port in ports: + # ports could potentially be an array like [80, 443], so we make sure they're strings + # before splitting + parts = str(port).split(':') + container_port = parts[-1] + if '/' not in container_port: + container_port = int(parts[-1]) + + p_len = len(parts) + if p_len == 1: + # Bind `container_port` of the container to a dynamically + # allocated TCP port on all available interfaces of the host + # machine. + bind = ('0.0.0.0',) + elif p_len == 2: + # Bind `container_port` of the container to port `parts[0]` on + # all available interfaces of the host machine. + bind = ('0.0.0.0', int(parts[0])) + elif p_len == 3: + # Bind `container_port` of the container to port `parts[1]` on + # IP `parts[0]` of the host machine. If `parts[1]` empty bind + # to a dynamically allocacted port of IP `parts[0]`. + bind = (parts[0], int(parts[1])) if parts[1] else (parts[0],) + + if container_port in binds: + old_bind = binds[container_port] + if isinstance(old_bind, list): + # append to list if it already exists + old_bind.append(bind) + else: + # otherwise create list that contains the old and new binds + binds[container_port] = [binds[container_port], bind] + else: + binds[container_port] = bind + + return binds + + + 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""" + 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 self.client.containers(all=True): + running_image, running_tag = self.get_split_image_tag(i['Image']) + running_command = i['Command'].strip() + + name_matches = False + if i["Names"]: + name_matches = (name and name in i['Names']) + image_matches = (running_image == image) + tag_matches = (not tag or running_tag == tag) + # if a container has an entrypoint, `command` will actually equal + # '{} {}'.format(entrypoint, command) + command_matches = (not command or running_command.endswith(command)) + + if name_matches or (name is None and image_matches and tag_matches and command_matches): + 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'].get('Ghost', False) == 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.exposed_ports, + 'volumes': self.volumes, + 'mem_limit': _human_to_bytes(self.module.params.get('memory_limit')), + 'environment': self.env, + 'hostname': self.module.params.get('hostname'), + 'detach': self.module.params.get('detach'), + 'name': self.module.params.get('name'), + 'stdin_open': self.module.params.get('stdin_open'), + 'tty': self.module.params.get('tty'), + } + + if docker.utils.compare_version('1.10', self.client.version()['ApiVersion']) < 0: + params['dns'] = self.module.params.get('dns') + params['volumes_from'] = self.module.params.get('volumes_from') + + 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): + params = { + 'lxc_conf': self.lxc_conf, + 'binds': self.binds, + 'port_bindings': self.port_bindings, + 'publish_all_ports': self.module.params.get('publish_all_ports'), + 'privileged': self.module.params.get('privileged'), + 'network_mode': self.module.params.get('net'), + 'links': self.links, + } + if docker.utils.compare_version('1.10', self.client.version()['ApiVersion']) >= 0 and hasattr(docker, '__version__') and docker.__version__ > '0.3.0': + params['dns'] = self.module.params.get('dns') + params['volumes_from'] = self.module.params.get('volumes_from') + + for i in containers: + self.client.start(i['Id'], **params) + 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 check_dependencies(module): + """ + Ensure `docker-py` >= 0.3.0 is installed, and call module.fail_json with a + helpful error message if it isn't. + """ + if not HAS_DOCKER_PY: + module.fail_json(msg="`docker-py` doesn't seem to be installed, but is required for the Ansible Docker module.") + else: + HAS_NEW_ENOUGH_DOCKER_PY = False + if hasattr(docker, '__version__'): + # a '__version__' attribute was added to the module but not until + # after 0.3.0 was added pushed to pip. If it's there, use it. + if docker.__version__ >= '0.3.0': + HAS_NEW_ENOUGH_DOCKER_PY = True + else: + # HACK: if '__version__' isn't there, we check for the existence of + # `_get_raw_response_socket` in the docker.Client class, which was + # added in 0.3.0 + if hasattr(docker.Client, '_get_raw_response_socket'): + HAS_NEW_ENOUGH_DOCKER_PY = True + + if not HAS_NEW_ENOUGH_DOCKER_PY: + module.fail_json(msg="The Ansible Docker module requires `docker-py` >= 0.3.0.") + + +def main(): + module = AnsibleModule( + argument_spec = dict( + count = dict(default=1), + image = dict(required=True), + command = dict(required=False, default=None), + expose = dict(required=False, default=None, type='list'), + ports = dict(required=False, default=None, type='list'), + publish_all_ports = dict(default=False, type='bool'), + volumes = dict(default=None, type='list'), + volumes_from = dict(default=None), + links = dict(default=None, type='list'), + memory_limit = dict(default=0), + memory_swap = dict(default=0), + docker_url = dict(default='unix://var/run/docker.sock'), + user = dict(default=None), + net = dict(default='bridge'), + password = dict(), + email = dict(), + hostname = dict(default=None), + env = dict(type='list'), + dns = dict(), + detach = dict(default=True, type='bool'), + state = dict(default='running', choices=['absent', 'present', 'running', 'stopped', 'killed', 'restarted']), + debug = dict(default=False, type='bool'), + privileged = dict(default=False, type='bool'), + stdin_open = dict(default=False, type='bool'), + tty = dict(default=False, type='bool'), + lxc_conf = dict(default=None, type='list'), + name = dict(default=None) + ) + ) + + check_dependencies(module) + + try: + manager = DockerManager(module) + state = module.params.get('state') + count = int(module.params.get('count')) + name = module.params.get('name') + + if count < 0: + module.fail_json(msg="Count must be greater than zero") + if count > 1 and name: + module.fail_json(msg="Count and name must not be used together") + + 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 in [ "running", "present" ]: + + # make sure a container with `name` exists, if not create and start it + if name and "/" + name not in map(lambda x: x.get('Name'), deployed_containers): + containers = manager.create_containers(1) + if state == "present": #otherwise it get (re)started later anyways.. + manager.start_containers(containers) + running_containers = manager.get_running_containers() + deployed_containers = manager.get_deployed_containers() + + if state == "running": + # make sure a container with `name` is running + if name and "/" + name not in map(lambda x: x.get('Name'), running_containers): + manager.start_containers(deployed_containers) + + # start more containers if we don't have enough + elif 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() + else: + facts = manager.get_deployed_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 DockerAPIError, 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() diff --git a/library/cloud/docker_facts b/library/cloud/docker_facts new file mode 100644 index 0000000..18d69e3 --- /dev/null +++ b/library/cloud/docker_facts @@ -0,0 +1,242 @@ +#!/usr/bin/python + +# vim: ts=4:expandtab:au BufWritePost +# (c) 2014, Patrick "CaptTofu" Galbraith +# Code also from rax_facts and the primary docker module +# +# +# 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 . + +# This is a DOCUMENTATION stub specific to this module, it extends +# a documentation fragment located in ansible.utils.module_docs_fragments +DOCUMENTATION = ''' +--- +module: docker_facts +short_description: Gather facts for Docker containers +description: + - Gather facts for Docker containers and images +version_added: "0.1" +options: + id: + description: + - container ID to retrieve facts for + default: null (all containers if neither name nor id specified) + name: + description: + - container name to retrieve facts for + default: null (all containers if neither name nor id specified) + images: + description: + - image name or 'all'. Off by default. + default: null +author: Patrick Galbraith +''' + +EXAMPLES = ''' +- name: Gather info about containers + hosts: localhost + gather_facts: False + tasks: + - name: Get facts about containers + local_action: + module: docker_facts + name: container1 + +- name: Gather info about all containers and images + hosts: docker + gather_facts: True + tasks: + - name: Get facts about containers + local_action: + module: docker_facts + images: all + + - name: containers debug info + debug: msg="Container Name {{ item.key }} IP Address {{ item.value.docker_networksettings.IPAddress }}" + with_dict: docker_containers + + - name: images info + debug: msg="Image ID {{ item.key }} Author {{ item.value.docker_author }} Repo Tags {{ item.value.docker_repotags }}" + with_dict: docker_images + +''' + +HAS_DOCKER_PY = True + +import re +from urlparse import urlparse +try: + import docker + try: + from docker.errors import APIError as DockerAPIError + except ImportError: + from docker.client import APIError as DockerAPIError +except ImportError: + HAS_DOCKER_PY = False + + +class DockerManager: + + def __init__(self, module): + self.module = module + docker_url = urlparse(module.params.get('docker_url')) + self.container_id = module.params.get('id') + self.name = module.params.get('name') + self.images = module.params.get('images') + self.all_containers = not (self.name or self.container_id) + + self.client = docker.Client(base_url=docker_url.geturl()) + if self.client is None: + module.fail_json(msg="Failed to instantiate docker client. This " + "could mean that docker isn't running.") + + def docker_facts_slugify(self, value): + return 'docker_%s' % (re.sub('[^\w-]', '_', value).lower().lstrip('_')) + + def get_inspect(self, id, type='container'): + facts = dict() + if type == 'images': + inspect = self.client.inspect_image(id) + else: + inspect = self.client.inspect_container(id) + + for key in inspect: + fact_key = self.docker_facts_slugify(key) + facts[fact_key] = inspect.get(key) + + facts['docker_short_id'] = id[:13] + return facts + + def entity_conform(self, entity): + new_entity = dict() + + for key in entity: + new_key = self.docker_facts_slugify(key) + new_entity[new_key] = entity.get(key) + + return new_entity + + def search(self, entities, type='containers'): + search_list = [] + + if type == 'images': + id = self.images + name = None + else: + id = self.container_id + name = self.name + + for entity in entities: + entity_name = '' + # by name, get by name + if name: + # names have a leading '/' + if not name.startswith('/'): + entity_name = '/' + name + if (entity_name in entity['Names']): + search_list.append(entity) + # by name, get by name + elif id: + # container_id could be any length, but internally + # with docker is 64 chars + if id in entity['Id']: + search_list.append(entity) + + # an ID should never yield more than one, but in case + if len(entity) > 1 and id and type == 'containers': + module.fail_json(msg='Multiple items found for container id ' + '%s' % id) + + return search_list + + def get_facts(self, entities, type='containers'): + facts = dict() + entity_dict = dict() + for entity in entities: + id = entity.get('Id') + short_id = id[:13] + name = short_id + + if type == 'containers': + try: + name = entity.get('Names', list()).pop(0).lstrip('/') + except IndexError: + name = short_id + + entity_dict[name] = self.get_inspect(id, type) + entity = self.entity_conform(entity) + # merge both, since both have different members that + # are useful information + entity_dict[name] = dict(entity_dict[name].items() + + entity.items()) + + facts['docker_' + type] = entity_dict + return facts + + def docker_facts(self, module): + changed = False + # get all containers + containers = self.client.containers(all=True) + + # limits the containers to only those specified by name or ID + if not self.all_containers: + # reduce list to specific containers + containers = self.search(containers) + + # obtain inspection info and convert to facts dict + ansible_facts = self.get_facts(containers) + + if self.images: + images = self.client.images(all=True) + # limits the images to only those specified by ID + if self.images != 'all': + images = self.search(images, 'images') + + # obtain inspection info and convert to facts dict + image_facts = self.get_facts(images, 'images') + if len(image_facts): + ansible_facts = dict(ansible_facts.items() + + image_facts.items()) + + module.exit_json(changed=changed, ansible_facts=ansible_facts) + + +def main(): + if not HAS_DOCKER_PY: + module.fail_json(msg= + 'The docker python client is required \ + for this module') + argument_spec = dict( + docker_url=dict(default='unix://var/run/docker.sock'), + id=dict(default=None), + name=dict(default=None), + images=dict(default=None) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[['id', 'name']] + ) + + manager = DockerManager(module) + + manager.docker_facts(module) + +# import module snippets +from ansible.module_utils.basic import * + +### invoke the module +main() diff --git a/test.yml b/test.yml new file mode 100644 index 0000000..10840f6 --- /dev/null +++ b/test.yml @@ -0,0 +1,42 @@ +--- + +- hosts: ansible-master + tasks: + - template: src=./base_system dest=/tmp/Dockerfile + - name: check ssh + debug: var=ansible_ssh_port + +- hosts: docker-host-nodes + sudo: yes + tasks: + - name: check ssh + debug: var=ansible_ssh_port + - name: install python-apt + apt: name=python-apt state=present + - name: get curl + apt: name=curl state=present + - name: download and install pip + shell: curl https://bootstrap.pypa.io/get-pip.py | sudo python + - name: install docker-py + pip: name=docker-py + - copy: src=/tmp/Dockerfile dest=/tmp/Dockerfile + - name: build base image + docker_image: name="ubuntu/ssh" path=/tmp/ tag=14.04 state=present + - name: run container + docker: state=running image=ubuntu/ssh:14.04 net=bridge ports=2222 count=3 + - name: Get facts about launched docker containers + docker_facts: +# add created containers to [docker-containers] + - name: add containers to [docker-containers] + add_host: hostname={{item.key}} ansible_ssh_host=172.17.42.1 ansible_ssh_port={{item.value.docker_ports[0].PublicPort}} ansible_ssh_user=root groups=docker-containers + when: item.value.docker_state.Running == True + with_dict: docker_containers + +# check added hosts entries +# - template: src=docker_containers_tmpl dest=/tmp/docker_containers_result +- hosts: docker-containers + tasks: + - name: install ganglia-monitor + apt: name=ganglia-monitor + - name: check ssh + debug: var=ansible_ssh_port diff --git a/tmpl-test b/tmpl-test new file mode 100644 index 0000000..efce2d4 --- /dev/null +++ b/tmpl-test @@ -0,0 +1 @@ +{{ docker_containers }}