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