# -*- coding: utf-8 -*- # Copyright (c) 2018, Stefan Heitmueller # Copyright (c) 2018 Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' name: docker_swarm plugin_type: inventory version_added: '2.8' author: - Stefan Heitmüller (@morph027) short_description: Ansible dynamic inventory plugin for Docker swarm nodes. requirements: - python >= 2.7 - L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 1.10.0 extends_documentation_fragment: - constructed description: - Reads inventories from the Docker swarm API. - Uses a YAML configuration file docker_swarm.[yml|yaml]. - "The plugin returns following groups of swarm nodes: I(all) - all hosts; I(workers) - all worker nodes; I(managers) - all manager nodes; I(leader) - the swarm leader node; I(nonleaders) - all nodes except the swarm leader." options: plugin: description: The name of this plugin, it should always be set to C(docker_swarm) for this plugin to recognize it as it's own. type: str required: true choices: docker_swarm docker_host: description: - Socket of a Docker swarm manager node (C(tcp), C(unix)). - "Use C(unix://var/run/docker.sock) to connect via local socket." type: str required: true aliases: [ docker_url ] verbose_output: description: Toggle to (not) include all available nodes metadata (e.g. C(Platform), C(Architecture), C(OS), C(EngineVersion)) type: bool default: yes tls: description: Connect using TLS without verifying the authenticity of the Docker host server. type: bool default: no validate_certs: description: Toggle if connecting using TLS with or without verifying the authenticity of the Docker host server. type: bool default: no aliases: [ tls_verify ] client_key: description: Path to the client's TLS key file. type: path aliases: [ tls_client_key, key_path ] ca_cert: description: Use a CA certificate when performing server verification by providing the path to a CA certificate file. type: path aliases: [ tls_ca_cert, cacert_path ] client_cert: description: Path to the client's TLS certificate file. type: path aliases: [ tls_client_cert, cert_path ] tls_hostname: description: When verifying the authenticity of the Docker host server, provide the expected name of the server. type: str ssl_version: description: Provide a valid SSL version number. Default value determined by ssl.py module. type: str api_version: description: - The version of the Docker API running on the Docker Host. - Defaults to the latest version of the API supported by docker-py. type: str aliases: [ docker_api_version ] timeout: description: - The maximum amount of time in seconds to wait on a response from the API. - If the value is not specified in the task, the value of environment variable C(DOCKER_TIMEOUT) will be used instead. If the environment variable is not set, the default value will be used. type: int default: 60 aliases: [ time_out ] include_host_uri: description: Toggle to return the additional attribute C(ansible_host_uri) which contains the URI of the swarm leader in format of C(tcp://172.16.0.1:2376). This value may be used without additional modification as value of option I(docker_host) in Docker Swarm modules when connecting via API. The port always defaults to C(2376). type: bool default: no include_host_uri_port: description: Override the detected port number included in I(ansible_host_uri) type: int ''' EXAMPLES = ''' # Minimal example using local docker plugin: docker_swarm docker_host: unix://var/run/docker.sock # Minimal example using remote docker plugin: docker_swarm docker_host: tcp://my-docker-host:2375 # Example using remote docker with unverified TLS plugin: docker_swarm docker_host: tcp://my-docker-host:2376 tls: yes # Example using remote docker with verified TLS and client certificate verification plugin: docker_swarm docker_host: tcp://my-docker-host:2376 validate_certs: yes ca_cert: /somewhere/ca.pem client_key: /somewhere/key.pem client_cert: /somewhere/cert.pem # Example using constructed features to create groups and set ansible_host plugin: docker_swarm docker_host: tcp://my-docker-host:2375 strict: False keyed_groups: # add e.g. x86_64 hosts to an arch_x86_64 group - prefix: arch key: 'Description.Platform.Architecture' # add e.g. linux hosts to an os_linux group - prefix: os key: 'Description.Platform.OS' # create a group per node label # e.g. a node labeled w/ "production" ends up in group "label_production" # hint: labels containing special characters will be converted to safe names - key: 'Spec.Labels' prefix: label ''' from ansible.errors import AnsibleError from ansible.module_utils._text import to_native from ansible.module_utils.six.moves.urllib.parse import urlparse from ansible.plugins.inventory import BaseInventoryPlugin, Constructable from ansible.parsing.utils.addresses import parse_address try: import docker from docker.errors import TLSParameterError from docker.tls import TLSConfig HAS_DOCKER = True except ImportError: HAS_DOCKER = False def update_tls_hostname(result): if result['tls_hostname'] is None: # get default machine name from the url parsed_url = urlparse(result['docker_host']) if ':' in parsed_url.netloc: result['tls_hostname'] = parsed_url.netloc[:parsed_url.netloc.rindex(':')] else: result['tls_hostname'] = parsed_url def _get_tls_config(fail_function, **kwargs): try: tls_config = TLSConfig(**kwargs) return tls_config except TLSParameterError as exc: fail_function("TLS config error: %s" % exc) def get_connect_params(auth, fail_function): if auth['tls'] or auth['tls_verify']: auth['docker_host'] = auth['docker_host'].replace('tcp://', 'https://') if auth['tls_verify'] and auth['cert_path'] and auth['key_path']: # TLS with certs and host verification if auth['cacert_path']: tls_config = _get_tls_config(client_cert=(auth['cert_path'], auth['key_path']), ca_cert=auth['cacert_path'], verify=True, assert_hostname=auth['tls_hostname'], ssl_version=auth['ssl_version'], fail_function=fail_function) else: tls_config = _get_tls_config(client_cert=(auth['cert_path'], auth['key_path']), verify=True, assert_hostname=auth['tls_hostname'], ssl_version=auth['ssl_version'], fail_function=fail_function) return dict(base_url=auth['docker_host'], tls=tls_config, version=auth['api_version'], timeout=auth['timeout']) if auth['tls_verify'] and auth['cacert_path']: # TLS with cacert only tls_config = _get_tls_config(ca_cert=auth['cacert_path'], assert_hostname=auth['tls_hostname'], verify=True, ssl_version=auth['ssl_version'], fail_function=fail_function) return dict(base_url=auth['docker_host'], tls=tls_config, version=auth['api_version'], timeout=auth['timeout']) if auth['tls_verify']: # TLS with verify and no certs tls_config = _get_tls_config(verify=True, assert_hostname=auth['tls_hostname'], ssl_version=auth['ssl_version'], fail_function=fail_function) return dict(base_url=auth['docker_host'], tls=tls_config, version=auth['api_version'], timeout=auth['timeout']) if auth['tls'] and auth['cert_path'] and auth['key_path']: # TLS with certs and no host verification tls_config = _get_tls_config(client_cert=(auth['cert_path'], auth['key_path']), verify=False, ssl_version=auth['ssl_version'], fail_function=fail_function) return dict(base_url=auth['docker_host'], tls=tls_config, version=auth['api_version'], timeout=auth['timeout']) if auth['tls']: # TLS with no certs and not host verification tls_config = _get_tls_config(verify=False, ssl_version=auth['ssl_version'], fail_function=fail_function) return dict(base_url=auth['docker_host'], tls=tls_config, version=auth['api_version'], timeout=auth['timeout']) # No TLS return dict(base_url=auth['docker_host'], version=auth['api_version'], timeout=auth['timeout']) class InventoryModule(BaseInventoryPlugin, Constructable): ''' Host inventory parser for ansible using Docker swarm as source. ''' NAME = 'docker_swarm' def _fail(self, msg): raise AnsibleError(msg) def _populate(self): raw_params = dict( docker_host=self.get_option('docker_host'), tls=self.get_option('tls'), tls_verify=self.get_option('validate_certs'), key_path=self.get_option('client_key'), cacert_path=self.get_option('ca_cert'), cert_path=self.get_option('client_cert'), tls_hostname=self.get_option('tls_hostname'), api_version=self.get_option('api_version'), timeout=self.get_option('timeout'), ssl_version=self.get_option('ssl_version'), debug=None, ) update_tls_hostname(raw_params) connect_params = get_connect_params(raw_params, fail_function=self._fail) self.client = docker.DockerClient(**connect_params) self.inventory.add_group('all') self.inventory.add_group('manager') self.inventory.add_group('worker') self.inventory.add_group('leader') self.inventory.add_group('nonleaders') if self.get_option('include_host_uri'): if self.get_option('include_host_uri_port'): host_uri_port = str(self.get_option('include_host_uri_port')) elif self.get_option('tls') or self.get_option('validate_certs'): host_uri_port = '2376' else: host_uri_port = '2375' try: self.nodes = self.client.nodes.list() for self.node in self.nodes: self.node_attrs = self.client.nodes.get(self.node.id).attrs self.inventory.add_host(self.node_attrs['ID']) self.inventory.add_host(self.node_attrs['ID'], group=self.node_attrs['Spec']['Role']) self.inventory.set_variable(self.node_attrs['ID'], 'ansible_host', self.node_attrs['Status']['Addr']) if self.get_option('include_host_uri'): self.inventory.set_variable(self.node_attrs['ID'], 'ansible_host_uri', 'tcp://' + self.node_attrs['Status']['Addr'] + ':' + host_uri_port) if self.get_option('verbose_output'): self.inventory.set_variable(self.node_attrs['ID'], 'docker_swarm_node_attributes', self.node_attrs) if 'ManagerStatus' in self.node_attrs: if self.node_attrs['ManagerStatus'].get('Leader'): # This is workaround of bug in Docker when in some cases the Leader IP is 0.0.0.0 # Check moby/moby#35437 for details swarm_leader_ip = parse_address(self.node_attrs['ManagerStatus']['Addr'])[0] or \ self.node_attrs['Status']['Addr'] if self.get_option('include_host_uri'): self.inventory.set_variable(self.node_attrs['ID'], 'ansible_host_uri', 'tcp://' + swarm_leader_ip + ':' + host_uri_port) self.inventory.set_variable(self.node_attrs['ID'], 'ansible_host', swarm_leader_ip) self.inventory.add_host(self.node_attrs['ID'], group='leader') else: self.inventory.add_host(self.node_attrs['ID'], group='nonleaders') else: self.inventory.add_host(self.node_attrs['ID'], group='nonleaders') # Use constructed if applicable strict = self.get_option('strict') # Composed variables self._set_composite_vars(self.get_option('compose'), self.node_attrs, self.node_attrs['ID'], strict=strict) # Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group self._add_host_to_composed_groups(self.get_option('groups'), self.node_attrs, self.node_attrs['ID'], strict=strict) # Create groups based on variable values and add the corresponding hosts to it self._add_host_to_keyed_groups(self.get_option('keyed_groups'), self.node_attrs, self.node_attrs['ID'], strict=strict) except Exception as e: raise AnsibleError('Unable to fetch hosts from Docker swarm API, this was the original exception: %s' % to_native(e)) def verify_file(self, path): """Return the possibly of a file being consumable by this plugin.""" return ( super(InventoryModule, self).verify_file(path) and path.endswith((self.NAME + '.yaml', self.NAME + '.yml'))) def parse(self, inventory, loader, path, cache=True): if not HAS_DOCKER: raise AnsibleError('The Docker swarm dynamic inventory plugin requires the Docker SDK for Python: ' 'https://github.com/docker/docker-py.') super(InventoryModule, self).parse(inventory, loader, path, cache) self._read_config_data(path) self._populate()