352 lines
16 KiB
Python
352 lines
16 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Copyright (c) 2018, Stefan Heitmueller <stefan.heitmueller@gmx.com>
|
||
|
# 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) <stefan.heitmueller@gmx.com>
|
||
|
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()
|