mirror of
https://github.com/ansible-collections/community.docker.git
synced 2025-12-15 19:42:06 +00:00
* Make all doc fragments, module utils, and plugin utils private. * Remove some unused and no longer needed imports. This hopefully also fixes the CI issues, which do not happen locally for me... * Fix formatting. * Try to make CI happy, again. * Fix imports. * Lint.
338 lines
13 KiB
Python
338 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2019, Ximon Eighteen <ximon.eighteen@gmail.com>
|
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
DOCUMENTATION = r"""
|
|
name: docker_machine
|
|
author: Ximon Eighteen (@ximon18)
|
|
short_description: Docker Machine inventory source
|
|
requirements:
|
|
- L(Docker Machine,https://docs.docker.com/machine/)
|
|
extends_documentation_fragment:
|
|
- ansible.builtin.constructed
|
|
- community.library_inventory_filtering_v1.inventory_filter
|
|
description:
|
|
- Get inventory hosts from Docker Machine.
|
|
- Uses a YAML configuration file that ends with V(docker_machine.(yml|yaml\)).
|
|
- The plugin sets standard host variables C(ansible_host), C(ansible_port), C(ansible_user) and C(ansible_ssh_private_key).
|
|
- The plugin stores the Docker Machine 'env' output variables in C(dm_) prefixed host variables.
|
|
notes:
|
|
- The configuration file must be a YAML file whose filename ends with V(docker_machine.yml) or V(docker_machine.yaml). Other
|
|
filenames will not be accepted.
|
|
options:
|
|
plugin:
|
|
description: Token that ensures this is a source file for the C(docker_machine) plugin.
|
|
required: true
|
|
choices: ['docker_machine', 'community.docker.docker_machine']
|
|
daemon_env:
|
|
description:
|
|
- Whether docker daemon connection environment variables should be fetched, and how to behave if they cannot be fetched.
|
|
- With V(require) and V(require-silently), fetch them and skip any host for which they cannot be fetched. A warning
|
|
will be issued for any skipped host if the choice is V(require).
|
|
- With V(optional) and V(optional-silently), fetch them and not skip hosts for which they cannot be fetched. A warning
|
|
will be issued for hosts where they cannot be fetched if the choice is V(optional).
|
|
- With V(skip), do not attempt to fetch the docker daemon connection environment variables.
|
|
- If fetched successfully, the variables will be prefixed with C(dm_) and stored as host variables.
|
|
type: str
|
|
choices:
|
|
- require
|
|
- require-silently
|
|
- optional
|
|
- optional-silently
|
|
- skip
|
|
default: require
|
|
running_required:
|
|
description:
|
|
- When V(true), hosts which Docker Machine indicates are in a state other than C(running) will be skipped.
|
|
type: bool
|
|
default: true
|
|
verbose_output:
|
|
description:
|
|
- When V(true), include all available nodes metadata (for example C(Image), C(Region), C(Size)) as a JSON object named
|
|
C(docker_machine_node_attributes).
|
|
type: bool
|
|
default: true
|
|
filters:
|
|
version_added: 3.5.0
|
|
"""
|
|
|
|
EXAMPLES = """
|
|
---
|
|
# Minimal example
|
|
plugin: community.docker.docker_machine
|
|
|
|
---
|
|
# Example using constructed features to create a group per Docker Machine driver
|
|
# (https://docs.docker.com/machine/drivers/), for example:
|
|
# $ docker-machine create --driver digitalocean ... mymachine
|
|
# $ ansible-inventory -i ./path/to/docker-machine.yml --host=mymachine
|
|
# {
|
|
# ...
|
|
# "digitalocean": {
|
|
# "hosts": [
|
|
# "mymachine"
|
|
# ]
|
|
# ...
|
|
# }
|
|
plugin: community.docker.docker_machine
|
|
strict: false
|
|
keyed_groups:
|
|
- separator: ''
|
|
key: docker_machine_node_attributes.DriverName
|
|
|
|
---
|
|
# Example grouping hosts by Digital Machine tag
|
|
plugin: community.docker.docker_machine
|
|
strict: false
|
|
keyed_groups:
|
|
- prefix: tag
|
|
key: 'dm_tags'
|
|
|
|
---
|
|
# Example using compose to override the default SSH behaviour of asking the user to accept the remote host key
|
|
plugin: community.docker.docker_machine
|
|
compose:
|
|
ansible_ssh_common_args: '"-o StrictHostKeyChecking=accept-new"'
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import subprocess
|
|
|
|
from ansible.errors import AnsibleError
|
|
from ansible.module_utils.common.process import get_bin_path
|
|
from ansible.module_utils.common.text.converters import to_native, to_text
|
|
from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable, Constructable
|
|
from ansible.utils.display import Display
|
|
from ansible_collections.community.docker.plugins.plugin_utils._unsafe import (
|
|
make_unsafe,
|
|
)
|
|
from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import (
|
|
filter_host,
|
|
parse_filters,
|
|
)
|
|
|
|
|
|
display = Display()
|
|
|
|
|
|
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
|
"""Host inventory parser for ansible using Docker machine as source."""
|
|
|
|
NAME = "community.docker.docker_machine"
|
|
|
|
DOCKER_MACHINE_PATH = None
|
|
|
|
def _run_command(self, args):
|
|
if not self.DOCKER_MACHINE_PATH:
|
|
try:
|
|
self.DOCKER_MACHINE_PATH = get_bin_path("docker-machine")
|
|
except ValueError as e:
|
|
raise AnsibleError(to_native(e))
|
|
|
|
command = [self.DOCKER_MACHINE_PATH]
|
|
command.extend(args)
|
|
display.debug(f"Executing command {command}")
|
|
try:
|
|
result = subprocess.check_output(command)
|
|
except subprocess.CalledProcessError as e:
|
|
display.warning(
|
|
f"Exception {type(e).__name__} caught while executing command {command}, this was the original exception: {e}"
|
|
)
|
|
raise e
|
|
|
|
return to_text(result).strip()
|
|
|
|
def _get_docker_daemon_variables(self, machine_name):
|
|
"""
|
|
Capture settings from Docker Machine that would be needed to connect to the remote Docker daemon installed on
|
|
the Docker Machine remote host. Note: passing '--shell=sh' is a workaround for 'Error: Unknown shell'.
|
|
"""
|
|
try:
|
|
env_lines = self._run_command(
|
|
["env", "--shell=sh", machine_name]
|
|
).splitlines()
|
|
except subprocess.CalledProcessError:
|
|
# This can happen when the machine is created but provisioning is incomplete
|
|
return []
|
|
|
|
# example output of docker-machine env --shell=sh:
|
|
# export DOCKER_TLS_VERIFY="1"
|
|
# export DOCKER_HOST="tcp://134.209.204.160:2376"
|
|
# export DOCKER_CERT_PATH="/root/.docker/machine/machines/routinator"
|
|
# export DOCKER_MACHINE_NAME="routinator"
|
|
# # Run this command to configure your shell:
|
|
# # eval $(docker-machine env --shell=bash routinator)
|
|
|
|
# capture any of the DOCKER_xxx variables that were output and create Ansible host vars
|
|
# with the same name and value but with a dm_ name prefix.
|
|
vars = []
|
|
for line in env_lines:
|
|
match = re.search('(DOCKER_[^=]+)="([^"]+)"', line)
|
|
if match:
|
|
env_var_name = match.group(1)
|
|
env_var_value = match.group(2)
|
|
vars.append((env_var_name, env_var_value))
|
|
|
|
return vars
|
|
|
|
def _get_machine_names(self):
|
|
# Filter out machines that are not in the Running state as we probably cannot do anything useful actions
|
|
# with them.
|
|
ls_command = ["ls", "-q"]
|
|
if self.get_option("running_required"):
|
|
ls_command.extend(["--filter", "state=Running"])
|
|
|
|
try:
|
|
ls_lines = self._run_command(ls_command)
|
|
except subprocess.CalledProcessError:
|
|
return []
|
|
|
|
return ls_lines.splitlines()
|
|
|
|
def _inspect_docker_machine_host(self, node):
|
|
try:
|
|
inspect_lines = self._run_command(["inspect", node])
|
|
except subprocess.CalledProcessError:
|
|
return None
|
|
|
|
return json.loads(inspect_lines)
|
|
|
|
def _ip_addr_docker_machine_host(self, node):
|
|
try:
|
|
ip_addr = self._run_command(["ip", node])
|
|
except subprocess.CalledProcessError:
|
|
return None
|
|
|
|
return ip_addr
|
|
|
|
def _should_skip_host(self, machine_name, env_var_tuples, daemon_env):
|
|
if not env_var_tuples:
|
|
warning_prefix = f"Unable to fetch Docker daemon env vars from Docker Machine for host {machine_name}"
|
|
if daemon_env in ("require", "require-silently"):
|
|
if daemon_env == "require":
|
|
display.warning(f"{warning_prefix}: host will be skipped")
|
|
return True
|
|
else: # 'optional', 'optional-silently'
|
|
if daemon_env == "optional":
|
|
display.warning(
|
|
f"{warning_prefix}: host will lack dm_DOCKER_xxx variables"
|
|
)
|
|
return False
|
|
|
|
def _populate(self):
|
|
daemon_env = self.get_option("daemon_env")
|
|
filters = parse_filters(self.get_option("filters"))
|
|
try:
|
|
for node in self._get_machine_names():
|
|
node_attrs = self._inspect_docker_machine_host(node)
|
|
if not node_attrs:
|
|
continue
|
|
|
|
unsafe_node_attrs = make_unsafe(node_attrs)
|
|
|
|
machine_name = unsafe_node_attrs["Driver"]["MachineName"]
|
|
if not filter_host(self, machine_name, unsafe_node_attrs, filters):
|
|
continue
|
|
|
|
# query `docker-machine env` to obtain remote Docker daemon connection settings in the form of commands
|
|
# that could be used to set environment variables to influence a local Docker client:
|
|
if daemon_env == "skip":
|
|
env_var_tuples = []
|
|
else:
|
|
env_var_tuples = self._get_docker_daemon_variables(machine_name)
|
|
if self._should_skip_host(machine_name, env_var_tuples, daemon_env):
|
|
continue
|
|
|
|
# add an entry in the inventory for this host
|
|
self.inventory.add_host(machine_name)
|
|
|
|
# check for valid ip address from inspect output, else explicitly use ip command to find host ip address
|
|
# this works around an issue seen with Google Compute Platform where the IP address was not available
|
|
# via the 'inspect' subcommand but was via the 'ip' subcomannd.
|
|
if unsafe_node_attrs["Driver"]["IPAddress"]:
|
|
ip_addr = unsafe_node_attrs["Driver"]["IPAddress"]
|
|
else:
|
|
ip_addr = self._ip_addr_docker_machine_host(node)
|
|
|
|
# set standard Ansible remote host connection settings to details captured from `docker-machine`
|
|
# see: https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html
|
|
self.inventory.set_variable(
|
|
machine_name, "ansible_host", make_unsafe(ip_addr)
|
|
)
|
|
self.inventory.set_variable(
|
|
machine_name, "ansible_port", unsafe_node_attrs["Driver"]["SSHPort"]
|
|
)
|
|
self.inventory.set_variable(
|
|
machine_name, "ansible_user", unsafe_node_attrs["Driver"]["SSHUser"]
|
|
)
|
|
self.inventory.set_variable(
|
|
machine_name,
|
|
"ansible_ssh_private_key_file",
|
|
unsafe_node_attrs["Driver"]["SSHKeyPath"],
|
|
)
|
|
|
|
# set variables based on Docker Machine tags
|
|
tags = unsafe_node_attrs["Driver"].get("Tags") or ""
|
|
self.inventory.set_variable(machine_name, "dm_tags", make_unsafe(tags))
|
|
|
|
# set variables based on Docker Machine env variables
|
|
for kv in env_var_tuples:
|
|
self.inventory.set_variable(
|
|
machine_name, f"dm_{kv[0]}", make_unsafe(kv[1])
|
|
)
|
|
|
|
if self.get_option("verbose_output"):
|
|
self.inventory.set_variable(
|
|
machine_name,
|
|
"docker_machine_node_attributes",
|
|
unsafe_node_attrs,
|
|
)
|
|
|
|
# Use constructed if applicable
|
|
strict = self.get_option("strict")
|
|
|
|
# Composed variables
|
|
self._set_composite_vars(
|
|
self.get_option("compose"),
|
|
unsafe_node_attrs,
|
|
machine_name,
|
|
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"),
|
|
unsafe_node_attrs,
|
|
machine_name,
|
|
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"),
|
|
unsafe_node_attrs,
|
|
machine_name,
|
|
strict=strict,
|
|
)
|
|
|
|
except Exception as e:
|
|
raise AnsibleError(
|
|
f"Unable to fetch hosts from Docker Machine, this was the original exception: {e}"
|
|
) from e
|
|
|
|
def verify_file(self, path):
|
|
"""Return the possibility of a file being consumable by this plugin."""
|
|
return super(InventoryModule, self).verify_file(path) and path.endswith(
|
|
("docker_machine.yaml", "docker_machine.yml")
|
|
)
|
|
|
|
def parse(self, inventory, loader, path, cache=True):
|
|
super(InventoryModule, self).parse(inventory, loader, path, cache)
|
|
self._read_config_data(path)
|
|
self._populate()
|