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