PageRenderTime 25ms CodeModel.GetById 7ms app.highlight 13ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/inventory/docker.py

https://github.com/ajanthanm/ansible
Python | 359 lines | 207 code | 19 blank | 133 comment | 29 complexity | 34b2f04e653d01872e7aedc2ccc416d0 MD5 | raw file
  1#!/usr/bin/env python
  2
  3# (c) 2013, Paul Durivage <paul.durivage@gmail.com>
  4#
  5# This file is part of Ansible.
  6#
  7# Ansible is free software: you can redistribute it and/or modify
  8# it under the terms of the GNU General Public License as published by
  9# the Free Software Foundation, either version 3 of the License, or
 10# (at your option) any later version.
 11#
 12# Ansible is distributed in the hope that it will be useful,
 13# but WITHOUT ANY WARRANTY; without even the implied warranty of
 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15# GNU General Public License for more details.
 16#
 17# You should have received a copy of the GNU General Public License
 18# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 19#
 20#
 21# Author: Paul Durivage <paul.durivage@gmail.com>
 22#
 23# Description:
 24# This module queries local or remote Docker daemons and generates
 25# inventory information.
 26#
 27# This plugin does not support targeting of specific hosts using the --host
 28# flag. Instead, it it queries the Docker API for each container, running
 29# or not, and returns this data all once.
 30#
 31# The plugin returns the following custom attributes on Docker containers:
 32#    docker_args
 33#    docker_config
 34#    docker_created
 35#    docker_driver
 36#    docker_exec_driver
 37#    docker_host_config
 38#    docker_hostname_path
 39#    docker_hosts_path
 40#    docker_id
 41#    docker_image
 42#    docker_name
 43#    docker_network_settings
 44#    docker_path
 45#    docker_resolv_conf_path
 46#    docker_state
 47#    docker_volumes
 48#    docker_volumes_rw
 49#
 50# Requirements:
 51# The docker-py module: https://github.com/dotcloud/docker-py
 52#
 53# Notes:
 54# A config file can be used to configure this inventory module, and there
 55# are several environment variables that can be set to modify the behavior
 56# of the plugin at runtime:
 57#    DOCKER_CONFIG_FILE
 58#    DOCKER_HOST
 59#    DOCKER_VERSION
 60#    DOCKER_TIMEOUT
 61#    DOCKER_PRIVATE_SSH_PORT
 62#    DOCKER_DEFAULT_IP
 63#
 64# Environment Variables:
 65# environment variable: DOCKER_CONFIG_FILE
 66#     description:
 67#         - A path to a Docker inventory hosts/defaults file in YAML format
 68#         - A sample file has been provided, colocated with the inventory
 69#           file called 'docker.yml'
 70#     required: false
 71#     default: Uses docker.docker.Client constructor defaults
 72# environment variable: DOCKER_HOST
 73#     description:
 74#         - The socket on which to connect to a Docker daemon API
 75#     required: false
 76#     default: Uses docker.docker.Client constructor defaults
 77# environment variable: DOCKER_VERSION
 78#     description:
 79#         - Version of the Docker API to use
 80#     default: Uses docker.docker.Client constructor defaults
 81#     required: false
 82# environment variable: DOCKER_TIMEOUT
 83#     description:
 84#         - Timeout in seconds for connections to Docker daemon API
 85#     default: Uses docker.docker.Client constructor defaults
 86#     required: false
 87# environment variable: DOCKER_PRIVATE_SSH_PORT
 88#     description:
 89#         - The private port (container port) on which SSH is listening
 90#           for connections
 91#     default: 22
 92#     required: false
 93# environment variable: DOCKER_DEFAULT_IP
 94#     description:
 95#         - This environment variable overrides the container SSH connection
 96#           IP address (aka, 'ansible_ssh_host')
 97#
 98#           This option allows one to override the ansible_ssh_host whenever
 99#           Docker has exercised its default behavior of binding private ports
100#           to all interfaces of the Docker host.  This behavior, when dealing
101#           with remote Docker hosts, does not allow Ansible to determine
102#           a proper host IP address on which to connect via SSH to containers.
103#           By default, this inventory module assumes all 0.0.0.0-exposed
104#           ports to be bound to localhost:<port>.  To override this
105#           behavior, for example, to bind a container's SSH port to the public
106#           interface of its host, one must manually set this IP.
107#
108#           It is preferable to begin to launch Docker containers with
109#           ports exposed on publicly accessible IP addresses, particularly
110#           if the containers are to be targeted by Ansible for remote
111#           configuration, not accessible via localhost SSH connections.
112#
113#           Docker containers can be explicitly exposed on IP addresses by
114#           a) starting the daemon with the --ip argument
115#           b) running containers with the -P/--publish ip::containerPort
116#              argument
117#     default: 127.0.0.1 if port exposed on 0.0.0.0 by Docker
118#     required: false
119#
120# Examples:
121#  Use the config file:
122#  DOCKER_CONFIG_FILE=./docker.yml docker.py --list
123#
124#  Connect to docker instance on localhost port 4243
125#  DOCKER_HOST=tcp://localhost:4243 docker.py --list
126#
127#  Any container's ssh port exposed on 0.0.0.0 will mapped to
128#  another IP address (where Ansible will attempt to connect via SSH)
129#  DOCKER_DEFAULT_IP=1.2.3.4 docker.py --list
130
131import os
132import sys
133import json
134import argparse
135
136from UserDict import UserDict
137from collections import defaultdict
138
139import yaml
140
141from requests import HTTPError, ConnectionError
142
143# Manipulation of the path is needed because the docker-py
144# module is imported by the name docker, and because this file
145# is also named docker
146for path in [os.getcwd(), '', os.path.dirname(os.path.abspath(__file__))]:
147    try:
148        del sys.path[sys.path.index(path)]
149    except:
150        pass
151
152try:
153    import docker
154except ImportError:
155    print('docker-py is required for this module')
156    sys.exit(1)
157
158
159class HostDict(UserDict):
160    def __setitem__(self, key, value):
161        if value is not None:
162            self.data[key] = value
163
164    def update(self, dict=None, **kwargs):
165        if dict is None:
166            pass
167        elif isinstance(dict, UserDict):
168            for k, v in dict.data.items():
169                self[k] = v
170        else:
171            for k, v in dict.items():
172                self[k] = v
173        if len(kwargs):
174            for k, v in kwargs.items():
175                self[k] = v
176
177
178def write_stderr(string):
179    sys.stderr.write('%s\n' % string)
180
181
182def setup():
183    config = dict()
184    config_file = os.environ.get('DOCKER_CONFIG_FILE')
185    if config_file:
186        try:
187            config_file = os.path.abspath(config_file)
188        except Exception as e:
189            write_stderr(e)
190            sys.exit(1)
191
192        with open(config_file) as f:
193            try:
194                config = yaml.safe_load(f.read())
195            except Exception as e:
196                write_stderr(e)
197                sys.exit(1)
198
199    # Enviroment Variables
200    env_base_url = os.environ.get('DOCKER_HOST')
201    env_version = os.environ.get('DOCKER_VERSION')
202    env_timeout = os.environ.get('DOCKER_TIMEOUT')
203    env_ssh_port = os.environ.get('DOCKER_PRIVATE_SSH_PORT', '22')
204    env_default_ip = os.environ.get('DOCKER_DEFAULT_IP', '127.0.0.1')
205    # Config file defaults
206    defaults = config.get('defaults', dict())
207    def_host = defaults.get('host')
208    def_version = defaults.get('version')
209    def_timeout = defaults.get('timeout')
210    def_default_ip = defaults.get('default_ip')
211    def_ssh_port = defaults.get('private_ssh_port')
212
213    hosts = list()
214
215    if config:
216        hosts_list = config.get('hosts', list())
217        # Look to the config file's defined hosts
218        if hosts_list:
219            for host in hosts_list:
220                baseurl = host.get('host') or def_host or env_base_url
221                version = host.get('version') or def_version or env_version
222                timeout = host.get('timeout') or def_timeout or env_timeout
223                default_ip = host.get('default_ip') or def_default_ip or env_default_ip
224                ssh_port = host.get('private_ssh_port') or def_ssh_port or env_ssh_port
225
226                hostdict = HostDict(
227                    base_url=baseurl,
228                    version=version,
229                    timeout=timeout,
230                    default_ip=default_ip,
231                    private_ssh_port=ssh_port,
232                )
233                hosts.append(hostdict)
234        # Look to the defaults
235        else:
236            hostdict = HostDict(
237                base_url=def_host,
238                version=def_version,
239                timeout=def_timeout,
240                default_ip=def_default_ip,
241                private_ssh_port=def_ssh_port,
242            )
243            hosts.append(hostdict)
244    # Look to the environment
245    else:
246        hostdict = HostDict(
247            base_url=env_base_url,
248            version=env_version,
249            timeout=env_timeout,
250            default_ip=env_default_ip,
251            private_ssh_port=env_ssh_port,
252        )
253        hosts.append(hostdict)
254
255    return hosts
256
257
258def list_groups():
259    hosts = setup()
260    groups = defaultdict(list)
261    hostvars = defaultdict(dict)
262
263    for host in hosts:
264        ssh_port = host.pop('private_ssh_port', None)
265        default_ip = host.pop('default_ip', None)
266        hostname = host.get('base_url')
267
268        try:
269            client = docker.Client(**host)
270            containers = client.containers(all=True)
271        except (HTTPError, ConnectionError) as e:
272            write_stderr(e)
273            sys.exit(1)
274
275        for container in containers:
276            id = container.get('Id')
277            short_id = id[:13]
278            try:
279                name = container.get('Names', list()).pop(0).lstrip('/')
280            except IndexError:
281                name = short_id
282
283            if not id:
284                continue
285
286            inspect = client.inspect_container(id)
287            running = inspect.get('State', dict()).get('Running')
288
289            groups[id].append(name)
290            groups[name].append(name)
291            if not short_id in groups.keys():
292                groups[short_id].append(name)
293            groups[hostname].append(name)
294
295            if running is True:
296                groups['running'].append(name)
297            else:
298                groups['stopped'].append(name)
299
300            try:
301                port = client.port(container, ssh_port)[0]
302            except (IndexError, AttributeError, TypeError):
303                port = dict()
304
305            try:
306                ip = default_ip if port['HostIp'] == '0.0.0.0' else port['HostIp']
307            except KeyError:
308                ip = ''
309
310            container_info = dict(
311                ansible_ssh_host=ip,
312                ansible_ssh_port=port.get('HostPort', int()),
313                docker_args=inspect.get('Args'),
314                docker_config=inspect.get('Config'),
315                docker_created=inspect.get('Created'),
316                docker_driver=inspect.get('Driver'),
317                docker_exec_driver=inspect.get('ExecDriver'),
318                docker_host_config=inspect.get('HostConfig'),
319                docker_hostname_path=inspect.get('HostnamePath'),
320                docker_hosts_path=inspect.get('HostsPath'),
321                docker_id=inspect.get('ID'),
322                docker_image=inspect.get('Image'),
323                docker_name=name,
324                docker_network_settings=inspect.get('NetworkSettings'),
325                docker_path=inspect.get('Path'),
326                docker_resolv_conf_path=inspect.get('ResolvConfPath'),
327                docker_state=inspect.get('State'),
328                docker_volumes=inspect.get('Volumes'),
329                docker_volumes_rw=inspect.get('VolumesRW'),
330            )
331
332            hostvars[name].update(container_info)
333
334    groups['docker_hosts'] = [host.get('base_url') for host in hosts]
335    groups['_meta'] = dict()
336    groups['_meta']['hostvars'] = hostvars
337    print json.dumps(groups, sort_keys=True, indent=4)
338    sys.exit(0)
339
340
341def parse_args():
342    parser = argparse.ArgumentParser()
343    group = parser.add_mutually_exclusive_group(required=True)
344    group.add_argument('--list', action='store_true')
345    group.add_argument('--host', action='store_true')
346    return parser.parse_args()
347
348
349def main():
350    args = parse_args()
351    if args.list:
352        list_groups()
353    elif args.host:
354        write_stderr('This option is not supported.')
355        sys.exit(1)
356    sys.exit(0)
357
358
359main()