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