1#!/usr/bin/env python
2#
3# (c) 2016 Paul Durivage <paul.durivage@gmail.com>
4#          Chris Houseknecht <house@redhat.com>
5#          James Tanner <jtanner@redhat.com>
6#
7# This file is part of Ansible.
8#
9# Ansible is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# Ansible is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
21#
22
23from __future__ import (absolute_import, division, print_function)
24__metaclass__ = type
25
26
27DOCUMENTATION = '''
28
29Docker Inventory Script
30=======================
31The inventory script generates dynamic inventory by making API requests to one or more Docker APIs. It's dynamic
32because the inventory is generated at run-time rather than being read from a static file. The script generates the
33inventory by connecting to one or many Docker APIs and inspecting the containers it finds at each API. Which APIs the
34script contacts can be defined using environment variables or a configuration file.
35
36Requirements
37------------
38
39Using the docker modules requires having docker-py <https://docker-py.readthedocs.io/en/stable/>
40installed on the host running Ansible. To install docker-py:
41
42   pip install docker-py
43
44
45Run for Specific Host
46---------------------
47When run for a specific container using the --host option this script returns the following hostvars:
48
49{
50    "ansible_ssh_host": "",
51    "ansible_ssh_port": 0,
52    "docker_apparmorprofile": "",
53    "docker_args": [],
54    "docker_config": {
55        "AttachStderr": false,
56        "AttachStdin": false,
57        "AttachStdout": false,
58        "Cmd": [
59            "/hello"
60        ],
61        "Domainname": "",
62        "Entrypoint": null,
63        "Env": null,
64        "Hostname": "9f2f80b0a702",
65        "Image": "hello-world",
66        "Labels": {},
67        "OnBuild": null,
68        "OpenStdin": false,
69        "StdinOnce": false,
70        "Tty": false,
71        "User": "",
72        "Volumes": null,
73        "WorkingDir": ""
74    },
75    "docker_created": "2016-04-18T02:05:59.659599249Z",
76    "docker_driver": "aufs",
77    "docker_execdriver": "native-0.2",
78    "docker_execids": null,
79    "docker_graphdriver": {
80        "Data": null,
81        "Name": "aufs"
82    },
83    "docker_hostconfig": {
84        "Binds": null,
85        "BlkioWeight": 0,
86        "CapAdd": null,
87        "CapDrop": null,
88        "CgroupParent": "",
89        "ConsoleSize": [
90            0,
91            0
92        ],
93        "ContainerIDFile": "",
94        "CpuPeriod": 0,
95        "CpuQuota": 0,
96        "CpuShares": 0,
97        "CpusetCpus": "",
98        "CpusetMems": "",
99        "Devices": null,
100        "Dns": null,
101        "DnsOptions": null,
102        "DnsSearch": null,
103        "ExtraHosts": null,
104        "GroupAdd": null,
105        "IpcMode": "",
106        "KernelMemory": 0,
107        "Links": null,
108        "LogConfig": {
109            "Config": {},
110            "Type": "json-file"
111        },
112        "LxcConf": null,
113        "Memory": 0,
114        "MemoryReservation": 0,
115        "MemorySwap": 0,
116        "MemorySwappiness": null,
117        "NetworkMode": "default",
118        "OomKillDisable": false,
119        "PidMode": "host",
120        "PortBindings": null,
121        "Privileged": false,
122        "PublishAllPorts": false,
123        "ReadonlyRootfs": false,
124        "RestartPolicy": {
125            "MaximumRetryCount": 0,
126            "Name": ""
127        },
128        "SecurityOpt": [
129            "label:disable"
130        ],
131        "UTSMode": "",
132        "Ulimits": null,
133        "VolumeDriver": "",
134        "VolumesFrom": null
135    },
136    "docker_hostnamepath": "/mnt/sda1/var/lib/docker/containers/9f2f80b0a702361d1ac432e6af816c19bda46da15c21264fb418c873de635a14/hostname",
137    "docker_hostspath": "/mnt/sda1/var/lib/docker/containers/9f2f80b0a702361d1ac432e6af816c19bda46da15c21264fb418c873de635a14/hosts",
138    "docker_id": "9f2f80b0a702361d1ac432e6af816c19bda46da15c21264fb418c873de635a14",
139    "docker_image": "0a6ba66e537a53a5ea94f7c6a99c534c6adb12e3ed09326d4bf3b38f7c3ba4e7",
140    "docker_logpath": "/mnt/sda1/var/lib/docker/containers/9f2f80b0a702361d1ac432e6af816c19bda46da15c21264fb418c873de635a14/9f2f80b0a702361d1ac432e6a-json.log",
141    "docker_mountlabel": "",
142    "docker_mounts": [],
143    "docker_name": "/hello-world",
144    "docker_networksettings": {
145        "Bridge": "",
146        "EndpointID": "",
147        "Gateway": "",
148        "GlobalIPv6Address": "",
149        "GlobalIPv6PrefixLen": 0,
150        "HairpinMode": false,
151        "IPAddress": "",
152        "IPPrefixLen": 0,
153        "IPv6Gateway": "",
154        "LinkLocalIPv6Address": "",
155        "LinkLocalIPv6PrefixLen": 0,
156        "MacAddress": "",
157        "Networks": {
158            "bridge": {
159                "EndpointID": "",
160                "Gateway": "",
161                "GlobalIPv6Address": "",
162                "GlobalIPv6PrefixLen": 0,
163                "IPAddress": "",
164                "IPPrefixLen": 0,
165                "IPv6Gateway": "",
166                "MacAddress": ""
167            }
168        },
169        "Ports": null,
170        "SandboxID": "",
171        "SandboxKey": "",
172        "SecondaryIPAddresses": null,
173        "SecondaryIPv6Addresses": null
174    },
175    "docker_path": "/hello",
176    "docker_processlabel": "",
177    "docker_resolvconfpath": "/mnt/sda1/var/lib/docker/containers/9f2f80b0a702361d1ac432e6af816c19bda46da15c21264fb418c873de635a14/resolv.conf",
178    "docker_restartcount": 0,
179    "docker_short_id": "9f2f80b0a7023",
180    "docker_state": {
181        "Dead": false,
182        "Error": "",
183        "ExitCode": 0,
184        "FinishedAt": "2016-04-18T02:06:00.296619369Z",
185        "OOMKilled": false,
186        "Paused": false,
187        "Pid": 0,
188        "Restarting": false,
189        "Running": false,
190        "StartedAt": "2016-04-18T02:06:00.272065041Z",
191        "Status": "exited"
192    }
193}
194
195Groups
196------
197When run in --list mode (the default), container instances are grouped by:
198
199 - container id
200 - container name
201 - container short id
202 - image_name  (image_<image name>)
203 - stack_name  (stack_<stack name>)
204 - service_name  (service_<service name>)
205 - docker_host
206 - running
207 - stopped
208
209
210Configuration:
211--------------
212You can control the behavior of the inventory script by passing arguments, defining environment variables, or
213creating a configuration file named docker.yml (sample provided in ansible/contrib/inventory). The order of precedence
214is command line args, then the docker.yml file and finally environment variables.
215
216Environment variables:
217......................
218
219To connect to a single Docker API the following variables can be defined in the environment to control the connection
220options. These are the same environment variables used by the Docker modules.
221
222    DOCKER_HOST
223        The URL or Unix socket path used to connect to the Docker API. Defaults to unix://var/run/docker.sock.
224
225    DOCKER_API_VERSION:
226        The version of the Docker API running on the Docker Host. Defaults to the latest version of the API supported
227        by docker-py.
228
229    DOCKER_TIMEOUT:
230        The maximum amount of time in seconds to wait on a response fromm the API. Defaults to 60 seconds.
231
232    DOCKER_TLS:
233        Secure the connection to the API by using TLS without verifying the authenticity of the Docker host server.
234        Defaults to False.
235
236    DOCKER_TLS_VERIFY:
237        Secure the connection to the API by using TLS and verifying the authenticity of the Docker host server.
238        Default is False
239
240    DOCKER_TLS_HOSTNAME:
241        When verifying the authenticity of the Docker Host server, provide the expected name of the server. Defaults
242        to localhost.
243
244    DOCKER_CERT_PATH:
245        Path to the directory containing the client certificate, client key and CA certificate.
246
247    DOCKER_SSL_VERSION:
248        Provide a valid SSL version number. Default value determined by docker-py, which at the time of this writing
249        was 1.0
250
251In addition to the connection variables there are a couple variables used to control the execution and output of the
252script:
253
254    DOCKER_CONFIG_FILE
255        Path to the configuration file. Defaults to ./docker.yml.
256
257    DOCKER_PRIVATE_SSH_PORT:
258        The private port (container port) on which SSH is listening for connections. Defaults to 22.
259
260    DOCKER_DEFAULT_IP:
261        The IP address to assign to ansible_host when the container's SSH port is mapped to interface '0.0.0.0'.
262
263
264Configuration File
265..................
266
267Using a configuration file provides a means for defining a set of Docker APIs from which to build an inventory.
268
269The default name of the file is derived from the name of the inventory script. By default the script will look for
270basename of the script (i.e. docker) with an extension of '.yml'.
271
272You can also override the default name of the script by defining DOCKER_CONFIG_FILE in the environment.
273
274Here's what you can define in docker_inventory.yml:
275
276    defaults
277        Defines a default connection. Defaults will be taken from this and applied to any values not provided
278        for a host defined in the hosts list.
279
280    hosts
281        If you wish to get inventory from more than one Docker host, define a hosts list.
282
283For the default host and each host in the hosts list define the following attributes:
284
285  host:
286      description: The URL or Unix socket path used to connect to the Docker API.
287      required: yes
288
289  tls:
290     description: Connect using TLS without verifying the authenticity of the Docker host server.
291     default: false
292     required: false
293
294  tls_verify:
295     description: Connect using TLS without verifying the authenticity of the Docker host server.
296     default: false
297     required: false
298
299  cert_path:
300     description: Path to the client's TLS certificate file.
301     default: null
302     required: false
303
304  cacert_path:
305     description: Use a CA certificate when performing server verification by providing the path to a CA certificate file.
306     default: null
307     required: false
308
309  key_path:
310     description: Path to the client's TLS key file.
311     default: null
312     required: false
313
314  version:
315     description: The Docker API version.
316     required: false
317     default: will be supplied by the docker-py module.
318
319  timeout:
320     description: The amount of time in seconds to wait on an API response.
321     required: false
322     default: 60
323
324  default_ip:
325     description: The IP address to assign to ansible_host when the container's SSH port is mapped to interface
326     '0.0.0.0'.
327     required: false
328     default: 127.0.0.1
329
330  private_ssh_port:
331     description: The port containers use for SSH
332     required: false
333     default: 22
334
335Examples
336--------
337
338# Connect to the Docker API on localhost port 4243 and format the JSON output
339DOCKER_HOST=tcp://localhost:4243 ./docker.py --pretty
340
341# Any container's ssh port exposed on 0.0.0.0 will be mapped to
342# another IP address (where Ansible will attempt to connect via SSH)
343DOCKER_DEFAULT_IP=1.2.3.4 ./docker.py --pretty
344
345# Run as input to a playbook:
346ansible-playbook -i ~/projects/ansible/contrib/inventory/docker.py docker_inventory_test.yml
347
348# Simple playbook to invoke with the above example:
349
350    - name: Test docker_inventory
351      hosts: all
352      connection: local
353      gather_facts: no
354      tasks:
355        - debug: msg="Container - {{ inventory_hostname }}"
356
357'''
358
359import os
360import sys
361import json
362import argparse
363import re
364import yaml
365
366from collections import defaultdict
367# Manipulation of the path is needed because the docker-py
368# module is imported by the name docker, and because this file
369# is also named docker
370for path in [os.getcwd(), '', os.path.dirname(os.path.abspath(__file__))]:
371    try:
372        del sys.path[sys.path.index(path)]
373    except Exception:
374        pass
375
376HAS_DOCKER_PY = True
377HAS_DOCKER_ERROR = False
378
379try:
380    from docker.errors import APIError, TLSParameterError
381    from docker.tls import TLSConfig
382    from docker.constants import DEFAULT_TIMEOUT_SECONDS, DEFAULT_DOCKER_API_VERSION
383except ImportError as exc:
384    HAS_DOCKER_ERROR = str(exc)
385    HAS_DOCKER_PY = False
386
387# Client has recently been split into DockerClient and APIClient
388try:
389    from docker import Client
390except ImportError as dummy:
391    try:
392        from docker import APIClient as Client
393    except ImportError as exc:
394        HAS_DOCKER_ERROR = str(exc)
395        HAS_DOCKER_PY = False
396
397        class Client:
398            pass
399
400DEFAULT_DOCKER_CONFIG_FILE = os.path.splitext(os.path.basename(__file__))[0] + '.yml'
401DEFAULT_DOCKER_HOST = 'unix://var/run/docker.sock'
402DEFAULT_TLS = False
403DEFAULT_TLS_VERIFY = False
404DEFAULT_TLS_HOSTNAME = "localhost"
405DEFAULT_IP = '127.0.0.1'
406DEFAULT_SSH_PORT = '22'
407
408BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 1, True]
409BOOLEANS_FALSE = ['no', 'off', '0', 'false', 0, False]
410
411
412DOCKER_ENV_ARGS = dict(
413    config_file='DOCKER_CONFIG_FILE',
414    docker_host='DOCKER_HOST',
415    api_version='DOCKER_API_VERSION',
416    cert_path='DOCKER_CERT_PATH',
417    ssl_version='DOCKER_SSL_VERSION',
418    tls='DOCKER_TLS',
419    tls_verify='DOCKER_TLS_VERIFY',
420    tls_hostname='DOCKER_TLS_HOSTNAME',
421    timeout='DOCKER_TIMEOUT',
422    private_ssh_port='DOCKER_DEFAULT_SSH_PORT',
423    default_ip='DOCKER_DEFAULT_IP',
424)
425
426
427def fail(msg):
428    sys.stderr.write("%s\n" % msg)
429    sys.exit(1)
430
431
432def log(msg, pretty_print=False):
433    if pretty_print:
434        print(json.dumps(msg, sort_keys=True, indent=2))
435    else:
436        print(msg + u'\n')
437
438
439class AnsibleDockerClient(Client):
440    def __init__(self, auth_params, debug):
441
442        self.auth_params = auth_params
443        self.debug = debug
444        self._connect_params = self._get_connect_params()
445
446        try:
447            super(AnsibleDockerClient, self).__init__(**self._connect_params)
448        except APIError as exc:
449            self.fail("Docker API error: %s" % exc)
450        except Exception as exc:
451            self.fail("Error connecting: %s" % exc)
452
453    def fail(self, msg):
454        fail(msg)
455
456    def log(self, msg, pretty_print=False):
457        if self.debug:
458            log(msg, pretty_print)
459
460    def _get_tls_config(self, **kwargs):
461        self.log("get_tls_config:")
462        for key in kwargs:
463            self.log("  %s: %s" % (key, kwargs[key]))
464        try:
465            tls_config = TLSConfig(**kwargs)
466            return tls_config
467        except TLSParameterError as exc:
468            self.fail("TLS config error: %s" % exc)
469
470    def _get_connect_params(self):
471        auth = self.auth_params
472
473        self.log("auth params:")
474        for key in auth:
475            self.log("  %s: %s" % (key, auth[key]))
476
477        if auth['tls'] or auth['tls_verify']:
478            auth['docker_host'] = auth['docker_host'].replace('tcp://', 'https://')
479
480        if auth['tls'] and auth['cert_path'] and auth['key_path']:
481            # TLS with certs and no host verification
482            tls_config = self._get_tls_config(client_cert=(auth['cert_path'], auth['key_path']),
483                                              verify=False,
484                                              ssl_version=auth['ssl_version'])
485            return dict(base_url=auth['docker_host'],
486                        tls=tls_config,
487                        version=auth['api_version'],
488                        timeout=auth['timeout'])
489
490        if auth['tls']:
491            # TLS with no certs and not host verification
492            tls_config = self._get_tls_config(verify=False,
493                                              ssl_version=auth['ssl_version'])
494            return dict(base_url=auth['docker_host'],
495                        tls=tls_config,
496                        version=auth['api_version'],
497                        timeout=auth['timeout'])
498
499        if auth['tls_verify'] and auth['cert_path'] and auth['key_path']:
500            # TLS with certs and host verification
501            if auth['cacert_path']:
502                tls_config = self._get_tls_config(client_cert=(auth['cert_path'], auth['key_path']),
503                                                  ca_cert=auth['cacert_path'],
504                                                  verify=True,
505                                                  assert_hostname=auth['tls_hostname'],
506                                                  ssl_version=auth['ssl_version'])
507            else:
508                tls_config = self._get_tls_config(client_cert=(auth['cert_path'], auth['key_path']),
509                                                  verify=True,
510                                                  assert_hostname=auth['tls_hostname'],
511                                                  ssl_version=auth['ssl_version'])
512
513            return dict(base_url=auth['docker_host'],
514                        tls=tls_config,
515                        version=auth['api_version'],
516                        timeout=auth['timeout'])
517
518        if auth['tls_verify'] and auth['cacert_path']:
519            # TLS with cacert only
520            tls_config = self._get_tls_config(ca_cert=auth['cacert_path'],
521                                              assert_hostname=auth['tls_hostname'],
522                                              verify=True,
523                                              ssl_version=auth['ssl_version'])
524            return dict(base_url=auth['docker_host'],
525                        tls=tls_config,
526                        version=auth['api_version'],
527                        timeout=auth['timeout'])
528
529        if auth['tls_verify']:
530            # TLS with verify and no certs
531            tls_config = self._get_tls_config(verify=True,
532                                              assert_hostname=auth['tls_hostname'],
533                                              ssl_version=auth['ssl_version'])
534            return dict(base_url=auth['docker_host'],
535                        tls=tls_config,
536                        version=auth['api_version'],
537                        timeout=auth['timeout'])
538        # No TLS
539        return dict(base_url=auth['docker_host'],
540                    version=auth['api_version'],
541                    timeout=auth['timeout'])
542
543    def _handle_ssl_error(self, error):
544        match = re.match(r"hostname.*doesn\'t match (\'.*\')", str(error))
545        if match:
546            msg = "You asked for verification that Docker host name matches %s. The actual hostname is %s. " \
547                "Most likely you need to set DOCKER_TLS_HOSTNAME or pass tls_hostname with a value of %s. " \
548                "You may also use TLS without verification by setting the tls parameter to true." \
549                % (self.auth_params['tls_hostname'], match.group(1), match.group(1))
550            self.fail(msg)
551        self.fail("SSL Exception: %s" % (error))
552
553
554class EnvArgs(object):
555    def __init__(self):
556        self.config_file = None
557        self.docker_host = None
558        self.api_version = None
559        self.cert_path = None
560        self.ssl_version = None
561        self.tls = None
562        self.tls_verify = None
563        self.tls_hostname = None
564        self.timeout = None
565        self.default_ssh_port = None
566        self.default_ip = None
567
568
569class DockerInventory(object):
570
571    def __init__(self):
572        self._args = self._parse_cli_args()
573        self._env_args = self._parse_env_args()
574        self.groups = defaultdict(list)
575        self.hostvars = defaultdict(dict)
576
577    def run(self):
578        config_from_file = self._parse_config_file()
579        if not config_from_file:
580            config_from_file = dict()
581        docker_hosts = self.get_hosts(config_from_file)
582
583        for host in docker_hosts:
584            client = AnsibleDockerClient(host, self._args.debug)
585            self.get_inventory(client, host)
586
587        if not self._args.host:
588            self.groups['docker_hosts'] = [host.get('docker_host') for host in docker_hosts]
589            self.groups['_meta'] = dict(
590                hostvars=self.hostvars
591            )
592            print(self._json_format_dict(self.groups, pretty_print=self._args.pretty))
593        else:
594            print(self._json_format_dict(self.hostvars.get(self._args.host, dict()), pretty_print=self._args.pretty))
595
596        sys.exit(0)
597
598    def get_inventory(self, client, host):
599
600        ssh_port = host.get('default_ssh_port')
601        default_ip = host.get('default_ip')
602        hostname = host.get('docker_host')
603
604        try:
605            containers = client.containers(all=True)
606        except Exception as exc:
607            self.fail("Error fetching containers for host %s - %s" % (hostname, str(exc)))
608
609        for container in containers:
610            id = container.get('Id')
611            short_id = id[:13]
612
613            try:
614                name = container.get('Names', list()).pop(0).lstrip('/')
615            except IndexError:
616                name = short_id
617
618            if not self._args.host or (self._args.host and self._args.host in [name, id, short_id]):
619                try:
620                    inspect = client.inspect_container(id)
621                except Exception as exc:
622                    self.fail("Error inspecting container %s - %s" % (name, str(exc)))
623
624                running = inspect.get('State', dict()).get('Running')
625
626                # Add container to groups
627                image_name = inspect.get('Config', dict()).get('Image')
628                if image_name:
629                    self.groups["image_%s" % (image_name)].append(name)
630
631                stack_name = inspect.get('Config', dict()).get('Labels', dict()).get('com.docker.stack.namespace')
632                if stack_name:
633                    self.groups["stack_%s" % stack_name].append(name)
634
635                service_name = inspect.get('Config', dict()).get('Labels', dict()).get('com.docker.swarm.service.name')
636                if service_name:
637                    self.groups["service_%s" % service_name].append(name)
638
639                self.groups[id].append(name)
640                self.groups[name].append(name)
641                if short_id not in self.groups:
642                    self.groups[short_id].append(name)
643                self.groups[hostname].append(name)
644
645                if running is True:
646                    self.groups['running'].append(name)
647                else:
648                    self.groups['stopped'].append(name)
649
650                # Figure ous ssh IP and Port
651                try:
652                    # Lookup the public facing port Nat'ed to ssh port.
653                    port = client.port(container, ssh_port)[0]
654                except (IndexError, AttributeError, TypeError):
655                    port = dict()
656
657                try:
658                    ip = default_ip if port['HostIp'] == '0.0.0.0' else port['HostIp']
659                except KeyError:
660                    ip = ''
661
662                facts = dict(
663                    ansible_ssh_host=ip,
664                    ansible_ssh_port=port.get('HostPort', int()),
665                    docker_name=name,
666                    docker_short_id=short_id
667                )
668
669                for key in inspect:
670                    fact_key = self._slugify(key)
671                    facts[fact_key] = inspect.get(key)
672
673                self.hostvars[name].update(facts)
674
675    def _slugify(self, value):
676        return 'docker_%s' % (re.sub(r'[^\w-]', '_', value).lower().lstrip('_'))
677
678    def get_hosts(self, config):
679        '''
680        Determine the list of docker hosts we need to talk to.
681
682        :param config: dictionary read from config file. can be empty.
683        :return: list of connection dictionaries
684        '''
685        hosts = list()
686
687        hosts_list = config.get('hosts')
688        defaults = config.get('defaults', dict())
689        self.log('defaults:')
690        self.log(defaults, pretty_print=True)
691        def_host = defaults.get('host')
692        def_tls = defaults.get('tls')
693        def_tls_verify = defaults.get('tls_verify')
694        def_tls_hostname = defaults.get('tls_hostname')
695        def_ssl_version = defaults.get('ssl_version')
696        def_cert_path = defaults.get('cert_path')
697        def_cacert_path = defaults.get('cacert_path')
698        def_key_path = defaults.get('key_path')
699        def_version = defaults.get('version')
700        def_timeout = defaults.get('timeout')
701        def_ip = defaults.get('default_ip')
702        def_ssh_port = defaults.get('private_ssh_port')
703
704        if hosts_list:
705            # use hosts from config file
706            for host in hosts_list:
707                docker_host = host.get('host') or def_host or self._args.docker_host or \
708                    self._env_args.docker_host or DEFAULT_DOCKER_HOST
709                api_version = host.get('version') or def_version or self._args.api_version or \
710                    self._env_args.api_version or DEFAULT_DOCKER_API_VERSION
711                tls_hostname = host.get('tls_hostname') or def_tls_hostname or self._args.tls_hostname or \
712                    self._env_args.tls_hostname or DEFAULT_TLS_HOSTNAME
713                tls_verify = host.get('tls_verify') or def_tls_verify or self._args.tls_verify or \
714                    self._env_args.tls_verify or DEFAULT_TLS_VERIFY
715                tls = host.get('tls') or def_tls or self._args.tls or self._env_args.tls or DEFAULT_TLS
716                ssl_version = host.get('ssl_version') or def_ssl_version or self._args.ssl_version or \
717                    self._env_args.ssl_version
718
719                cert_path = host.get('cert_path') or def_cert_path or self._args.cert_path or \
720                    self._env_args.cert_path
721                if cert_path and cert_path == self._env_args.cert_path:
722                    cert_path = os.path.join(cert_path, 'cert.pem')
723
724                cacert_path = host.get('cacert_path') or def_cacert_path or self._args.cacert_path or \
725                    self._env_args.cert_path
726                if cacert_path and cacert_path == self._env_args.cert_path:
727                    cacert_path = os.path.join(cacert_path, 'ca.pem')
728
729                key_path = host.get('key_path') or def_key_path or self._args.key_path or \
730                    self._env_args.cert_path
731                if key_path and key_path == self._env_args.cert_path:
732                    key_path = os.path.join(key_path, 'key.pem')
733
734                timeout = host.get('timeout') or def_timeout or self._args.timeout or self._env_args.timeout or \
735                    DEFAULT_TIMEOUT_SECONDS
736                default_ip = host.get('default_ip') or def_ip or self._env_args.default_ip or \
737                    self._args.default_ip_address or DEFAULT_IP
738                default_ssh_port = host.get('private_ssh_port') or def_ssh_port or self._args.private_ssh_port or \
739                    DEFAULT_SSH_PORT
740                host_dict = dict(
741                    docker_host=docker_host,
742                    api_version=api_version,
743                    tls=tls,
744                    tls_verify=tls_verify,
745                    tls_hostname=tls_hostname,
746                    cert_path=cert_path,
747                    cacert_path=cacert_path,
748                    key_path=key_path,
749                    ssl_version=ssl_version,
750                    timeout=timeout,
751                    default_ip=default_ip,
752                    default_ssh_port=default_ssh_port,
753                )
754                hosts.append(host_dict)
755        else:
756            # use default definition
757            docker_host = def_host or self._args.docker_host or self._env_args.docker_host or DEFAULT_DOCKER_HOST
758            api_version = def_version or self._args.api_version or self._env_args.api_version or \
759                DEFAULT_DOCKER_API_VERSION
760            tls_hostname = def_tls_hostname or self._args.tls_hostname or self._env_args.tls_hostname or \
761                DEFAULT_TLS_HOSTNAME
762            tls_verify = def_tls_verify or self._args.tls_verify or self._env_args.tls_verify or DEFAULT_TLS_VERIFY
763            tls = def_tls or self._args.tls or self._env_args.tls or DEFAULT_TLS
764            ssl_version = def_ssl_version or self._args.ssl_version or self._env_args.ssl_version
765
766            cert_path = def_cert_path or self._args.cert_path or self._env_args.cert_path
767            if cert_path and cert_path == self._env_args.cert_path:
768                cert_path = os.path.join(cert_path, 'cert.pem')
769
770            cacert_path = def_cacert_path or self._args.cacert_path or self._env_args.cert_path
771            if cacert_path and cacert_path == self._env_args.cert_path:
772                cacert_path = os.path.join(cacert_path, 'ca.pem')
773
774            key_path = def_key_path or self._args.key_path or self._env_args.cert_path
775            if key_path and key_path == self._env_args.cert_path:
776                key_path = os.path.join(key_path, 'key.pem')
777
778            timeout = def_timeout or self._args.timeout or self._env_args.timeout or DEFAULT_TIMEOUT_SECONDS
779            default_ip = def_ip or self._env_args.default_ip or self._args.default_ip_address or DEFAULT_IP
780            default_ssh_port = def_ssh_port or self._args.private_ssh_port or DEFAULT_SSH_PORT
781            host_dict = dict(
782                docker_host=docker_host,
783                api_version=api_version,
784                tls=tls,
785                tls_verify=tls_verify,
786                tls_hostname=tls_hostname,
787                cert_path=cert_path,
788                cacert_path=cacert_path,
789                key_path=key_path,
790                ssl_version=ssl_version,
791                timeout=timeout,
792                default_ip=default_ip,
793                default_ssh_port=default_ssh_port,
794            )
795            hosts.append(host_dict)
796        self.log("hosts: ")
797        self.log(hosts, pretty_print=True)
798        return hosts
799
800    def _parse_config_file(self):
801        config = dict()
802        config_file = DEFAULT_DOCKER_CONFIG_FILE
803
804        if self._args.config_file:
805            config_file = self._args.config_file
806        elif self._env_args.config_file:
807            config_file = self._env_args.config_file
808
809        config_file = os.path.abspath(config_file)
810
811        if os.path.isfile(config_file):
812            with open(config_file) as f:
813                try:
814                    config = yaml.safe_load(f.read())
815                except Exception as exc:
816                    self.fail("Error: parsing %s - %s" % (config_file, str(exc)))
817        else:
818            msg = "Error: config file given by {} does not exist - " + config_file
819            if self._args.config_file:
820                self.fail(msg.format('command line argument'))
821            elif self._env_args.config_file:
822                self.fail(msg.format(DOCKER_ENV_ARGS.get('config_file')))
823            else:
824                self.log(msg.format('DEFAULT_DOCKER_CONFIG_FILE'))
825        return config
826
827    def log(self, msg, pretty_print=False):
828        if self._args.debug:
829            log(msg, pretty_print)
830
831    def fail(self, msg):
832        fail(msg)
833
834    def _parse_env_args(self):
835        args = EnvArgs()
836        for key, value in DOCKER_ENV_ARGS.items():
837            if os.environ.get(value):
838                val = os.environ.get(value)
839                if val in BOOLEANS_TRUE:
840                    val = True
841                if val in BOOLEANS_FALSE:
842                    val = False
843                setattr(args, key, val)
844        return args
845
846    def _parse_cli_args(self):
847        # Parse command line arguments
848
849        parser = argparse.ArgumentParser(
850            description='Return Ansible inventory for one or more Docker hosts.')
851        parser.add_argument('--list', action='store_true', default=True,
852                            help='List all containers (default: True)')
853        parser.add_argument('--debug', action='store_true', default=False,
854                            help='Send debug messages to STDOUT')
855        parser.add_argument('--host', action='store',
856                            help='Only get information for a specific container.')
857        parser.add_argument('--pretty', action='store_true', default=False,
858                            help='Pretty print JSON output(default: False)')
859        parser.add_argument('--config-file', action='store', default=None,
860                            help="Name of the config file to use. Default is %s" % (DEFAULT_DOCKER_CONFIG_FILE))
861        parser.add_argument('--docker-host', action='store', default=None,
862                            help="The base url or Unix sock path to connect to the docker daemon. Defaults to %s"
863                                 % (DEFAULT_DOCKER_HOST))
864        parser.add_argument('--tls-hostname', action='store', default=None,
865                            help="Host name to expect in TLS certs. Defaults to %s" % DEFAULT_TLS_HOSTNAME)
866        parser.add_argument('--api-version', action='store', default=None,
867                            help="Docker daemon API version. Defaults to %s" % (DEFAULT_DOCKER_API_VERSION))
868        parser.add_argument('--timeout', action='store', default=None,
869                            help="Docker connection timeout in seconds. Defaults to %s"
870                                 % (DEFAULT_TIMEOUT_SECONDS))
871        parser.add_argument('--cacert-path', action='store', default=None,
872                            help="Path to the TLS certificate authority pem file.")
873        parser.add_argument('--cert-path', action='store', default=None,
874                            help="Path to the TLS certificate pem file.")
875        parser.add_argument('--key-path', action='store', default=None,
876                            help="Path to the TLS encryption key pem file.")
877        parser.add_argument('--ssl-version', action='store', default=None,
878                            help="TLS version number")
879        parser.add_argument('--tls', action='store_true', default=None,
880                            help="Use TLS. Defaults to %s" % (DEFAULT_TLS))
881        parser.add_argument('--tls-verify', action='store_true', default=None,
882                            help="Verify TLS certificates. Defaults to %s" % (DEFAULT_TLS_VERIFY))
883        parser.add_argument('--private-ssh-port', action='store', default=None,
884                            help="Default private container SSH Port. Defaults to %s" % (DEFAULT_SSH_PORT))
885        parser.add_argument('--default-ip-address', action='store', default=None,
886                            help="Default container SSH IP address. Defaults to %s" % (DEFAULT_IP))
887        return parser.parse_args()
888
889    def _json_format_dict(self, data, pretty_print=False):
890        # format inventory data for output
891        if pretty_print:
892            return json.dumps(data, sort_keys=True, indent=4)
893        else:
894            return json.dumps(data)
895
896
897def main():
898
899    if not HAS_DOCKER_PY:
900        fail("Failed to import docker-py. Try `pip install docker-py` - %s" % (HAS_DOCKER_ERROR))
901
902    DockerInventory().run()
903
904
905main()
906