diff --git a/lib/ansible/plugins/inventory/docker_swarm.py b/lib/ansible/plugins/inventory/docker_swarm.py new file mode 100644 index 00000000000..4a4414effe1 --- /dev/null +++ b/lib/ansible/plugins/inventory/docker_swarm.py @@ -0,0 +1,207 @@ +# -*- 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' + authors: + - Stefan Heitmüller (@morph027) + short_description: Ansible dynamic inventory plugin for Docker swarm nodes. + requirements: + - python >= 2.7 + - Docker SDK for Python > 1.10.0 + extends_documentation_fragment: + - constructed + description: + - Reads inventories from the Docker swarm API. + - Uses a YAML configuration file docker_swarm.[yml|yaml]. + options: + plugin: + description: The name of this plugin, it should always be set to 'docker_swarm' for this plugin to recognize it as it's own. + type: str + required: true + choices: docker_swarm + host: + description: Socket of a Docker swarm manager node (tcp,unix). + type: str + required: true + verbose_output: + description: Toggle to (not) include all available nodes metadata (e.g. Platform, Architecture, OS, EngineVersion) + type: bool + default: yes + tls: + description: Connect using TLS without verifying the authenticity of the Docker host server. + type: bool + default: no + tls_verify: + description: Toggle if connecting using TLS with or without verifying the authenticity of the Docker host server. + type: bool + default: no + key_path: + description: Path to the client's TLS key file. + type: path + cacert_path: + description: Use a CA certificate when performing server verification by providing the path to a CA certificate file. + type: path + cert_path: + description: Path to the client's TLS certificate file. + type: path + tls_hostname: + description: When verifying the authenticity of the Docker Host server, provide the expected name of the server. + type: str +''' + +EXAMPLES = ''' +# Minimal example using local docker +plugin: docker_swarm +host: unix://var/run/docker.sock + +# Minimal example using remote docker +plugin: docker_swarm +host: tcp://my-docker-host:2375 + +# Example using remote docker with unverified TLS +plugin: docker_swarm +host: tcp://my-docker-host:2375 +tls: yes + +# Example using remote docker with verified TLS and client certificate verification +plugin: docker_swarm +host: tcp://my-docker-host:2375 +tls_verify: yes +cacert_path: /somewhere/ca.pem +key_path: /somewhere/key.pem +cert_path: /somewhere/cert.pem + +# Example using constructed features to create groups and set ansible_host +plugin: docker_swarm +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, AnsibleParserError +from ansible.module_utils._text import to_native +from ansible.plugins.inventory import BaseInventoryPlugin, Constructable +from ansible.release import __version__ + +try: + import docker + HAS_DOCKER = True +except ImportError: + HAS_DOCKER = False + + +class InventoryModule(BaseInventoryPlugin, Constructable): + ''' Host inventory parser for ansible using Docker swarm as source. ''' + + NAME = 'docker_swarm' + + def _get_tls_config(self, **kwargs): + try: + tls_config = docker.tls.TLSConfig(**kwargs) + return tls_config + except Exception as e: + raise AnsibleError('Unable to setup TLS, this was the original exception: %s' % to_native(e)) + + def _get_tls_connect_params(self): + if self.get_option('tls') and self.get_option('cert_path') and self.get_option('key_path'): + # TLS with certs and no host verification + tls_config = self._get_tls_config(client_cert=(self.get_option('cert_path'), self.get_option('key_path')), + verify=False) + return tls_config + + if self.get_option('tls'): + # TLS with no certs and not host verification + tls_config = self._get_tls_config(verify=False) + return tls_config + + if self.get_option('tls_verify') and self.get_option('cert_path') and self.get_option('key_path'): + # TLS with certs and host verification + if self.get_option('cacert_path'): + tls_config = self._get_tls_config(client_cert=(self.get_option('cert_path'), self.get_option('key_path')), + ca_cert=self.get_option('cacert_path'), + verify=True, + assert_hostname=self.get_option('tls_hostname')) + else: + tls_config = self._get_tls_config(client_cert=(self.get_option('cert_path'), self.get_option('key_path')), + verify=True, + assert_hostname=self.get_option('tls_hostname')) + + return tls_config + + if self.get_option('tls_verify') and self.get_option('cacert_path'): + # TLS with cacert only + tls_config = self._get_tls_config(ca_cert=self.get_option('cacert_path'), + assert_hostname=self.get_option('tls_hostname'), + verify=True) + return tls_config + + if self.get_option('tls_verify'): + # TLS with verify and no certs + tls_config = self._get_tls_config(verify=True, + assert_hostname=self.get_option('tls_hostname')) + return tls_config + + # No TLS + return None + + def _populate(self): + self.client = docker.DockerClient(base_url=self.get_option('host'), tls=self._get_tls_connect_params()) + self.inventory.add_group('all') + self.inventory.add_group('manager') + self.inventory.add_group('worker') + self.inventory.add_group('leader') + 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('verbose_output', True): + 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'): + self.inventory.add_host(self.node_attrs['ID'], group='leader') + # 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()