1# -*- coding: utf-8 -*- 2# Copyright (c) 2018, Stefan Heitmueller <stefan.heitmueller@gmx.com> 3# Copyright (c) 2018 Ansible Project 4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 6from __future__ import (absolute_import, division, print_function) 7 8__metaclass__ = type 9 10DOCUMENTATION = ''' 11 name: docker_swarm 12 plugin_type: inventory 13 version_added: '2.8' 14 author: 15 - Stefan Heitmüller (@morph027) <stefan.heitmueller@gmx.com> 16 short_description: Ansible dynamic inventory plugin for Docker swarm nodes. 17 requirements: 18 - python >= 2.7 19 - L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 1.10.0 20 extends_documentation_fragment: 21 - constructed 22 description: 23 - Reads inventories from the Docker swarm API. 24 - Uses a YAML configuration file docker_swarm.[yml|yaml]. 25 - "The plugin returns following groups of swarm nodes: I(all) - all hosts; I(workers) - all worker nodes; 26 I(managers) - all manager nodes; I(leader) - the swarm leader node; 27 I(nonleaders) - all nodes except the swarm leader." 28 options: 29 plugin: 30 description: The name of this plugin, it should always be set to C(docker_swarm) for this plugin to 31 recognize it as it's own. 32 type: str 33 required: true 34 choices: docker_swarm 35 docker_host: 36 description: 37 - Socket of a Docker swarm manager node (C(tcp), C(unix)). 38 - "Use C(unix://var/run/docker.sock) to connect via local socket." 39 type: str 40 required: true 41 aliases: [ docker_url ] 42 verbose_output: 43 description: Toggle to (not) include all available nodes metadata (e.g. C(Platform), C(Architecture), C(OS), 44 C(EngineVersion)) 45 type: bool 46 default: yes 47 tls: 48 description: Connect using TLS without verifying the authenticity of the Docker host server. 49 type: bool 50 default: no 51 validate_certs: 52 description: Toggle if connecting using TLS with or without verifying the authenticity of the Docker 53 host server. 54 type: bool 55 default: no 56 aliases: [ tls_verify ] 57 client_key: 58 description: Path to the client's TLS key file. 59 type: path 60 aliases: [ tls_client_key, key_path ] 61 ca_cert: 62 description: Use a CA certificate when performing server verification by providing the path to a CA 63 certificate file. 64 type: path 65 aliases: [ tls_ca_cert, cacert_path ] 66 client_cert: 67 description: Path to the client's TLS certificate file. 68 type: path 69 aliases: [ tls_client_cert, cert_path ] 70 tls_hostname: 71 description: When verifying the authenticity of the Docker host server, provide the expected name of 72 the server. 73 type: str 74 ssl_version: 75 description: Provide a valid SSL version number. Default value determined by ssl.py module. 76 type: str 77 api_version: 78 description: 79 - The version of the Docker API running on the Docker Host. 80 - Defaults to the latest version of the API supported by docker-py. 81 type: str 82 aliases: [ docker_api_version ] 83 timeout: 84 description: 85 - The maximum amount of time in seconds to wait on a response from the API. 86 - If the value is not specified in the task, the value of environment variable C(DOCKER_TIMEOUT) 87 will be used instead. If the environment variable is not set, the default value will be used. 88 type: int 89 default: 60 90 aliases: [ time_out ] 91 include_host_uri: 92 description: Toggle to return the additional attribute C(ansible_host_uri) which contains the URI of the 93 swarm leader in format of C(tcp://172.16.0.1:2376). This value may be used without additional 94 modification as value of option I(docker_host) in Docker Swarm modules when connecting via API. 95 The port always defaults to C(2376). 96 type: bool 97 default: no 98 include_host_uri_port: 99 description: Override the detected port number included in I(ansible_host_uri) 100 type: int 101''' 102 103EXAMPLES = ''' 104# Minimal example using local docker 105plugin: docker_swarm 106docker_host: unix://var/run/docker.sock 107 108# Minimal example using remote docker 109plugin: docker_swarm 110docker_host: tcp://my-docker-host:2375 111 112# Example using remote docker with unverified TLS 113plugin: docker_swarm 114docker_host: tcp://my-docker-host:2376 115tls: yes 116 117# Example using remote docker with verified TLS and client certificate verification 118plugin: docker_swarm 119docker_host: tcp://my-docker-host:2376 120validate_certs: yes 121ca_cert: /somewhere/ca.pem 122client_key: /somewhere/key.pem 123client_cert: /somewhere/cert.pem 124 125# Example using constructed features to create groups and set ansible_host 126plugin: docker_swarm 127docker_host: tcp://my-docker-host:2375 128strict: False 129keyed_groups: 130 # add e.g. x86_64 hosts to an arch_x86_64 group 131 - prefix: arch 132 key: 'Description.Platform.Architecture' 133 # add e.g. linux hosts to an os_linux group 134 - prefix: os 135 key: 'Description.Platform.OS' 136 # create a group per node label 137 # e.g. a node labeled w/ "production" ends up in group "label_production" 138 # hint: labels containing special characters will be converted to safe names 139 - key: 'Spec.Labels' 140 prefix: label 141''' 142 143from ansible.errors import AnsibleError 144from ansible.module_utils._text import to_native 145from ansible.module_utils.six.moves.urllib.parse import urlparse 146from ansible.plugins.inventory import BaseInventoryPlugin, Constructable 147from ansible.parsing.utils.addresses import parse_address 148 149try: 150 import docker 151 from docker.errors import TLSParameterError 152 from docker.tls import TLSConfig 153 HAS_DOCKER = True 154except ImportError: 155 HAS_DOCKER = False 156 157 158def update_tls_hostname(result): 159 if result['tls_hostname'] is None: 160 # get default machine name from the url 161 parsed_url = urlparse(result['docker_host']) 162 if ':' in parsed_url.netloc: 163 result['tls_hostname'] = parsed_url.netloc[:parsed_url.netloc.rindex(':')] 164 else: 165 result['tls_hostname'] = parsed_url 166 167 168def _get_tls_config(fail_function, **kwargs): 169 try: 170 tls_config = TLSConfig(**kwargs) 171 return tls_config 172 except TLSParameterError as exc: 173 fail_function("TLS config error: %s" % exc) 174 175 176def get_connect_params(auth, fail_function): 177 if auth['tls'] or auth['tls_verify']: 178 auth['docker_host'] = auth['docker_host'].replace('tcp://', 'https://') 179 180 if auth['tls_verify'] and auth['cert_path'] and auth['key_path']: 181 # TLS with certs and host verification 182 if auth['cacert_path']: 183 tls_config = _get_tls_config(client_cert=(auth['cert_path'], auth['key_path']), 184 ca_cert=auth['cacert_path'], 185 verify=True, 186 assert_hostname=auth['tls_hostname'], 187 ssl_version=auth['ssl_version'], 188 fail_function=fail_function) 189 else: 190 tls_config = _get_tls_config(client_cert=(auth['cert_path'], auth['key_path']), 191 verify=True, 192 assert_hostname=auth['tls_hostname'], 193 ssl_version=auth['ssl_version'], 194 fail_function=fail_function) 195 196 return dict(base_url=auth['docker_host'], 197 tls=tls_config, 198 version=auth['api_version'], 199 timeout=auth['timeout']) 200 201 if auth['tls_verify'] and auth['cacert_path']: 202 # TLS with cacert only 203 tls_config = _get_tls_config(ca_cert=auth['cacert_path'], 204 assert_hostname=auth['tls_hostname'], 205 verify=True, 206 ssl_version=auth['ssl_version'], 207 fail_function=fail_function) 208 return dict(base_url=auth['docker_host'], 209 tls=tls_config, 210 version=auth['api_version'], 211 timeout=auth['timeout']) 212 213 if auth['tls_verify']: 214 # TLS with verify and no certs 215 tls_config = _get_tls_config(verify=True, 216 assert_hostname=auth['tls_hostname'], 217 ssl_version=auth['ssl_version'], 218 fail_function=fail_function) 219 return dict(base_url=auth['docker_host'], 220 tls=tls_config, 221 version=auth['api_version'], 222 timeout=auth['timeout']) 223 224 if auth['tls'] and auth['cert_path'] and auth['key_path']: 225 # TLS with certs and no host verification 226 tls_config = _get_tls_config(client_cert=(auth['cert_path'], auth['key_path']), 227 verify=False, 228 ssl_version=auth['ssl_version'], 229 fail_function=fail_function) 230 return dict(base_url=auth['docker_host'], 231 tls=tls_config, 232 version=auth['api_version'], 233 timeout=auth['timeout']) 234 235 if auth['tls']: 236 # TLS with no certs and not host verification 237 tls_config = _get_tls_config(verify=False, 238 ssl_version=auth['ssl_version'], 239 fail_function=fail_function) 240 return dict(base_url=auth['docker_host'], 241 tls=tls_config, 242 version=auth['api_version'], 243 timeout=auth['timeout']) 244 245 # No TLS 246 return dict(base_url=auth['docker_host'], 247 version=auth['api_version'], 248 timeout=auth['timeout']) 249 250 251class InventoryModule(BaseInventoryPlugin, Constructable): 252 ''' Host inventory parser for ansible using Docker swarm as source. ''' 253 254 NAME = 'docker_swarm' 255 256 def _fail(self, msg): 257 raise AnsibleError(msg) 258 259 def _populate(self): 260 raw_params = dict( 261 docker_host=self.get_option('docker_host'), 262 tls=self.get_option('tls'), 263 tls_verify=self.get_option('validate_certs'), 264 key_path=self.get_option('client_key'), 265 cacert_path=self.get_option('ca_cert'), 266 cert_path=self.get_option('client_cert'), 267 tls_hostname=self.get_option('tls_hostname'), 268 api_version=self.get_option('api_version'), 269 timeout=self.get_option('timeout'), 270 ssl_version=self.get_option('ssl_version'), 271 debug=None, 272 ) 273 update_tls_hostname(raw_params) 274 connect_params = get_connect_params(raw_params, fail_function=self._fail) 275 self.client = docker.DockerClient(**connect_params) 276 self.inventory.add_group('all') 277 self.inventory.add_group('manager') 278 self.inventory.add_group('worker') 279 self.inventory.add_group('leader') 280 self.inventory.add_group('nonleaders') 281 282 if self.get_option('include_host_uri'): 283 if self.get_option('include_host_uri_port'): 284 host_uri_port = str(self.get_option('include_host_uri_port')) 285 elif self.get_option('tls') or self.get_option('validate_certs'): 286 host_uri_port = '2376' 287 else: 288 host_uri_port = '2375' 289 290 try: 291 self.nodes = self.client.nodes.list() 292 for self.node in self.nodes: 293 self.node_attrs = self.client.nodes.get(self.node.id).attrs 294 self.inventory.add_host(self.node_attrs['ID']) 295 self.inventory.add_host(self.node_attrs['ID'], group=self.node_attrs['Spec']['Role']) 296 self.inventory.set_variable(self.node_attrs['ID'], 'ansible_host', 297 self.node_attrs['Status']['Addr']) 298 if self.get_option('include_host_uri'): 299 self.inventory.set_variable(self.node_attrs['ID'], 'ansible_host_uri', 300 'tcp://' + self.node_attrs['Status']['Addr'] + ':' + host_uri_port) 301 if self.get_option('verbose_output'): 302 self.inventory.set_variable(self.node_attrs['ID'], 'docker_swarm_node_attributes', self.node_attrs) 303 if 'ManagerStatus' in self.node_attrs: 304 if self.node_attrs['ManagerStatus'].get('Leader'): 305 # This is workaround of bug in Docker when in some cases the Leader IP is 0.0.0.0 306 # Check moby/moby#35437 for details 307 swarm_leader_ip = parse_address(self.node_attrs['ManagerStatus']['Addr'])[0] or \ 308 self.node_attrs['Status']['Addr'] 309 if self.get_option('include_host_uri'): 310 self.inventory.set_variable(self.node_attrs['ID'], 'ansible_host_uri', 311 'tcp://' + swarm_leader_ip + ':' + host_uri_port) 312 self.inventory.set_variable(self.node_attrs['ID'], 'ansible_host', swarm_leader_ip) 313 self.inventory.add_host(self.node_attrs['ID'], group='leader') 314 else: 315 self.inventory.add_host(self.node_attrs['ID'], group='nonleaders') 316 else: 317 self.inventory.add_host(self.node_attrs['ID'], group='nonleaders') 318 # Use constructed if applicable 319 strict = self.get_option('strict') 320 # Composed variables 321 self._set_composite_vars(self.get_option('compose'), 322 self.node_attrs, 323 self.node_attrs['ID'], 324 strict=strict) 325 # Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group 326 self._add_host_to_composed_groups(self.get_option('groups'), 327 self.node_attrs, 328 self.node_attrs['ID'], 329 strict=strict) 330 # Create groups based on variable values and add the corresponding hosts to it 331 self._add_host_to_keyed_groups(self.get_option('keyed_groups'), 332 self.node_attrs, 333 self.node_attrs['ID'], 334 strict=strict) 335 except Exception as e: 336 raise AnsibleError('Unable to fetch hosts from Docker swarm API, this was the original exception: %s' % 337 to_native(e)) 338 339 def verify_file(self, path): 340 """Return the possibly of a file being consumable by this plugin.""" 341 return ( 342 super(InventoryModule, self).verify_file(path) and 343 path.endswith((self.NAME + '.yaml', self.NAME + '.yml'))) 344 345 def parse(self, inventory, loader, path, cache=True): 346 if not HAS_DOCKER: 347 raise AnsibleError('The Docker swarm dynamic inventory plugin requires the Docker SDK for Python: ' 348 'https://github.com/docker/docker-py.') 349 super(InventoryModule, self).parse(inventory, loader, path, cache) 350 self._read_config_data(path) 351 self._populate() 352