summaryrefslogtreecommitdiffstats
path: root/ansible_collections/hetzner/hcloud/plugins/inventory
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 16:03:42 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 16:03:42 +0000
commit66cec45960ce1d9c794e9399de15c138acb18aed (patch)
tree59cd19d69e9d56b7989b080da7c20ef1a3fe2a5a /ansible_collections/hetzner/hcloud/plugins/inventory
parentInitial commit. (diff)
downloadansible-66cec45960ce1d9c794e9399de15c138acb18aed.tar.xz
ansible-66cec45960ce1d9c794e9399de15c138acb18aed.zip
Adding upstream version 7.3.0+dfsg.upstream/7.3.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/hetzner/hcloud/plugins/inventory')
-rw-r--r--ansible_collections/hetzner/hcloud/plugins/inventory/__init__.py0
-rw-r--r--ansible_collections/hetzner/hcloud/plugins/inventory/hcloud.py350
2 files changed, 350 insertions, 0 deletions
diff --git a/ansible_collections/hetzner/hcloud/plugins/inventory/__init__.py b/ansible_collections/hetzner/hcloud/plugins/inventory/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ansible_collections/hetzner/hcloud/plugins/inventory/__init__.py
diff --git a/ansible_collections/hetzner/hcloud/plugins/inventory/hcloud.py b/ansible_collections/hetzner/hcloud/plugins/inventory/hcloud.py
new file mode 100644
index 00000000..f032eb89
--- /dev/null
+++ b/ansible_collections/hetzner/hcloud/plugins/inventory/hcloud.py
@@ -0,0 +1,350 @@
+# Copyright (c) 2019 Hetzner Cloud GmbH <info@hetzner-cloud.de>
+# 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 = r'''
+ name: hcloud
+ author:
+ - Lukas Kaemmerling (@lkaemmerling)
+ short_description: Ansible dynamic inventory plugin for the Hetzner Cloud.
+ requirements:
+ - python >= 3.5
+ - hcloud-python >= 1.0.0
+ description:
+ - Reads inventories from the Hetzner Cloud API.
+ - Uses a YAML configuration file that ends with hcloud.(yml|yaml).
+ extends_documentation_fragment:
+ - constructed
+ options:
+ plugin:
+ description: marks this as an instance of the "hcloud" plugin
+ required: true
+ choices: ["hcloud", "hetzner.hcloud.hcloud"]
+ token:
+ description: The Hetzner Cloud API Token.
+ required: false
+ group:
+ description: The group all servers are automatically added to.
+ default: hcloud
+ type: str
+ required: false
+ token_env:
+ description: Environment variable to load the Hetzner Cloud API Token from.
+ default: HCLOUD_TOKEN
+ type: str
+ required: false
+ connect_with:
+ description: |
+ Connect to the server using the value from this field. This sets the `ansible_host`
+ variable to the value indicated, if that value is available. If you need further
+ customization, like falling back to private ipv4 if the server has no public ipv4,
+ you can use `compose` top-level key.
+ default: public_ipv4
+ type: str
+ choices:
+ - public_ipv4
+ - public_ipv6
+ - hostname
+ - ipv4_dns_ptr
+ - private_ipv4
+ locations:
+ description: Populate inventory with instances in this location.
+ default: []
+ type: list
+ elements: str
+ required: false
+ types:
+ description: Populate inventory with instances with this type.
+ default: []
+ type: list
+ elements: str
+ required: false
+ images:
+ description: Populate inventory with instances with this image name, only available for system images.
+ default: []
+ type: list
+ elements: str
+ required: false
+ label_selector:
+ description: Populate inventory with instances with this label.
+ default: ""
+ type: str
+ required: false
+ network:
+ description: Populate inventory with instances which are attached to this network name or ID.
+ default: ""
+ type: str
+ required: false
+ status:
+ description: Populate inventory with instances with this status.
+ default: []
+ type: list
+ elements: str
+ required: false
+'''
+
+EXAMPLES = r"""
+# Minimal example. `HCLOUD_TOKEN` is exposed in environment.
+plugin: hcloud
+
+# Example with templated token, e.g. provided through extra vars.
+plugin: hcloud
+token: "{{ hetzner_apitoken }}"
+
+# Example with locations, types, status and token
+plugin: hcloud
+token: foobar
+locations:
+ - nbg1
+types:
+ - cx11
+status:
+ - running
+
+# Group by a location with prefix e.g. "hcloud_location_nbg1"
+# and image_os_flavor without prefix and separator e.g. "ubuntu"
+# and status with prefix e.g. "server_status_running"
+plugin: hcloud
+keyed_groups:
+ - key: location
+ prefix: hcloud_location
+ - key: image_os_flavor
+ separator: ""
+ - key: status
+ prefix: server_status
+"""
+
+import os
+from ansible.errors import AnsibleError
+from ansible.module_utils._text import to_native
+from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
+from ansible.release import __version__
+from ipaddress import IPv6Network
+
+try:
+ from hcloud import hcloud
+ from hcloud import APIException
+ HAS_HCLOUD = True
+except ImportError:
+ HAS_HCLOUD = False
+
+
+class InventoryModule(BaseInventoryPlugin, Constructable):
+ NAME = 'hetzner.hcloud.hcloud'
+
+ def _configure_hcloud_client(self):
+ self.token_env = self.get_option("token_env")
+ self.templar.available_variables = self._vars
+ self.api_token = self.templar.template(self.get_option("token"), fail_on_undefined=False) or os.getenv(self.token_env)
+ if self.api_token is None:
+ raise AnsibleError(
+ "Please specify a token, via the option token, via environment variable HCLOUD_TOKEN "
+ "or via custom environment variable set by token_env option."
+ )
+
+ self.endpoint = os.getenv("HCLOUD_ENDPOINT") or "https://api.hetzner.cloud/v1"
+
+ self.client = hcloud.Client(token=self.api_token,
+ api_endpoint=self.endpoint,
+ application_name="ansible-inventory",
+ application_version=__version__)
+
+ def _test_hcloud_token(self):
+ try:
+ # We test the API Token against the location API, because this is the API with the smallest result
+ # and not controllable from the customer.
+ self.client.locations.get_all()
+ except APIException:
+ raise AnsibleError("Invalid Hetzner Cloud API Token.")
+
+ def _get_servers(self):
+ if len(self.get_option("label_selector")) > 0:
+ self.servers = self.client.servers.get_all(label_selector=self.get_option("label_selector"))
+ else:
+ self.servers = self.client.servers.get_all()
+
+ def _filter_servers(self):
+ if self.get_option("network"):
+ network = self.templar.template(self.get_option("network"), fail_on_undefined=False) or self.get_option("network")
+ try:
+ self.network = self.client.networks.get_by_name(network)
+ if self.network is None:
+ self.network = self.client.networks.get_by_id(network)
+ except APIException:
+ raise AnsibleError(
+ "The given network is not found.")
+
+ tmp = []
+ for server in self.servers:
+ for server_private_network in server.private_net:
+ if server_private_network.network.id == self.network.id:
+ tmp.append(server)
+ self.servers = tmp
+
+ if self.get_option("locations"):
+ tmp = []
+ for server in self.servers:
+ if server.datacenter.location.name in self.get_option("locations"):
+ tmp.append(server)
+ self.servers = tmp
+
+ if self.get_option("types"):
+ tmp = []
+ for server in self.servers:
+ if server.server_type.name in self.get_option("types"):
+ tmp.append(server)
+ self.servers = tmp
+
+ if self.get_option("images"):
+ tmp = []
+ for server in self.servers:
+ if server.image is not None and server.image.os_flavor in self.get_option("images"):
+ tmp.append(server)
+ self.servers = tmp
+
+ if self.get_option("status"):
+ tmp = []
+ for server in self.servers:
+ if server.status in self.get_option("status"):
+ tmp.append(server)
+ self.servers = tmp
+
+ def _set_server_attributes(self, server):
+ self.inventory.set_variable(server.name, "id", to_native(server.id))
+ self.inventory.set_variable(server.name, "name", to_native(server.name))
+ self.inventory.set_variable(server.name, "status", to_native(server.status))
+ self.inventory.set_variable(server.name, "type", to_native(server.server_type.name))
+
+ # Network
+ if server.public_net.ipv4:
+ self.inventory.set_variable(server.name, "ipv4", to_native(server.public_net.ipv4.ip))
+
+ if server.public_net.ipv6:
+ self.inventory.set_variable(server.name, "ipv6_network", to_native(server.public_net.ipv6.network))
+ self.inventory.set_variable(server.name, "ipv6_network_mask", to_native(server.public_net.ipv6.network_mask))
+ self.inventory.set_variable(server.name, "ipv6", to_native(self._first_ipv6_address(server.public_net.ipv6.ip)))
+
+ self.inventory.set_variable(
+ server.name,
+ "private_networks",
+ [
+ {"name": n.network.name, "id": n.network.id, "ip": n.ip}
+ for n in server.private_net
+ ],
+ )
+
+ if self.get_option("network"):
+ for server_private_network in server.private_net:
+ # Set private_ipv4 if user filtered for one network
+ if server_private_network.network.id == self.network.id:
+ self.inventory.set_variable(server.name, "private_ipv4", to_native(server_private_network.ip))
+
+ try:
+ self.inventory.set_variable(server.name, "ansible_host", self._get_server_ansible_host(server))
+ except AnsibleError as e:
+ # Log warning that for this host can not be connected to, using the
+ # method specified in `connect_with`. Users might use `compose` to
+ # override the connection method, or implement custom logic, so we
+ # do not need to abort if nothing matched.
+ self.display.v("[hcloud] %s" % e, server.name)
+
+ # Server Type
+ if server.server_type is not None:
+ self.inventory.set_variable(server.name, "server_type", to_native(server.server_type.name))
+
+ # Datacenter
+ self.inventory.set_variable(server.name, "datacenter", to_native(server.datacenter.name))
+ self.inventory.set_variable(server.name, "location", to_native(server.datacenter.location.name))
+
+ # Image
+ if server.image is not None:
+ self.inventory.set_variable(server.name, "image_id", to_native(server.image.id))
+ self.inventory.set_variable(server.name, "image_os_flavor", to_native(server.image.os_flavor))
+ if server.image.name is not None:
+ self.inventory.set_variable(server.name, "image_name", to_native(server.image.name))
+ else:
+ self.inventory.set_variable(server.name, "image_name", to_native(server.image.description))
+ else:
+ self.inventory.set_variable(server.name, "image_id", to_native("No Image ID found"))
+ self.inventory.set_variable(server.name, "image_name", to_native("No Image Name found"))
+ self.inventory.set_variable(server.name, "image_os_flavor", to_native("No Image OS Flavor found"))
+
+ # Labels
+ self.inventory.set_variable(server.name, "labels", dict(server.labels))
+
+ def _get_server_ansible_host(self, server):
+ if self.get_option("connect_with") == "public_ipv4":
+ if server.public_net.ipv4:
+ return to_native(server.public_net.ipv4.ip)
+ else:
+ raise AnsibleError("Server has no public ipv4, but connect_with=public_ipv4 was specified")
+
+ if self.get_option("connect_with") == "public_ipv6":
+ if server.public_net.ipv6:
+ return to_native(self._first_ipv6_address(server.public_net.ipv6.ip))
+ else:
+ raise AnsibleError("Server has no public ipv6, but connect_with=public_ipv6 was specified")
+
+ elif self.get_option("connect_with") == "hostname":
+ # every server has a name, no need to guard this
+ return to_native(server.name)
+
+ elif self.get_option("connect_with") == "ipv4_dns_ptr":
+ if server.public_net.ipv4:
+ return to_native(server.public_net.ipv4.dns_ptr)
+ else:
+ raise AnsibleError("Server has no public ipv4, but connect_with=ipv4_dns_ptr was specified")
+
+ elif self.get_option("connect_with") == "private_ipv4":
+ if self.get_option("network"):
+ for server_private_network in server.private_net:
+ if server_private_network.network.id == self.network.id:
+ return to_native(server_private_network.ip)
+
+ else:
+ raise AnsibleError(
+ "You can only connect via private IPv4 if you specify a network")
+
+ def _first_ipv6_address(self, network):
+ return next(IPv6Network(network).hosts())
+
+ 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(("hcloud.yaml", "hcloud.yml"))
+ )
+
+ def parse(self, inventory, loader, path, cache=True):
+ super(InventoryModule, self).parse(inventory, loader, path, cache)
+
+ if not HAS_HCLOUD:
+ raise AnsibleError("The Hetzner Cloud dynamic inventory plugin requires hcloud-python.")
+
+ self._read_config_data(path)
+ self._configure_hcloud_client()
+ self._test_hcloud_token()
+ self._get_servers()
+ self._filter_servers()
+
+ # Add a top group
+ self.inventory.add_group(group=self.get_option("group"))
+
+ for server in self.servers:
+ self.inventory.add_host(server.name, group=self.get_option("group"))
+ self._set_server_attributes(server)
+
+ # Use constructed if applicable
+ strict = self.get_option('strict')
+
+ # Composed variables
+ self._set_composite_vars(self.get_option('compose'), self.inventory.get_host(server.name).get_vars(), server.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'), {}, server.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'), {}, server.name, strict=strict)