From f0bb3aeeea56eecd8911433bebd0a1a648e17e41 Mon Sep 17 00:00:00 2001
From: Paul Durivage <pauldurivage@gmail.com>
Date: Mon, 7 Apr 2014 15:12:32 -0500
Subject: [PATCH] Add Docker inventory plugin

---
 plugins/inventory/docker.py  | 359 +++++++++++++++++++++++++++++++++++
 plugins/inventory/docker.yml | 134 +++++++++++++
 2 files changed, 493 insertions(+)
 create mode 100755 plugins/inventory/docker.py
 create mode 100644 plugins/inventory/docker.yml

diff --git a/plugins/inventory/docker.py b/plugins/inventory/docker.py
new file mode 100755
index 00000000000000..275be2b301b5f6
--- /dev/null
+++ b/plugins/inventory/docker.py
@@ -0,0 +1,359 @@
+#!/usr/bin/env python
+
+# (c) 2013, Paul Durivage <paul.durivage@gmail.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+#
+# Author: Paul Durivage <paul.durivage@gmail.com>
+#
+# Description:
+# This module queries local or remote Docker daemons and generates
+# inventory information.
+#
+# This plugin does not support targeting of specific hosts using the --host
+# flag. Instead, it it queries the Docker API for each container, running
+# or not, and returns this data all once.
+#
+# The plugin returns the following custom attributes on Docker containers:
+#    docker_args
+#    docker_config
+#    docker_created
+#    docker_driver
+#    docker_exec_driver
+#    docker_host_config
+#    docker_hostname_path
+#    docker_hosts_path
+#    docker_id
+#    docker_image
+#    docker_name
+#    docker_network_settings
+#    docker_path
+#    docker_resolv_conf_path
+#    docker_state
+#    docker_volumes
+#    docker_volumes_rw
+#
+# Requirements:
+# The docker-py module: https://github.com/dotcloud/docker-py
+#
+# Notes:
+# A config file can be used to configure this inventory module, and there
+# are several environment variables that can be set to modify the behavior
+# of the plugin at runtime:
+#    DOCKER_CONFIG_FILE
+#    DOCKER_HOST
+#    DOCKER_VERSION
+#    DOCKER_TIMEOUT
+#    DOCKER_PRIVATE_SSH_PORT
+#    DOCKER_DEFAULT_IP
+#
+# Environment Variables:
+# environment variable: DOCKER_CONFIG_FILE
+#     description:
+#         - A path to a Docker inventory hosts/defaults file in YAML format
+#         - A sample file has been provided, colocated with the inventory
+#           file called 'docker.yml'
+#     required: false
+#     default: Uses docker.docker.Client constructor defaults
+# environment variable: DOCKER_HOST
+#     description:
+#         - The socket on which to connect to a Docker daemon API
+#     required: false
+#     default: Uses docker.docker.Client constructor defaults
+# environment variable: DOCKER_VERSION
+#     description:
+#         - Version of the Docker API to use
+#     default: Uses docker.docker.Client constructor defaults
+#     required: false
+# environment variable: DOCKER_TIMEOUT
+#     description:
+#         - Timeout in seconds for connections to Docker daemon API
+#     default: Uses docker.docker.Client constructor defaults
+#     required: false
+# environment variable: DOCKER_PRIVATE_SSH_PORT
+#     description:
+#         - The private port (container port) on which SSH is listening
+#           for connections
+#     default: 22
+#     required: false
+# environment variable: DOCKER_DEFAULT_IP
+#     description:
+#         - This environment variable overrides the container SSH connection
+#           IP address (aka, 'ansible_ssh_host')
+#
+#           This option allows one to override the ansible_ssh_host whenever
+#           Docker has exercised its default behavior of binding private ports
+#           to all interfaces of the Docker host.  This behavior, when dealing
+#           with remote Docker hosts, does not allow Ansible to determine
+#           a proper host IP address on which to connect via SSH to containers.
+#           By default, this inventory module assumes all 0.0.0.0-exposed
+#           ports to be bound to localhost:<port>.  To override this
+#           behavior, for example, to bind a container's SSH port to the public
+#           interface of its host, one must manually set this IP.
+#
+#           It is preferable to begin to launch Docker containers with
+#           ports exposed on publicly accessible IP addresses, particularly
+#           if the containers are to be targeted by Ansible for remote
+#           configuration, not accessible via localhost SSH connections.
+#
+#           Docker containers can be explicitly exposed on IP addresses by
+#           a) starting the daemon with the --ip argument
+#           b) running containers with the -P/--publish ip::containerPort
+#              argument
+#     default: 127.0.0.1 if port exposed on 0.0.0.0 by Docker
+#     required: false
+#
+# Examples:
+#  Use the config file:
+#  DOCKER_CONFIG_FILE=./docker.yml docker.py --list
+#
+#  Connect to docker instance on localhost port 4243
+#  DOCKER_HOST=tcp://localhost:4243 docker.py --list
+#
+#  Any container's ssh port exposed on 0.0.0.0 will mapped to
+#  another IP address (where Ansible will attempt to connect via SSH)
+#  DOCKER_DEFAULT_IP=1.2.3.4 docker.py --list
+
+import os
+import sys
+import json
+import argparse
+
+from UserDict import UserDict
+from collections import defaultdict
+
+import yaml
+
+from requests import HTTPError, ConnectionError
+
+# Manipulation of the path is needed because the docker-py
+# module is imported by the name docker, and because this file
+# is also named docker
+for path in [os.getcwd(), '', os.path.dirname(os.path.abspath(__file__))]:
+    try:
+        del sys.path[sys.path.index(path)]
+    except:
+        pass
+
+try:
+    import docker
+except ImportError:
+    print('docker-py is required for this module')
+    sys.exit(1)
+
+
+class HostDict(UserDict):
+    def __setitem__(self, key, value):
+        if value is not None:
+            self.data[key] = value
+
+    def update(self, dict=None, **kwargs):
+        if dict is None:
+            pass
+        elif isinstance(dict, UserDict):
+            for k, v in dict.data.items():
+                self[k] = v
+        else:
+            for k, v in dict.items():
+                self[k] = v
+        if len(kwargs):
+            for k, v in kwargs.items():
+                self[k] = v
+
+
+def write_stderr(string):
+    sys.stderr.write('%s\n' % string)
+
+
+def setup():
+    config = dict()
+    config_file = os.environ.get('DOCKER_CONFIG_FILE')
+    if config_file:
+        try:
+            config_file = os.path.abspath(config_file)
+        except Exception as e:
+            write_stderr(e)
+            sys.exit(1)
+
+        with open(config_file) as f:
+            try:
+                config = yaml.safe_load(f.read())
+            except Exception as e:
+                write_stderr(e)
+                sys.exit(1)
+
+    # Enviroment Variables
+    env_base_url = os.environ.get('DOCKER_HOST')
+    env_version = os.environ.get('DOCKER_VERSION')
+    env_timeout = os.environ.get('DOCKER_TIMEOUT')
+    env_ssh_port = os.environ.get('DOCKER_PRIVATE_SSH_PORT', '22')
+    env_default_ip = os.environ.get('DOCKER_DEFAULT_IP', '127.0.0.1')
+    # Config file defaults
+    defaults = config.get('defaults', dict())
+    def_host = defaults.get('host')
+    def_version = defaults.get('version')
+    def_timeout = defaults.get('timeout')
+    def_default_ip = defaults.get('default_ip')
+    def_ssh_port = defaults.get('private_ssh_port')
+
+    hosts = list()
+
+    if config:
+        hosts_list = config.get('hosts', list())
+        # Look to the config file's defined hosts
+        if hosts_list:
+            for host in hosts_list:
+                baseurl = host.get('host') or def_host or env_base_url
+                version = host.get('version') or def_version or env_version
+                timeout = host.get('timeout') or def_timeout or env_timeout
+                default_ip = host.get('default_ip') or def_default_ip or env_default_ip
+                ssh_port = host.get('private_ssh_port') or def_ssh_port or env_ssh_port
+
+                hostdict = HostDict(
+                    base_url=baseurl,
+                    version=version,
+                    timeout=timeout,
+                    default_ip=default_ip,
+                    private_ssh_port=ssh_port,
+                )
+                hosts.append(hostdict)
+        # Look to the defaults
+        else:
+            hostdict = HostDict(
+                base_url=def_host,
+                version=def_version,
+                timeout=def_timeout,
+                default_ip=def_default_ip,
+                private_ssh_port=def_ssh_port,
+            )
+            hosts.append(hostdict)
+    # Look to the environment
+    else:
+        hostdict = HostDict(
+            base_url=env_base_url,
+            version=env_version,
+            timeout=env_timeout,
+            default_ip=env_default_ip,
+            private_ssh_port=env_ssh_port,
+        )
+        hosts.append(hostdict)
+
+    return hosts
+
+
+def list_groups():
+    hosts = setup()
+    groups = defaultdict(list)
+    hostvars = defaultdict(dict)
+
+    for host in hosts:
+        ssh_port = host.pop('private_ssh_port', None)
+        default_ip = host.pop('default_ip', None)
+        hostname = host.get('base_url')
+
+        try:
+            client = docker.Client(**host)
+            containers = client.containers(all=True)
+        except (HTTPError, ConnectionError) as e:
+            write_stderr(e)
+            sys.exit(1)
+
+        for container in containers:
+            id = container.get('Id')
+            short_id = id[:13]
+            try:
+                name = container.get('Names', list()).pop(0).lstrip('/')
+            except IndexError:
+                name = short_id
+
+            if not id:
+                continue
+
+            inspect = client.inspect_container(id)
+            running = inspect.get('State', dict()).get('Running')
+
+            groups[id].append(name)
+            groups[name].append(name)
+            if not short_id in groups.keys():
+                groups[short_id].append(name)
+            groups[hostname].append(name)
+
+            if running is True:
+                groups['running'].append(name)
+            else:
+                groups['stopped'].append(name)
+
+            try:
+                port = client.port(container, ssh_port)[0]
+            except (IndexError, AttributeError):
+                port = dict()
+
+            try:
+                ip = default_ip if port['HostIp'] == '0.0.0.0' else port['HostIp']
+            except KeyError:
+                ip = ''
+
+            container_info = dict(
+                ansible_ssh_host=ip,
+                ansible_ssh_port=port.get('HostPort', int()),
+                docker_args=inspect.get('Args'),
+                docker_config=inspect.get('Config'),
+                docker_created=inspect.get('Created'),
+                docker_driver=inspect.get('Driver'),
+                docker_exec_driver=inspect.get('ExecDriver'),
+                docker_host_config=inspect.get('HostConfig'),
+                docker_hostname_path=inspect.get('HostnamePath'),
+                docker_hosts_path=inspect.get('HostsPath'),
+                docker_id=inspect.get('ID'),
+                docker_image=inspect.get('Image'),
+                docker_name=name,
+                docker_network_settings=inspect.get('NetworkSettings'),
+                docker_path=inspect.get('Path'),
+                docker_resolv_conf_path=inspect.get('ResolvConfPath'),
+                docker_state=inspect.get('State'),
+                docker_volumes=inspect.get('Volumes'),
+                docker_volumes_rw=inspect.get('VolumesRW'),
+            )
+
+            hostvars[name].update(container_info)
+
+    groups['docker_hosts'] = [host.get('base_url') for host in hosts]
+    groups['_meta'] = dict()
+    groups['_meta']['hostvars'] = hostvars
+    print json.dumps(groups, sort_keys=True, indent=4)
+    sys.exit(0)
+
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+    group = parser.add_mutually_exclusive_group(required=True)
+    group.add_argument('--list', action='store_true')
+    group.add_argument('--host', action='store_true')
+    return parser.parse_args()
+
+
+def main():
+    args = parse_args()
+    if args.list:
+        list_groups()
+    elif args.host:
+        write_stderr('This option is not supported.')
+        sys.exit(1)
+    sys.exit(0)
+
+
+main()
diff --git a/plugins/inventory/docker.yml b/plugins/inventory/docker.yml
new file mode 100644
index 00000000000000..a7f81683fb9ba9
--- /dev/null
+++ b/plugins/inventory/docker.yml
@@ -0,0 +1,134 @@
+# This is the configuration file for the Ansible plugin for Docker inventory.
+#
+# Author: Paul Durivage <paul.durivage@gmail.com>
+#
+# Description:
+# This module queries local or remote Docker daemons and generates
+# inventory information.
+#
+# This plugin does not support targeting of specific hosts using the --host
+# flag. Instead, it it queries the Docker API for each container, running
+# or not, and returns this data all once.
+#
+# The plugin returns the following custom attributes on Docker containers:
+#    docker_args
+#    docker_config
+#    docker_created
+#    docker_driver
+#    docker_exec_driver
+#    docker_host_config
+#    docker_hostname_path
+#    docker_hosts_path
+#    docker_id
+#    docker_image
+#    docker_name
+#    docker_network_settings
+#    docker_path
+#    docker_resolv_conf_path
+#    docker_state
+#    docker_volumes
+#    docker_volumes_rw
+#
+# Requirements:
+# The docker-py module: https://github.com/dotcloud/docker-py
+#
+# Notes:
+# A config file can be used to configure this inventory module, and there
+# are several environment variables that can be set to modify the behavior
+# of the plugin at runtime:
+#    DOCKER_CONFIG_FILE
+#    DOCKER_HOST
+#    DOCKER_VERSION
+#    DOCKER_TIMEOUT
+#    DOCKER_PRIVATE_SSH_PORT
+#    DOCKER_DEFAULT_IP
+#
+# Environment Variables:
+# environment variable: DOCKER_CONFIG_FILE
+#     description:
+#         - A path to a Docker inventory hosts/defaults file in YAML format
+#         - A sample file has been provided, colocated with the inventory
+#           file called 'docker.yml'
+#     required: false
+#     default: Uses docker.docker.Client constructor defaults
+# environment variable: DOCKER_HOST
+#     description:
+#         - The socket on which to connect to a Docker daemon API
+#     required: false
+#     default: Uses docker.docker.Client constructor defaults
+# environment variable: DOCKER_VERSION
+#     description:
+#         - Version of the Docker API to use
+#     default: Uses docker.docker.Client constructor defaults
+#     required: false
+# environment variable: DOCKER_TIMEOUT
+#     description:
+#         - Timeout in seconds for connections to Docker daemon API
+#     default: Uses docker.docker.Client constructor defaults
+#     required: false
+# environment variable: DOCKER_PRIVATE_SSH_PORT
+#     description:
+#         - The private port (container port) on which SSH is listening
+#           for connections
+#     default: 22
+#     required: false
+# environment variable: DOCKER_DEFAULT_IP
+#     description:
+#         - This environment variable overrides the container SSH connection
+#           IP address (aka, 'ansible_ssh_host')
+#
+#           This option allows one to override the ansible_ssh_host whenever
+#           Docker has exercised its default behavior of binding private ports
+#           to all interfaces of the Docker host.  This behavior, when dealing
+#           with remote Docker hosts, does not allow Ansible to determine
+#           a proper host IP address on which to connect via SSH to containers.
+#           By default, this inventory module assumes all 0.0.0.0-exposed
+#           ports to be bound to localhost:<port>.  To override this
+#           behavior, for example, to bind a container's SSH port to the public
+#           interface of its host, one must manually set this IP.
+#
+#           It is preferable to begin to launch Docker containers with
+#           ports exposed on publicly accessible IP addresses, particularly
+#           if the containers are to be targeted by Ansible for remote
+#           configuration, not accessible via localhost SSH connections.
+#
+#           Docker containers can be explicitly exposed on IP addresses by
+#           a) starting the daemon with the --ip argument
+#           b) running containers with the -P/--publish ip::containerPort
+#              argument
+#     default: 127.0.0.1 if port exposed on 0.0.0.0 by Docker
+#     required: false
+#
+# Examples:
+#  Use the config file:
+#  DOCKER_CONFIG_FILE=./docker.yml docker.py --list
+#
+#  Connect to docker instance on localhost port 4243
+#  DOCKER_HOST=tcp://localhost:4243 docker.py --list
+#
+#  Any container's ssh port exposed on 0.0.0.0 will mapped to
+#  another IP address (where Ansible will attempt to connect via SSH)
+#  DOCKER_DEFAULT_IP=1.2.3.4 docker.py --list
+#
+#
+#
+# The Docker inventory plugin provides several enviroment variables that
+# may be overridden here.  This configuration file always takes precedence
+# over environment variables.
+#
+# Variable precedence is: hosts > defaults > environment
+
+---
+defaults:
+  host: unix:///var/run/docker.sock
+  version: 1.9
+  timeout: 60
+  private_ssh_port: 22
+  default_ip: 127.0.0.1
+hosts:
+#  - host: tcp://10.45.5.16:4243
+#    version: 1.9
+#    timeout: 60
+#    private_ssh_port: 2022
+#    default_ip: 172.16.3.45
+#  - host: tcp://localhost:4243
\ No newline at end of file