diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:22 +0000 |
commit | 38b7c80217c4e72b1d8988eb1e60bb6e77334114 (patch) | |
tree | 356e9fd3762877d07cde52d21e77070aeff7e789 /ansible_collections/hetzner/hcloud/plugins | |
parent | Adding upstream version 7.7.0+dfsg. (diff) | |
download | ansible-38b7c80217c4e72b1d8988eb1e60bb6e77334114.tar.xz ansible-38b7c80217c4e72b1d8988eb1e60bb6e77334114.zip |
Adding upstream version 9.4.0+dfsg.upstream/9.4.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
143 files changed, 19496 insertions, 10098 deletions
diff --git a/ansible_collections/hetzner/hcloud/plugins/__init__.py b/ansible_collections/hetzner/hcloud/plugins/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/__init__.py diff --git a/ansible_collections/hetzner/hcloud/plugins/doc_fragments/hcloud.py b/ansible_collections/hetzner/hcloud/plugins/doc_fragments/hcloud.py index d19e64616..eee4a9c83 100644 --- a/ansible_collections/hetzner/hcloud/plugins/doc_fragments/hcloud.py +++ b/ansible_collections/hetzner/hcloud/plugins/doc_fragments/hcloud.py @@ -1,29 +1,33 @@ -# -*- coding: utf-8 -*- # 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 +from __future__ import annotations -class ModuleDocFragment(object): - DOCUMENTATION = ''' + +class ModuleDocFragment: + DOCUMENTATION = """ options: - api_token: - description: - - This is the API Token for the Hetzner Cloud. - - You can also set this option by using the environment variable HCLOUD_TOKEN - required: True - type: str - endpoint: - description: - - This is the API Endpoint for the Hetzner Cloud. - default: https://api.hetzner.cloud/v1 - type: str + api_token: + description: + - The API Token for the Hetzner Cloud. + - You can also set this option by using the C(HCLOUD_TOKEN) environment variable. + required: True + type: str + api_endpoint: + description: + - The API Endpoint for the Hetzner Cloud. + - You can also set this option by using the C(HCLOUD_ENDPOINT) environment variable. + default: https://api.hetzner.cloud/v1 + type: str + aliases: [endpoint] + requirements: - - hcloud-python >= 1.0.0 + - python-dateutil >= 2.7.5 + - requests >=2.20 + seealso: -- name: Documentation for Hetzner Cloud API - description: Complete reference for the Hetzner Cloud API. - link: https://docs.hetzner.cloud/ -''' + - name: Documentation for Hetzner Cloud API + description: Complete reference for the Hetzner Cloud API. + link: https://docs.hetzner.cloud +""" diff --git a/ansible_collections/hetzner/hcloud/plugins/inventory/hcloud.py b/ansible_collections/hetzner/hcloud/plugins/inventory/hcloud.py index 390dedc4c..449342271 100644 --- a/ansible_collections/hetzner/hcloud/plugins/inventory/hcloud.py +++ b/ansible_collections/hetzner/hcloud/plugins/inventory/hcloud.py @@ -1,102 +1,139 @@ # 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 +from __future__ import annotations + +DOCUMENTATION = """ +name: hcloud +short_description: Ansible dynamic inventory plugin for the Hetzner Cloud. + +description: + - Reads inventories from the Hetzner Cloud API. + - Uses a YAML configuration file that ends with C(hcloud.yml) or C(hcloud.yaml). + +author: + - Lukas Kaemmerling (@lkaemmerling) + +requirements: + - python-dateutil >= 2.7.5 + - requests >=2.20 + +extends_documentation_fragment: + - constructed + - inventory_cache + +options: + plugin: + description: Mark this as an P(hetzner.hcloud.hcloud#inventory) inventory instance. + required: true + choices: [hcloud, hetzner.hcloud.hcloud] + + api_token: + description: + - The API Token for the Hetzner Cloud. + type: str + required: false # TODO: Mark as required once 'api_token_env' is removed. + aliases: [token] + env: + - name: HCLOUD_TOKEN + api_token_env: + description: + - Environment variable name to load the Hetzner Cloud API Token from. + type: str + default: HCLOUD_TOKEN + aliases: [token_env] + deprecated: + why: The option is adding too much complexity, while the alternatives are preferred. + collection_name: hetzner.hcloud + version: 3.0.0 + alternatives: Use the P(ansible.builtin.env#lookup) lookup plugin instead. + api_endpoint: 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 + - The API Endpoint for the Hetzner Cloud. + type: str + default: https://api.hetzner.cloud/v1 + env: + - name: HCLOUD_ENDPOINT + + group: + description: The group all servers are automatically added to. + default: hcloud + type: str + required: false + connect_with: + description: | + Connect to the server using the value from this field. This sets the C(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 O(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 + + hostvars_prefix: + description: + - The prefix for host variables names coming from Hetzner Cloud. + type: str + version_added: 2.5.0 + hostvars_suffix: + description: + - The suffix for host variables names coming from Hetzner Cloud. + type: str + version_added: 2.5.0 +""" +EXAMPLES = """ +# Minimal example. 'HCLOUD_TOKEN' is exposed in environment. +plugin: hetzner.hcloud.hcloud + +--- # Example with templated token, e.g. provided through extra vars. -plugin: hcloud -token: "{{ hetzner_apitoken }}" +plugin: hetzner.hcloud.hcloud +api_token: "{{ _vault_hetzner_cloud_token }}" -# Example with locations, types, status and token -plugin: hcloud -token: foobar +--- +# Example with locations, types, status +plugin: hetzner.hcloud.hcloud locations: - nbg1 types: @@ -104,10 +141,11 @@ types: 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 +plugin: hetzner.hcloud.hcloud keyed_groups: - key: location prefix: hcloud_location @@ -118,234 +156,355 @@ keyed_groups: """ 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__ +import sys from ipaddress import IPv6Network -try: - from hcloud import hcloud - from hcloud import APIException - HAS_HCLOUD = True -except ImportError: - HAS_HCLOUD = False +from ansible.errors import AnsibleError +from ansible.inventory.manager import InventoryData +from ansible.module_utils.common.text.converters import to_native +from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable, Constructable +from ansible.utils.display import Display + +from ..module_utils.client import ( + Client, + ClientException, + client_check_required_lib, + client_get_by_name_or_id, +) +from ..module_utils.vendor.hcloud import APIException +from ..module_utils.vendor.hcloud.networks import Network +from ..module_utils.vendor.hcloud.servers import Server +from ..module_utils.version import version + +if sys.version_info >= (3, 11): + # The typed dicts are only used to help development and we prefer not requiring + # the additional typing-extensions dependency + from typing import NotRequired, TypedDict + + class InventoryPrivateNetwork(TypedDict): + id: int + name: str + ip: str + + class InventoryServer(TypedDict): + id: int + name: str + status: str + + # Server Type + type: str + server_type: str + architecture: str + # Datacenter + datacenter: str + location: str -class InventoryModule(BaseInventoryPlugin, Constructable): - NAME = 'hetzner.hcloud.hcloud' + # Labels + labels: dict[str, str] + + # Network + ipv4: NotRequired[str] + ipv6: NotRequired[str] + ipv6_network: NotRequired[str] + ipv6_network_mask: NotRequired[str] + private_ipv4: NotRequired[str] + private_networks: list[InventoryPrivateNetwork] + + # Image + image_id: int + image_name: str + image_os_flavor: str + + # Ansible + ansible_host: str + +else: + InventoryServer = dict + + +def first_ipv6_address(network: str) -> str: + """ + Return the first address for a ipv6 network. + + :param network: IPv6 Network. + """ + return next(IPv6Network(network).hosts()) + + +class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): + NAME = "hetzner.hcloud.hcloud" + + inventory: InventoryData + display: Display + + client: Client + + network: Network | None 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: + # If api_token_env is not the default, print a deprecation warning and load the + # environment variable. + api_token_env = self.get_option("api_token_env") + if api_token_env != "HCLOUD_TOKEN": + self.display.deprecated( + "The 'api_token_env' option is deprecated, please use the 'HCLOUD_TOKEN' " + "environment variable or use the 'ansible.builtin.env' lookup instead.", + version="3.0.0", + collection_name="hetzner.hcloud", + ) + if api_token_env in os.environ: + self.set_option("api_token", os.environ.get(api_token_env)) + + api_token = self.get_option("api_token") + api_endpoint = self.get_option("api_endpoint") + + if api_token is None: # TODO: Remove once I(api_token_env) is removed. 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." + "No setting was provided for required configuration setting: " + "plugin_type: inventory " + "plugin: hetzner.hcloud.hcloud " + "setting: api_token" ) - self.endpoint = os.getenv("HCLOUD_ENDPOINT") or "https://api.hetzner.cloud/v1" + # Resolve template string + api_token = self.templar.template(api_token) - self.client = hcloud.Client(token=self.api_token, - api_endpoint=self.endpoint, - application_name="ansible-inventory", - application_version=__version__) + self.client = Client( + token=api_token, + api_endpoint=api_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): + # Ensure the api token is valid + self.client.locations.get_list() + except APIException as exception: + raise AnsibleError("Invalid Hetzner Cloud API Token.") from exception + + def _validate_options(self) -> None: if self.get_option("network"): - network = self.templar.template(self.get_option("network"), fail_on_undefined=False) or self.get_option("network") + network_param: str = self.get_option("network") + network_param = self.templar.template(network_param) + 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 + self.network = client_get_by_name_or_id(self.client, "networks", network_param) + except (ClientException, APIException) as exception: + raise AnsibleError(to_native(exception)) from exception + + def _fetch_servers(self) -> list[Server]: + self._validate_options() + + get_servers_params = {} + if self.get_option("label_selector"): + get_servers_params["label_selector"] = self.get_option("label_selector") + + if self.get_option("status"): + get_servers_params["status"] = self.get_option("status") + + servers = self.client.servers.get_all(**get_servers_params) + + if self.get_option("network"): + servers = [s for s in servers if self.network.id in [p.network.id for p in s.private_net]] 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 + locations: list[str] = self.get_option("locations") + servers = [s for s in servers if s.datacenter.location.name in locations] 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 + server_types: list[str] = self.get_option("types") + servers = [s for s in servers if s.server_type.name in server_types] 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 + images: list[str] = self.get_option("images") + servers = [s for s in servers if s.image is not None and s.image.os_flavor in images] - 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)) - self.inventory.set_variable(server.name, "architecture", to_native(server.server_type.architecture)) + return servers + + def _build_inventory_server(self, server: Server) -> InventoryServer: + server_dict: InventoryServer = {} + server_dict["id"] = server.id + server_dict["name"] = to_native(server.name) + server_dict["status"] = to_native(server.status) + + # Server Type + server_dict["type"] = to_native(server.server_type.name) + server_dict["server_type"] = to_native(server.server_type.name) + server_dict["architecture"] = to_native(server.server_type.architecture) # Network if server.public_net.ipv4: - self.inventory.set_variable(server.name, "ipv4", to_native(server.public_net.ipv4.ip)) + server_dict["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 - ], - ) + server_dict["ipv6"] = to_native(first_ipv6_address(server.public_net.ipv6.ip)) + server_dict["ipv6_network"] = to_native(server.public_net.ipv6.network) + server_dict["ipv6_network_mask"] = to_native(server.public_net.ipv6.network_mask) + + server_dict["private_networks"] = [ + {"id": v.network.id, "name": to_native(v.network.name), "ip": to_native(v.ip)} for v in server.private_net + ] if self.get_option("network"): - for server_private_network in server.private_net: + for private_net 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)) + if private_net.network.id == self.network.id: + server_dict["private_ipv4"] = to_native(private_net.ip) + break # 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)) + server_dict["datacenter"] = to_native(server.datacenter.name) + server_dict["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")) + server_dict["image_id"] = server.image.id + server_dict["image_os_flavor"] = to_native(server.image.os_flavor) + server_dict["image_name"] = to_native(server.image.name or server.image.description) # Labels - self.inventory.set_variable(server.name, "labels", dict(server.labels)) + server_dict["labels"] = dict(server.labels) - def _get_server_ansible_host(self, server): + try: + server_dict["ansible_host"] = self._get_server_ansible_host(server) + except AnsibleError as exception: + # 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(f"[hcloud] {exception}", server.name) + + return server_dict + + def _get_server_ansible_host(self, server: 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") + 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") + return to_native(first_ipv6_address(server.public_net.ipv6.ip)) + raise AnsibleError("Server has no public ipv6, but connect_with=public_ipv6 was specified") - elif self.get_option("connect_with") == "hostname": + if 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 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") + 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("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) + for private_net in server.private_net: + if private_net.network.id == self.network.id: + return to_native(private_net.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()) + raise AnsibleError("You can only connect via private IPv4 if you specify a network") 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")) - ) + return super().verify_file(path) and path.endswith(("hcloud.yaml", "hcloud.yml")) + + def _get_cached_result(self, path, cache) -> tuple[list[InventoryServer], bool]: + # false when refresh_cache or --flush-cache is used + if not cache: + return [], False + + # get the user-specified directive + if not self.get_option("cache"): + return [], False + + cache_key = self.get_cache_key(path) + try: + cached_result = self._cache[cache_key] + except KeyError: + # if cache expires or cache file doesn"t exist + return [], False + + return cached_result, True + + def _update_cached_result(self, path, cache, result: list[InventoryServer]): + if not self.get_option("cache"): + return + + cache_key = self.get_cache_key(path) + # We weren't explicitly told to flush the cache, and there's already a cache entry, + # this means that the result we're being passed came from the cache. As such we don't + # want to "update" the cache as that could reset a TTL on the cache entry. + if cache and cache_key in self._cache: + return + + self._cache[cache_key] = result def parse(self, inventory, loader, path, cache=True): - super(InventoryModule, self).parse(inventory, loader, path, cache) + super().parse(inventory, loader, path, cache) + + try: + client_check_required_lib() + except ClientException as exception: + raise AnsibleError(to_native(exception)) from exception - if not HAS_HCLOUD: - raise AnsibleError("The Hetzner Cloud dynamic inventory plugin requires hcloud-python.") + # Allow using extra variables arguments as template variables (e.g. + # '--extra-vars my_var=my_value') + self.templar.available_variables = self._vars self._read_config_data(path) self._configure_hcloud_client() - self._test_hcloud_token() - self._get_servers() - self._filter_servers() + + servers, cached = self._get_cached_result(path, cache) + if not cached: + with self.client.cached_session(): + servers = [self._build_inventory_server(s) for s in self._fetch_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) + hostvars_prefix = self.get_option("hostvars_prefix") + hostvars_suffix = self.get_option("hostvars_suffix") + + for server in servers: + self.inventory.add_host(server["name"], group=self.get_option("group")) + for key, value in server.items(): + # Add hostvars prefix and suffix for variables coming from the Hetzner Cloud. + if hostvars_prefix or hostvars_suffix: + if key not in ("ansible_host",): + if hostvars_prefix: + key = hostvars_prefix + key + if hostvars_suffix: + key = key + hostvars_suffix + + self.inventory.set_variable(server["name"], key, value) # Use constructed if applicable - strict = self.get_option('strict') + 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) + 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) + 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) + self._add_host_to_keyed_groups( + self.get_option("keyed_groups"), + {}, + server["name"], + strict=strict, + ) + + self._update_cached_result(path, cache, servers) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/client.py new file mode 100644 index 000000000..d82a63007 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/client.py @@ -0,0 +1,108 @@ +# Copyright: (c) 2023, Hetzner Cloud GmbH <info@hetzner-cloud.de> + +from __future__ import annotations + +from contextlib import contextmanager + +from ansible.module_utils.basic import missing_required_lib + +from .vendor.hcloud import APIException, Client as ClientBase + +HAS_REQUESTS = True +HAS_DATEUTIL = True + +try: + import requests # pylint: disable=unused-import +except ImportError: + HAS_REQUESTS = False + +try: + import dateutil # pylint: disable=unused-import +except ImportError: + HAS_DATEUTIL = False + + +class ClientException(Exception): + """An error related to the client occurred.""" + + +def client_check_required_lib(): + if not HAS_REQUESTS: + raise ClientException(missing_required_lib("requests")) + if not HAS_DATEUTIL: + raise ClientException(missing_required_lib("python-dateutil")) + + +def _client_resource_not_found(resource: str, param: str | int): + return ClientException(f"resource ({resource.rstrip('s')}) does not exist: {param}") + + +def client_get_by_name_or_id(client: Client, resource: str, param: str | int): + """ + Get a resource by name, and if not found by its ID. + + :param client: Client to use to make the call + :param resource: Name of the resource client that implements both `get_by_name` and `get_by_id` methods + :param param: Name or ID of the resource to query + """ + resource_client = getattr(client, resource) + + result = resource_client.get_by_name(param) + if result is not None: + return result + + # If the param is not a valid ID, prevent an unnecessary call to the API. + try: + int(param) + except ValueError as exception: + raise _client_resource_not_found(resource, param) from exception + + try: + return resource_client.get_by_id(param) + except APIException as exception: + if exception.code == "not_found": + raise _client_resource_not_found(resource, param) from exception + raise exception + + +if HAS_REQUESTS: + + class CachedSession(requests.Session): + cache: dict[str, requests.Response] + + def __init__(self) -> None: + super().__init__() + self.cache = {} + + def send(self, request: requests.PreparedRequest, **kwargs) -> requests.Response: # type: ignore[no-untyped-def] + """ + Send a given PreparedRequest. + """ + if request.method != "GET" or request.url is None: + return super().send(request, **kwargs) + + if request.url in self.cache: + return self.cache[request.url] + + response = super().send(request, **kwargs) + if response.ok: + self.cache[request.url] = response + + return response + + +class Client(ClientBase): + @contextmanager + def cached_session(self): + """ + Swap the client session during the scope of the context. The session will cache + all GET requests. + + Cached response will not expire, therefore the cached client must not be used + for long living scopes. + """ + self._requests_session = CachedSession() + try: + yield + finally: + self._requests_session = requests.Session() diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/hcloud.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/hcloud.py index 932b0c529..eab0aef59 100644 --- a/ansible_collections/hetzner/hcloud/plugins/module_utils/hcloud.py +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/hcloud.py @@ -1,45 +1,101 @@ -# -*- coding: utf-8 -*- # Copyright: (c) 2019, Hetzner Cloud GmbH <info@hetzner-cloud.de> # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) -from __future__ import absolute_import, division, print_function -__metaclass__ = type +from __future__ import annotations -from ansible.module_utils.ansible_release import __version__ -from ansible.module_utils.basic import env_fallback, missing_required_lib +import traceback +from typing import Any, NoReturn -try: - import hcloud +from ansible.module_utils.basic import AnsibleModule as AnsibleModuleBase, env_fallback +from ansible.module_utils.common.text.converters import to_native - HAS_HCLOUD = True -except ImportError: - HAS_HCLOUD = False +from .client import ClientException, client_check_required_lib, client_get_by_name_or_id +from .vendor.hcloud import APIException, Client, HCloudException +from .vendor.hcloud.actions import ActionException +from .version import version -class Hcloud(object): - def __init__(self, module, represent): +# Provide typing definitions to the AnsibleModule class +class AnsibleModule(AnsibleModuleBase): + params: dict + + +class AnsibleHCloud: + represent: str + + module: AnsibleModule + + def __init__(self, module: AnsibleModule): + if not self.represent: + raise NotImplementedError(f"represent property is not defined for {self.__class__.__name__}") + self.module = module - self.represent = represent self.result = {"changed": False, self.represent: None} - if not HAS_HCLOUD: - module.fail_json(msg=missing_required_lib("hcloud-python")) + + try: + client_check_required_lib() + except ClientException as exception: + module.fail_json(msg=to_native(exception)) + self._build_client() - def _build_client(self): - self.client = hcloud.Client( + def fail_json_hcloud( + self, + exception: HCloudException, + msg: str | None = None, + params: Any = None, + **kwargs, + ) -> NoReturn: + last_traceback = traceback.format_exc() + + failure = {} + + if params is not None: + failure["params"] = params + + if isinstance(exception, APIException): + failure["message"] = exception.message + failure["code"] = exception.code + failure["details"] = exception.details + + elif isinstance(exception, ActionException): + failure["action"] = {k: getattr(exception.action, k) for k in exception.action.__slots__} + + exception_message = to_native(exception) + if msg is not None: + msg = f"{exception_message}: {msg}" + else: + msg = exception_message + + self.module.fail_json(msg=msg, exception=last_traceback, failure=failure, **kwargs) + + def _build_client(self) -> None: + self.client = Client( token=self.module.params["api_token"], - api_endpoint=self.module.params["endpoint"], + api_endpoint=self.module.params["api_endpoint"], application_name="ansible-module", - application_version=__version__, + application_version=version, ) - def _mark_as_changed(self): + def _client_get_by_name_or_id(self, resource: str, param: str | int): + """ + Get a resource by name, and if not found by its ID. + + :param resource: Name of the resource client that implements both `get_by_name` and `get_by_id` methods + :param param: Name or ID of the resource to query + """ + try: + return client_get_by_name_or_id(self.client, resource, param) + except ClientException as exception: + self.module.fail_json(msg=to_native(exception)) + + def _mark_as_changed(self) -> None: self.result["changed"] = True - @staticmethod - def base_module_arguments(): + @classmethod + def base_module_arguments(cls): return { "api_token": { "type": "str", @@ -47,17 +103,19 @@ class Hcloud(object): "fallback": (env_fallback, ["HCLOUD_TOKEN"]), "no_log": True, }, - "endpoint": {"type": "str", "default": "https://api.hetzner.cloud/v1"}, + "api_endpoint": { + "type": "str", + "fallback": (env_fallback, ["HCLOUD_ENDPOINT"]), + "default": "https://api.hetzner.cloud/v1", + "aliases": ["endpoint"], + }, } - def _prepare_result(self): - """Prepare the result for every module - - :return: dict - """ + def _prepare_result(self) -> dict[str, Any]: + """Prepare the result for every module""" return {} - def get_result(self): + def get_result(self) -> dict[str, Any]: if getattr(self, self.represent) is not None: self.result[self.represent] = self._prepare_result() return self.result diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/__init__.py diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/__init__.py new file mode 100644 index 000000000..6b69b1699 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/__init__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from ._client import Client as Client # noqa pylint: disable=C0414 +from ._exceptions import ( # noqa pylint: disable=C0414 + APIException as APIException, + HCloudException as HCloudException, +) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/_client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/_client.py new file mode 100644 index 000000000..257d361c7 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/_client.py @@ -0,0 +1,236 @@ +from __future__ import annotations + +import time +from typing import NoReturn + +try: + import requests +except ImportError: + requests = None + +from ._version import VERSION +from ._exceptions import APIException +from .actions import ActionsClient +from .certificates import CertificatesClient +from .datacenters import DatacentersClient +from .firewalls import FirewallsClient +from .floating_ips import FloatingIPsClient +from .images import ImagesClient +from .isos import IsosClient +from .load_balancer_types import LoadBalancerTypesClient +from .load_balancers import LoadBalancersClient +from .locations import LocationsClient +from .networks import NetworksClient +from .placement_groups import PlacementGroupsClient +from .primary_ips import PrimaryIPsClient +from .server_types import ServerTypesClient +from .servers import ServersClient +from .ssh_keys import SSHKeysClient +from .volumes import VolumesClient + + +class Client: + """Base Client for accessing the Hetzner Cloud API""" + + _version = VERSION + _retry_wait_time = 0.5 + __user_agent_prefix = "hcloud-python" + + def __init__( + self, + token: str, + api_endpoint: str = "https://api.hetzner.cloud/v1", + application_name: str | None = None, + application_version: str | None = None, + poll_interval: int = 1, + timeout: float | tuple[float, float] | None = None, + ): + """Create a new Client instance + + :param token: Hetzner Cloud API token + :param api_endpoint: Hetzner Cloud API endpoint + :param application_name: Your application name + :param application_version: Your application _version + :param poll_interval: Interval for polling information from Hetzner Cloud API in seconds + :param timeout: Requests timeout in seconds + """ + self.token = token + self._api_endpoint = api_endpoint + self._application_name = application_name + self._application_version = application_version + self._requests_session = requests.Session() + self._requests_timeout = timeout + self.poll_interval = poll_interval + + self.datacenters = DatacentersClient(self) + """DatacentersClient Instance + + :type: :class:`DatacentersClient <hcloud.datacenters.client.DatacentersClient>` + """ + self.locations = LocationsClient(self) + """LocationsClient Instance + + :type: :class:`LocationsClient <hcloud.locations.client.LocationsClient>` + """ + self.servers = ServersClient(self) + """ServersClient Instance + + :type: :class:`ServersClient <hcloud.servers.client.ServersClient>` + """ + self.server_types = ServerTypesClient(self) + """ServerTypesClient Instance + + :type: :class:`ServerTypesClient <hcloud.server_types.client.ServerTypesClient>` + """ + self.volumes = VolumesClient(self) + """VolumesClient Instance + + :type: :class:`VolumesClient <hcloud.volumes.client.VolumesClient>` + """ + self.actions = ActionsClient(self) + """ActionsClient Instance + + :type: :class:`ActionsClient <hcloud.actions.client.ActionsClient>` + """ + self.images = ImagesClient(self) + """ImagesClient Instance + + :type: :class:`ImagesClient <hcloud.images.client.ImagesClient>` + """ + self.isos = IsosClient(self) + """ImagesClient Instance + + :type: :class:`IsosClient <hcloud.isos.client.IsosClient>` + """ + self.ssh_keys = SSHKeysClient(self) + """SSHKeysClient Instance + + :type: :class:`SSHKeysClient <hcloud.ssh_keys.client.SSHKeysClient>` + """ + self.floating_ips = FloatingIPsClient(self) + """FloatingIPsClient Instance + + :type: :class:`FloatingIPsClient <hcloud.floating_ips.client.FloatingIPsClient>` + """ + self.primary_ips = PrimaryIPsClient(self) + """PrimaryIPsClient Instance + + :type: :class:`PrimaryIPsClient <hcloud.primary_ips.client.PrimaryIPsClient>` + """ + self.networks = NetworksClient(self) + """NetworksClient Instance + + :type: :class:`NetworksClient <hcloud.networks.client.NetworksClient>` + """ + self.certificates = CertificatesClient(self) + """CertificatesClient Instance + + :type: :class:`CertificatesClient <hcloud.certificates.client.CertificatesClient>` + """ + + self.load_balancers = LoadBalancersClient(self) + """LoadBalancersClient Instance + + :type: :class:`LoadBalancersClient <hcloud.load_balancers.client.LoadBalancersClient>` + """ + + self.load_balancer_types = LoadBalancerTypesClient(self) + """LoadBalancerTypesClient Instance + + :type: :class:`LoadBalancerTypesClient <hcloud.load_balancer_types.client.LoadBalancerTypesClient>` + """ + + self.firewalls = FirewallsClient(self) + """FirewallsClient Instance + + :type: :class:`FirewallsClient <hcloud.firewalls.client.FirewallsClient>` + """ + + self.placement_groups = PlacementGroupsClient(self) + """PlacementGroupsClient Instance + + :type: :class:`PlacementGroupsClient <hcloud.placement_groups.client.PlacementGroupsClient>` + """ + + def _get_user_agent(self) -> str: + """Get the user agent of the hcloud-python instance with the user application name (if specified) + + :return: The user agent of this hcloud-python instance + """ + user_agents = [] + for name, version in [ + (self._application_name, self._application_version), + (self.__user_agent_prefix, self._version), + ]: + if name is not None: + user_agents.append(name if version is None else f"{name}/{version}") + + return " ".join(user_agents) + + def _get_headers(self) -> dict: + headers = { + "User-Agent": self._get_user_agent(), + "Authorization": f"Bearer {self.token}", + } + return headers + + def _raise_exception_from_response(self, response) -> NoReturn: + raise APIException( + code=response.status_code, + message=response.reason, + details={"content": response.content}, + ) + + def _raise_exception_from_content(self, content: dict) -> NoReturn: + raise APIException( + code=content["error"]["code"], + message=content["error"]["message"], + details=content["error"]["details"], + ) + + def request( # type: ignore[no-untyped-def] + self, + method: str, + url: str, + tries: int = 1, + **kwargs, + ) -> dict: + """Perform a request to the Hetzner Cloud API, wrapper around requests.request + + :param method: HTTP Method to perform the Request + :param url: URL of the Endpoint + :param tries: Tries of the request (used internally, should not be set by the user) + :param timeout: Requests timeout in seconds + :return: Response + """ + timeout = kwargs.pop("timeout", self._requests_timeout) + + response = self._requests_session.request( + method=method, + url=self._api_endpoint + url, + headers=self._get_headers(), + timeout=timeout, + **kwargs, + ) + + content = response.content + try: + if len(content) > 0: + content = response.json() + except (TypeError, ValueError): + self._raise_exception_from_response(response) + + if not response.ok: + if content: + assert isinstance(content, dict) + if content["error"]["code"] == "rate_limit_exceeded" and tries < 5: + time.sleep(tries * self._retry_wait_time) + tries = tries + 1 + return self.request(method, url, tries, **kwargs) + + self._raise_exception_from_content(content) + else: + self._raise_exception_from_response(response) + + # TODO: return an empty dict instead of an empty string when content == "". + return content # type: ignore[return-value] diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/_exceptions.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/_exceptions.py new file mode 100644 index 000000000..877083f87 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/_exceptions.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import Any + + +class HCloudException(Exception): + """There was an error while using the hcloud library""" + + +class APIException(HCloudException): + """There was an error while performing an API Request""" + + def __init__(self, code: int | str, message: str | None, details: Any): + super().__init__(code if message is None and isinstance(code, str) else message) + self.code = code + self.message = message + self.details = details diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/_version.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/_version.py new file mode 100644 index 000000000..e03c1b434 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/_version.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +VERSION = "1.33.2" # x-release-please-version diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/actions/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/actions/__init__.py new file mode 100644 index 000000000..ca93c89f5 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/actions/__init__.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from .client import ( # noqa: F401 + ActionsClient, + ActionsPageResult, + BoundAction, + ResourceActionsClient, +) +from .domain import ( # noqa: F401 + Action, + ActionException, + ActionFailedException, + ActionTimeoutException, +) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/actions/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/actions/client.py new file mode 100644 index 000000000..a188f6247 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/actions/client.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +import time +import warnings +from typing import TYPE_CHECKING, Any, NamedTuple + +from ..core import BoundModelBase, ClientEntityBase, Meta +from .domain import Action, ActionFailedException, ActionTimeoutException + +if TYPE_CHECKING: + from .._client import Client + + +class BoundAction(BoundModelBase, Action): + _client: ActionsClient + + model = Action + + def wait_until_finished(self, max_retries: int = 100) -> None: + """Wait until the specific action has status="finished" (set Client.poll_interval to specify a delay between checks) + + :param max_retries: int + Specify how many retries will be performed before an ActionTimeoutException will be raised + :raises: ActionFailedException when action is finished with status=="error" + :raises: ActionTimeoutException when Action is still in "running" state after max_retries reloads. + """ + while self.status == Action.STATUS_RUNNING: + if max_retries > 0: + self.reload() + # pylint: disable=protected-access + time.sleep(self._client._client.poll_interval) + max_retries = max_retries - 1 + else: + raise ActionTimeoutException(action=self) + + if self.status == Action.STATUS_ERROR: + raise ActionFailedException(action=self) + + +class ActionsPageResult(NamedTuple): + actions: list[BoundAction] + meta: Meta | None + + +class ResourceActionsClient(ClientEntityBase): + _resource: str + + def __init__(self, client: Client, resource: str | None): + super().__init__(client) + self._resource = resource or "" + + def get_by_id(self, id: int) -> BoundAction: + """Get a specific action by its ID. + + :param id: int + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"{self._resource}/actions/{id}", + method="GET", + ) + return BoundAction(self._client.actions, response["action"]) + + def get_list( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Get a list of actions. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default) + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url=f"{self._resource}/actions", + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return ActionsPageResult(actions, Meta.parse_meta(response)) + + def get_all( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Get all actions. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default) + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._iter_pages(self.get_list, status=status, sort=sort) + + +class ActionsClient(ResourceActionsClient): + def __init__(self, client: Client): + super().__init__(client, None) + + def get_list( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """ + .. deprecated:: 1.28 + Use :func:`client.<resource>.actions.get_list` instead, + e.g. using :attr:`hcloud.certificates.client.CertificatesClient.actions`. + + `Starting 1 October 2023, it will no longer be available. <https://docs.hetzner.cloud/changelog#2023-07-20-actions-list-endpoint-is-deprecated>`_ + """ + warnings.warn( + "The 'client.actions.get_list' method is deprecated, please use the " + "'client.<resource>.actions.get_list' method instead (e.g. " + "'client.certificates.actions.get_list').", + DeprecationWarning, + stacklevel=2, + ) + return super().get_list(status=status, sort=sort, page=page, per_page=per_page) + + def get_all( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """ + .. deprecated:: 1.28 + Use :func:`client.<resource>.actions.get_all` instead, + e.g. using :attr:`hcloud.certificates.client.CertificatesClient.actions`. + + `Starting 1 October 2023, it will no longer be available. <https://docs.hetzner.cloud/changelog#2023-07-20-actions-list-endpoint-is-deprecated>`_ + """ + warnings.warn( + "The 'client.actions.get_all' method is deprecated, please use the " + "'client.<resource>.actions.get_all' method instead (e.g. " + "'client.certificates.actions.get_all').", + DeprecationWarning, + stacklevel=2, + ) + return super().get_all(status=status, sort=sort) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/actions/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/actions/domain.py new file mode 100644 index 000000000..16b74ac68 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/actions/domain.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from .._exceptions import HCloudException +from ..core import BaseDomain + +if TYPE_CHECKING: + from .client import BoundAction + + +class Action(BaseDomain): + """Action Domain + + :param id: int ID of an action + :param command: Command executed in the action + :param status: Status of the action + :param progress: Progress of action in percent + :param started: Point in time when the action was started + :param datetime,None finished: Point in time when the action was finished. Only set if the action is finished otherwise None + :param resources: Resources the action relates to + :param error: Error message for the action if error occurred, otherwise None. + """ + + STATUS_RUNNING = "running" + """Action Status running""" + STATUS_SUCCESS = "success" + """Action Status success""" + STATUS_ERROR = "error" + """Action Status error""" + + __slots__ = ( + "id", + "command", + "status", + "progress", + "resources", + "error", + "started", + "finished", + ) + + def __init__( + self, + id: int, + command: str | None = None, + status: str | None = None, + progress: int | None = None, + started: str | None = None, + finished: str | None = None, + resources: list[dict] | None = None, + error: dict | None = None, + ): + self.id = id + self.command = command + + self.status = status + self.progress = progress + self.started = isoparse(started) if started else None + self.finished = isoparse(finished) if finished else None + self.resources = resources + self.error = error + + +class ActionException(HCloudException): + """A generic action exception""" + + def __init__(self, action: Action | BoundAction): + assert self.__doc__ is not None + message = self.__doc__ + if action.error is not None and "message" in action.error: + message += f": {action.error['message']}" + + super().__init__(message) + self.message = message + self.action = action + + +class ActionFailedException(ActionException): + """The pending action failed""" + + +class ActionTimeoutException(ActionException): + """The pending action timed out""" diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/certificates/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/certificates/__init__.py new file mode 100644 index 000000000..4e63df576 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/certificates/__init__.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from .client import ( # noqa: F401 + BoundCertificate, + CertificatesClient, + CertificatesPageResult, +) +from .domain import ( # noqa: F401 + Certificate, + CreateManagedCertificateResponse, + ManagedCertificateError, + ManagedCertificateStatus, +) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/certificates/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/certificates/client.py new file mode 100644 index 000000000..a5fe1d77f --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/certificates/client.py @@ -0,0 +1,371 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple + +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient +from ..core import BoundModelBase, ClientEntityBase, Meta +from .domain import ( + Certificate, + CreateManagedCertificateResponse, + ManagedCertificateError, + ManagedCertificateStatus, +) + +if TYPE_CHECKING: + from .._client import Client + + +class BoundCertificate(BoundModelBase, Certificate): + _client: CertificatesClient + + model = Certificate + + def __init__(self, client: CertificatesClient, data: dict, complete: bool = True): + status = data.get("status") + if status is not None: + error_data = status.get("error") + error = None + if error_data: + error = ManagedCertificateError( + code=error_data["code"], message=error_data["message"] + ) + data["status"] = ManagedCertificateStatus( + issuance=status["issuance"], renewal=status["renewal"], error=error + ) + super().__init__(client, data, complete) + + def get_actions_list( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Returns all action objects for a Certificate. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + return self._client.get_actions_list(self, status, sort, page, per_page) + + def get_actions( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for a Certificate. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._client.get_actions(self, status, sort) + + def update( + self, + name: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundCertificate: + """Updates an certificate. You can update an certificate name and the certificate labels. + + :param name: str (optional) + New name to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` + """ + return self._client.update(self, name, labels) + + def delete(self) -> bool: + """Deletes a certificate. + :return: boolean + """ + return self._client.delete(self) + + def retry_issuance(self) -> BoundAction: + """Retry a failed Certificate issuance or renewal. + :return: BoundAction + """ + return self._client.retry_issuance(self) + + +class CertificatesPageResult(NamedTuple): + certificates: list[BoundCertificate] + meta: Meta | None + + +class CertificatesClient(ClientEntityBase): + _client: Client + + actions: ResourceActionsClient + """Certificates scoped actions client + + :type: :class:`ResourceActionsClient <hcloud.actions.client.ResourceActionsClient>` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/certificates") + + def get_by_id(self, id: int) -> BoundCertificate: + """Get a specific certificate by its ID. + + :param id: int + :return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` + """ + response = self._client.request(url=f"/certificates/{id}", method="GET") + return BoundCertificate(self, response["certificate"]) + + def get_list( + self, + name: str | None = None, + label_selector: str | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> CertificatesPageResult: + """Get a list of certificates + + :param name: str (optional) + Can be used to filter certificates by their name. + :param label_selector: str (optional) + Can be used to filter certificates by labels. The response will only contain certificates matching the label selector. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if name is not None: + params["name"] = name + + if label_selector is not None: + params["label_selector"] = label_selector + + if page is not None: + params["page"] = page + + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url="/certificates", method="GET", params=params + ) + + certificates = [ + BoundCertificate(self, certificate_data) + for certificate_data in response["certificates"] + ] + + return CertificatesPageResult(certificates, Meta.parse_meta(response)) + + def get_all( + self, + name: str | None = None, + label_selector: str | None = None, + ) -> list[BoundCertificate]: + """Get all certificates + + :param name: str (optional) + Can be used to filter certificates by their name. + :param label_selector: str (optional) + Can be used to filter certificates by labels. The response will only contain certificates matching the label selector. + :return: List[:class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`] + """ + return self._iter_pages(self.get_list, name=name, label_selector=label_selector) + + def get_by_name(self, name: str) -> BoundCertificate | None: + """Get certificate by name + + :param name: str + Used to get certificate by name. + :return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` + """ + return self._get_first_by(name=name) + + def create( + self, + name: str, + certificate: str, + private_key: str, + labels: dict[str, str] | None = None, + ) -> BoundCertificate: + """Creates a new Certificate with the given name, certificate and private_key. This methods allows only creating + custom uploaded certificates. If you want to create a managed certificate use :func:`~hcloud.certificates.client.CertificatesClient.create_managed` + + :param name: str + :param certificate: str + Certificate and chain in PEM format, in order so that each record directly certifies the one preceding + :param private_key: str + Certificate key in PEM format + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` + """ + data: dict[str, Any] = { + "name": name, + "certificate": certificate, + "private_key": private_key, + "type": Certificate.TYPE_UPLOADED, + } + if labels is not None: + data["labels"] = labels + response = self._client.request(url="/certificates", method="POST", json=data) + return BoundCertificate(self, response["certificate"]) + + def create_managed( + self, + name: str, + domain_names: list[str], + labels: dict[str, str] | None = None, + ) -> CreateManagedCertificateResponse: + """Creates a new managed Certificate with the given name and domain names. This methods allows only creating + managed certificates for domains that are using the Hetzner DNS service. If you want to create a custom uploaded certificate use :func:`~hcloud.certificates.client.CertificatesClient.create` + + :param name: str + :param domain_names: List[str] + Domains and subdomains that should be contained in the Certificate + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` + """ + data: dict[str, Any] = { + "name": name, + "type": Certificate.TYPE_MANAGED, + "domain_names": domain_names, + } + if labels is not None: + data["labels"] = labels + response = self._client.request(url="/certificates", method="POST", json=data) + return CreateManagedCertificateResponse( + certificate=BoundCertificate(self, response["certificate"]), + action=BoundAction(self._client.actions, response["action"]), + ) + + def update( + self, + certificate: Certificate | BoundCertificate, + name: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundCertificate: + """Updates a Certificate. You can update a certificate name and labels. + + :param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>` + :param name: str (optional) + New name to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` + """ + data: dict[str, Any] = {} + if name is not None: + data["name"] = name + if labels is not None: + data["labels"] = labels + response = self._client.request( + url=f"/certificates/{certificate.id}", + method="PUT", + json=data, + ) + return BoundCertificate(self, response["certificate"]) + + def delete(self, certificate: Certificate | BoundCertificate) -> bool: + """Deletes a certificate. + + :param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>` + :return: True + """ + self._client.request( + url=f"/certificates/{certificate.id}", + method="DELETE", + ) + # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised + return True + + def get_actions_list( + self, + certificate: Certificate | BoundCertificate, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Returns all action objects for a Certificate. + + :param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url=f"/certificates/{certificate.id}/actions", + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return ActionsPageResult(actions, Meta.parse_meta(response)) + + def get_actions( + self, + certificate: Certificate | BoundCertificate, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for a Certificate. + + :param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._iter_pages( + self.get_actions_list, + certificate, + status=status, + sort=sort, + ) + + def retry_issuance( + self, + certificate: Certificate | BoundCertificate, + ) -> BoundAction: + """Returns all action objects for a Certificate. + + :param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/certificates/{certificate.id}/actions/retry", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/certificates/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/certificates/domain.py new file mode 100644 index 000000000..c09c288d3 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/certificates/domain.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core import BaseDomain, DomainIdentityMixin + +if TYPE_CHECKING: + from ..actions import BoundAction + from .client import BoundCertificate + + +class Certificate(BaseDomain, DomainIdentityMixin): + """Certificate Domain + + :param id: int ID of Certificate + :param name: str Name of Certificate + :param certificate: str Certificate and chain in PEM format, in order so that each record directly certifies the one preceding + :param not_valid_before: datetime + Point in time when the Certificate becomes valid + :param not_valid_after: datetime + Point in time when the Certificate becomes invalid + :param domain_names: List[str] List of domains and subdomains covered by this certificate + :param fingerprint: str Fingerprint of the Certificate + :param labels: dict + User-defined labels (key-value pairs) + :param created: datetime + Point in time when the certificate was created + :param type: str Type of Certificate + :param status: ManagedCertificateStatus Current status of a type managed Certificate, always none for type uploaded Certificates + """ + + __slots__ = ( + "id", + "name", + "certificate", + "not_valid_before", + "not_valid_after", + "domain_names", + "fingerprint", + "created", + "labels", + "type", + "status", + ) + TYPE_UPLOADED = "uploaded" + TYPE_MANAGED = "managed" + + def __init__( + self, + id: int | None = None, + name: str | None = None, + certificate: str | None = None, + not_valid_before: str | None = None, + not_valid_after: str | None = None, + domain_names: list[str] | None = None, + fingerprint: str | None = None, + created: str | None = None, + labels: dict[str, str] | None = None, + type: str | None = None, + status: ManagedCertificateStatus | None = None, + ): + self.id = id + self.name = name + self.type = type + self.certificate = certificate + self.domain_names = domain_names + self.fingerprint = fingerprint + self.not_valid_before = isoparse(not_valid_before) if not_valid_before else None + self.not_valid_after = isoparse(not_valid_after) if not_valid_after else None + self.created = isoparse(created) if created else None + self.labels = labels + self.status = status + + +class ManagedCertificateStatus(BaseDomain): + """ManagedCertificateStatus Domain + + :param issuance: str + Status of the issuance process of the Certificate + :param renewal: str + Status of the renewal process of the Certificate + :param error: ManagedCertificateError + If issuance or renewal reports failure, this property contains information about what happened + """ + + def __init__( + self, + issuance: str | None = None, + renewal: str | None = None, + error: ManagedCertificateError | None = None, + ): + self.issuance = issuance + self.renewal = renewal + self.error = error + + +class ManagedCertificateError(BaseDomain): + """ManagedCertificateError Domain + + :param code: str + Error code identifying the error + :param message: + Message detailing the error + """ + + def __init__(self, code: str | None = None, message: str | None = None): + self.code = code + self.message = message + + +class CreateManagedCertificateResponse(BaseDomain): + """Create Managed Certificate Response Domain + + :param certificate: :class:`BoundCertificate <hcloud.certificate.client.BoundCertificate>` + The created server + :param action: :class:`BoundAction <hcloud.actions.client.BoundAction>` + Shows the progress of the certificate creation + """ + + __slots__ = ("certificate", "action") + + def __init__( + self, + certificate: BoundCertificate, + action: BoundAction, + ): + self.certificate = certificate + self.action = action diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/core/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/core/__init__.py new file mode 100644 index 000000000..4e17dac97 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/core/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .client import BoundModelBase, ClientEntityBase # noqa: F401 +from .domain import BaseDomain, DomainIdentityMixin, Meta, Pagination # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/core/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/core/client.py new file mode 100644 index 000000000..d213daf00 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/core/client.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable + +if TYPE_CHECKING: + from .._client import Client + + +class ClientEntityBase: + _client: Client + + max_per_page: int = 50 + + def __init__(self, client: Client): + """ + :param client: Client + :return self + """ + self._client = client + + def _iter_pages( # type: ignore[no-untyped-def] + self, + list_function: Callable, + *args, + **kwargs, + ) -> list: + results = [] + + page = 1 + while page: + # The *PageResult tuples MUST have the following structure + # `(result: List[Bound*], meta: Meta)` + result, meta = list_function( + *args, page=page, per_page=self.max_per_page, **kwargs + ) + if result: + results.extend(result) + + if meta and meta.pagination and meta.pagination.next_page: + page = meta.pagination.next_page + else: + page = 0 + + return results + + def _get_first_by(self, **kwargs): # type: ignore[no-untyped-def] + assert hasattr(self, "get_list") + # pylint: disable=no-member + entities, _ = self.get_list(**kwargs) + return entities[0] if entities else None + + +class BoundModelBase: + """Bound Model Base""" + + model: Any + + def __init__( + self, + client: ClientEntityBase, + data: dict, + complete: bool = True, + ): + """ + :param client: + The client for the specific model to use + :param data: + The data of the model + :param complete: bool + False if not all attributes of the model fetched + """ + self._client = client + self.complete = complete + self.data_model = self.model.from_dict(data) + + def __getattr__(self, name: str): # type: ignore[no-untyped-def] + """Allow magical access to the properties of the model + :param name: str + :return: + """ + value = getattr(self.data_model, name) + if not value and not self.complete: + self.reload() + value = getattr(self.data_model, name) + return value + + def reload(self) -> None: + """Reloads the model and tries to get all data from the APIx""" + assert hasattr(self._client, "get_by_id") + bound_model = self._client.get_by_id(self.data_model.id) + self.data_model = bound_model.data_model + self.complete = True + + def __repr__(self) -> str: + # Override and reset hcloud.core.domain.BaseDomain.__repr__ method for bound + # models, as they will generate a lot of API call trying to print all the fields + # of the model. + return object.__repr__(self) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/core/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/core/domain.py new file mode 100644 index 000000000..692f7488b --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/core/domain.py @@ -0,0 +1,84 @@ +from __future__ import annotations + + +class BaseDomain: + __slots__ = () + + @classmethod + def from_dict(cls, data: dict): # type: ignore[no-untyped-def] + """ + Build the domain object from the data dict. + """ + supported_data = {k: v for k, v in data.items() if k in cls.__slots__} + return cls(**supported_data) + + def __repr__(self) -> str: + kwargs = [f"{key}={getattr(self, key)!r}" for key in self.__slots__] # type: ignore[var-annotated] + return f"{self.__class__.__qualname__}({', '.join(kwargs)})" + + +class DomainIdentityMixin: + __slots__ = () + + id: int | None + name: str | None + + @property + def id_or_name(self) -> int | str: + """ + Return the first defined value, and fails if none is defined. + """ + if self.id is not None: + return self.id + if self.name is not None: + return self.name + raise ValueError("id or name must be set") + + +class Pagination(BaseDomain): + __slots__ = ( + "page", + "per_page", + "previous_page", + "next_page", + "last_page", + "total_entries", + ) + + def __init__( + self, + page: int, + per_page: int, + previous_page: int | None = None, + next_page: int | None = None, + last_page: int | None = None, + total_entries: int | None = None, + ): + self.page = page + self.per_page = per_page + self.previous_page = previous_page + self.next_page = next_page + self.last_page = last_page + self.total_entries = total_entries + + +class Meta(BaseDomain): + __slots__ = ("pagination",) + + def __init__(self, pagination: Pagination | None = None): + self.pagination = pagination + + @classmethod + def parse_meta(cls, response: dict) -> Meta | None: + """ + If present, extract the meta details from the response and return a meta object. + """ + meta = None + if response and "meta" in response: + meta = cls() + try: + meta.pagination = Pagination(**response["meta"]["pagination"]) + except KeyError: + pass + + return meta diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/datacenters/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/datacenters/__init__.py new file mode 100644 index 000000000..559694c06 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/datacenters/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .client import ( # noqa: F401 + BoundDatacenter, + DatacentersClient, + DatacentersPageResult, +) +from .domain import Datacenter, DatacenterServerTypes # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/datacenters/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/datacenters/client.py new file mode 100644 index 000000000..1be1e126e --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/datacenters/client.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple + +from ..core import BoundModelBase, ClientEntityBase, Meta +from ..locations import BoundLocation +from ..server_types import BoundServerType +from .domain import Datacenter, DatacenterServerTypes + +if TYPE_CHECKING: + from .._client import Client + + +class BoundDatacenter(BoundModelBase, Datacenter): + _client: DatacentersClient + + model = Datacenter + + def __init__(self, client: DatacentersClient, data: dict): + location = data.get("location") + if location is not None: + data["location"] = BoundLocation(client._client.locations, location) + + server_types = data.get("server_types") + if server_types is not None: + available = [ + BoundServerType( + client._client.server_types, {"id": server_type}, complete=False + ) + for server_type in server_types["available"] + ] + supported = [ + BoundServerType( + client._client.server_types, {"id": server_type}, complete=False + ) + for server_type in server_types["supported"] + ] + available_for_migration = [ + BoundServerType( + client._client.server_types, {"id": server_type}, complete=False + ) + for server_type in server_types["available_for_migration"] + ] + data["server_types"] = DatacenterServerTypes( + available=available, + supported=supported, + available_for_migration=available_for_migration, + ) + + super().__init__(client, data) + + +class DatacentersPageResult(NamedTuple): + datacenters: list[BoundDatacenter] + meta: Meta | None + + +class DatacentersClient(ClientEntityBase): + _client: Client + + def get_by_id(self, id: int) -> BoundDatacenter: + """Get a specific datacenter by its ID. + + :param id: int + :return: :class:`BoundDatacenter <hcloud.datacenters.client.BoundDatacenter>` + """ + response = self._client.request(url=f"/datacenters/{id}", method="GET") + return BoundDatacenter(self, response["datacenter"]) + + def get_list( + self, + name: str | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> DatacentersPageResult: + """Get a list of datacenters + + :param name: str (optional) + Can be used to filter datacenters by their name. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundDatacenter <hcloud.datacenters.client.BoundDatacenter>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if name is not None: + params["name"] = name + + if page is not None: + params["page"] = page + + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/datacenters", method="GET", params=params) + + datacenters = [ + BoundDatacenter(self, datacenter_data) + for datacenter_data in response["datacenters"] + ] + + return DatacentersPageResult(datacenters, Meta.parse_meta(response)) + + def get_all(self, name: str | None = None) -> list[BoundDatacenter]: + """Get all datacenters + + :param name: str (optional) + Can be used to filter datacenters by their name. + :return: List[:class:`BoundDatacenter <hcloud.datacenters.client.BoundDatacenter>`] + """ + return self._iter_pages(self.get_list, name=name) + + def get_by_name(self, name: str) -> BoundDatacenter | None: + """Get datacenter by name + + :param name: str + Used to get datacenter by name. + :return: :class:`BoundDatacenter <hcloud.datacenters.client.BoundDatacenter>` + """ + return self._get_first_by(name=name) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/datacenters/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/datacenters/domain.py new file mode 100644 index 000000000..05d5f793f --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/datacenters/domain.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..core import BaseDomain, DomainIdentityMixin + +if TYPE_CHECKING: + from ..locations import Location + from ..server_types import BoundServerType + + +class Datacenter(BaseDomain, DomainIdentityMixin): + """Datacenter Domain + + :param id: int ID of Datacenter + :param name: str Name of Datacenter + :param description: str Description of Datacenter + :param location: :class:`BoundLocation <hcloud.locations.client.BoundLocation>` + :param server_types: :class:`DatacenterServerTypes <hcloud.datacenters.domain.DatacenterServerTypes>` + """ + + __slots__ = ("id", "name", "description", "location", "server_types") + + def __init__( + self, + id: int | None = None, + name: str | None = None, + description: str | None = None, + location: Location | None = None, + server_types: DatacenterServerTypes | None = None, + ): + self.id = id + self.name = name + self.description = description + self.location = location + self.server_types = server_types + + +class DatacenterServerTypes(BaseDomain): + """DatacenterServerTypes Domain + + :param available: List[:class:`BoundServerTypes <hcloud.server_types.client.BoundServerTypes>`] + All available server types for this datacenter + :param supported: List[:class:`BoundServerTypes <hcloud.server_types.client.BoundServerTypes>`] + All supported server types for this datacenter + :param available_for_migration: List[:class:`BoundServerTypes <hcloud.server_types.client.BoundServerTypes>`] + All available for migration (change type) server types for this datacenter + """ + + __slots__ = ("available", "supported", "available_for_migration") + + def __init__( + self, + available: list[BoundServerType], + supported: list[BoundServerType], + available_for_migration: list[BoundServerType], + ): + self.available = available + self.supported = supported + self.available_for_migration = available_for_migration diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/deprecation/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/deprecation/__init__.py new file mode 100644 index 000000000..315576b11 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/deprecation/__init__.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from .domain import DeprecationInfo # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/deprecation/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/deprecation/domain.py new file mode 100644 index 000000000..b79e70943 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/deprecation/domain.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core import BaseDomain + + +class DeprecationInfo(BaseDomain): + """Describes if, when & how the resources was deprecated. If this field is set to ``None`` the resource is not + deprecated. If it has a value, it is considered deprecated. + + :param announced: datetime + Date of when the deprecation was announced. + :param unavailable_after: datetime + After the time in this field, the resource will not be available from the general listing endpoint of the + resource type, and it can not be used in new resources. For example, if this is an image, you can not create + new servers with this image after the mentioned date. + """ + + __slots__ = ( + "announced", + "unavailable_after", + ) + + def __init__( + self, + announced: str | None = None, + unavailable_after: str | None = None, + ): + self.announced = isoparse(announced) if announced else None + self.unavailable_after = ( + isoparse(unavailable_after) if unavailable_after else None + ) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/firewalls/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/firewalls/__init__.py new file mode 100644 index 000000000..5205d7664 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/firewalls/__init__.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from .client import BoundFirewall, FirewallsClient, FirewallsPageResult # noqa: F401 +from .domain import ( # noqa: F401 + CreateFirewallResponse, + Firewall, + FirewallResource, + FirewallResourceAppliedToResources, + FirewallResourceLabelSelector, + FirewallRule, +) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/firewalls/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/firewalls/client.py new file mode 100644 index 000000000..fbcd10080 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/firewalls/client.py @@ -0,0 +1,501 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple + +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient +from ..core import BoundModelBase, ClientEntityBase, Meta +from .domain import ( + CreateFirewallResponse, + Firewall, + FirewallResource, + FirewallResourceAppliedToResources, + FirewallResourceLabelSelector, + FirewallRule, +) + +if TYPE_CHECKING: + from .._client import Client + + +class BoundFirewall(BoundModelBase, Firewall): + _client: FirewallsClient + + model = Firewall + + def __init__(self, client: FirewallsClient, data: dict, complete: bool = True): + rules = data.get("rules", []) + if rules: + rules = [ + FirewallRule( + direction=rule["direction"], + source_ips=rule["source_ips"], + destination_ips=rule["destination_ips"], + protocol=rule["protocol"], + port=rule["port"], + description=rule["description"], + ) + for rule in rules + ] + data["rules"] = rules + + applied_to = data.get("applied_to", []) + if applied_to: + # pylint: disable=import-outside-toplevel + from ..servers import BoundServer + + data_applied_to = [] + for firewall_resource in applied_to: + applied_to_resources = None + if firewall_resource.get("applied_to_resources"): + applied_to_resources = [ + FirewallResourceAppliedToResources( + type=resource["type"], + server=( + BoundServer( + client._client.servers, + resource.get("server"), + complete=False, + ) + if resource.get("server") is not None + else None + ), + ) + for resource in firewall_resource.get("applied_to_resources") + ] + + if firewall_resource["type"] == FirewallResource.TYPE_SERVER: + data_applied_to.append( + FirewallResource( + type=firewall_resource["type"], + server=BoundServer( + client._client.servers, + firewall_resource["server"], + complete=False, + ), + applied_to_resources=applied_to_resources, + ) + ) + elif firewall_resource["type"] == FirewallResource.TYPE_LABEL_SELECTOR: + data_applied_to.append( + FirewallResource( + type=firewall_resource["type"], + label_selector=FirewallResourceLabelSelector( + selector=firewall_resource["label_selector"]["selector"] + ), + applied_to_resources=applied_to_resources, + ) + ) + + data["applied_to"] = data_applied_to + + super().__init__(client, data, complete) + + def get_actions_list( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Returns all action objects for a Firewall. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + return self._client.get_actions_list(self, status, sort, page, per_page) + + def get_actions( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for a Firewall. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._client.get_actions(self, status, sort) + + def update( + self, + name: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundFirewall: + """Updates the name or labels of a Firewall. + + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New Name to set + :return: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` + """ + return self._client.update(self, labels, name) + + def delete(self) -> bool: + """Deletes a Firewall. + + :return: boolean + """ + return self._client.delete(self) + + def set_rules(self, rules: list[FirewallRule]) -> list[BoundAction]: + """Sets the rules of a Firewall. All existing rules will be overwritten. Pass an empty rules array to remove all rules. + :param rules: List[:class:`FirewallRule <hcloud.firewalls.domain.FirewallRule>`] + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + + return self._client.set_rules(self, rules) + + def apply_to_resources( + self, + resources: list[FirewallResource], + ) -> list[BoundAction]: + """Applies one Firewall to multiple resources. + :param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`] + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._client.apply_to_resources(self, resources) + + def remove_from_resources( + self, + resources: list[FirewallResource], + ) -> list[BoundAction]: + """Removes one Firewall from multiple resources. + :param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`] + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._client.remove_from_resources(self, resources) + + +class FirewallsPageResult(NamedTuple): + firewalls: list[BoundFirewall] + meta: Meta | None + + +class FirewallsClient(ClientEntityBase): + _client: Client + + actions: ResourceActionsClient + """Firewalls scoped actions client + + :type: :class:`ResourceActionsClient <hcloud.actions.client.ResourceActionsClient>` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/firewalls") + + def get_actions_list( + self, + firewall: Firewall | BoundFirewall, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Returns all action objects for a Firewall. + + :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + response = self._client.request( + url=f"/firewalls/{firewall.id}/actions", + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return ActionsPageResult(actions, Meta.parse_meta(response)) + + def get_actions( + self, + firewall: Firewall | BoundFirewall, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for a Firewall. + + :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._iter_pages( + self.get_actions_list, + firewall, + status=status, + sort=sort, + ) + + def get_by_id(self, id: int) -> BoundFirewall: + """Returns a specific Firewall object. + + :param id: int + :return: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` + """ + response = self._client.request(url=f"/firewalls/{id}", method="GET") + return BoundFirewall(self, response["firewall"]) + + def get_list( + self, + label_selector: str | None = None, + page: int | None = None, + per_page: int | None = None, + name: str | None = None, + sort: list[str] | None = None, + ) -> FirewallsPageResult: + """Get a list of floating ips from this account + + :param label_selector: str (optional) + Can be used to filter Firewalls by labels. The response will only contain Firewalls matching the label selector values. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :param name: str (optional) + Can be used to filter networks by their name. + :param sort: List[str] (optional) + Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) + :return: (List[:class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + if name is not None: + params["name"] = name + if sort is not None: + params["sort"] = sort + response = self._client.request(url="/firewalls", method="GET", params=params) + firewalls = [ + BoundFirewall(self, firewall_data) + for firewall_data in response["firewalls"] + ] + + return FirewallsPageResult(firewalls, Meta.parse_meta(response)) + + def get_all( + self, + label_selector: str | None = None, + name: str | None = None, + sort: list[str] | None = None, + ) -> list[BoundFirewall]: + """Get all floating ips from this account + + :param label_selector: str (optional) + Can be used to filter Firewalls by labels. The response will only contain Firewalls matching the label selector values. + :param name: str (optional) + Can be used to filter networks by their name. + :param sort: List[str] (optional) + Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) + :return: List[:class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>`] + """ + return self._iter_pages( + self.get_list, + label_selector=label_selector, + name=name, + sort=sort, + ) + + def get_by_name(self, name: str) -> BoundFirewall | None: + """Get Firewall by name + + :param name: str + Used to get Firewall by name. + :return: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` + """ + return self._get_first_by(name=name) + + def create( + self, + name: str, + rules: list[FirewallRule] | None = None, + labels: str | None = None, + resources: list[FirewallResource] | None = None, + ) -> CreateFirewallResponse: + """Creates a new Firewall. + + :param name: str + Firewall Name + :param rules: List[:class:`FirewallRule <hcloud.firewalls.domain.FirewallRule>`] (optional) + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`] (optional) + :return: :class:`CreateFirewallResponse <hcloud.firewalls.domain.CreateFirewallResponse>` + """ + + data: dict[str, Any] = {"name": name} + if labels is not None: + data["labels"] = labels + + if rules is not None: + data.update({"rules": []}) + for rule in rules: + data["rules"].append(rule.to_payload()) + if resources is not None: + data.update({"apply_to": []}) + for resource in resources: + data["apply_to"].append(resource.to_payload()) + response = self._client.request(url="/firewalls", json=data, method="POST") + + actions = [] + if response.get("actions") is not None: + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + + result = CreateFirewallResponse( + firewall=BoundFirewall(self, response["firewall"]), actions=actions + ) + return result + + def update( + self, + firewall: Firewall | BoundFirewall, + labels: dict[str, str] | None = None, + name: str | None = None, + ) -> BoundFirewall: + """Updates the description or labels of a Firewall. + + :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>` + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New name to set + :return: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` + """ + data: dict[str, Any] = {} + if labels is not None: + data["labels"] = labels + if name is not None: + data["name"] = name + + response = self._client.request( + url=f"/firewalls/{firewall.id}", + method="PUT", + json=data, + ) + return BoundFirewall(self, response["firewall"]) + + def delete(self, firewall: Firewall | BoundFirewall) -> bool: + """Deletes a Firewall. + + :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>` + :return: boolean + """ + self._client.request( + url=f"/firewalls/{firewall.id}", + method="DELETE", + ) + # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised + return True + + def set_rules( + self, + firewall: Firewall | BoundFirewall, + rules: list[FirewallRule], + ) -> list[BoundAction]: + """Sets the rules of a Firewall. All existing rules will be overwritten. Pass an empty rules array to remove all rules. + + :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>` + :param rules: List[:class:`FirewallRule <hcloud.firewalls.domain.FirewallRule>`] + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + data: dict[str, Any] = {"rules": []} + for rule in rules: + data["rules"].append(rule.to_payload()) + response = self._client.request( + url=f"/firewalls/{firewall.id}/actions/set_rules", + method="POST", + json=data, + ) + return [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + + def apply_to_resources( + self, + firewall: Firewall | BoundFirewall, + resources: list[FirewallResource], + ) -> list[BoundAction]: + """Applies one Firewall to multiple resources. + + :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>` + :param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`] + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + data: dict[str, Any] = {"apply_to": []} + for resource in resources: + data["apply_to"].append(resource.to_payload()) + response = self._client.request( + url=f"/firewalls/{firewall.id}/actions/apply_to_resources", + method="POST", + json=data, + ) + return [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + + def remove_from_resources( + self, + firewall: Firewall | BoundFirewall, + resources: list[FirewallResource], + ) -> list[BoundAction]: + """Removes one Firewall from multiple resources. + + :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>` + :param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`] + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + data: dict[str, Any] = {"remove_from": []} + for resource in resources: + data["remove_from"].append(resource.to_payload()) + response = self._client.request( + url=f"/firewalls/{firewall.id}/actions/remove_from_resources", + method="POST", + json=data, + ) + return [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/firewalls/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/firewalls/domain.py new file mode 100644 index 000000000..5ce9281d9 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/firewalls/domain.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core import BaseDomain + +if TYPE_CHECKING: + from ..actions import BoundAction + from ..servers import BoundServer, Server + from .client import BoundFirewall + + +class Firewall(BaseDomain): + """Firewall Domain + + :param id: int + ID of the Firewall + :param name: str + Name of the Firewall + :param labels: dict + User-defined labels (key-value pairs) + :param rules: List[:class:`FirewallRule <hcloud.firewalls.domain.FirewallRule>`] + Rules of the Firewall + :param applied_to: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`] + Resources currently using the Firewall + :param created: datetime + Point in time when the image was created + """ + + __slots__ = ("id", "name", "labels", "rules", "applied_to", "created") + + def __init__( + self, + id: int | None = None, + name: str | None = None, + labels: dict[str, str] | None = None, + rules: list[FirewallRule] | None = None, + applied_to: list[FirewallResource] | None = None, + created: str | None = None, + ): + self.id = id + self.name = name + self.rules = rules + self.applied_to = applied_to + self.labels = labels + self.created = isoparse(created) if created else None + + +class FirewallRule(BaseDomain): + """Firewall Rule Domain + + :param direction: str + The Firewall which was created + :param port: str + Port to which traffic will be allowed, only applicable for protocols TCP and UDP, specify port ranges by using + - as a indicator, Sample: 80-85 means all ports between 80 & 85 (80, 82, 83, 84, 85) + :param protocol: str + Select traffic direction on which rule should be applied. Use source_ips for direction in and destination_ips for direction out. + :param source_ips: List[str] + List of permitted IPv4/IPv6 addresses in CIDR notation. Use 0.0.0.0/0 to allow all IPv4 addresses and ::/0 to allow all IPv6 addresses. You can specify 100 CIDRs at most. + :param destination_ips: List[str] + List of permitted IPv4/IPv6 addresses in CIDR notation. Use 0.0.0.0/0 to allow all IPv4 addresses and ::/0 to allow all IPv6 addresses. You can specify 100 CIDRs at most. + :param description: str + Short description of the firewall rule + """ + + __slots__ = ( + "direction", + "port", + "protocol", + "source_ips", + "destination_ips", + "description", + ) + + DIRECTION_IN = "in" + """Firewall Rule Direction In""" + DIRECTION_OUT = "out" + """Firewall Rule Direction Out""" + + PROTOCOL_UDP = "udp" + """Firewall Rule Protocol UDP""" + PROTOCOL_ICMP = "icmp" + """Firewall Rule Protocol ICMP""" + PROTOCOL_TCP = "tcp" + """Firewall Rule Protocol TCP""" + PROTOCOL_ESP = "esp" + """Firewall Rule Protocol ESP""" + PROTOCOL_GRE = "gre" + """Firewall Rule Protocol GRE""" + + def __init__( + self, + direction: str, + protocol: str, + source_ips: list[str], + port: str | None = None, + destination_ips: list[str] | None = None, + description: str | None = None, + ): + self.direction = direction + self.port = port + self.protocol = protocol + self.source_ips = source_ips + self.destination_ips = destination_ips or [] + self.description = description + + def to_payload(self) -> dict[str, Any]: + """ + Generates the request payload from this domain object. + """ + payload: dict[str, Any] = { + "direction": self.direction, + "protocol": self.protocol, + "source_ips": self.source_ips, + } + if len(self.destination_ips) > 0: + payload["destination_ips"] = self.destination_ips + if self.port is not None: + payload["port"] = self.port + if self.description is not None: + payload["description"] = self.description + return payload + + +class FirewallResource(BaseDomain): + """Firewall Used By Domain + + :param type: str + Type of resource referenced + :param server: Optional[Server] + Server the Firewall is applied to + :param label_selector: Optional[FirewallResourceLabelSelector] + Label Selector for Servers the Firewall should be applied to + :param applied_to_resources: (read-only) List of effective resources the firewall is + applied to. + """ + + __slots__ = ("type", "server", "label_selector", "applied_to_resources") + + TYPE_SERVER = "server" + """Firewall Used By Type Server""" + TYPE_LABEL_SELECTOR = "label_selector" + """Firewall Used By Type label_selector""" + + def __init__( + self, + type: str, + server: Server | BoundServer | None = None, + label_selector: FirewallResourceLabelSelector | None = None, + applied_to_resources: list[FirewallResourceAppliedToResources] | None = None, + ): + self.type = type + self.server = server + self.label_selector = label_selector + self.applied_to_resources = applied_to_resources + + def to_payload(self) -> dict[str, Any]: + """ + Generates the request payload from this domain object. + """ + payload: dict[str, Any] = {"type": self.type} + if self.server is not None: + payload["server"] = {"id": self.server.id} + + if self.label_selector is not None: + payload["label_selector"] = {"selector": self.label_selector.selector} + return payload + + +class FirewallResourceAppliedToResources(BaseDomain): + """Firewall Resource applied to Domain + + :param type: Type of resource referenced + :param server: Server the Firewall is applied to + """ + + __slots__ = ("type", "server") + + def __init__( + self, + type: str, + server: BoundServer | None = None, + ): + self.type = type + self.server = server + + +class FirewallResourceLabelSelector(BaseDomain): + """FirewallResourceLabelSelector Domain + + :param selector: str Target label selector + """ + + def __init__(self, selector: str | None = None): + self.selector = selector + + +class CreateFirewallResponse(BaseDomain): + """Create Firewall Response Domain + + :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` + The Firewall which was created + :param actions: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + The Action which shows the progress of the Firewall Creation + """ + + __slots__ = ("firewall", "actions") + + def __init__( + self, + firewall: BoundFirewall, + actions: list[BoundAction] | None, + ): + self.firewall = firewall + self.actions = actions diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/floating_ips/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/floating_ips/__init__.py new file mode 100644 index 000000000..4e55bf5fb --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/floating_ips/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .client import ( # noqa: F401 + BoundFloatingIP, + FloatingIPsClient, + FloatingIPsPageResult, +) +from .domain import CreateFloatingIPResponse, FloatingIP # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/floating_ips/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/floating_ips/client.py new file mode 100644 index 000000000..00600e48b --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/floating_ips/client.py @@ -0,0 +1,459 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple + +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient +from ..core import BoundModelBase, ClientEntityBase, Meta +from ..locations import BoundLocation +from .domain import CreateFloatingIPResponse, FloatingIP + +if TYPE_CHECKING: + from .._client import Client + from ..locations import Location + from ..servers import BoundServer, Server + + +class BoundFloatingIP(BoundModelBase, FloatingIP): + _client: FloatingIPsClient + + model = FloatingIP + + def __init__(self, client: FloatingIPsClient, data: dict, complete: bool = True): + # pylint: disable=import-outside-toplevel + from ..servers import BoundServer + + server = data.get("server") + if server is not None: + data["server"] = BoundServer( + client._client.servers, {"id": server}, complete=False + ) + + home_location = data.get("home_location") + if home_location is not None: + data["home_location"] = BoundLocation( + client._client.locations, home_location + ) + + super().__init__(client, data, complete) + + def get_actions_list( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Returns all action objects for a Floating IP. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + return self._client.get_actions_list(self, status, sort, page, per_page) + + def get_actions( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for a Floating IP. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._client.get_actions(self, status, sort) + + def update( + self, + description: str | None = None, + labels: dict[str, str] | None = None, + name: str | None = None, + ) -> BoundFloatingIP: + """Updates the description or labels of a Floating IP. + + :param description: str (optional) + New Description to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New Name to set + :return: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` + """ + return self._client.update(self, description, labels, name) + + def delete(self) -> bool: + """Deletes a Floating IP. If it is currently assigned to a server it will automatically get unassigned. + + :return: boolean + """ + return self._client.delete(self) + + def change_protection(self, delete: bool | None = None) -> BoundAction: + """Changes the protection configuration of the Floating IP. + + :param delete: boolean + If true, prevents the Floating IP from being deleted + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_protection(self, delete) + + def assign(self, server: Server | BoundServer) -> BoundAction: + """Assigns a Floating IP to a server. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + Server the Floating IP shall be assigned to + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.assign(self, server) + + def unassign(self) -> BoundAction: + """Unassigns a Floating IP, resulting in it being unreachable. You may assign it to a server again at a later time. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.unassign(self) + + def change_dns_ptr(self, ip: str, dns_ptr: str) -> BoundAction: + """Changes the hostname that will appear when getting the hostname belonging to this Floating IP. + + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: str + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_dns_ptr(self, ip, dns_ptr) + + +class FloatingIPsPageResult(NamedTuple): + floating_ips: list[BoundFloatingIP] + meta: Meta | None + + +class FloatingIPsClient(ClientEntityBase): + _client: Client + + actions: ResourceActionsClient + """Floating IPs scoped actions client + + :type: :class:`ResourceActionsClient <hcloud.actions.client.ResourceActionsClient>` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/floating_ips") + + def get_actions_list( + self, + floating_ip: FloatingIP | BoundFloatingIP, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Returns all action objects for a Floating IP. + + :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + response = self._client.request( + url=f"/floating_ips/{floating_ip.id}/actions", + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return ActionsPageResult(actions, Meta.parse_meta(response)) + + def get_actions( + self, + floating_ip: FloatingIP | BoundFloatingIP, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for a Floating IP. + + :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._iter_pages( + self.get_actions_list, + floating_ip, + status=status, + sort=sort, + ) + + def get_by_id(self, id: int) -> BoundFloatingIP: + """Returns a specific Floating IP object. + + :param id: int + :return: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` + """ + response = self._client.request(url=f"/floating_ips/{id}", method="GET") + return BoundFloatingIP(self, response["floating_ip"]) + + def get_list( + self, + label_selector: str | None = None, + page: int | None = None, + per_page: int | None = None, + name: str | None = None, + ) -> FloatingIPsPageResult: + """Get a list of floating ips from this account + + :param label_selector: str (optional) + Can be used to filter Floating IPs by labels. The response will only contain Floating IPs matching the label selector.able values. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :param name: str (optional) + Can be used to filter networks by their name. + :return: (List[:class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + if name is not None: + params["name"] = name + + response = self._client.request( + url="/floating_ips", method="GET", params=params + ) + floating_ips = [ + BoundFloatingIP(self, floating_ip_data) + for floating_ip_data in response["floating_ips"] + ] + + return FloatingIPsPageResult(floating_ips, Meta.parse_meta(response)) + + def get_all( + self, + label_selector: str | None = None, + name: str | None = None, + ) -> list[BoundFloatingIP]: + """Get all floating ips from this account + + :param label_selector: str (optional) + Can be used to filter Floating IPs by labels. The response will only contain Floating IPs matching the label selector.able values. + :param name: str (optional) + Can be used to filter networks by their name. + :return: List[:class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>`] + """ + return self._iter_pages(self.get_list, label_selector=label_selector, name=name) + + def get_by_name(self, name: str) -> BoundFloatingIP | None: + """Get Floating IP by name + + :param name: str + Used to get Floating IP by name. + :return: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` + """ + return self._get_first_by(name=name) + + def create( + self, + type: str, + description: str | None = None, + labels: str | None = None, + home_location: Location | BoundLocation | None = None, + server: Server | BoundServer | None = None, + name: str | None = None, + ) -> CreateFloatingIPResponse: + """Creates a new Floating IP assigned to a server. + + :param type: str + Floating IP type Choices: ipv4, ipv6 + :param description: str (optional) + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param home_location: :class:`BoundLocation <hcloud.locations.client.BoundLocation>` or :class:`Location <hcloud.locations.domain.Location>` ( + Home location (routing is optimized for that location). Only optional if server argument is passed. + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + Server to assign the Floating IP to + :param name: str (optional) + :return: :class:`CreateFloatingIPResponse <hcloud.floating_ips.domain.CreateFloatingIPResponse>` + """ + + data: dict[str, Any] = {"type": type} + if description is not None: + data["description"] = description + if labels is not None: + data["labels"] = labels + if home_location is not None: + data["home_location"] = home_location.id_or_name + if server is not None: + data["server"] = server.id + if name is not None: + data["name"] = name + + response = self._client.request(url="/floating_ips", json=data, method="POST") + + action = None + if response.get("action") is not None: + action = BoundAction(self._client.actions, response["action"]) + + result = CreateFloatingIPResponse( + floating_ip=BoundFloatingIP(self, response["floating_ip"]), action=action + ) + return result + + def update( + self, + floating_ip: FloatingIP | BoundFloatingIP, + description: str | None = None, + labels: dict[str, str] | None = None, + name: str | None = None, + ) -> BoundFloatingIP: + """Updates the description or labels of a Floating IP. + + :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` + :param description: str (optional) + New Description to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New name to set + :return: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` + """ + data: dict[str, Any] = {} + if description is not None: + data["description"] = description + if labels is not None: + data["labels"] = labels + if name is not None: + data["name"] = name + + response = self._client.request( + url=f"/floating_ips/{floating_ip.id}", + method="PUT", + json=data, + ) + return BoundFloatingIP(self, response["floating_ip"]) + + def delete(self, floating_ip: FloatingIP | BoundFloatingIP) -> bool: + """Deletes a Floating IP. If it is currently assigned to a server it will automatically get unassigned. + + :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` + :return: boolean + """ + self._client.request( + url=f"/floating_ips/{floating_ip.id}", + method="DELETE", + ) + # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised + return True + + def change_protection( + self, + floating_ip: FloatingIP | BoundFloatingIP, + delete: bool | None = None, + ) -> BoundAction: + """Changes the protection configuration of the Floating IP. + + :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` + :param delete: boolean + If true, prevents the Floating IP from being deleted + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {} + if delete is not None: + data.update({"delete": delete}) + + response = self._client.request( + url=f"/floating_ips/{floating_ip.id}/actions/change_protection", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def assign( + self, + floating_ip: FloatingIP | BoundFloatingIP, + server: Server | BoundServer, + ) -> BoundAction: + """Assigns a Floating IP to a server. + + :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + Server the Floating IP shall be assigned to + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/floating_ips/{floating_ip.id}/actions/assign", + method="POST", + json={"server": server.id}, + ) + return BoundAction(self._client.actions, response["action"]) + + def unassign(self, floating_ip: FloatingIP | BoundFloatingIP) -> BoundAction: + """Unassigns a Floating IP, resulting in it being unreachable. You may assign it to a server again at a later time. + + :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/floating_ips/{floating_ip.id}/actions/unassign", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def change_dns_ptr( + self, + floating_ip: FloatingIP | BoundFloatingIP, + ip: str, + dns_ptr: str, + ) -> BoundAction: + """Changes the hostname that will appear when getting the hostname belonging to this Floating IP. + + :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: str + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/floating_ips/{floating_ip.id}/actions/change_dns_ptr", + method="POST", + json={"ip": ip, "dns_ptr": dns_ptr}, + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/floating_ips/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/floating_ips/domain.py new file mode 100644 index 000000000..e1f295bd6 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/floating_ips/domain.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core import BaseDomain + +if TYPE_CHECKING: + from ..actions import BoundAction + from ..locations import BoundLocation + from ..servers import BoundServer + from .client import BoundFloatingIP + + +class FloatingIP(BaseDomain): + """Floating IP Domain + + :param id: int + ID of the Floating IP + :param description: str, None + Description of the Floating IP + :param ip: str + IP address of the Floating IP + :param type: str + Type of Floating IP. Choices: `ipv4`, `ipv6` + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>`, None + Server the Floating IP is assigned to, None if it is not assigned at all + :param dns_ptr: List[Dict] + Array of reverse DNS entries + :param home_location: :class:`BoundLocation <hcloud.locations.client.BoundLocation>` + Location the Floating IP was created in. Routing is optimized for this location. + :param blocked: boolean + Whether the IP is blocked + :param protection: dict + Protection configuration for the Floating IP + :param labels: dict + User-defined labels (key-value pairs) + :param created: datetime + Point in time when the Floating IP was created + :param name: str + Name of the Floating IP + """ + + __slots__ = ( + "id", + "type", + "description", + "ip", + "server", + "dns_ptr", + "home_location", + "blocked", + "protection", + "labels", + "name", + "created", + ) + + def __init__( + self, + id: int | None = None, + type: str | None = None, + description: str | None = None, + ip: str | None = None, + server: BoundServer | None = None, + dns_ptr: list[dict] | None = None, + home_location: BoundLocation | None = None, + blocked: bool | None = None, + protection: dict | None = None, + labels: dict[str, str] | None = None, + created: str | None = None, + name: str | None = None, + ): + self.id = id + self.type = type + self.description = description + self.ip = ip + self.server = server + self.dns_ptr = dns_ptr + self.home_location = home_location + self.blocked = blocked + self.protection = protection + self.labels = labels + self.created = isoparse(created) if created else None + self.name = name + + +class CreateFloatingIPResponse(BaseDomain): + """Create Floating IP Response Domain + + :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` + The Floating IP which was created + :param action: :class:`BoundAction <hcloud.actions.client.BoundAction>` + The Action which shows the progress of the Floating IP Creation + """ + + __slots__ = ("floating_ip", "action") + + def __init__( + self, + floating_ip: BoundFloatingIP, + action: BoundAction | None, + ): + self.floating_ip = floating_ip + self.action = action diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/hcloud.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/hcloud.py new file mode 100644 index 000000000..9de1cfe5b --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/hcloud.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +import warnings + +warnings.warn( + "The 'hcloud.hcloud' module is deprecated, please import from the 'hcloud' module instead (e.g. 'from hcloud import Client').", + DeprecationWarning, + stacklevel=2, +) + +# pylint: disable=wildcard-import,wrong-import-position,unused-wildcard-import +from ._client import * # noqa diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/helpers/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/helpers/__init__.py new file mode 100644 index 000000000..b6a4cd73d --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/helpers/__init__.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from .labels import LabelValidator # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/helpers/labels.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/helpers/labels.py new file mode 100644 index 000000000..360415784 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/helpers/labels.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import re + + +class LabelValidator: + KEY_REGEX = re.compile( + r"^([a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]){0,253}[a-z0-9A-Z])?/)?[a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]|){0,61}[a-z0-9A-Z])?$" + ) + VALUE_REGEX = re.compile( + r"^(([a-z0-9A-Z](?:[\-_.]|[a-z0-9A-Z]){0,61})?[a-z0-9A-Z]$|$)" + ) + + @staticmethod + def validate(labels: dict[str, str]) -> bool: + """Validates Labels. If you want to know which key/value pair of the dict is not correctly formatted + use :func:`~hcloud.helpers.labels.validate_verbose`. + + :return: bool + """ + for key, value in labels.items(): + if LabelValidator.KEY_REGEX.match(key) is None: + return False + if LabelValidator.VALUE_REGEX.match(value) is None: + return False + return True + + @staticmethod + def validate_verbose(labels: dict[str, str]) -> tuple[bool, str]: + """Validates Labels and returns the corresponding error message if something is wrong. Returns True, <empty string> + if everything is fine. + + :return: bool, str + """ + for key, value in labels.items(): + if LabelValidator.KEY_REGEX.match(key) is None: + return ( + False, + f"label key {key} is not correctly formatted", + ) + if LabelValidator.VALUE_REGEX.match(value) is None: + return ( + False, + f"label value {value} (key: {key}) is not correctly formatted", + ) + return True, "" diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/images/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/images/__init__.py new file mode 100644 index 000000000..78cb6868f --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/images/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .client import BoundImage, ImagesClient, ImagesPageResult # noqa: F401 +from .domain import CreateImageResponse, Image # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/images/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/images/client.py new file mode 100644 index 000000000..65b7546a8 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/images/client.py @@ -0,0 +1,393 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple + +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient +from ..core import BoundModelBase, ClientEntityBase, Meta +from .domain import Image + +if TYPE_CHECKING: + from .._client import Client + + +class BoundImage(BoundModelBase, Image): + _client: ImagesClient + + model = Image + + def __init__(self, client: ImagesClient, data: dict): + # pylint: disable=import-outside-toplevel + from ..servers import BoundServer + + created_from = data.get("created_from") + if created_from is not None: + data["created_from"] = BoundServer( + client._client.servers, created_from, complete=False + ) + bound_to = data.get("bound_to") + if bound_to is not None: + data["bound_to"] = BoundServer( + client._client.servers, {"id": bound_to}, complete=False + ) + + super().__init__(client, data) + + def get_actions_list( + self, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + status: list[str] | None = None, + ) -> ActionsPageResult: + """Returns a list of action objects for the image. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + return self._client.get_actions_list( + self, sort=sort, page=page, per_page=per_page, status=status + ) + + def get_actions( + self, + sort: list[str] | None = None, + status: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for the image. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._client.get_actions(self, status=status, sort=sort) + + def update( + self, + description: str | None = None, + type: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundImage: + """Updates the Image. You may change the description, convert a Backup image to a Snapshot Image or change the image labels. + + :param description: str (optional) + New description of Image + :param type: str (optional) + Destination image type to convert to + Choices: snapshot + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundImage <hcloud.images.client.BoundImage>` + """ + return self._client.update(self, description, type, labels) + + def delete(self) -> bool: + """Deletes an Image. Only images of type snapshot and backup can be deleted. + + :return: bool + """ + return self._client.delete(self) + + def change_protection(self, delete: bool | None = None) -> BoundAction: + """Changes the protection configuration of the image. Can only be used on snapshots. + + :param delete: bool + If true, prevents the snapshot from being deleted + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_protection(self, delete) + + +class ImagesPageResult(NamedTuple): + images: list[BoundImage] + meta: Meta | None + + +class ImagesClient(ClientEntityBase): + _client: Client + + actions: ResourceActionsClient + """Images scoped actions client + + :type: :class:`ResourceActionsClient <hcloud.actions.client.ResourceActionsClient>` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/images") + + def get_actions_list( + self, + image: Image | BoundImage, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + status: list[str] | None = None, + ) -> ActionsPageResult: + """Returns a list of action objects for an image. + + :param image: :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if sort is not None: + params["sort"] = sort + if status is not None: + params["status"] = status + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + response = self._client.request( + url=f"/images/{image.id}/actions", + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return ActionsPageResult(actions, Meta.parse_meta(response)) + + def get_actions( + self, + image: Image | BoundImage, + sort: list[str] | None = None, + status: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for an image. + + :param image: :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default) + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._iter_pages( + self.get_actions_list, + image, + sort=sort, + status=status, + ) + + def get_by_id(self, id: int) -> BoundImage: + """Get a specific Image + + :param id: int + :return: :class:`BoundImage <hcloud.images.client.BoundImage` + """ + response = self._client.request(url=f"/images/{id}", method="GET") + return BoundImage(self, response["image"]) + + def get_list( + self, + name: str | None = None, + label_selector: str | None = None, + bound_to: list[str] | None = None, + type: list[str] | None = None, + architecture: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + status: list[str] | None = None, + include_deprecated: bool | None = None, + ) -> ImagesPageResult: + """Get all images + + :param name: str (optional) + Can be used to filter images by their name. + :param label_selector: str (optional) + Can be used to filter servers by labels. The response will only contain servers matching the label selector. + :param bound_to: List[str] (optional) + Server Id linked to the image. Only available for images of type backup + :param type: List[str] (optional) + Choices: system snapshot backup + :param architecture: List[str] (optional) + Choices: x86 arm + :param status: List[str] (optional) + Can be used to filter images by their status. The response will only contain images matching the status. + :param sort: List[str] (optional) + Choices: id id:asc id:desc name name:asc name:desc created created:asc created:desc + :param include_deprecated: bool (optional) + Include deprecated images in the response. Default: False + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundImage <hcloud.images.client.BoundImage>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if name is not None: + params["name"] = name + if label_selector is not None: + params["label_selector"] = label_selector + if bound_to is not None: + params["bound_to"] = bound_to + if type is not None: + params["type"] = type + if architecture is not None: + params["architecture"] = architecture + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + if status is not None: + params["status"] = per_page + if include_deprecated is not None: + params["include_deprecated"] = include_deprecated + response = self._client.request(url="/images", method="GET", params=params) + images = [BoundImage(self, image_data) for image_data in response["images"]] + + return ImagesPageResult(images, Meta.parse_meta(response)) + + def get_all( + self, + name: str | None = None, + label_selector: str | None = None, + bound_to: list[str] | None = None, + type: list[str] | None = None, + architecture: list[str] | None = None, + sort: list[str] | None = None, + status: list[str] | None = None, + include_deprecated: bool | None = None, + ) -> list[BoundImage]: + """Get all images + + :param name: str (optional) + Can be used to filter images by their name. + :param label_selector: str (optional) + Can be used to filter servers by labels. The response will only contain servers matching the label selector. + :param bound_to: List[str] (optional) + Server Id linked to the image. Only available for images of type backup + :param type: List[str] (optional) + Choices: system snapshot backup + :param architecture: List[str] (optional) + Choices: x86 arm + :param status: List[str] (optional) + Can be used to filter images by their status. The response will only contain images matching the status. + :param sort: List[str] (optional) + Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) + :param include_deprecated: bool (optional) + Include deprecated images in the response. Default: False + :return: List[:class:`BoundImage <hcloud.images.client.BoundImage>`] + """ + return self._iter_pages( + self.get_list, + name=name, + label_selector=label_selector, + bound_to=bound_to, + type=type, + architecture=architecture, + sort=sort, + status=status, + include_deprecated=include_deprecated, + ) + + def get_by_name(self, name: str) -> BoundImage | None: + """Get image by name + + Deprecated: Use get_by_name_and_architecture instead. + + :param name: str + Used to get image by name. + :return: :class:`BoundImage <hcloud.images.client.BoundImage>` + """ + return self._get_first_by(name=name) + + def get_by_name_and_architecture( + self, + name: str, + architecture: str, + ) -> BoundImage | None: + """Get image by name + + :param name: str + Used to identify the image. + :param architecture: str + Used to identify the image. + :return: :class:`BoundImage <hcloud.images.client.BoundImage>` + """ + return self._get_first_by(name=name, architecture=[architecture]) + + def update( + self, + image: Image | BoundImage, + description: str | None = None, + type: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundImage: + """Updates the Image. You may change the description, convert a Backup image to a Snapshot Image or change the image labels. + + :param image: :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>` + :param description: str (optional) + New description of Image + :param type: str (optional) + Destination image type to convert to + Choices: snapshot + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundImage <hcloud.images.client.BoundImage>` + """ + data: dict[str, Any] = {} + if description is not None: + data.update({"description": description}) + if type is not None: + data.update({"type": type}) + if labels is not None: + data.update({"labels": labels}) + response = self._client.request( + url=f"/images/{image.id}", method="PUT", json=data + ) + return BoundImage(self, response["image"]) + + def delete(self, image: Image | BoundImage) -> bool: + """Deletes an Image. Only images of type snapshot and backup can be deleted. + + :param :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>` + :return: bool + """ + self._client.request(url=f"/images/{image.id}", method="DELETE") + # Return allays true, because the API does not return an action for it. When an error occurs a APIException will be raised + return True + + def change_protection( + self, + image: Image | BoundImage, + delete: bool | None = None, + ) -> BoundAction: + """Changes the protection configuration of the image. Can only be used on snapshots. + + :param image: :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>` + :param delete: bool + If true, prevents the snapshot from being deleted + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {} + if delete is not None: + data.update({"delete": delete}) + + response = self._client.request( + url=f"/images/{image.id}/actions/change_protection", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/images/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/images/domain.py new file mode 100644 index 000000000..9a58a3cc2 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/images/domain.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core import BaseDomain, DomainIdentityMixin + +if TYPE_CHECKING: + from ..actions import BoundAction + from ..servers import BoundServer, Server + from .client import BoundImage + + +class Image(BaseDomain, DomainIdentityMixin): + """Image Domain + + :param id: int + ID of the image + :param type: str + Type of the image Choices: `system`, `snapshot`, `backup`, `app` + :param status: str + Whether the image can be used or if it’s still being created Choices: `available`, `creating` + :param name: str, None + Unique identifier of the image. This value is only set for system images. + :param description: str + Description of the image + :param image_size: number, None + Size of the image file in our storage in GB. For snapshot images this is the value relevant for calculating costs for the image. + :param disk_size: number + Size of the disk contained in the image in GB. + :param created: datetime + Point in time when the image was created + :param created_from: :class:`BoundServer <hcloud.servers.client.BoundServer>`, None + Information about the server the image was created from + :param bound_to: :class:`BoundServer <hcloud.servers.client.BoundServer>`, None + ID of server the image is bound to. Only set for images of type `backup`. + :param os_flavor: str + Flavor of operating system contained in the image Choices: `ubuntu`, `centos`, `debian`, `fedora`, `unknown` + :param os_version: str, None + Operating system version + :param architecture: str + CPU Architecture that the image is compatible with. Choices: `x86`, `arm` + :param rapid_deploy: bool + Indicates that rapid deploy of the image is available + :param protection: dict + Protection configuration for the image + :param deprecated: datetime, None + Point in time when the image is considered to be deprecated (in ISO-8601 format) + :param labels: Dict + User-defined labels (key-value pairs) + """ + + __slots__ = ( + "id", + "name", + "type", + "description", + "image_size", + "disk_size", + "bound_to", + "os_flavor", + "os_version", + "architecture", + "rapid_deploy", + "created_from", + "status", + "protection", + "labels", + "created", + "deprecated", + ) + + # pylint: disable=too-many-locals + def __init__( + self, + id: int | None = None, + name: str | None = None, + type: str | None = None, + created: str | None = None, + description: str | None = None, + image_size: int | None = None, + disk_size: int | None = None, + deprecated: str | None = None, + bound_to: Server | BoundServer | None = None, + os_flavor: str | None = None, + os_version: str | None = None, + architecture: str | None = None, + rapid_deploy: bool | None = None, + created_from: Server | BoundServer | None = None, + protection: dict | None = None, + labels: dict[str, str] | None = None, + status: str | None = None, + ): + self.id = id + self.name = name + self.type = type + self.created = isoparse(created) if created else None + self.description = description + self.image_size = image_size + self.disk_size = disk_size + self.deprecated = isoparse(deprecated) if deprecated else None + self.bound_to = bound_to + self.os_flavor = os_flavor + self.os_version = os_version + self.architecture = architecture + self.rapid_deploy = rapid_deploy + self.created_from = created_from + self.protection = protection + self.labels = labels + self.status = status + + +class CreateImageResponse(BaseDomain): + """Create Image Response Domain + + :param image: :class:`BoundImage <hcloud.images.client.BoundImage>` + The Image which was created + :param action: :class:`BoundAction <hcloud.actions.client.BoundAction>` + The Action which shows the progress of the Floating IP Creation + """ + + __slots__ = ("action", "image") + + def __init__( + self, + action: BoundAction, + image: BoundImage, + ): + self.action = action + self.image = image diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/isos/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/isos/__init__.py new file mode 100644 index 000000000..0d5e38fae --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/isos/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .client import BoundIso, IsosClient, IsosPageResult # noqa: F401 +from .domain import Iso # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/isos/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/isos/client.py new file mode 100644 index 000000000..cc46af7f9 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/isos/client.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple +from warnings import warn + +from ..core import BoundModelBase, ClientEntityBase, Meta +from .domain import Iso + +if TYPE_CHECKING: + from .._client import Client + + +class BoundIso(BoundModelBase, Iso): + _client: IsosClient + + model = Iso + + +class IsosPageResult(NamedTuple): + isos: list[BoundIso] + meta: Meta | None + + +class IsosClient(ClientEntityBase): + _client: Client + + def get_by_id(self, id: int) -> BoundIso: + """Get a specific ISO by its id + + :param id: int + :return: :class:`BoundIso <hcloud.isos.client.BoundIso>` + """ + response = self._client.request(url=f"/isos/{id}", method="GET") + return BoundIso(self, response["iso"]) + + def get_list( + self, + name: str | None = None, + architecture: list[str] | None = None, + include_wildcard_architecture: bool | None = None, + include_architecture_wildcard: bool | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> IsosPageResult: + """Get a list of ISOs + + :param name: str (optional) + Can be used to filter ISOs by their name. + :param architecture: List[str] (optional) + Can be used to filter ISOs by their architecture. Choices: x86 arm + :param include_wildcard_architecture: bool (optional) + Deprecated, please use `include_architecture_wildcard` instead. + :param include_architecture_wildcard: bool (optional) + Custom ISOs do not have an architecture set. You must also set this flag to True if you are filtering by + architecture and also want custom ISOs. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundIso <hcloud.isos.client.BoundIso>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + + if include_wildcard_architecture is not None: + warn( + "The `include_wildcard_architecture` argument is deprecated, please use the `include_architecture_wildcard` argument instead.", + DeprecationWarning, + ) + include_architecture_wildcard = include_wildcard_architecture + + params: dict[str, Any] = {} + if name is not None: + params["name"] = name + if architecture is not None: + params["architecture"] = architecture + if include_architecture_wildcard is not None: + params["include_architecture_wildcard"] = include_architecture_wildcard + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/isos", method="GET", params=params) + isos = [BoundIso(self, iso_data) for iso_data in response["isos"]] + return IsosPageResult(isos, Meta.parse_meta(response)) + + def get_all( + self, + name: str | None = None, + architecture: list[str] | None = None, + include_wildcard_architecture: bool | None = None, + include_architecture_wildcard: bool | None = None, + ) -> list[BoundIso]: + """Get all ISOs + + :param name: str (optional) + Can be used to filter ISOs by their name. + :param architecture: List[str] (optional) + Can be used to filter ISOs by their architecture. Choices: x86 arm + :param include_wildcard_architecture: bool (optional) + Deprecated, please use `include_architecture_wildcard` instead. + :param include_architecture_wildcard: bool (optional) + Custom ISOs do not have an architecture set. You must also set this flag to True if you are filtering by + architecture and also want custom ISOs. + :return: List[:class:`BoundIso <hcloud.isos.client.BoundIso>`] + """ + + if include_wildcard_architecture is not None: + warn( + "The `include_wildcard_architecture` argument is deprecated, please use the `include_architecture_wildcard` argument instead.", + DeprecationWarning, + ) + include_architecture_wildcard = include_wildcard_architecture + + return self._iter_pages( + self.get_list, + name=name, + architecture=architecture, + include_architecture_wildcard=include_architecture_wildcard, + ) + + def get_by_name(self, name: str) -> BoundIso | None: + """Get iso by name + + :param name: str + Used to get iso by name. + :return: :class:`BoundIso <hcloud.isos.client.BoundIso>` + """ + return self._get_first_by(name=name) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/isos/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/isos/domain.py new file mode 100644 index 000000000..b2f4d30cf --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/isos/domain.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from datetime import datetime +from warnings import warn + +from ..core import BaseDomain, DomainIdentityMixin +from ..deprecation import DeprecationInfo + + +class Iso(BaseDomain, DomainIdentityMixin): + """Iso Domain + + :param id: int + ID of the ISO + :param name: str, None + Unique identifier of the ISO. Only set for public ISOs + :param description: str + Description of the ISO + :param type: str + Type of the ISO. Choices: `public`, `private` + :param architecture: str, None + CPU Architecture that the ISO is compatible with. None means that the compatibility is unknown. Choices: `x86`, `arm` + :param deprecated: datetime, None + ISO 8601 timestamp of deprecation, None if ISO is still available. After the deprecation time it will no longer be possible to attach the ISO to servers. This field is deprecated. Use `deprecation` instead. + :param deprecation: :class:`DeprecationInfo <hcloud.deprecation.domain.DeprecationInfo>`, None + Describes if, when & how the resources was deprecated. If this field is set to None the resource is not + deprecated. If it has a value, it is considered deprecated. + """ + + __slots__ = ( + "id", + "name", + "type", + "architecture", + "description", + "deprecation", + ) + + def __init__( + self, + id: int | None = None, + name: str | None = None, + type: str | None = None, + architecture: str | None = None, + description: str | None = None, + deprecated: str | None = None, # pylint: disable=unused-argument + deprecation: dict | None = None, + ): + self.id = id + self.name = name + self.type = type + self.architecture = architecture + self.description = description + self.deprecation = ( + DeprecationInfo.from_dict(deprecation) if deprecation is not None else None + ) + + @property + def deprecated(self) -> datetime | None: + """ + ISO 8601 timestamp of deprecation, None if ISO is still available. + """ + warn( + "The `deprecated` field is deprecated, please use the `deprecation` field instead.", + DeprecationWarning, + ) + if self.deprecation is None: + return None + return self.deprecation.unavailable_after diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancer_types/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancer_types/__init__.py new file mode 100644 index 000000000..fa1dc33c7 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancer_types/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .client import ( # noqa: F401 + BoundLoadBalancerType, + LoadBalancerTypesClient, + LoadBalancerTypesPageResult, +) +from .domain import LoadBalancerType # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancer_types/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancer_types/client.py new file mode 100644 index 000000000..9a83dc707 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancer_types/client.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple + +from ..core import BoundModelBase, ClientEntityBase, Meta +from .domain import LoadBalancerType + +if TYPE_CHECKING: + from .._client import Client + + +class BoundLoadBalancerType(BoundModelBase, LoadBalancerType): + _client: LoadBalancerTypesClient + + model = LoadBalancerType + + +class LoadBalancerTypesPageResult(NamedTuple): + load_balancer_types: list[BoundLoadBalancerType] + meta: Meta | None + + +class LoadBalancerTypesClient(ClientEntityBase): + _client: Client + + def get_by_id(self, id: int) -> BoundLoadBalancerType: + """Returns a specific Load Balancer Type. + + :param id: int + :return: :class:`BoundLoadBalancerType <hcloud.load_balancer_type.client.BoundLoadBalancerType>` + """ + response = self._client.request( + url=f"/load_balancer_types/{id}", + method="GET", + ) + return BoundLoadBalancerType(self, response["load_balancer_type"]) + + def get_list( + self, + name: str | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> LoadBalancerTypesPageResult: + """Get a list of Load Balancer types + + :param name: str (optional) + Can be used to filter Load Balancer type by their name. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundLoadBalancerType <hcloud.load_balancer_types.client.BoundLoadBalancerType>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if name is not None: + params["name"] = name + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url="/load_balancer_types", method="GET", params=params + ) + load_balancer_types = [ + BoundLoadBalancerType(self, load_balancer_type_data) + for load_balancer_type_data in response["load_balancer_types"] + ] + return LoadBalancerTypesPageResult( + load_balancer_types, Meta.parse_meta(response) + ) + + def get_all(self, name: str | None = None) -> list[BoundLoadBalancerType]: + """Get all Load Balancer types + + :param name: str (optional) + Can be used to filter Load Balancer type by their name. + :return: List[:class:`BoundLoadBalancerType <hcloud.load_balancer_types.client.BoundLoadBalancerType>`] + """ + return self._iter_pages(self.get_list, name=name) + + def get_by_name(self, name: str) -> BoundLoadBalancerType | None: + """Get Load Balancer type by name + + :param name: str + Used to get Load Balancer type by name. + :return: :class:`BoundLoadBalancerType <hcloud.load_balancer_types.client.BoundLoadBalancerType>` + """ + return self._get_first_by(name=name) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancer_types/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancer_types/domain.py new file mode 100644 index 000000000..35719dba3 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancer_types/domain.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from ..core import BaseDomain, DomainIdentityMixin + + +class LoadBalancerType(BaseDomain, DomainIdentityMixin): + """LoadBalancerType Domain + + :param id: int + ID of the Load Balancer type + :param name: str + Name of the Load Balancer type + :param description: str + Description of the Load Balancer type + :param max_connections: int + Max amount of connections the Load Balancer can handle + :param max_services: int + Max amount of services the Load Balancer can handle + :param max_targets: int + Max amount of targets the Load Balancer can handle + :param max_assigned_certificates: int + Max amount of certificates the Load Balancer can serve + :param prices: Dict + Prices in different locations + + """ + + __slots__ = ( + "id", + "name", + "description", + "max_connections", + "max_services", + "max_targets", + "max_assigned_certificates", + "prices", + ) + + def __init__( + self, + id: int | None = None, + name: str | None = None, + description: str | None = None, + max_connections: int | None = None, + max_services: int | None = None, + max_targets: int | None = None, + max_assigned_certificates: int | None = None, + prices: dict | None = None, + ): + self.id = id + self.name = name + self.description = description + self.max_connections = max_connections + self.max_services = max_services + self.max_targets = max_targets + self.max_assigned_certificates = max_assigned_certificates + self.prices = prices diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancers/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancers/__init__.py new file mode 100644 index 000000000..4bfd79940 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancers/__init__.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from .client import ( # noqa: F401 + BoundLoadBalancer, + LoadBalancersClient, + LoadBalancersPageResult, +) +from .domain import ( # noqa: F401 + CreateLoadBalancerResponse, + GetMetricsResponse, + IPv4Address, + IPv6Network, + LoadBalancer, + LoadBalancerAlgorithm, + LoadBalancerHealtCheckHttp, + LoadBalancerHealthCheck, + LoadBalancerService, + LoadBalancerServiceHttp, + LoadBalancerTarget, + LoadBalancerTargetHealthStatus, + LoadBalancerTargetIP, + LoadBalancerTargetLabelSelector, + PrivateNet, + PublicNetwork, +) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancers/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancers/client.py new file mode 100644 index 000000000..492121354 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancers/client.py @@ -0,0 +1,939 @@ +from __future__ import annotations + +from datetime import datetime +from typing import TYPE_CHECKING, Any, NamedTuple + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient +from ..certificates import BoundCertificate +from ..core import BoundModelBase, ClientEntityBase, Meta +from ..load_balancer_types import BoundLoadBalancerType +from ..locations import BoundLocation +from ..metrics import Metrics +from ..networks import BoundNetwork +from ..servers import BoundServer +from .domain import ( + CreateLoadBalancerResponse, + GetMetricsResponse, + IPv4Address, + IPv6Network, + LoadBalancer, + LoadBalancerAlgorithm, + LoadBalancerHealtCheckHttp, + LoadBalancerHealthCheck, + LoadBalancerService, + LoadBalancerServiceHttp, + LoadBalancerTarget, + LoadBalancerTargetHealthStatus, + LoadBalancerTargetIP, + LoadBalancerTargetLabelSelector, + MetricsType, + PrivateNet, + PublicNetwork, +) + +if TYPE_CHECKING: + from .._client import Client + from ..load_balancer_types import LoadBalancerType + from ..locations import Location + from ..networks import Network + + +class BoundLoadBalancer(BoundModelBase, LoadBalancer): + _client: LoadBalancersClient + + model = LoadBalancer + + # pylint: disable=too-many-branches,too-many-locals + def __init__(self, client: LoadBalancersClient, data: dict, complete: bool = True): + algorithm = data.get("algorithm") + if algorithm: + data["algorithm"] = LoadBalancerAlgorithm(type=algorithm["type"]) + + public_net = data.get("public_net") + if public_net: + ipv4_address = IPv4Address.from_dict(public_net["ipv4"]) + ipv6_network = IPv6Network.from_dict(public_net["ipv6"]) + data["public_net"] = PublicNetwork( + ipv4=ipv4_address, ipv6=ipv6_network, enabled=public_net["enabled"] + ) + + private_nets = data.get("private_net") + if private_nets: + private_nets = [ + PrivateNet( + network=BoundNetwork( + client._client.networks, + {"id": private_net["network"]}, + complete=False, + ), + ip=private_net["ip"], + ) + for private_net in private_nets + ] + data["private_net"] = private_nets + + targets = data.get("targets") + if targets: + tmp_targets = [] + for target in targets: + tmp_target = LoadBalancerTarget(type=target["type"]) + if target["type"] == "server": + tmp_target.server = BoundServer( + client._client.servers, data=target["server"], complete=False + ) + tmp_target.use_private_ip = target["use_private_ip"] + elif target["type"] == "label_selector": + tmp_target.label_selector = LoadBalancerTargetLabelSelector( + selector=target["label_selector"]["selector"] + ) + tmp_target.use_private_ip = target["use_private_ip"] + elif target["type"] == "ip": + tmp_target.ip = LoadBalancerTargetIP(ip=target["ip"]["ip"]) + + target_health_status = target.get("health_status") + if target_health_status is not None: + tmp_target.health_status = [ + LoadBalancerTargetHealthStatus( + listen_port=target_health_status_item["listen_port"], + status=target_health_status_item["status"], + ) + for target_health_status_item in target_health_status + ] + + tmp_targets.append(tmp_target) + data["targets"] = tmp_targets + + services = data.get("services") + if services: + tmp_services = [] + for service in services: + tmp_service = LoadBalancerService( + protocol=service["protocol"], + listen_port=service["listen_port"], + destination_port=service["destination_port"], + proxyprotocol=service["proxyprotocol"], + ) + if service["protocol"] != "tcp": + tmp_service.http = LoadBalancerServiceHttp( + sticky_sessions=service["http"]["sticky_sessions"], + redirect_http=service["http"]["redirect_http"], + cookie_name=service["http"]["cookie_name"], + cookie_lifetime=service["http"]["cookie_lifetime"], + ) + tmp_service.http.certificates = [ + BoundCertificate( + client._client.certificates, + {"id": certificate}, + complete=False, + ) + for certificate in service["http"]["certificates"] + ] + + tmp_service.health_check = LoadBalancerHealthCheck( + protocol=service["health_check"]["protocol"], + port=service["health_check"]["port"], + interval=service["health_check"]["interval"], + retries=service["health_check"]["retries"], + timeout=service["health_check"]["timeout"], + ) + if tmp_service.health_check.protocol != "tcp": + tmp_service.health_check.http = LoadBalancerHealtCheckHttp( + domain=service["health_check"]["http"]["domain"], + path=service["health_check"]["http"]["path"], + response=service["health_check"]["http"]["response"], + tls=service["health_check"]["http"]["tls"], + status_codes=service["health_check"]["http"]["status_codes"], + ) + tmp_services.append(tmp_service) + data["services"] = tmp_services + + load_balancer_type = data.get("load_balancer_type") + if load_balancer_type is not None: + data["load_balancer_type"] = BoundLoadBalancerType( + client._client.load_balancer_types, load_balancer_type + ) + + location = data.get("location") + if location is not None: + data["location"] = BoundLocation(client._client.locations, location) + + super().__init__(client, data, complete) + + def update( + self, + name: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundLoadBalancer: + """Updates a Load Balancer. You can update a Load Balancers name and a Load Balancers labels. + + :param name: str (optional) + New name to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` + """ + return self._client.update(self, name, labels) + + def delete(self) -> bool: + """Deletes a Load Balancer. + + :return: boolean + """ + return self._client.delete(self) + + def get_metrics( + self, + type: MetricsType, + start: datetime | str, + end: datetime | str, + step: float | None = None, + ) -> GetMetricsResponse: + """Get Metrics for a LoadBalancer. + + :param type: Type of metrics to get. + :param start: Start of period to get Metrics for (in ISO-8601 format). + :param end: End of period to get Metrics for (in ISO-8601 format). + :param step: Resolution of results in seconds. + """ + return self._client.get_metrics( + self, + type=type, + start=start, + end=end, + step=step, + ) + + def get_actions_list( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Returns all action objects for a Load Balancer. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + return self._client.get_actions_list(self, status, sort, page, per_page) + + def get_actions( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for a Load Balancer. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._client.get_actions(self, status, sort) + + def add_service(self, service: LoadBalancerService) -> BoundAction: + """Adds a service to a Load Balancer. + + :param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>` + The LoadBalancerService you want to add to the Load Balancer + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.add_service(self, service=service) + + def update_service(self, service: LoadBalancerService) -> BoundAction: + """Updates a service of an Load Balancer. + + :param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>` + The LoadBalancerService you want to update + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.update_service(self, service=service) + + def delete_service(self, service: LoadBalancerService) -> BoundAction: + """Deletes a service from a Load Balancer. + + :param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>` + The LoadBalancerService you want to delete from the Load Balancer + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.delete_service(self, service) + + def add_target(self, target: LoadBalancerTarget) -> BoundAction: + """Adds a target to a Load Balancer. + + :param target: :class:`LoadBalancerTarget <hcloud.load_balancers.domain.LoadBalancerTarget>` + The LoadBalancerTarget you want to add to the Load Balancer + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.add_target(self, target) + + def remove_target(self, target: LoadBalancerTarget) -> BoundAction: + """Removes a target from a Load Balancer. + + :param target: :class:`LoadBalancerTarget <hcloud.load_balancers.domain.LoadBalancerTarget>` + The LoadBalancerTarget you want to remove from the Load Balancer + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.remove_target(self, target) + + def change_algorithm(self, algorithm: LoadBalancerAlgorithm) -> BoundAction: + """Changes the algorithm used by the Load Balancer + + :param algorithm: :class:`LoadBalancerAlgorithm <hcloud.load_balancers.domain.LoadBalancerAlgorithm>` + The LoadBalancerAlgorithm you want to use + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_algorithm(self, algorithm) + + def change_dns_ptr(self, ip: str, dns_ptr: str) -> BoundAction: + """Changes the hostname that will appear when getting the hostname belonging to the public IPs (IPv4 and IPv6) of this Load Balancer. + + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: str + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_dns_ptr(self, ip, dns_ptr) + + def change_protection(self, delete: bool) -> BoundAction: + """Changes the protection configuration of a Load Balancer. + + :param delete: boolean + If True, prevents the Load Balancer from being deleted + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_protection(self, delete) + + def attach_to_network( + self, + network: Network | BoundNetwork, + ip: str | None = None, + ) -> BoundAction: + """Attaches a Load Balancer to a Network + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param ip: str + IP to request to be assigned to this Load Balancer + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.attach_to_network(self, network, ip) + + def detach_from_network(self, network: Network | BoundNetwork) -> BoundAction: + """Detaches a Load Balancer from a Network. + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.detach_from_network(self, network) + + def enable_public_interface(self) -> BoundAction: + """Enables the public interface of a Load Balancer. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.enable_public_interface(self) + + def disable_public_interface(self) -> BoundAction: + """Disables the public interface of a Load Balancer. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.disable_public_interface(self) + + def change_type( + self, + load_balancer_type: LoadBalancerType | BoundLoadBalancerType, + ) -> BoundAction: + """Changes the type of a Load Balancer. + + :param load_balancer_type: :class:`BoundLoadBalancerType <hcloud.load_balancer_types.client.BoundLoadBalancerType>` or :class:`LoadBalancerType <hcloud.load_balancer_types.domain.LoadBalancerType>` + Load Balancer type the Load Balancer should migrate to + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_type(self, load_balancer_type) + + +class LoadBalancersPageResult(NamedTuple): + load_balancers: list[BoundLoadBalancer] + meta: Meta | None + + +class LoadBalancersClient(ClientEntityBase): + _client: Client + + actions: ResourceActionsClient + """Load Balancers scoped actions client + + :type: :class:`ResourceActionsClient <hcloud.actions.client.ResourceActionsClient>` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/load_balancers") + + def get_by_id(self, id: int) -> BoundLoadBalancer: + """Get a specific Load Balancer + + :param id: int + :return: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` + """ + response = self._client.request( + url=f"/load_balancers/{id}", + method="GET", + ) + return BoundLoadBalancer(self, response["load_balancer"]) + + def get_list( + self, + name: str | None = None, + label_selector: str | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> LoadBalancersPageResult: + """Get a list of Load Balancers from this account + + :param name: str (optional) + Can be used to filter Load Balancers by their name. + :param label_selector: str (optional) + Can be used to filter Load Balancers by labels. The response will only contain Load Balancers matching the label selector. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if name is not None: + params["name"] = name + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url="/load_balancers", method="GET", params=params + ) + + load_balancers = [ + BoundLoadBalancer(self, load_balancer_data) + for load_balancer_data in response["load_balancers"] + ] + return LoadBalancersPageResult(load_balancers, Meta.parse_meta(response)) + + def get_all( + self, + name: str | None = None, + label_selector: str | None = None, + ) -> list[BoundLoadBalancer]: + """Get all Load Balancers from this account + + :param name: str (optional) + Can be used to filter Load Balancers by their name. + :param label_selector: str (optional) + Can be used to filter Load Balancers by labels. The response will only contain Load Balancers matching the label selector. + :return: List[:class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>`] + """ + return self._iter_pages(self.get_list, name=name, label_selector=label_selector) + + def get_by_name(self, name: str) -> BoundLoadBalancer | None: + """Get Load Balancer by name + + :param name: str + Used to get Load Balancer by name. + :return: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` + """ + return self._get_first_by(name=name) + + def create( + self, + name: str, + load_balancer_type: LoadBalancerType | BoundLoadBalancerType, + algorithm: LoadBalancerAlgorithm | None = None, + services: list[LoadBalancerService] | None = None, + targets: list[LoadBalancerTarget] | None = None, + labels: dict[str, str] | None = None, + location: Location | BoundLocation | None = None, + network_zone: str | None = None, + public_interface: bool | None = None, + network: Network | BoundNetwork | None = None, + ) -> CreateLoadBalancerResponse: + """Creates a Load Balancer . + + :param name: str + Name of the Load Balancer + :param load_balancer_type: LoadBalancerType + Type of the Load Balancer + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param location: Location + Location of the Load Balancer + :param network_zone: str + Network Zone of the Load Balancer + :param algorithm: LoadBalancerAlgorithm (optional) + The algorithm the Load Balancer is currently using + :param services: LoadBalancerService + The services the Load Balancer is currently serving + :param targets: LoadBalancerTarget + The targets the Load Balancer is currently serving + :param public_interface: bool + Enable or disable the public interface of the Load Balancer + :param network: Network + Adds the Load Balancer to a Network + :return: :class:`CreateLoadBalancerResponse <hcloud.load_balancers.domain.CreateLoadBalancerResponse>` + """ + data: dict[str, Any] = { + "name": name, + "load_balancer_type": load_balancer_type.id_or_name, + } + if network is not None: + data["network"] = network.id + if public_interface is not None: + data["public_interface"] = public_interface + if labels is not None: + data["labels"] = labels + if algorithm is not None: + data["algorithm"] = {"type": algorithm.type} + if services is not None: + data["services"] = [service.to_payload() for service in services] + if targets is not None: + data["targets"] = [target.to_payload() for target in targets] + if network_zone is not None: + data["network_zone"] = network_zone + if location is not None: + data["location"] = location.id_or_name + + response = self._client.request(url="/load_balancers", method="POST", json=data) + + return CreateLoadBalancerResponse( + load_balancer=BoundLoadBalancer(self, response["load_balancer"]), + action=BoundAction(self._client.actions, response["action"]), + ) + + def update( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + name: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundLoadBalancer: + """Updates a LoadBalancer. You can update a LoadBalancer’s name and a LoadBalancer’s labels. + + :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + :param name: str (optional) + New name to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` + """ + data: dict[str, Any] = {} + if name is not None: + data.update({"name": name}) + if labels is not None: + data.update({"labels": labels}) + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}", + method="PUT", + json=data, + ) + return BoundLoadBalancer(self, response["load_balancer"]) + + def delete(self, load_balancer: LoadBalancer | BoundLoadBalancer) -> bool: + """Deletes a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + :return: boolean + """ + self._client.request( + url=f"/load_balancers/{load_balancer.id}", + method="DELETE", + ) + return True + + def get_metrics( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + type: MetricsType | list[MetricsType], + start: datetime | str, + end: datetime | str, + step: float | None = None, + ) -> GetMetricsResponse: + """Get Metrics for a LoadBalancer. + + :param load_balancer: The Load Balancer to get the metrics for. + :param type: Type of metrics to get. + :param start: Start of period to get Metrics for (in ISO-8601 format). + :param end: End of period to get Metrics for (in ISO-8601 format). + :param step: Resolution of results in seconds. + """ + if not isinstance(type, list): + type = [type] + if isinstance(start, str): + start = isoparse(start) + if isinstance(end, str): + end = isoparse(end) + + params: dict[str, Any] = { + "type": ",".join(type), + "start": start.isoformat(), + "end": end.isoformat(), + } + if step is not None: + params["step"] = step + + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/metrics", + method="GET", + params=params, + ) + return GetMetricsResponse( + metrics=Metrics(**response["metrics"]), + ) + + def get_actions_list( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Returns all action objects for a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/actions", + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return ActionsPageResult(actions, Meta.parse_meta(response)) + + def get_actions( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._iter_pages( + self.get_actions_list, + load_balancer, + status=status, + sort=sort, + ) + + def add_service( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + service: LoadBalancerService, + ) -> BoundAction: + """Adds a service to a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + :param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>` + The LoadBalancerService you want to add to the Load Balancer + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = service.to_payload() + + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/actions/add_service", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def update_service( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + service: LoadBalancerService, + ) -> BoundAction: + """Updates a service of an Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + :param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>` + The LoadBalancerService with updated values within for the Load Balancer + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = service.to_payload() + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/actions/update_service", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def delete_service( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + service: LoadBalancerService, + ) -> BoundAction: + """Deletes a service from a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + :param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>` + The LoadBalancerService you want to delete from the Load Balancer + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {"listen_port": service.listen_port} + + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/actions/delete_service", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def add_target( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + target: LoadBalancerTarget, + ) -> BoundAction: + """Adds a target to a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + :param target: :class:`LoadBalancerTarget <hcloud.load_balancers.domain.LoadBalancerTarget>` + The LoadBalancerTarget you want to add to the Load Balancer + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = target.to_payload() + + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/actions/add_target", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def remove_target( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + target: LoadBalancerTarget, + ) -> BoundAction: + """Removes a target from a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + :param target: :class:`LoadBalancerTarget <hcloud.load_balancers.domain.LoadBalancerTarget>` + The LoadBalancerTarget you want to remove from the Load Balancer + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = target.to_payload() + # Do not send use_private_ip on remove_target + data.pop("use_private_ip", None) + + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/actions/remove_target", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def change_algorithm( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + algorithm: LoadBalancerAlgorithm, + ) -> BoundAction: + """Changes the algorithm used by the Load Balancer + + :param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + :param algorithm: :class:`LoadBalancerAlgorithm <hcloud.load_balancers.domain.LoadBalancerAlgorithm>` + The LoadBalancerSubnet you want to add to the Load Balancer + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {"type": algorithm.type} + + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/actions/change_algorithm", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def change_dns_ptr( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + ip: str, + dns_ptr: str, + ) -> BoundAction: + """Changes the hostname that will appear when getting the hostname belonging to the public IPs (IPv4 and IPv6) of this Load Balancer. + + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: str + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/actions/change_dns_ptr", + method="POST", + json={"ip": ip, "dns_ptr": dns_ptr}, + ) + return BoundAction(self._client.actions, response["action"]) + + def change_protection( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + delete: bool | None = None, + ) -> BoundAction: + """Changes the protection configuration of a Load Balancer. + + :param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + :param delete: boolean + If True, prevents the Load Balancer from being deleted + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {} + if delete is not None: + data.update({"delete": delete}) + + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/actions/change_protection", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def attach_to_network( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + network: Network | BoundNetwork, + ip: str | None = None, + ) -> BoundAction: + """Attach a Load Balancer to a Network. + + :param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param ip: str + IP to request to be assigned to this Load Balancer + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {"network": network.id} + if ip is not None: + data.update({"ip": ip}) + + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/actions/attach_to_network", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def detach_from_network( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + network: Network | BoundNetwork, + ) -> BoundAction: + """Detaches a Load Balancer from a Network. + + :param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {"network": network.id} + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/actions/detach_from_network", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def enable_public_interface( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + ) -> BoundAction: + """Enables the public interface of a Load Balancer. + + :param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/actions/enable_public_interface", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def disable_public_interface( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + ) -> BoundAction: + """Disables the public interface of a Load Balancer. + + :param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/actions/disable_public_interface", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def change_type( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + load_balancer_type: LoadBalancerType | BoundLoadBalancerType, + ) -> BoundAction: + """Changes the type of a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` + :param load_balancer_type: :class:`BoundLoadBalancerType <hcloud.load_balancer_types.client.BoundLoadBalancerType>` or :class:`LoadBalancerType <hcloud.load_balancer_types.domain.LoadBalancerType>` + Load Balancer type the Load Balancer should migrate to + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {"load_balancer_type": load_balancer_type.id_or_name} + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/actions/change_type", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancers/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancers/domain.py new file mode 100644 index 000000000..6d6f0700a --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/load_balancers/domain.py @@ -0,0 +1,537 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Literal + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core import BaseDomain + +if TYPE_CHECKING: + from ..actions import BoundAction + from ..certificates import BoundCertificate + from ..load_balancer_types import BoundLoadBalancerType + from ..locations import BoundLocation + from ..metrics import Metrics + from ..networks import BoundNetwork + from ..servers import BoundServer + from .client import BoundLoadBalancer + + +class LoadBalancer(BaseDomain): + """LoadBalancer Domain + + :param id: int + ID of the Load Balancer + :param name: str + Name of the Load Balancer (must be unique per project) + :param created: datetime + Point in time when the Load Balancer was created + :param protection: dict + Protection configuration for the Load Balancer + :param labels: dict + User-defined labels (key-value pairs) + :param location: Location + Location of the Load Balancer + :param public_net: :class:`PublicNetwork <hcloud.load_balancers.domain.PublicNetwork>` + Public network information. + :param private_net: List[:class:`PrivateNet <hcloud.load_balancers.domain.PrivateNet`] + Private networks information. + :param algorithm: LoadBalancerAlgorithm + The algorithm the Load Balancer is currently using + :param services: List[LoadBalancerService] + The services the LoadBalancer is currently serving + :param targets: LoadBalancerTarget + The targets the LoadBalancer is currently serving + :param load_balancer_type: LoadBalancerType + The type of the Load Balancer + :param outgoing_traffic: int, None + Outbound Traffic for the current billing period in bytes + :param ingoing_traffic: int, None + Inbound Traffic for the current billing period in bytes + :param included_traffic: int + Free Traffic for the current billing period in bytes + """ + + __slots__ = ( + "id", + "name", + "public_net", + "private_net", + "location", + "algorithm", + "services", + "load_balancer_type", + "protection", + "labels", + "targets", + "created", + "outgoing_traffic", + "ingoing_traffic", + "included_traffic", + ) + + # pylint: disable=too-many-locals + def __init__( + self, + id: int, + name: str | None = None, + public_net: PublicNetwork | None = None, + private_net: PrivateNet | None = None, + location: BoundLocation | None = None, + algorithm: LoadBalancerAlgorithm | None = None, + services: list[LoadBalancerService] | None = None, + load_balancer_type: BoundLoadBalancerType | None = None, + protection: dict | None = None, + labels: dict[str, str] | None = None, + targets: list[LoadBalancerTarget] | None = None, + created: str | None = None, + outgoing_traffic: int | None = None, + ingoing_traffic: int | None = None, + included_traffic: int | None = None, + ): + self.id = id + self.name = name + self.created = isoparse(created) if created else None + self.public_net = public_net + self.private_net = private_net + self.location = location + self.algorithm = algorithm + self.services = services + self.load_balancer_type = load_balancer_type + self.targets = targets + self.protection = protection + self.labels = labels + self.outgoing_traffic = outgoing_traffic + self.ingoing_traffic = ingoing_traffic + self.included_traffic = included_traffic + + +class LoadBalancerService(BaseDomain): + """LoadBalancerService Domain + + :param protocol: str + Protocol of the service Choices: tcp, http, https + :param listen_port: int + Required when protocol is tcp, must be unique per Load Balancer. + :param destination_port: int + Required when protocol is tcp + :param proxyprotocol: bool + Enable proxyprotocol + :param health_check: LoadBalancerHealthCheck + Configuration for health checks + :param http: LoadBalancerServiceHttp + Configuration for http/https protocols, required when protocol is http/https + """ + + def __init__( + self, + protocol: str | None = None, + listen_port: int | None = None, + destination_port: int | None = None, + proxyprotocol: bool | None = None, + health_check: LoadBalancerHealthCheck | None = None, + http: LoadBalancerServiceHttp | None = None, + ): + self.protocol = protocol + self.listen_port = listen_port + self.destination_port = destination_port + self.proxyprotocol = proxyprotocol + self.health_check = health_check + self.http = http + + # pylint: disable=too-many-branches + def to_payload(self) -> dict[str, Any]: + """ + Generates the request payload from this domain object. + """ + payload: dict[str, Any] = {} + + if self.protocol is not None: + payload["protocol"] = self.protocol + if self.listen_port is not None: + payload["listen_port"] = self.listen_port + if self.destination_port is not None: + payload["destination_port"] = self.destination_port + if self.proxyprotocol is not None: + payload["proxyprotocol"] = self.proxyprotocol + + if self.http is not None: + http: dict[str, Any] = {} + if self.http.cookie_name is not None: + http["cookie_name"] = self.http.cookie_name + if self.http.cookie_lifetime is not None: + http["cookie_lifetime"] = self.http.cookie_lifetime + if self.http.redirect_http is not None: + http["redirect_http"] = self.http.redirect_http + if self.http.sticky_sessions is not None: + http["sticky_sessions"] = self.http.sticky_sessions + + http["certificates"] = [ + certificate.id for certificate in self.http.certificates or [] + ] + + payload["http"] = http + + if self.health_check is not None: + health_check: dict[str, Any] = { + "protocol": self.health_check.protocol, + "port": self.health_check.port, + "interval": self.health_check.interval, + "timeout": self.health_check.timeout, + "retries": self.health_check.retries, + } + if self.health_check.protocol is not None: + health_check["protocol"] = self.health_check.protocol + if self.health_check.port is not None: + health_check["port"] = self.health_check.port + if self.health_check.interval is not None: + health_check["interval"] = self.health_check.interval + if self.health_check.timeout is not None: + health_check["timeout"] = self.health_check.timeout + if self.health_check.retries is not None: + health_check["retries"] = self.health_check.retries + + if self.health_check.http is not None: + health_check_http: dict[str, Any] = {} + if self.health_check.http.domain is not None: + health_check_http["domain"] = self.health_check.http.domain + if self.health_check.http.path is not None: + health_check_http["path"] = self.health_check.http.path + if self.health_check.http.response is not None: + health_check_http["response"] = self.health_check.http.response + if self.health_check.http.status_codes is not None: + health_check_http[ + "status_codes" + ] = self.health_check.http.status_codes + if self.health_check.http.tls is not None: + health_check_http["tls"] = self.health_check.http.tls + + health_check["http"] = health_check_http + + payload["health_check"] = health_check + return payload + + +class LoadBalancerServiceHttp(BaseDomain): + """LoadBalancerServiceHttp Domain + + :param cookie_name: str + Name of the cookie used for Session Stickness + :param cookie_lifetime: str + Lifetime of the cookie used for Session Stickness + :param certificates: list + IDs of the Certificates to use for TLS/SSL termination by the Load Balancer; empty for TLS/SSL passthrough or if protocol is "http" + :param redirect_http: bool + Redirect traffic from http port 80 to port 443 + :param sticky_sessions: bool + Use sticky sessions. Only available if protocol is "http" or "https". + """ + + def __init__( + self, + cookie_name: str | None = None, + cookie_lifetime: str | None = None, + certificates: list[BoundCertificate] | None = None, + redirect_http: bool | None = None, + sticky_sessions: bool | None = None, + ): + self.cookie_name = cookie_name + self.cookie_lifetime = cookie_lifetime + self.certificates = certificates + self.redirect_http = redirect_http + self.sticky_sessions = sticky_sessions + + +class LoadBalancerHealthCheck(BaseDomain): + """LoadBalancerHealthCheck Domain + + :param protocol: str + Protocol of the service Choices: tcp, http, https + :param port: int + Port the healthcheck will be performed on + :param interval: int + Interval we trigger health check in + :param timeout: int + Timeout in sec after a try is assumed as timeout + :param retries: int + Retries we perform until we assume a target as unhealthy + :param http: LoadBalancerHealtCheckHttp + HTTP Config + """ + + def __init__( + self, + protocol: str | None = None, + port: int | None = None, + interval: int | None = None, + timeout: int | None = None, + retries: int | None = None, + http: LoadBalancerHealtCheckHttp | None = None, + ): + self.protocol = protocol + self.port = port + self.interval = interval + self.timeout = timeout + self.retries = retries + self.http = http + + +class LoadBalancerHealtCheckHttp(BaseDomain): + """LoadBalancerHealtCheckHttp Domain + + :param domain: str + Domain name to send in HTTP request. Can be null: In that case we will not send a domain name + :param path: str + HTTP Path send in Request + :param response: str + Optional HTTP response to receive in order to pass the health check + :param status_codes: list + List of HTTP status codes to receive in order to pass the health check + :param tls: bool + Type of health check + """ + + def __init__( + self, + domain: str | None = None, + path: str | None = None, + response: str | None = None, + status_codes: list | None = None, + tls: bool | None = None, + ): + self.domain = domain + self.path = path + self.response = response + self.status_codes = status_codes + self.tls = tls + + +class LoadBalancerTarget(BaseDomain): + """LoadBalancerTarget Domain + + :param type: str + Type of the resource, can be server or label_selector + :param server: Server + Target server + :param label_selector: LoadBalancerTargetLabelSelector + Target label selector + :param ip: LoadBalancerTargetIP + Target IP + :param use_private_ip: bool + use the private IP instead of primary public IP + :param health_status: list + List of health statuses of the services on this target. Only present for target types "server" and "ip". + """ + + def __init__( + self, + type: str | None = None, + server: BoundServer | None = None, + label_selector: LoadBalancerTargetLabelSelector | None = None, + ip: LoadBalancerTargetIP | None = None, + use_private_ip: bool | None = None, + health_status: list[LoadBalancerTargetHealthStatus] | None = None, + ): + self.type = type + self.server = server + self.label_selector = label_selector + self.ip = ip + self.use_private_ip = use_private_ip + self.health_status = health_status + + def to_payload(self) -> dict[str, Any]: + """ + Generates the request payload from this domain object. + """ + payload: dict[str, Any] = { + "type": self.type, + } + if self.use_private_ip is not None: + payload["use_private_ip"] = self.use_private_ip + + if self.type == "server": + if self.server is None: + raise ValueError(f"server is not defined in target {self!r}") + payload["server"] = {"id": self.server.id} + + elif self.type == "label_selector": + if self.label_selector is None: + raise ValueError(f"label_selector is not defined in target {self!r}") + payload["label_selector"] = {"selector": self.label_selector.selector} + + elif self.type == "ip": + if self.ip is None: + raise ValueError(f"ip is not defined in target {self!r}") + payload["ip"] = {"ip": self.ip.ip} + + return payload + + +class LoadBalancerTargetHealthStatus(BaseDomain): + """LoadBalancerTargetHealthStatus Domain + + :param listen_port: Load Balancer Target listen port + :param status: Load Balancer Target status. Choices: healthy, unhealthy, unknown + """ + + def __init__( + self, + listen_port: int | None = None, + status: str | None = None, + ): + self.listen_port = listen_port + self.status = status + + +class LoadBalancerTargetLabelSelector(BaseDomain): + """LoadBalancerTargetLabelSelector Domain + + :param selector: str Target label selector + """ + + def __init__(self, selector: str | None = None): + self.selector = selector + + +class LoadBalancerTargetIP(BaseDomain): + """LoadBalancerTargetIP Domain + + :param ip: str Target IP + """ + + def __init__(self, ip: str | None = None): + self.ip = ip + + +class LoadBalancerAlgorithm(BaseDomain): + """LoadBalancerAlgorithm Domain + + :param type: str + Algorithm of the Load Balancer. Choices: round_robin, least_connections + """ + + def __init__(self, type: str | None = None): + self.type = type + + +class PublicNetwork(BaseDomain): + """Public Network Domain + + :param ipv4: :class:`IPv4Address <hcloud.load_balancers.domain.IPv4Address>` + :param ipv6: :class:`IPv6Network <hcloud.load_balancers.domain.IPv6Network>` + :param enabled: boolean + """ + + __slots__ = ("ipv4", "ipv6", "enabled") + + def __init__( + self, + ipv4: IPv4Address, + ipv6: IPv6Network, + enabled: bool, + ): + self.ipv4 = ipv4 + self.ipv6 = ipv6 + self.enabled = enabled + + +class IPv4Address(BaseDomain): + """IPv4 Address Domain + + :param ip: str + The IPv4 Address + """ + + __slots__ = ("ip", "dns_ptr") + + def __init__( + self, + ip: str, + dns_ptr: str, + ): + self.ip = ip + self.dns_ptr = dns_ptr + + +class IPv6Network(BaseDomain): + """IPv6 Network Domain + + :param ip: str + The IPv6 Network as CIDR Notation + """ + + __slots__ = ("ip", "dns_ptr") + + def __init__( + self, + ip: str, + dns_ptr: str, + ): + self.ip = ip + self.dns_ptr = dns_ptr + + +class PrivateNet(BaseDomain): + """PrivateNet Domain + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` + The Network the LoadBalancer is attached to + :param ip: str + The main IP Address of the LoadBalancer in the Network + """ + + __slots__ = ("network", "ip") + + def __init__( + self, + network: BoundNetwork, + ip: str, + ): + self.network = network + self.ip = ip + + +class CreateLoadBalancerResponse(BaseDomain): + """Create Load Balancer Response Domain + + :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` + The created Load Balancer + :param action: :class:`BoundAction <hcloud.actions.client.BoundAction>` + Shows the progress of the Load Balancer creation + """ + + __slots__ = ("load_balancer", "action") + + def __init__( + self, + load_balancer: BoundLoadBalancer, + action: BoundAction, + ): + self.load_balancer = load_balancer + self.action = action + + +MetricsType = Literal[ + "open_connections", + "connections_per_second", + "requests_per_second", + "bandwidth", +] + + +class GetMetricsResponse(BaseDomain): + """Get a Load Balancer Metrics Response Domain + + :param metrics: The Load Balancer metrics + """ + + __slots__ = ("metrics",) + + def __init__( + self, + metrics: Metrics, + ): + self.metrics = metrics diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/locations/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/locations/__init__.py new file mode 100644 index 000000000..1c23a5178 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/locations/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .client import BoundLocation, LocationsClient, LocationsPageResult # noqa: F401 +from .domain import Location # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/locations/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/locations/client.py new file mode 100644 index 000000000..047ad9d4b --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/locations/client.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple + +from ..core import BoundModelBase, ClientEntityBase, Meta +from .domain import Location + +if TYPE_CHECKING: + from .._client import Client + + +class BoundLocation(BoundModelBase, Location): + _client: LocationsClient + + model = Location + + +class LocationsPageResult(NamedTuple): + locations: list[BoundLocation] + meta: Meta | None + + +class LocationsClient(ClientEntityBase): + _client: Client + + def get_by_id(self, id: int) -> BoundLocation: + """Get a specific location by its ID. + + :param id: int + :return: :class:`BoundLocation <hcloud.locations.client.BoundLocation>` + """ + response = self._client.request(url=f"/locations/{id}", method="GET") + return BoundLocation(self, response["location"]) + + def get_list( + self, + name: str | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> LocationsPageResult: + """Get a list of locations + + :param name: str (optional) + Can be used to filter locations by their name. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundLocation <hcloud.locations.client.BoundLocation>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if name is not None: + params["name"] = name + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/locations", method="GET", params=params) + locations = [ + BoundLocation(self, location_data) + for location_data in response["locations"] + ] + return LocationsPageResult(locations, Meta.parse_meta(response)) + + def get_all(self, name: str | None = None) -> list[BoundLocation]: + """Get all locations + + :param name: str (optional) + Can be used to filter locations by their name. + :return: List[:class:`BoundLocation <hcloud.locations.client.BoundLocation>`] + """ + return self._iter_pages(self.get_list, name=name) + + def get_by_name(self, name: str) -> BoundLocation | None: + """Get location by name + + :param name: str + Used to get location by name. + :return: :class:`BoundLocation <hcloud.locations.client.BoundLocation>` + """ + return self._get_first_by(name=name) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/locations/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/locations/domain.py new file mode 100644 index 000000000..7d4af2230 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/locations/domain.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from ..core import BaseDomain, DomainIdentityMixin + + +class Location(BaseDomain, DomainIdentityMixin): + """Location Domain + + :param id: int + ID of location + :param name: str + Name of location + :param description: str + Description of location + :param country: str + ISO 3166-1 alpha-2 code of the country the location resides in + :param city: str + City the location is closest to + :param latitude: float + Latitude of the city closest to the location + :param longitude: float + Longitude of the city closest to the location + :param network_zone: str + Name of network zone this location resides in + """ + + __slots__ = ( + "id", + "name", + "description", + "country", + "city", + "latitude", + "longitude", + "network_zone", + ) + + def __init__( + self, + id: int | None = None, + name: str | None = None, + description: str | None = None, + country: str | None = None, + city: str | None = None, + latitude: float | None = None, + longitude: float | None = None, + network_zone: str | None = None, + ): + self.id = id + self.name = name + self.description = description + self.country = country + self.city = city + self.latitude = latitude + self.longitude = longitude + self.network_zone = network_zone diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/metrics/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/metrics/__init__.py new file mode 100644 index 000000000..65d393c8a --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/metrics/__init__.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from .domain import Metrics, TimeSeries # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/metrics/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/metrics/domain.py new file mode 100644 index 000000000..a9e737774 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/metrics/domain.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Dict, List, Literal, Tuple + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core import BaseDomain + +TimeSeries = Dict[str, Dict[Literal["values"], List[Tuple[float, str]]]] + + +class Metrics(BaseDomain): + """Metrics Domain + + :param start: Start of period of metrics reported. + :param end: End of period of metrics reported. + :param step: Resolution of results in seconds. + :param time_series: Dict with time series data, using the name of the time series as + key. The metrics timestamps and values are stored in a list of tuples + ``[(timestamp, value), ...]``. + """ + + start: datetime + end: datetime + step: float + time_series: TimeSeries + + __slots__ = ( + "start", + "end", + "step", + "time_series", + ) + + def __init__( + self, + start: str, + end: str, + step: float, + time_series: TimeSeries, + ): + self.start = isoparse(start) + self.end = isoparse(end) + self.step = step + self.time_series = time_series diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/networks/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/networks/__init__.py new file mode 100644 index 000000000..5bf4a88f0 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/networks/__init__.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from .client import BoundNetwork, NetworksClient, NetworksPageResult # noqa: F401 +from .domain import ( # noqa: F401 + CreateNetworkResponse, + Network, + NetworkRoute, + NetworkSubnet, +) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/networks/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/networks/client.py new file mode 100644 index 000000000..d819d5805 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/networks/client.py @@ -0,0 +1,556 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple + +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient +from ..core import BoundModelBase, ClientEntityBase, Meta +from .domain import Network, NetworkRoute, NetworkSubnet + +if TYPE_CHECKING: + from .._client import Client + + +class BoundNetwork(BoundModelBase, Network): + _client: NetworksClient + + model = Network + + def __init__(self, client: NetworksClient, data: dict, complete: bool = True): + subnets = data.get("subnets", []) + if subnets is not None: + subnets = [NetworkSubnet.from_dict(subnet) for subnet in subnets] + data["subnets"] = subnets + + routes = data.get("routes", []) + if routes is not None: + routes = [NetworkRoute.from_dict(route) for route in routes] + data["routes"] = routes + + # pylint: disable=import-outside-toplevel + from ..servers import BoundServer + + servers = data.get("servers", []) + if servers is not None: + servers = [ + BoundServer(client._client.servers, {"id": server}, complete=False) + for server in servers + ] + data["servers"] = servers + + super().__init__(client, data, complete) + + def update( + self, + name: str | None = None, + expose_routes_to_vswitch: bool | None = None, + labels: dict[str, str] | None = None, + ) -> BoundNetwork: + """Updates a network. You can update a network’s name and a networks’s labels. + + :param name: str (optional) + New name to set + :param expose_routes_to_vswitch: Optional[bool] + Indicates if the routes from this network should be exposed to the vSwitch connection. + The exposing only takes effect if a vSwitch connection is active. + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` + """ + return self._client.update( + self, + name=name, + expose_routes_to_vswitch=expose_routes_to_vswitch, + labels=labels, + ) + + def delete(self) -> bool: + """Deletes a network. + + :return: boolean + """ + return self._client.delete(self) + + def get_actions_list( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Returns all action objects for a network. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + return self._client.get_actions_list(self, status, sort, page, per_page) + + def get_actions( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for a network. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._client.get_actions(self, status, sort) + + def add_subnet(self, subnet: NetworkSubnet) -> BoundAction: + """Adds a subnet entry to a network. + + :param subnet: :class:`NetworkSubnet <hcloud.networks.domain.NetworkSubnet>` + The NetworkSubnet you want to add to the Network + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.add_subnet(self, subnet=subnet) + + def delete_subnet(self, subnet: NetworkSubnet) -> BoundAction: + """Removes a subnet entry from a network + + :param subnet: :class:`NetworkSubnet <hcloud.networks.domain.NetworkSubnet>` + The NetworkSubnet you want to remove from the Network + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.delete_subnet(self, subnet=subnet) + + def add_route(self, route: NetworkRoute) -> BoundAction: + """Adds a route entry to a network. + + :param route: :class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>` + The NetworkRoute you want to add to the Network + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.add_route(self, route=route) + + def delete_route(self, route: NetworkRoute) -> BoundAction: + """Removes a route entry to a network. + + :param route: :class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>` + The NetworkRoute you want to remove from the Network + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.delete_route(self, route=route) + + def change_ip_range(self, ip_range: str) -> BoundAction: + """Changes the IP range of a network. + + :param ip_range: str + The new prefix for the whole network. + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_ip_range(self, ip_range=ip_range) + + def change_protection(self, delete: bool | None = None) -> BoundAction: + """Changes the protection configuration of a network. + + :param delete: boolean + If True, prevents the network from being deleted + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_protection(self, delete=delete) + + +class NetworksPageResult(NamedTuple): + networks: list[BoundNetwork] + meta: Meta | None + + +class NetworksClient(ClientEntityBase): + _client: Client + + actions: ResourceActionsClient + """Networks scoped actions client + + :type: :class:`ResourceActionsClient <hcloud.actions.client.ResourceActionsClient>` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/networks") + + def get_by_id(self, id: int) -> BoundNetwork: + """Get a specific network + + :param id: int + :return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` + """ + response = self._client.request(url=f"/networks/{id}", method="GET") + return BoundNetwork(self, response["network"]) + + def get_list( + self, + name: str | None = None, + label_selector: str | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> NetworksPageResult: + """Get a list of networks from this account + + :param name: str (optional) + Can be used to filter networks by their name. + :param label_selector: str (optional) + Can be used to filter networks by labels. The response will only contain networks matching the label selector. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if name is not None: + params["name"] = name + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/networks", method="GET", params=params) + + networks = [ + BoundNetwork(self, network_data) for network_data in response["networks"] + ] + return NetworksPageResult(networks, Meta.parse_meta(response)) + + def get_all( + self, + name: str | None = None, + label_selector: str | None = None, + ) -> list[BoundNetwork]: + """Get all networks from this account + + :param name: str (optional) + Can be used to filter networks by their name. + :param label_selector: str (optional) + Can be used to filter networks by labels. The response will only contain networks matching the label selector. + :return: List[:class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`] + """ + return self._iter_pages(self.get_list, name=name, label_selector=label_selector) + + def get_by_name(self, name: str) -> BoundNetwork | None: + """Get network by name + + :param name: str + Used to get network by name. + :return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` + """ + return self._get_first_by(name=name) + + def create( + self, + name: str, + ip_range: str, + subnets: list[NetworkSubnet] | None = None, + routes: list[NetworkRoute] | None = None, + expose_routes_to_vswitch: bool | None = None, + labels: dict[str, str] | None = None, + ) -> BoundNetwork: + """Creates a network with range ip_range. + + :param name: str + Name of the network + :param ip_range: str + IP range of the whole network which must span all included subnets and route destinations + :param subnets: List[:class:`NetworkSubnet <hcloud.networks.domain.NetworkSubnet>`] + Array of subnets allocated + :param routes: List[:class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>`] + Array of routes set in this network + :param expose_routes_to_vswitch: Optional[bool] + Indicates if the routes from this network should be exposed to the vSwitch connection. + The exposing only takes effect if a vSwitch connection is active. + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` + """ + data: dict[str, Any] = {"name": name, "ip_range": ip_range} + if subnets is not None: + data_subnets = [] + for subnet in subnets: + data_subnet: dict[str, Any] = { + "type": subnet.type, + "ip_range": subnet.ip_range, + "network_zone": subnet.network_zone, + } + if subnet.vswitch_id is not None: + data_subnet["vswitch_id"] = subnet.vswitch_id + + data_subnets.append(data_subnet) + data["subnets"] = data_subnets + + if routes is not None: + data["routes"] = [ + {"destination": route.destination, "gateway": route.gateway} + for route in routes + ] + + if expose_routes_to_vswitch is not None: + data["expose_routes_to_vswitch"] = expose_routes_to_vswitch + + if labels is not None: + data["labels"] = labels + + response = self._client.request(url="/networks", method="POST", json=data) + + return BoundNetwork(self, response["network"]) + + def update( + self, + network: Network | BoundNetwork, + name: str | None = None, + expose_routes_to_vswitch: bool | None = None, + labels: dict[str, str] | None = None, + ) -> BoundNetwork: + """Updates a network. You can update a network’s name and a network’s labels. + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param name: str (optional) + New name to set + :param expose_routes_to_vswitch: Optional[bool] + Indicates if the routes from this network should be exposed to the vSwitch connection. + The exposing only takes effect if a vSwitch connection is active. + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` + """ + data: dict[str, Any] = {} + if name is not None: + data.update({"name": name}) + + if expose_routes_to_vswitch is not None: + data["expose_routes_to_vswitch"] = expose_routes_to_vswitch + + if labels is not None: + data.update({"labels": labels}) + + response = self._client.request( + url=f"/networks/{network.id}", + method="PUT", + json=data, + ) + return BoundNetwork(self, response["network"]) + + def delete(self, network: Network | BoundNetwork) -> bool: + """Deletes a network. + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :return: boolean + """ + self._client.request(url=f"/networks/{network.id}", method="DELETE") + return True + + def get_actions_list( + self, + network: Network | BoundNetwork, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Returns all action objects for a network. + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url=f"/networks/{network.id}/actions", + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return ActionsPageResult(actions, Meta.parse_meta(response)) + + def get_actions( + self, + network: Network | BoundNetwork, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for a network. + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._iter_pages( + self.get_actions_list, + network, + status=status, + sort=sort, + ) + + def add_subnet( + self, + network: Network | BoundNetwork, + subnet: NetworkSubnet, + ) -> BoundAction: + """Adds a subnet entry to a network. + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param subnet: :class:`NetworkSubnet <hcloud.networks.domain.NetworkSubnet>` + The NetworkSubnet you want to add to the Network + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = { + "type": subnet.type, + "network_zone": subnet.network_zone, + } + if subnet.ip_range is not None: + data["ip_range"] = subnet.ip_range + if subnet.vswitch_id is not None: + data["vswitch_id"] = subnet.vswitch_id + + response = self._client.request( + url=f"/networks/{network.id}/actions/add_subnet", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def delete_subnet( + self, + network: Network | BoundNetwork, + subnet: NetworkSubnet, + ) -> BoundAction: + """Removes a subnet entry from a network + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param subnet: :class:`NetworkSubnet <hcloud.networks.domain.NetworkSubnet>` + The NetworkSubnet you want to remove from the Network + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {"ip_range": subnet.ip_range} + + response = self._client.request( + url=f"/networks/{network.id}/actions/delete_subnet", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def add_route( + self, + network: Network | BoundNetwork, + route: NetworkRoute, + ) -> BoundAction: + """Adds a route entry to a network. + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param route: :class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>` + The NetworkRoute you want to add to the Network + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = { + "destination": route.destination, + "gateway": route.gateway, + } + + response = self._client.request( + url=f"/networks/{network.id}/actions/add_route", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def delete_route( + self, + network: Network | BoundNetwork, + route: NetworkRoute, + ) -> BoundAction: + """Removes a route entry to a network. + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param route: :class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>` + The NetworkRoute you want to remove from the Network + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = { + "destination": route.destination, + "gateway": route.gateway, + } + + response = self._client.request( + url=f"/networks/{network.id}/actions/delete_route", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def change_ip_range( + self, + network: Network | BoundNetwork, + ip_range: str, + ) -> BoundAction: + """Changes the IP range of a network. + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param ip_range: str + The new prefix for the whole network. + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {"ip_range": ip_range} + + response = self._client.request( + url=f"/networks/{network.id}/actions/change_ip_range", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def change_protection( + self, + network: Network | BoundNetwork, + delete: bool | None = None, + ) -> BoundAction: + """Changes the protection configuration of a network. + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param delete: boolean + If True, prevents the network from being deleted + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {} + if delete is not None: + data.update({"delete": delete}) + + response = self._client.request( + url=f"/networks/{network.id}/actions/change_protection", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/networks/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/networks/domain.py new file mode 100644 index 000000000..c307bf989 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/networks/domain.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core import BaseDomain + +if TYPE_CHECKING: + from ..actions import BoundAction + from ..servers import BoundServer + from .client import BoundNetwork + + +class Network(BaseDomain): + """Network Domain + + :param id: int + ID of the network + :param name: str + Name of the network + :param ip_range: str + IPv4 prefix of the whole network + :param subnets: List[:class:`NetworkSubnet <hcloud.networks.domain.NetworkSubnet>`] + Subnets allocated in this network + :param routes: List[:class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>`] + Routes set in this network + :param expose_routes_to_vswitch: bool + Indicates if the routes from this network should be exposed to the vSwitch connection. + :param servers: List[:class:`BoundServer <hcloud.servers.client.BoundServer>`] + Servers attached to this network + :param protection: dict + Protection configuration for the network + :param labels: dict + User-defined labels (key-value pairs) + """ + + __slots__ = ( + "id", + "name", + "ip_range", + "subnets", + "routes", + "expose_routes_to_vswitch", + "servers", + "protection", + "labels", + "created", + ) + + def __init__( + self, + id: int, + name: str | None = None, + created: str | None = None, + ip_range: str | None = None, + subnets: list[NetworkSubnet] | None = None, + routes: list[NetworkRoute] | None = None, + expose_routes_to_vswitch: bool | None = None, + servers: list[BoundServer] | None = None, + protection: dict | None = None, + labels: dict[str, str] | None = None, + ): + self.id = id + self.name = name + self.created = isoparse(created) if created else None + self.ip_range = ip_range + self.subnets = subnets + self.routes = routes + self.expose_routes_to_vswitch = expose_routes_to_vswitch + self.servers = servers + self.protection = protection + self.labels = labels + + +class NetworkSubnet(BaseDomain): + """Network Subnet Domain + + :param type: str + Type of sub network. + :param ip_range: str + Range to allocate IPs from. + :param network_zone: str + Name of network zone. + :param gateway: str + Gateway for the route. + :param vswitch_id: int + ID of the vSwitch. + """ + + TYPE_SERVER = "server" + """Subnet Type server, deprecated, use TYPE_CLOUD instead""" + TYPE_CLOUD = "cloud" + """Subnet Type cloud""" + TYPE_VSWITCH = "vswitch" + """Subnet Type vSwitch""" + __slots__ = ("type", "ip_range", "network_zone", "gateway", "vswitch_id") + + def __init__( + self, + ip_range: str, + type: str | None = None, + network_zone: str | None = None, + gateway: str | None = None, + vswitch_id: int | None = None, + ): + self.type = type + self.ip_range = ip_range + self.network_zone = network_zone + self.gateway = gateway + self.vswitch_id = vswitch_id + + +class NetworkRoute(BaseDomain): + """Network Route Domain + + :param destination: str + Destination network or host of this route. + :param gateway: str + Gateway for the route. + """ + + __slots__ = ("destination", "gateway") + + def __init__(self, destination: str, gateway: str): + self.destination = destination + self.gateway = gateway + + +class CreateNetworkResponse(BaseDomain): + """Create Network Response Domain + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` + The network which was created + :param action: :class:`BoundAction <hcloud.actions.client.BoundAction>` + The Action which shows the progress of the network Creation + """ + + __slots__ = ("network", "action") + + def __init__( + self, + network: BoundNetwork, + action: BoundAction, + ): + self.network = network + self.action = action diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/placement_groups/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/placement_groups/__init__.py new file mode 100644 index 000000000..9c25dd7f6 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/placement_groups/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .client import ( # noqa: F401 + BoundPlacementGroup, + PlacementGroupsClient, + PlacementGroupsPageResult, +) +from .domain import CreatePlacementGroupResponse, PlacementGroup # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/placement_groups/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/placement_groups/client.py new file mode 100644 index 000000000..fcfd86aec --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/placement_groups/client.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple + +from ..actions import BoundAction +from ..core import BoundModelBase, ClientEntityBase, Meta +from .domain import CreatePlacementGroupResponse, PlacementGroup + +if TYPE_CHECKING: + from .._client import Client + + +class BoundPlacementGroup(BoundModelBase, PlacementGroup): + _client: PlacementGroupsClient + + model = PlacementGroup + + def update( + self, + labels: dict[str, str] | None = None, + name: str | None = None, + ) -> BoundPlacementGroup: + """Updates the name or labels of a Placement Group + + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str, (optional) + New Name to set + :return: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` + """ + return self._client.update(self, labels, name) + + def delete(self) -> bool: + """Deletes a Placement Group + + :return: boolean + """ + return self._client.delete(self) + + +class PlacementGroupsPageResult(NamedTuple): + placement_groups: list[BoundPlacementGroup] + meta: Meta | None + + +class PlacementGroupsClient(ClientEntityBase): + _client: Client + + def get_by_id(self, id: int) -> BoundPlacementGroup: + """Returns a specific Placement Group object + + :param id: int + :return: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` + """ + response = self._client.request( + url=f"/placement_groups/{id}", + method="GET", + ) + return BoundPlacementGroup(self, response["placement_group"]) + + def get_list( + self, + label_selector: str | None = None, + page: int | None = None, + per_page: int | None = None, + name: str | None = None, + sort: list[str] | None = None, + type: str | None = None, + ) -> PlacementGroupsPageResult: + """Get a list of Placement Groups + + :param label_selector: str (optional) + Can be used to filter Placement Groups by labels. The response will only contain Placement Groups matching the label selector values. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :param name: str (optional) + Can be used to filter Placement Groups by their name. + :param sort: List[str] (optional) + Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) + :return: (List[:class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + + params: dict[str, Any] = {} + + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + if name is not None: + params["name"] = name + if sort is not None: + params["sort"] = sort + if type is not None: + params["type"] = type + response = self._client.request( + url="/placement_groups", method="GET", params=params + ) + placement_groups = [ + BoundPlacementGroup(self, placement_group_data) + for placement_group_data in response["placement_groups"] + ] + + return PlacementGroupsPageResult(placement_groups, Meta.parse_meta(response)) + + def get_all( + self, + label_selector: str | None = None, + name: str | None = None, + sort: list[str] | None = None, + ) -> list[BoundPlacementGroup]: + """Get all Placement Groups + + :param label_selector: str (optional) + Can be used to filter Placement Groups by labels. The response will only contain Placement Groups matching the label selector values. + :param name: str (optional) + Can be used to filter Placement Groups by their name. + :param sort: List[str] (optional) + Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) + :return: List[:class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>`] + """ + return self._iter_pages( + self.get_list, + label_selector=label_selector, + name=name, + sort=sort, + ) + + def get_by_name(self, name: str) -> BoundPlacementGroup | None: + """Get Placement Group by name + + :param name: str + Used to get Placement Group by name + :return: class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` + """ + return self._get_first_by(name=name) + + def create( + self, + name: str, + type: str, + labels: dict[str, str] | None = None, + ) -> CreatePlacementGroupResponse: + """Creates a new Placement Group. + + :param name: str + Placement Group Name + :param type: str + Type of the Placement Group + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + + :return: :class:`CreatePlacementGroupResponse <hcloud.placement_groups.domain.CreatePlacementGroupResponse>` + """ + data: dict[str, Any] = {"name": name, "type": type} + if labels is not None: + data["labels"] = labels + response = self._client.request( + url="/placement_groups", json=data, method="POST" + ) + + action = None + if response.get("action") is not None: + action = BoundAction(self._client.actions, response["action"]) + + result = CreatePlacementGroupResponse( + placement_group=BoundPlacementGroup(self, response["placement_group"]), + action=action, + ) + return result + + def update( + self, + placement_group: PlacementGroup | BoundPlacementGroup, + labels: dict[str, str] | None = None, + name: str | None = None, + ) -> BoundPlacementGroup: + """Updates the description or labels of a Placement Group. + + :param placement_group: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` or :class:`PlacementGroup <hcloud.placement_groups.domain.PlacementGroup>` + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New name to set + :return: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` + """ + + data: dict[str, Any] = {} + if labels is not None: + data["labels"] = labels + if name is not None: + data["name"] = name + + response = self._client.request( + url=f"/placement_groups/{placement_group.id}", + method="PUT", + json=data, + ) + return BoundPlacementGroup(self, response["placement_group"]) + + def delete(self, placement_group: PlacementGroup | BoundPlacementGroup) -> bool: + """Deletes a Placement Group. + + :param placement_group: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` or :class:`PlacementGroup <hcloud.placement_groups.domain.PlacementGroup>` + :return: boolean + """ + self._client.request( + url=f"/placement_groups/{placement_group.id}", + method="DELETE", + ) + return True diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/placement_groups/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/placement_groups/domain.py new file mode 100644 index 000000000..16b2a390d --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/placement_groups/domain.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core import BaseDomain + +if TYPE_CHECKING: + from ..actions import BoundAction + from .client import BoundPlacementGroup + + +class PlacementGroup(BaseDomain): + """Placement Group Domain + + :param id: int + ID of the Placement Group + :param name: str + Name of the Placement Group + :param labels: dict + User-defined labels (key-value pairs) + :param servers: List[ int ] + List of server IDs assigned to the Placement Group + :param type: str + Type of the Placement Group + :param created: datetime + Point in time when the image was created + """ + + __slots__ = ("id", "name", "labels", "servers", "type", "created") + + """Placement Group type spread + spreads all servers in the group on different vhosts + """ + TYPE_SPREAD = "spread" + + def __init__( + self, + id: int | None = None, + name: str | None = None, + labels: dict[str, str] | None = None, + servers: list[int] | None = None, + type: str | None = None, + created: str | None = None, + ): + self.id = id + self.name = name + self.labels = labels + self.servers = servers + self.type = type + self.created = isoparse(created) if created else None + + +class CreatePlacementGroupResponse(BaseDomain): + """Create Placement Group Response Domain + + :param placement_group: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` + The Placement Group which was created + :param action: :class:`BoundAction <hcloud.actions.client.BoundAction>` + The Action which shows the progress of the Placement Group Creation + """ + + __slots__ = ("placement_group", "action") + + def __init__( + self, + placement_group: BoundPlacementGroup, + action: BoundAction | None, + ): + self.placement_group = placement_group + self.action = action diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/primary_ips/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/primary_ips/__init__.py new file mode 100644 index 000000000..d079a23b1 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/primary_ips/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .client import BoundPrimaryIP, PrimaryIPsClient, PrimaryIPsPageResult # noqa: F401 +from .domain import CreatePrimaryIPResponse, PrimaryIP # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/primary_ips/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/primary_ips/client.py new file mode 100644 index 000000000..ece8d88f4 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/primary_ips/client.py @@ -0,0 +1,361 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple + +from ..actions import BoundAction, ResourceActionsClient +from ..core import BoundModelBase, ClientEntityBase, Meta +from .domain import CreatePrimaryIPResponse, PrimaryIP + +if TYPE_CHECKING: + from .._client import Client + from ..datacenters import BoundDatacenter, Datacenter + + +class BoundPrimaryIP(BoundModelBase, PrimaryIP): + _client: PrimaryIPsClient + + model = PrimaryIP + + def __init__(self, client: PrimaryIPsClient, data: dict, complete: bool = True): + # pylint: disable=import-outside-toplevel + from ..datacenters import BoundDatacenter + + datacenter = data.get("datacenter", {}) + if datacenter: + data["datacenter"] = BoundDatacenter(client._client.datacenters, datacenter) + + super().__init__(client, data, complete) + + def update( + self, + auto_delete: bool | None = None, + labels: dict[str, str] | None = None, + name: str | None = None, + ) -> BoundPrimaryIP: + """Updates the description or labels of a Primary IP. + + :param auto_delete: bool (optional) + Auto delete IP when assignee gets deleted + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New Name to set + :return: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` + """ + return self._client.update( + self, auto_delete=auto_delete, labels=labels, name=name + ) + + def delete(self) -> bool: + """Deletes a Primary IP. If it is currently assigned to a server it will automatically get unassigned. + + :return: boolean + """ + return self._client.delete(self) + + def change_protection(self, delete: bool | None = None) -> BoundAction: + """Changes the protection configuration of the Primary IP. + + :param delete: boolean + If true, prevents the Primary IP from being deleted + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_protection(self, delete) + + def assign(self, assignee_id: int, assignee_type: str) -> BoundAction: + """Assigns a Primary IP to a assignee. + + :param assignee_id: int` + Id of an assignee the Primary IP shall be assigned to + :param assignee_type: string` + Assignee type (e.g server) the Primary IP shall be assigned to + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.assign(self, assignee_id, assignee_type) + + def unassign(self) -> BoundAction: + """Unassigns a Primary IP, resulting in it being unreachable. You may assign it to a server again at a later time. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.unassign(self) + + def change_dns_ptr(self, ip: str, dns_ptr: str) -> BoundAction: + """Changes the hostname that will appear when getting the hostname belonging to this Primary IP. + + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: str + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_dns_ptr(self, ip, dns_ptr) + + +class PrimaryIPsPageResult(NamedTuple): + primary_ips: list[BoundPrimaryIP] + meta: Meta | None + + +class PrimaryIPsClient(ClientEntityBase): + _client: Client + + actions: ResourceActionsClient + """Primary IPs scoped actions client + + :type: :class:`ResourceActionsClient <hcloud.actions.client.ResourceActionsClient>` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/primary_ips") + + def get_by_id(self, id: int) -> BoundPrimaryIP: + """Returns a specific Primary IP object. + + :param id: int + :return: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` + """ + response = self._client.request(url=f"/primary_ips/{id}", method="GET") + return BoundPrimaryIP(self, response["primary_ip"]) + + def get_list( + self, + label_selector: str | None = None, + page: int | None = None, + per_page: int | None = None, + name: str | None = None, + ip: str | None = None, + ) -> PrimaryIPsPageResult: + """Get a list of primary ips from this account + + :param label_selector: str (optional) + Can be used to filter Primary IPs by labels. The response will only contain Primary IPs matching the label selectorable values. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :param name: str (optional) + Can be used to filter networks by their name. + :param ip: str (optional) + Can be used to filter resources by their ip. The response will only contain the resources matching the specified ip. + :return: (List[:class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + if name is not None: + params["name"] = name + if ip is not None: + params["ip"] = ip + + response = self._client.request(url="/primary_ips", method="GET", params=params) + primary_ips = [ + BoundPrimaryIP(self, primary_ip_data) + for primary_ip_data in response["primary_ips"] + ] + + return PrimaryIPsPageResult(primary_ips, Meta.parse_meta(response)) + + def get_all( + self, + label_selector: str | None = None, + name: str | None = None, + ) -> list[BoundPrimaryIP]: + """Get all primary ips from this account + + :param label_selector: str (optional) + Can be used to filter Primary IPs by labels. The response will only contain Primary IPs matching the label selector.able values. + :param name: str (optional) + Can be used to filter networks by their name. + :return: List[:class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`] + """ + return self._iter_pages(self.get_list, label_selector=label_selector, name=name) + + def get_by_name(self, name: str) -> BoundPrimaryIP | None: + """Get Primary IP by name + + :param name: str + Used to get Primary IP by name. + :return: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` + """ + return self._get_first_by(name=name) + + def create( + self, + type: str, + # TODO: Make the datacenter argument optional + datacenter: Datacenter | BoundDatacenter | None, + name: str, + assignee_type: str | None = "server", + assignee_id: int | None = None, + auto_delete: bool | None = False, + labels: dict | None = None, + ) -> CreatePrimaryIPResponse: + """Creates a new Primary IP assigned to a server. + + :param type: str + Primary IP type Choices: ipv4, ipv6 + :param assignee_type: str + :param assignee_id: int (optional) + :param datacenter: Datacenter + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str + :param auto_delete: bool (optional) + :return: :class:`CreatePrimaryIPResponse <hcloud.primary_ips.domain.CreatePrimaryIPResponse>` + """ + + data: dict[str, Any] = { + "type": type, + "assignee_type": assignee_type, + "auto_delete": auto_delete, + "name": name, + } + if datacenter is not None: + data["datacenter"] = datacenter.id_or_name + if assignee_id is not None: + data["assignee_id"] = assignee_id + if labels is not None: + data["labels"] = labels + + response = self._client.request(url="/primary_ips", json=data, method="POST") + + action = None + if response.get("action") is not None: + action = BoundAction(self._client.actions, response["action"]) + + result = CreatePrimaryIPResponse( + primary_ip=BoundPrimaryIP(self, response["primary_ip"]), action=action + ) + return result + + def update( + self, + primary_ip: PrimaryIP | BoundPrimaryIP, + auto_delete: bool | None = None, + labels: dict[str, str] | None = None, + name: str | None = None, + ) -> BoundPrimaryIP: + """Updates the name, auto_delete or labels of a Primary IP. + + :param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>` + :param auto_delete: bool (optional) + Delete this Primary IP when the resource it is assigned to is deleted + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New name to set + :return: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` + """ + data: dict[str, Any] = {} + if auto_delete is not None: + data["auto_delete"] = auto_delete + if labels is not None: + data["labels"] = labels + if name is not None: + data["name"] = name + + response = self._client.request( + url=f"/primary_ips/{primary_ip.id}", + method="PUT", + json=data, + ) + return BoundPrimaryIP(self, response["primary_ip"]) + + def delete(self, primary_ip: PrimaryIP | BoundPrimaryIP) -> bool: + """Deletes a Primary IP. If it is currently assigned to an assignee it will automatically get unassigned. + + :param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>` + :return: boolean + """ + self._client.request( + url=f"/primary_ips/{primary_ip.id}", + method="DELETE", + ) + # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised + return True + + def change_protection( + self, + primary_ip: PrimaryIP | BoundPrimaryIP, + delete: bool | None = None, + ) -> BoundAction: + """Changes the protection configuration of the Primary IP. + + :param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>` + :param delete: boolean + If true, prevents the Primary IP from being deleted + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {} + if delete is not None: + data.update({"delete": delete}) + + response = self._client.request( + url=f"/primary_ips/{primary_ip.id}/actions/change_protection", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def assign( + self, + primary_ip: PrimaryIP | BoundPrimaryIP, + assignee_id: int, + assignee_type: str = "server", + ) -> BoundAction: + """Assigns a Primary IP to a assignee_id. + + :param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>` + :param assignee_id: int + Assignee the Primary IP shall be assigned to + :param assignee_type: str + Assignee the Primary IP shall be assigned to + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/primary_ips/{primary_ip.id}/actions/assign", + method="POST", + json={"assignee_id": assignee_id, "assignee_type": assignee_type}, + ) + return BoundAction(self._client.actions, response["action"]) + + def unassign(self, primary_ip: PrimaryIP | BoundPrimaryIP) -> BoundAction: + """Unassigns a Primary IP, resulting in it being unreachable. You may assign it to a server again at a later time. + + :param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/primary_ips/{primary_ip.id}/actions/unassign", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def change_dns_ptr( + self, + primary_ip: PrimaryIP | BoundPrimaryIP, + ip: str, + dns_ptr: str, + ) -> BoundAction: + """Changes the dns ptr that will appear when getting the dns ptr belonging to this Primary IP. + + :param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>` + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: str + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/primary_ips/{primary_ip.id}/actions/change_dns_ptr", + method="POST", + json={"ip": ip, "dns_ptr": dns_ptr}, + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/primary_ips/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/primary_ips/domain.py new file mode 100644 index 000000000..aeb943f0a --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/primary_ips/domain.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core import BaseDomain + +if TYPE_CHECKING: + from ..actions import BoundAction + from ..datacenters import BoundDatacenter + from .client import BoundPrimaryIP + + +class PrimaryIP(BaseDomain): + """Primary IP Domain + + :param id: int + ID of the Primary IP + :param ip: str + IP address of the Primary IP + :param type: str + Type of Primary IP. Choices: `ipv4`, `ipv6` + :param dns_ptr: List[Dict] + Array of reverse DNS entries + :param datacenter: :class:`Datacenter <hcloud.datacenters.client.BoundDatacenter>` + Datacenter the Primary IP was created in. + :param blocked: boolean + Whether the IP is blocked + :param protection: dict + Protection configuration for the Primary IP + :param labels: dict + User-defined labels (key-value pairs) + :param created: datetime + Point in time when the Primary IP was created + :param name: str + Name of the Primary IP + :param assignee_id: int + Assignee ID the Primary IP is assigned to + :param assignee_type: str + Assignee Type of entity the Primary IP is assigned to + :param auto_delete: bool + Delete the Primary IP when the Assignee it is assigned to is deleted. + """ + + __slots__ = ( + "id", + "ip", + "type", + "dns_ptr", + "datacenter", + "blocked", + "protection", + "labels", + "created", + "name", + "assignee_id", + "assignee_type", + "auto_delete", + ) + + def __init__( + self, + id: int | None = None, + type: str | None = None, + ip: str | None = None, + dns_ptr: list[dict] | None = None, + datacenter: BoundDatacenter | None = None, + blocked: bool | None = None, + protection: dict | None = None, + labels: dict[str, dict] | None = None, + created: str | None = None, + name: str | None = None, + assignee_id: int | None = None, + assignee_type: str | None = None, + auto_delete: bool | None = None, + ): + self.id = id + self.type = type + self.ip = ip + self.dns_ptr = dns_ptr + self.datacenter = datacenter + self.blocked = blocked + self.protection = protection + self.labels = labels + self.created = isoparse(created) if created else None + self.name = name + self.assignee_id = assignee_id + self.assignee_type = assignee_type + self.auto_delete = auto_delete + + +class CreatePrimaryIPResponse(BaseDomain): + """Create Primary IP Response Domain + + :param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` + The Primary IP which was created + :param action: :class:`BoundAction <hcloud.actions.client.BoundAction>` + The Action which shows the progress of the Primary IP Creation + """ + + __slots__ = ("primary_ip", "action") + + def __init__( + self, + primary_ip: BoundPrimaryIP, + action: BoundAction | None, + ): + self.primary_ip = primary_ip + self.action = action diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/py.typed b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/py.typed new file mode 100644 index 000000000..1242d4327 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/server_types/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/server_types/__init__.py new file mode 100644 index 000000000..1e978d094 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/server_types/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .client import ( # noqa: F401 + BoundServerType, + ServerTypesClient, + ServerTypesPageResult, +) +from .domain import ServerType # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/server_types/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/server_types/client.py new file mode 100644 index 000000000..31f56a20c --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/server_types/client.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple + +from ..core import BoundModelBase, ClientEntityBase, Meta +from .domain import ServerType + +if TYPE_CHECKING: + from .._client import Client + + +class BoundServerType(BoundModelBase, ServerType): + _client: ServerTypesClient + + model = ServerType + + +class ServerTypesPageResult(NamedTuple): + server_types: list[BoundServerType] + meta: Meta | None + + +class ServerTypesClient(ClientEntityBase): + _client: Client + + def get_by_id(self, id: int) -> BoundServerType: + """Returns a specific Server Type. + + :param id: int + :return: :class:`BoundServerType <hcloud.server_types.client.BoundServerType>` + """ + response = self._client.request(url=f"/server_types/{id}", method="GET") + return BoundServerType(self, response["server_type"]) + + def get_list( + self, + name: str | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ServerTypesPageResult: + """Get a list of Server types + + :param name: str (optional) + Can be used to filter server type by their name. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundServerType <hcloud.server_types.client.BoundServerType>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if name is not None: + params["name"] = name + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url="/server_types", method="GET", params=params + ) + server_types = [ + BoundServerType(self, server_type_data) + for server_type_data in response["server_types"] + ] + return ServerTypesPageResult(server_types, Meta.parse_meta(response)) + + def get_all(self, name: str | None = None) -> list[BoundServerType]: + """Get all Server types + + :param name: str (optional) + Can be used to filter server type by their name. + :return: List[:class:`BoundServerType <hcloud.server_types.client.BoundServerType>`] + """ + return self._iter_pages(self.get_list, name=name) + + def get_by_name(self, name: str) -> BoundServerType | None: + """Get Server type by name + + :param name: str + Used to get Server type by name. + :return: :class:`BoundServerType <hcloud.server_types.client.BoundServerType>` + """ + return self._get_first_by(name=name) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/server_types/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/server_types/domain.py new file mode 100644 index 000000000..ab2553b10 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/server_types/domain.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from ..core import BaseDomain, DomainIdentityMixin +from ..deprecation import DeprecationInfo + + +class ServerType(BaseDomain, DomainIdentityMixin): + """ServerType Domain + + :param id: int + ID of the server type + :param name: str + Unique identifier of the server type + :param description: str + Description of the server type + :param cores: int + Number of cpu cores a server of this type will have + :param memory: int + Memory a server of this type will have in GB + :param disk: int + Disk size a server of this type will have in GB + :param prices: Dict + Prices in different locations + :param storage_type: str + Type of server boot drive. Local has higher speed. Network has better availability. Choices: `local`, `network` + :param cpu_type: string + Type of cpu. Choices: `shared`, `dedicated` + :param architecture: string + Architecture of cpu. Choices: `x86`, `arm` + :param deprecated: bool + True if server type is deprecated. This field is deprecated. Use `deprecation` instead. + :param deprecation: :class:`DeprecationInfo <hcloud.deprecation.domain.DeprecationInfo>`, None + Describes if, when & how the resources was deprecated. If this field is set to None the resource is not + deprecated. If it has a value, it is considered deprecated. + :param included_traffic: int + Free traffic per month in bytes + """ + + __slots__ = ( + "id", + "name", + "description", + "cores", + "memory", + "disk", + "prices", + "storage_type", + "cpu_type", + "architecture", + "deprecated", + "deprecation", + "included_traffic", + ) + + def __init__( + self, + id: int | None = None, + name: str | None = None, + description: str | None = None, + cores: int | None = None, + memory: int | None = None, + disk: int | None = None, + prices: dict | None = None, + storage_type: str | None = None, + cpu_type: str | None = None, + architecture: str | None = None, + deprecated: bool | None = None, + deprecation: dict | None = None, + included_traffic: int | None = None, + ): + self.id = id + self.name = name + self.description = description + self.cores = cores + self.memory = memory + self.disk = disk + self.prices = prices + self.storage_type = storage_type + self.cpu_type = cpu_type + self.architecture = architecture + self.deprecated = deprecated + self.deprecation = ( + DeprecationInfo.from_dict(deprecation) if deprecation is not None else None + ) + self.included_traffic = included_traffic diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/servers/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/servers/__init__.py new file mode 100644 index 000000000..58c811e51 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/servers/__init__.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from .client import BoundServer, ServersClient, ServersPageResult # noqa: F401 +from .domain import ( # noqa: F401 + CreateServerResponse, + EnableRescueResponse, + GetMetricsResponse, + IPv4Address, + IPv6Network, + PrivateNet, + PublicNetwork, + PublicNetworkFirewall, + RequestConsoleResponse, + ResetPasswordResponse, + Server, + ServerCreatePublicNetwork, +) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/servers/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/servers/client.py new file mode 100644 index 000000000..b959b9d87 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/servers/client.py @@ -0,0 +1,1265 @@ +from __future__ import annotations + +import warnings +from datetime import datetime +from typing import TYPE_CHECKING, Any, NamedTuple + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient +from ..core import BoundModelBase, ClientEntityBase, Meta +from ..datacenters import BoundDatacenter +from ..firewalls import BoundFirewall +from ..floating_ips import BoundFloatingIP +from ..images import BoundImage, CreateImageResponse +from ..isos import BoundIso +from ..metrics import Metrics +from ..placement_groups import BoundPlacementGroup +from ..primary_ips import BoundPrimaryIP +from ..server_types import BoundServerType +from ..volumes import BoundVolume +from .domain import ( + CreateServerResponse, + EnableRescueResponse, + GetMetricsResponse, + IPv4Address, + IPv6Network, + MetricsType, + PrivateNet, + PublicNetwork, + PublicNetworkFirewall, + RebuildResponse, + RequestConsoleResponse, + ResetPasswordResponse, + Server, +) + +if TYPE_CHECKING: + from .._client import Client + from ..datacenters import Datacenter + from ..firewalls import Firewall + from ..images import Image + from ..isos import Iso + from ..locations import BoundLocation, Location + from ..networks import BoundNetwork, Network + from ..placement_groups import PlacementGroup + from ..server_types import ServerType + from ..ssh_keys import BoundSSHKey, SSHKey + from ..volumes import Volume + from .domain import ServerCreatePublicNetwork + + +class BoundServer(BoundModelBase, Server): + _client: ServersClient + + model = Server + + # pylint: disable=too-many-locals + def __init__(self, client: ServersClient, data: dict, complete: bool = True): + datacenter = data.get("datacenter") + if datacenter is not None: + data["datacenter"] = BoundDatacenter(client._client.datacenters, datacenter) + + volumes = data.get("volumes", []) + if volumes: + volumes = [ + BoundVolume(client._client.volumes, {"id": volume}, complete=False) + for volume in volumes + ] + data["volumes"] = volumes + + image = data.get("image", None) + if image is not None: + data["image"] = BoundImage(client._client.images, image) + + iso = data.get("iso", None) + if iso is not None: + data["iso"] = BoundIso(client._client.isos, iso) + + server_type = data.get("server_type") + if server_type is not None: + data["server_type"] = BoundServerType( + client._client.server_types, server_type + ) + + public_net = data.get("public_net") + if public_net: + ipv4_address = ( + IPv4Address.from_dict(public_net["ipv4"]) + if public_net["ipv4"] is not None + else None + ) + ipv4_primary_ip = ( + BoundPrimaryIP( + client._client.primary_ips, + {"id": public_net["ipv4"]["id"]}, + complete=False, + ) + if public_net["ipv4"] is not None + else None + ) + ipv6_network = ( + IPv6Network.from_dict(public_net["ipv6"]) + if public_net["ipv6"] is not None + else None + ) + ipv6_primary_ip = ( + BoundPrimaryIP( + client._client.primary_ips, + {"id": public_net["ipv6"]["id"]}, + complete=False, + ) + if public_net["ipv6"] is not None + else None + ) + floating_ips = [ + BoundFloatingIP( + client._client.floating_ips, {"id": floating_ip}, complete=False + ) + for floating_ip in public_net["floating_ips"] + ] + firewalls = [ + PublicNetworkFirewall( + BoundFirewall( + client._client.firewalls, {"id": firewall["id"]}, complete=False + ), + status=firewall["status"], + ) + for firewall in public_net.get("firewalls", []) + ] + data["public_net"] = PublicNetwork( + ipv4=ipv4_address, + ipv6=ipv6_network, + primary_ipv4=ipv4_primary_ip, + primary_ipv6=ipv6_primary_ip, + floating_ips=floating_ips, + firewalls=firewalls, + ) + + private_nets = data.get("private_net") + if private_nets: + # pylint: disable=import-outside-toplevel + from ..networks import BoundNetwork + + private_nets = [ + PrivateNet( + network=BoundNetwork( + client._client.networks, + {"id": private_net["network"]}, + complete=False, + ), + ip=private_net["ip"], + alias_ips=private_net["alias_ips"], + mac_address=private_net["mac_address"], + ) + for private_net in private_nets + ] + data["private_net"] = private_nets + + placement_group = data.get("placement_group") + if placement_group: + placement_group = BoundPlacementGroup( + client._client.placement_groups, placement_group + ) + data["placement_group"] = placement_group + + super().__init__(client, data, complete) + + def get_actions_list( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Returns all action objects for a server. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + return self._client.get_actions_list(self, status, sort, page, per_page) + + def get_actions( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for a server. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._client.get_actions(self, status, sort) + + def update( + self, + name: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundServer: + """Updates a server. You can update a server’s name and a server’s labels. + + :param name: str (optional) + New name to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundServer <hcloud.servers.client.BoundServer>` + """ + return self._client.update(self, name, labels) + + def get_metrics( + self, + type: MetricsType | list[MetricsType], + start: datetime | str, + end: datetime | str, + step: float | None = None, + ) -> GetMetricsResponse: + """Get Metrics for a Server. + + :param server: The Server to get the metrics for. + :param type: Type of metrics to get. + :param start: Start of period to get Metrics for (in ISO-8601 format). + :param end: End of period to get Metrics for (in ISO-8601 format). + :param step: Resolution of results in seconds. + """ + return self._client.get_metrics( + self, + type=type, + start=start, + end=end, + step=step, + ) + + def delete(self) -> BoundAction: + """Deletes a server. This immediately removes the server from your account, and it is no longer accessible. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.delete(self) + + def power_off(self) -> BoundAction: + """Cuts power to the server. This forcefully stops it without giving the server operating system time to gracefully stop + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.power_off(self) + + def power_on(self) -> BoundAction: + """Starts a server by turning its power on. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.power_on(self) + + def reboot(self) -> BoundAction: + """Reboots a server gracefully by sending an ACPI request. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.reboot(self) + + def reset(self) -> BoundAction: + """Cuts power to a server and starts it again. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.reset(self) + + def shutdown(self) -> BoundAction: + """Shuts down a server gracefully by sending an ACPI shutdown request. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.shutdown(self) + + def reset_password(self) -> ResetPasswordResponse: + """Resets the root password. Only works for Linux systems that are running the qemu guest agent. + + :return: :class:`ResetPasswordResponse <hcloud.servers.domain.ResetPasswordResponse>` + """ + return self._client.reset_password(self) + + def enable_rescue( + self, + type: str | None = None, + ssh_keys: list[str] | None = None, + ) -> EnableRescueResponse: + """Enable the Hetzner Rescue System for this server. + + :param type: str + Type of rescue system to boot (default: linux64) + Choices: linux64, linux32, freebsd64 + :param ssh_keys: List[str] + Array of SSH key IDs which should be injected into the rescue system. Only available for types: linux64 and linux32. + :return: :class:`EnableRescueResponse <hcloud.servers.domain.EnableRescueResponse>` + """ + return self._client.enable_rescue(self, type=type, ssh_keys=ssh_keys) + + def disable_rescue(self) -> BoundAction: + """Disables the Hetzner Rescue System for a server. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.disable_rescue(self) + + def create_image( + self, + description: str | None = None, + type: str | None = None, + labels: dict[str, str] | None = None, + ) -> CreateImageResponse: + """Creates an image (snapshot) from a server by copying the contents of its disks. + + :param description: str (optional) + Description of the image. If you do not set this we auto-generate one for you. + :param type: str (optional) + Type of image to create (default: snapshot) + Choices: snapshot, backup + :param labels: Dict[str, str] + User-defined labels (key-value pairs) + :return: :class:`CreateImageResponse <hcloud.images.domain.CreateImageResponse>` + """ + return self._client.create_image(self, description, type, labels) + + def rebuild( + self, + image: Image | BoundImage, + *, + return_response: bool = False, + ) -> RebuildResponse | BoundAction: + """Rebuilds a server overwriting its disk with the content of an image, thereby destroying all data on the target server. + + :param image: Image to use for the rebuilt server + :param return_response: Whether to return the full response or only the action. + """ + return self._client.rebuild(self, image, return_response=return_response) + + def change_type( + self, + server_type: ServerType | BoundServerType, + upgrade_disk: bool, + ) -> BoundAction: + """Changes the type (Cores, RAM and disk sizes) of a server. + + :param server_type: :class:`BoundServerType <hcloud.server_types.client.BoundServerType>` or :class:`ServerType <hcloud.server_types.domain.ServerType>` + Server type the server should migrate to + :param upgrade_disk: boolean + If false, do not upgrade the disk. This allows downgrading the server type later. + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_type(self, server_type, upgrade_disk) + + def enable_backup(self) -> BoundAction: + """Enables and configures the automatic daily backup option for the server. Enabling automatic backups will increase the price of the server by 20%. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.enable_backup(self) + + def disable_backup(self) -> BoundAction: + """Disables the automatic backup option and deletes all existing Backups for a Server. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.disable_backup(self) + + def attach_iso(self, iso: Iso | BoundIso) -> BoundAction: + """Attaches an ISO to a server. + + :param iso: :class:`BoundIso <hcloud.isos.client.BoundIso>` or :class:`Server <hcloud.isos.domain.Iso>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.attach_iso(self, iso) + + def detach_iso(self) -> BoundAction: + """Detaches an ISO from a server. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.detach_iso(self) + + def change_dns_ptr(self, ip: str, dns_ptr: str | None) -> BoundAction: + """Changes the hostname that will appear when getting the hostname belonging to the primary IPs (ipv4 and ipv6) of this server. + + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_dns_ptr(self, ip, dns_ptr) + + def change_protection( + self, + delete: bool | None = None, + rebuild: bool | None = None, + ) -> BoundAction: + """Changes the protection configuration of the server. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param delete: boolean + If true, prevents the server from being deleted (currently delete and rebuild attribute needs to have the same value) + :param rebuild: boolean + If true, prevents the server from being rebuilt (currently delete and rebuild attribute needs to have the same value) + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_protection(self, delete, rebuild) + + def request_console(self) -> RequestConsoleResponse: + """Requests credentials for remote access via vnc over websocket to keyboard, monitor, and mouse for a server. + + :return: :class:`RequestConsoleResponse <hcloud.servers.domain.RequestConsoleResponse>` + """ + return self._client.request_console(self) + + def attach_to_network( + self, + network: Network | BoundNetwork, + ip: str | None = None, + alias_ips: list[str] | None = None, + ) -> BoundAction: + """Attaches a server to a network + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param ip: str + IP to request to be assigned to this server + :param alias_ips: List[str] + New alias IPs to set for this server. + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.attach_to_network(self, network, ip, alias_ips) + + def detach_from_network(self, network: Network | BoundNetwork) -> BoundAction: + """Detaches a server from a network. + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.detach_from_network(self, network) + + def change_alias_ips( + self, + network: Network | BoundNetwork, + alias_ips: list[str], + ) -> BoundAction: + """Changes the alias IPs of an already attached network. + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param alias_ips: List[str] + New alias IPs to set for this server. + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_alias_ips(self, network, alias_ips) + + def add_to_placement_group( + self, + placement_group: PlacementGroup | BoundPlacementGroup, + ) -> BoundAction: + """Adds a server to a placement group. + + :param placement_group: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` or :class:`Network <hcloud.placement_groups.domain.PlacementGroup>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.add_to_placement_group(self, placement_group) + + def remove_from_placement_group(self) -> BoundAction: + """Removes a server from a placement group. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.remove_from_placement_group(self) + + +class ServersPageResult(NamedTuple): + servers: list[BoundServer] + meta: Meta | None + + +class ServersClient(ClientEntityBase): + _client: Client + + actions: ResourceActionsClient + """Servers scoped actions client + + :type: :class:`ResourceActionsClient <hcloud.actions.client.ResourceActionsClient>` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/servers") + + def get_by_id(self, id: int) -> BoundServer: + """Get a specific server + + :param id: int + :return: :class:`BoundServer <hcloud.servers.client.BoundServer>` + """ + response = self._client.request(url=f"/servers/{id}", method="GET") + return BoundServer(self, response["server"]) + + def get_list( + self, + name: str | None = None, + label_selector: str | None = None, + page: int | None = None, + per_page: int | None = None, + status: list[str] | None = None, + ) -> ServersPageResult: + """Get a list of servers from this account + + :param name: str (optional) + Can be used to filter servers by their name. + :param label_selector: str (optional) + Can be used to filter servers by labels. The response will only contain servers matching the label selector. + :param status: List[str] (optional) + Can be used to filter servers by their status. The response will only contain servers matching the status. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundServer <hcloud.servers.client.BoundServer>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if name is not None: + params["name"] = name + if label_selector is not None: + params["label_selector"] = label_selector + if status is not None: + params["status"] = status + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/servers", method="GET", params=params) + + ass_servers = [ + BoundServer(self, server_data) for server_data in response["servers"] + ] + return ServersPageResult(ass_servers, Meta.parse_meta(response)) + + def get_all( + self, + name: str | None = None, + label_selector: str | None = None, + status: list[str] | None = None, + ) -> list[BoundServer]: + """Get all servers from this account + + :param name: str (optional) + Can be used to filter servers by their name. + :param label_selector: str (optional) + Can be used to filter servers by labels. The response will only contain servers matching the label selector. + :param status: List[str] (optional) + Can be used to filter servers by their status. The response will only contain servers matching the status. + :return: List[:class:`BoundServer <hcloud.servers.client.BoundServer>`] + """ + return self._iter_pages( + self.get_list, + name=name, + label_selector=label_selector, + status=status, + ) + + def get_by_name(self, name: str) -> BoundServer | None: + """Get server by name + + :param name: str + Used to get server by name. + :return: :class:`BoundServer <hcloud.servers.client.BoundServer>` + """ + return self._get_first_by(name=name) + + # pylint: disable=too-many-branches,too-many-locals + def create( + self, + name: str, + server_type: ServerType | BoundServerType, + image: Image, + ssh_keys: list[SSHKey | BoundSSHKey] | None = None, + volumes: list[Volume | BoundVolume] | None = None, + firewalls: list[Firewall | BoundFirewall] | None = None, + networks: list[Network | BoundNetwork] | None = None, + user_data: str | None = None, + labels: dict[str, str] | None = None, + location: Location | BoundLocation | None = None, + datacenter: Datacenter | BoundDatacenter | None = None, + start_after_create: bool | None = True, + automount: bool | None = None, + placement_group: PlacementGroup | BoundPlacementGroup | None = None, + public_net: ServerCreatePublicNetwork | None = None, + ) -> CreateServerResponse: + """Creates a new server. Returns preliminary information about the server as well as an action that covers progress of creation. + + :param name: str + Name of the server to create (must be unique per project and a valid hostname as per RFC 1123) + :param server_type: :class:`BoundServerType <hcloud.server_types.client.BoundServerType>` or :class:`ServerType <hcloud.server_types.domain.ServerType>` + Server type this server should be created with + :param image: :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>` + Image the server is created from + :param ssh_keys: List[:class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` or :class:`SSHKey <hcloud.ssh_keys.domain.SSHKey>`] (optional) + SSH keys which should be injected into the server at creation time + :param volumes: List[:class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`] (optional) + Volumes which should be attached to the server at the creation time. Volumes must be in the same location. + :param networks: List[:class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`] (optional) + Networks which should be attached to the server at the creation time. + :param user_data: str (optional) + Cloud-Init user data to use during server creation. This field is limited to 32KiB. + :param labels: Dict[str,str] (optional) + User-defined labels (key-value pairs) + :param location: :class:`BoundLocation <hcloud.locations.client.BoundLocation>` or :class:`Location <hcloud.locations.domain.Location>` + :param datacenter: :class:`BoundDatacenter <hcloud.datacenters.client.BoundDatacenter>` or :class:`Datacenter <hcloud.datacenters.domain.Datacenter>` + :param start_after_create: boolean (optional) + Start Server right after creation. Defaults to True. + :param automount: boolean (optional) + Auto mount volumes after attach. + :param placement_group: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` or :class:`Location <hcloud.placement_groups.domain.PlacementGroup>` + Placement Group where server should be added during creation + :param public_net: :class:`ServerCreatePublicNetwork <hcloud.servers.domain.ServerCreatePublicNetwork>` + Options to configure the public network of a server on creation + :return: :class:`CreateServerResponse <hcloud.servers.domain.CreateServerResponse>` + """ + data: dict[str, Any] = { + "name": name, + "server_type": server_type.id_or_name, + "start_after_create": start_after_create, + "image": image.id_or_name, + } + + if location is not None: + data["location"] = location.id_or_name + if datacenter is not None: + data["datacenter"] = datacenter.id_or_name + if ssh_keys is not None: + data["ssh_keys"] = [ssh_key.id_or_name for ssh_key in ssh_keys] + if volumes is not None: + data["volumes"] = [volume.id for volume in volumes] + if networks is not None: + data["networks"] = [network.id for network in networks] + if firewalls is not None: + data["firewalls"] = [{"firewall": firewall.id} for firewall in firewalls] + if user_data is not None: + data["user_data"] = user_data + if labels is not None: + data["labels"] = labels + if automount is not None: + data["automount"] = automount + if placement_group is not None: + data["placement_group"] = placement_group.id + + if public_net is not None: + data_public_net: dict[str, Any] = { + "enable_ipv4": public_net.enable_ipv4, + "enable_ipv6": public_net.enable_ipv6, + } + if public_net.ipv4 is not None: + data_public_net["ipv4"] = public_net.ipv4.id + if public_net.ipv6 is not None: + data_public_net["ipv6"] = public_net.ipv6.id + data["public_net"] = data_public_net + + response = self._client.request(url="/servers", method="POST", json=data) + + result = CreateServerResponse( + server=BoundServer(self, response["server"]), + action=BoundAction(self._client.actions, response["action"]), + next_actions=[ + BoundAction(self._client.actions, action) + for action in response["next_actions"] + ], + root_password=response["root_password"], + ) + return result + + def get_actions_list( + self, + server: Server | BoundServer, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Returns all action objects for a server. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url=f"/servers/{server.id}/actions", + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return ActionsPageResult(actions, Meta.parse_meta(response)) + + def get_actions( + self, + server: Server | BoundServer, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for a server. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._iter_pages( + self.get_actions_list, + server, + status=status, + sort=sort, + ) + + def update( + self, + server: Server | BoundServer, + name: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundServer: + """Updates a server. You can update a server’s name and a server’s labels. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param name: str (optional) + New name to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundServer <hcloud.servers.client.BoundServer>` + """ + data: dict[str, Any] = {} + if name is not None: + data.update({"name": name}) + if labels is not None: + data.update({"labels": labels}) + response = self._client.request( + url=f"/servers/{server.id}", + method="PUT", + json=data, + ) + return BoundServer(self, response["server"]) + + def get_metrics( + self, + server: Server | BoundServer, + type: MetricsType | list[MetricsType], + start: datetime | str, + end: datetime | str, + step: float | None = None, + ) -> GetMetricsResponse: + """Get Metrics for a Server. + + :param server: The Server to get the metrics for. + :param type: Type of metrics to get. + :param start: Start of period to get Metrics for (in ISO-8601 format). + :param end: End of period to get Metrics for (in ISO-8601 format). + :param step: Resolution of results in seconds. + """ + if not isinstance(type, list): + type = [type] + if isinstance(start, str): + start = isoparse(start) + if isinstance(end, str): + end = isoparse(end) + + params: dict[str, Any] = { + "type": ",".join(type), + "start": start.isoformat(), + "end": end.isoformat(), + } + if step is not None: + params["step"] = step + + response = self._client.request( + url=f"/servers/{server.id}/metrics", + method="GET", + params=params, + ) + return GetMetricsResponse( + metrics=Metrics(**response["metrics"]), + ) + + def delete(self, server: Server | BoundServer) -> BoundAction: + """Deletes a server. This immediately removes the server from your account, and it is no longer accessible. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request(url=f"/servers/{server.id}", method="DELETE") + return BoundAction(self._client.actions, response["action"]) + + def power_off(self, server: Server | BoundServer) -> BoundAction: + """Cuts power to the server. This forcefully stops it without giving the server operating system time to gracefully stop + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/poweroff", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def power_on(self, server: Server | BoundServer) -> BoundAction: + """Starts a server by turning its power on. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/poweron", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def reboot(self, server: Server | BoundServer) -> BoundAction: + """Reboots a server gracefully by sending an ACPI request. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/reboot", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def reset(self, server: Server | BoundServer) -> BoundAction: + """Cuts power to a server and starts it again. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/reset", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def shutdown(self, server: Server | BoundServer) -> BoundAction: + """Shuts down a server gracefully by sending an ACPI shutdown request. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/shutdown", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def reset_password(self, server: Server | BoundServer) -> ResetPasswordResponse: + """Resets the root password. Only works for Linux systems that are running the qemu guest agent. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :return: :class:`ResetPasswordResponse <hcloud.servers.domain.ResetPasswordResponse>` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/reset_password", + method="POST", + ) + return ResetPasswordResponse( + action=BoundAction(self._client.actions, response["action"]), + root_password=response["root_password"], + ) + + def change_type( + self, + server: Server | BoundServer, + server_type: ServerType | BoundServerType, + upgrade_disk: bool, + ) -> BoundAction: + """Changes the type (Cores, RAM and disk sizes) of a server. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param server_type: :class:`BoundServerType <hcloud.server_types.client.BoundServerType>` or :class:`ServerType <hcloud.server_types.domain.ServerType>` + Server type the server should migrate to + :param upgrade_disk: boolean + If false, do not upgrade the disk. This allows downgrading the server type later. + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = { + "server_type": server_type.id_or_name, + "upgrade_disk": upgrade_disk, + } + response = self._client.request( + url=f"/servers/{server.id}/actions/change_type", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def enable_rescue( + self, + server: Server | BoundServer, + type: str | None = None, + ssh_keys: list[str] | None = None, + ) -> EnableRescueResponse: + """Enable the Hetzner Rescue System for this server. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param type: str + Type of rescue system to boot (default: linux64) + Choices: linux64, linux32, freebsd64 + :param ssh_keys: List[str] + Array of SSH key IDs which should be injected into the rescue system. Only available for types: linux64 and linux32. + :return: :class:`EnableRescueResponse <hcloud.servers.domain.EnableRescueResponse>` + """ + data: dict[str, Any] = {"type": type} + if ssh_keys is not None: + data.update({"ssh_keys": ssh_keys}) + + response = self._client.request( + url=f"/servers/{server.id}/actions/enable_rescue", + method="POST", + json=data, + ) + return EnableRescueResponse( + action=BoundAction(self._client.actions, response["action"]), + root_password=response["root_password"], + ) + + def disable_rescue(self, server: Server | BoundServer) -> BoundAction: + """Disables the Hetzner Rescue System for a server. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/disable_rescue", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def create_image( + self, + server: Server | BoundServer, + description: str | None = None, + type: str | None = None, + labels: dict[str, str] | None = None, + ) -> CreateImageResponse: + """Creates an image (snapshot) from a server by copying the contents of its disks. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param description: str (optional) + Description of the image. If you do not set this we auto-generate one for you. + :param type: str (optional) + Type of image to create (default: snapshot) + Choices: snapshot, backup + :param labels: Dict[str, str] + User-defined labels (key-value pairs) + :return: :class:`CreateImageResponse <hcloud.images.domain.CreateImageResponse>` + """ + data: dict[str, Any] = {} + if description is not None: + data.update({"description": description}) + + if type is not None: + data.update({"type": type}) + + if labels is not None: + data.update({"labels": labels}) + + response = self._client.request( + url=f"/servers/{server.id}/actions/create_image", + method="POST", + json=data, + ) + return CreateImageResponse( + action=BoundAction(self._client.actions, response["action"]), + image=BoundImage(self._client.images, response["image"]), + ) + + def rebuild( + self, + server: Server | BoundServer, + image: Image | BoundImage, + *, + return_response: bool = False, + ) -> RebuildResponse | BoundAction: + """Rebuilds a server overwriting its disk with the content of an image, thereby destroying all data on the target server. + + :param server: Server to rebuild + :param image: Image to use for the rebuilt server + :param return_response: Whether to return the full response or only the action. + """ + data: dict[str, Any] = {"image": image.id_or_name} + response = self._client.request( + url=f"/servers/{server.id}/actions/rebuild", + method="POST", + json=data, + ) + + rebuild_response = RebuildResponse( + action=BoundAction(self._client.actions, response["action"]), + root_password=response.get("root_password"), + ) + + if not return_response: + warnings.warn( + "Returning only the 'action' is deprecated, please set the " + "'return_response' keyword argument to 'True' to return the full " + "rebuild response and update your code accordingly.", + DeprecationWarning, + stacklevel=2, + ) + return rebuild_response.action + return rebuild_response + + def enable_backup(self, server: Server | BoundServer) -> BoundAction: + """Enables and configures the automatic daily backup option for the server. Enabling automatic backups will increase the price of the server by 20%. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/enable_backup", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def disable_backup(self, server: Server | BoundServer) -> BoundAction: + """Disables the automatic backup option and deletes all existing Backups for a Server. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/disable_backup", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def attach_iso( + self, + server: Server | BoundServer, + iso: Iso | BoundIso, + ) -> BoundAction: + """Attaches an ISO to a server. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param iso: :class:`BoundIso <hcloud.isos.client.BoundIso>` or :class:`Server <hcloud.isos.domain.Iso>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {"iso": iso.id_or_name} + response = self._client.request( + url=f"/servers/{server.id}/actions/attach_iso", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def detach_iso(self, server: Server | BoundServer) -> BoundAction: + """Detaches an ISO from a server. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/detach_iso", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def change_dns_ptr( + self, + server: Server | BoundServer, + ip: str, + dns_ptr: str | None, + ) -> BoundAction: + """Changes the hostname that will appear when getting the hostname belonging to the primary IPs (ipv4 and ipv6) of this server. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {"ip": ip, "dns_ptr": dns_ptr} + response = self._client.request( + url=f"/servers/{server.id}/actions/change_dns_ptr", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def change_protection( + self, + server: Server | BoundServer, + delete: bool | None = None, + rebuild: bool | None = None, + ) -> BoundAction: + """Changes the protection configuration of the server. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param delete: boolean + If true, prevents the server from being deleted (currently delete and rebuild attribute needs to have the same value) + :param rebuild: boolean + If true, prevents the server from being rebuilt (currently delete and rebuild attribute needs to have the same value) + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {} + if delete is not None: + data.update({"delete": delete}) + if rebuild is not None: + data.update({"rebuild": rebuild}) + + response = self._client.request( + url=f"/servers/{server.id}/actions/change_protection", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def request_console(self, server: Server | BoundServer) -> RequestConsoleResponse: + """Requests credentials for remote access via vnc over websocket to keyboard, monitor, and mouse for a server. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :return: :class:`RequestConsoleResponse <hcloud.servers.domain.RequestConsoleResponse>` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/request_console", + method="POST", + ) + return RequestConsoleResponse( + action=BoundAction(self._client.actions, response["action"]), + wss_url=response["wss_url"], + password=response["password"], + ) + + def attach_to_network( + self, + server: Server | BoundServer, + network: Network | BoundNetwork, + ip: str | None = None, + alias_ips: list[str] | None = None, + ) -> BoundAction: + """Attaches a server to a network + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param ip: str + IP to request to be assigned to this server + :param alias_ips: List[str] + New alias IPs to set for this server. + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {"network": network.id} + if ip is not None: + data.update({"ip": ip}) + if alias_ips is not None: + data.update({"alias_ips": alias_ips}) + response = self._client.request( + url=f"/servers/{server.id}/actions/attach_to_network", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def detach_from_network( + self, + server: Server | BoundServer, + network: Network | BoundNetwork, + ) -> BoundAction: + """Detaches a server from a network. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {"network": network.id} + response = self._client.request( + url=f"/servers/{server.id}/actions/detach_from_network", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def change_alias_ips( + self, + server: Server | BoundServer, + network: Network | BoundNetwork, + alias_ips: list[str], + ) -> BoundAction: + """Changes the alias IPs of an already attached network. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` + :param alias_ips: List[str] + New alias IPs to set for this server. + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {"network": network.id, "alias_ips": alias_ips} + response = self._client.request( + url=f"/servers/{server.id}/actions/change_alias_ips", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def add_to_placement_group( + self, + server: Server | BoundServer, + placement_group: PlacementGroup | BoundPlacementGroup, + ) -> BoundAction: + """Adds a server to a placement group. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param placement_group: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` or :class:`Network <hcloud.placement_groups.domain.PlacementGroup>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {"placement_group": str(placement_group.id)} + response = self._client.request( + url=f"/servers/{server.id}/actions/add_to_placement_group", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def remove_from_placement_group(self, server: Server | BoundServer) -> BoundAction: + """Removes a server from a placement group. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/remove_from_placement_group", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/servers/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/servers/domain.py new file mode 100644 index 000000000..0a0d34688 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/servers/domain.py @@ -0,0 +1,455 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core import BaseDomain + +if TYPE_CHECKING: + from ..actions import BoundAction + from ..datacenters import BoundDatacenter + from ..firewalls import BoundFirewall + from ..floating_ips import BoundFloatingIP + from ..images import BoundImage + from ..isos import BoundIso + from ..metrics import Metrics + from ..networks import BoundNetwork + from ..placement_groups import BoundPlacementGroup + from ..primary_ips import BoundPrimaryIP, PrimaryIP + from ..server_types import BoundServerType + from ..volumes import BoundVolume + from .client import BoundServer + + +class Server(BaseDomain): + """Server Domain + + :param id: int + ID of the server + :param name: str + Name of the server (must be unique per project and a valid hostname as per RFC 1123) + :param status: str + Status of the server Choices: `running`, `initializing`, `starting`, `stopping`, `off`, `deleting`, `migrating`, `rebuilding`, `unknown` + :param created: datetime + Point in time when the server was created + :param public_net: :class:`PublicNetwork <hcloud.servers.domain.PublicNetwork>` + Public network information. + :param server_type: :class:`BoundServerType <hcloud.server_types.client.BoundServerType>` + :param datacenter: :class:`BoundDatacenter <hcloud.datacenters.client.BoundDatacenter>` + :param image: :class:`BoundImage <hcloud.images.client.BoundImage>`, None + :param iso: :class:`BoundIso <hcloud.isos.client.BoundIso>`, None + :param rescue_enabled: bool + True if rescue mode is enabled: Server will then boot into rescue system on next reboot. + :param locked: bool + True if server has been locked and is not available to user. + :param backup_window: str, None + Time window (UTC) in which the backup will run, or None if the backups are not enabled + :param outgoing_traffic: int, None + Outbound Traffic for the current billing period in bytes + :param ingoing_traffic: int, None + Inbound Traffic for the current billing period in bytes + :param included_traffic: int + Free Traffic for the current billing period in bytes + :param primary_disk_size: int + Size of the primary Disk + :param protection: dict + Protection configuration for the server + :param labels: dict + User-defined labels (key-value pairs) + :param volumes: List[:class:`BoundVolume <hcloud.volumes.client.BoundVolume>`] + Volumes assigned to this server. + :param private_net: List[:class:`PrivateNet <hcloud.servers.domain.PrivateNet>`] + Private networks information. + """ + + STATUS_RUNNING = "running" + """Server Status running""" + STATUS_INIT = "initializing" + """Server Status initializing""" + STATUS_STARTING = "starting" + """Server Status starting""" + STATUS_STOPPING = "stopping" + """Server Status stopping""" + STATUS_OFF = "off" + """Server Status off""" + STATUS_DELETING = "deleting" + """Server Status deleting""" + STATUS_MIGRATING = "migrating" + """Server Status migrating""" + STATUS_REBUILDING = "rebuilding" + """Server Status rebuilding""" + STATUS_UNKNOWN = "unknown" + """Server Status unknown""" + __slots__ = ( + "id", + "name", + "status", + "public_net", + "server_type", + "datacenter", + "image", + "iso", + "rescue_enabled", + "locked", + "backup_window", + "outgoing_traffic", + "ingoing_traffic", + "included_traffic", + "protection", + "labels", + "volumes", + "private_net", + "created", + "primary_disk_size", + "placement_group", + ) + + # pylint: disable=too-many-locals + def __init__( + self, + id: int, + name: str | None = None, + status: str | None = None, + created: str | None = None, + public_net: PublicNetwork | None = None, + server_type: BoundServerType | None = None, + datacenter: BoundDatacenter | None = None, + image: BoundImage | None = None, + iso: BoundIso | None = None, + rescue_enabled: bool | None = None, + locked: bool | None = None, + backup_window: str | None = None, + outgoing_traffic: int | None = None, + ingoing_traffic: int | None = None, + included_traffic: int | None = None, + protection: dict | None = None, + labels: dict[str, str] | None = None, + volumes: list[BoundVolume] | None = None, + private_net: list[PrivateNet] | None = None, + primary_disk_size: int | None = None, + placement_group: BoundPlacementGroup | None = None, + ): + self.id = id + self.name = name + self.status = status + self.created = isoparse(created) if created else None + self.public_net = public_net + self.server_type = server_type + self.datacenter = datacenter + self.image = image + self.iso = iso + self.rescue_enabled = rescue_enabled + self.locked = locked + self.backup_window = backup_window + self.outgoing_traffic = outgoing_traffic + self.ingoing_traffic = ingoing_traffic + self.included_traffic = included_traffic + self.protection = protection + self.labels = labels + self.volumes = volumes + self.private_net = private_net + self.primary_disk_size = primary_disk_size + self.placement_group = placement_group + + +class CreateServerResponse(BaseDomain): + """Create Server Response Domain + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` + The created server + :param action: :class:`BoundAction <hcloud.actions.client.BoundAction>` + Shows the progress of the server creation + :param next_actions: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + Additional actions like a `start_server` action after the server creation + :param root_password: str, None + The root password of the server if no SSH-Key was given on server creation + """ + + __slots__ = ("server", "action", "next_actions", "root_password") + + def __init__( + self, + server: BoundServer, + action: BoundAction, + next_actions: list[BoundAction], + root_password: str | None, + ): + self.server = server + self.action = action + self.next_actions = next_actions + self.root_password = root_password + + +class ResetPasswordResponse(BaseDomain): + """Reset Password Response Domain + + :param action: :class:`BoundAction <hcloud.actions.client.BoundAction>` + Shows the progress of the server passwort reset action + :param root_password: str + The root password of the server + """ + + __slots__ = ("action", "root_password") + + def __init__( + self, + action: BoundAction, + root_password: str, + ): + self.action = action + self.root_password = root_password + + +class EnableRescueResponse(BaseDomain): + """Enable Rescue Response Domain + + :param action: :class:`BoundAction <hcloud.actions.client.BoundAction>` + Shows the progress of the server enable rescue action + :param root_password: str + The root password of the server in the rescue mode + """ + + __slots__ = ("action", "root_password") + + def __init__( + self, + action: BoundAction, + root_password: str, + ): + self.action = action + self.root_password = root_password + + +class RequestConsoleResponse(BaseDomain): + """Request Console Response Domain + + :param action: :class:`BoundAction <hcloud.actions.client.BoundAction>` + Shows the progress of the server request console action + :param wss_url: str + URL of websocket proxy to use. This includes a token which is valid for a limited time only. + :param password: str + VNC password to use for this connection. This password only works in combination with a wss_url with valid token. + """ + + __slots__ = ("action", "wss_url", "password") + + def __init__( + self, + action: BoundAction, + wss_url: str, + password: str, + ): + self.action = action + self.wss_url = wss_url + self.password = password + + +class RebuildResponse(BaseDomain): + """Rebuild Response Domain + + :param action: Shows the progress of the server rebuild action + :param root_password: The root password of the server when not using SSH keys + """ + + __slots__ = ("action", "root_password") + + def __init__( + self, + action: BoundAction, + root_password: str | None, + ): + self.action = action + self.root_password = root_password + + +class PublicNetwork(BaseDomain): + """Public Network Domain + + :param ipv4: :class:`IPv4Address <hcloud.servers.domain.IPv4Address>` + :param ipv6: :class:`IPv6Network <hcloud.servers.domain.IPv6Network>` + :param floating_ips: List[:class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>`] + :param primary_ipv4: :class:`BoundPrimaryIP <hcloud.primary_ips.domain.BoundPrimaryIP>` + :param primary_ipv6: :class:`BoundPrimaryIP <hcloud.primary_ips.domain.BoundPrimaryIP>` + :param firewalls: List[:class:`PublicNetworkFirewall <hcloud.servers.client.PublicNetworkFirewall>`] + """ + + __slots__ = ( + "ipv4", + "ipv6", + "floating_ips", + "firewalls", + "primary_ipv4", + "primary_ipv6", + ) + + def __init__( + self, + ipv4: IPv4Address, + ipv6: IPv6Network, + floating_ips: list[BoundFloatingIP], + primary_ipv4: BoundPrimaryIP | None, + primary_ipv6: BoundPrimaryIP | None, + firewalls: list[PublicNetworkFirewall] | None = None, + ): + self.ipv4 = ipv4 + self.ipv6 = ipv6 + self.floating_ips = floating_ips + self.firewalls = firewalls + self.primary_ipv4 = primary_ipv4 + self.primary_ipv6 = primary_ipv6 + + +class PublicNetworkFirewall(BaseDomain): + """Public Network Domain + + :param firewall: :class:`BoundFirewall <hcloud.firewalls.domain.BoundFirewall>` + :param status: str + """ + + __slots__ = ("firewall", "status") + + STATUS_APPLIED = "applied" + """Public Network Firewall Status applied""" + STATUS_PENDING = "pending" + """Public Network Firewall Status pending""" + + def __init__( + self, + firewall: BoundFirewall, + status: str, + ): + self.firewall = firewall + self.status = status + + +class IPv4Address(BaseDomain): + """IPv4 Address Domain + + :param ip: str + The IPv4 Address + :param blocked: bool + Determine if the IP is blocked + :param dns_ptr: str + DNS PTR for the ip + """ + + __slots__ = ("ip", "blocked", "dns_ptr") + + def __init__( + self, + ip: str, + blocked: bool, + dns_ptr: str, + ): + self.ip = ip + self.blocked = blocked + self.dns_ptr = dns_ptr + + +class IPv6Network(BaseDomain): + """IPv6 Network Domain + + :param ip: str + The IPv6 Network as CIDR Notation + :param blocked: bool + Determine if the Network is blocked + :param dns_ptr: dict + DNS PTR Records for the Network as Dict + :param network: str + The network without the network mask + :param network_mask: str + The network mask + """ + + __slots__ = ("ip", "blocked", "dns_ptr", "network", "network_mask") + + def __init__( + self, + ip: str, + blocked: bool, + dns_ptr: list, + ): + self.ip = ip + self.blocked = blocked + self.dns_ptr = dns_ptr + ip_parts = self.ip.split("/") # 2001:db8::/64 to 2001:db8:: and 64 + self.network = ip_parts[0] + self.network_mask = ip_parts[1] + + +class PrivateNet(BaseDomain): + """PrivateNet Domain + + :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` + The network the server is attached to + :param ip: str + The main IP Address of the server in the Network + :param alias_ips: List[str] + The alias ips for a server + :param mac_address: str + The mac address of the interface on the server + """ + + __slots__ = ("network", "ip", "alias_ips", "mac_address") + + def __init__( + self, + network: BoundNetwork, + ip: str, + alias_ips: list[str], + mac_address: str, + ): + self.network = network + self.ip = ip + self.alias_ips = alias_ips + self.mac_address = mac_address + + +class ServerCreatePublicNetwork(BaseDomain): + """Server Create Public Network Domain + + :param ipv4: Optional[:class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`] + :param ipv6: Optional[:class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`] + :param enable_ipv4: bool + :param enable_ipv6: bool + """ + + __slots__ = ("ipv4", "ipv6", "enable_ipv4", "enable_ipv6") + + def __init__( + self, + ipv4: PrimaryIP | None = None, + ipv6: PrimaryIP | None = None, + enable_ipv4: bool = True, + enable_ipv6: bool = True, + ): + self.ipv4 = ipv4 + self.ipv6 = ipv6 + self.enable_ipv4 = enable_ipv4 + self.enable_ipv6 = enable_ipv6 + + +MetricsType = Literal[ + "cpu", + "disk", + "network", +] + + +class GetMetricsResponse(BaseDomain): + """Get a Server Metrics Response Domain + + :param metrics: The Server metrics + """ + + __slots__ = ("metrics",) + + def __init__( + self, + metrics: Metrics, + ): + self.metrics = metrics diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/ssh_keys/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/ssh_keys/__init__.py new file mode 100644 index 000000000..251559060 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/ssh_keys/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .client import BoundSSHKey, SSHKeysClient, SSHKeysPageResult # noqa: F401 +from .domain import SSHKey # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/ssh_keys/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/ssh_keys/client.py new file mode 100644 index 000000000..69c1683d7 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/ssh_keys/client.py @@ -0,0 +1,194 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple + +from ..core import BoundModelBase, ClientEntityBase, Meta +from .domain import SSHKey + +if TYPE_CHECKING: + from .._client import Client + + +class BoundSSHKey(BoundModelBase, SSHKey): + _client: SSHKeysClient + + model = SSHKey + + def update( + self, + name: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundSSHKey: + """Updates an SSH key. You can update an SSH key name and an SSH key labels. + + :param description: str (optional) + New Description to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` + """ + return self._client.update(self, name, labels) + + def delete(self) -> bool: + """Deletes an SSH key. It cannot be used anymore. + :return: boolean + """ + return self._client.delete(self) + + +class SSHKeysPageResult(NamedTuple): + ssh_keys: list[BoundSSHKey] + meta: Meta | None + + +class SSHKeysClient(ClientEntityBase): + _client: Client + + def get_by_id(self, id: int) -> BoundSSHKey: + """Get a specific SSH Key by its ID + + :param id: int + :return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` + """ + response = self._client.request(url=f"/ssh_keys/{id}", method="GET") + return BoundSSHKey(self, response["ssh_key"]) + + def get_list( + self, + name: str | None = None, + fingerprint: str | None = None, + label_selector: str | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> SSHKeysPageResult: + """Get a list of SSH keys from the account + + :param name: str (optional) + Can be used to filter SSH keys by their name. The response will only contain the SSH key matching the specified name. + :param fingerprint: str (optional) + Can be used to filter SSH keys by their fingerprint. The response will only contain the SSH key matching the specified fingerprint. + :param label_selector: str (optional) + Can be used to filter SSH keys by labels. The response will only contain SSH keys matching the label selector. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if name is not None: + params["name"] = name + if fingerprint is not None: + params["fingerprint"] = fingerprint + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/ssh_keys", method="GET", params=params) + + ssh_keys = [ + BoundSSHKey(self, server_data) for server_data in response["ssh_keys"] + ] + return SSHKeysPageResult(ssh_keys, Meta.parse_meta(response)) + + def get_all( + self, + name: str | None = None, + fingerprint: str | None = None, + label_selector: str | None = None, + ) -> list[BoundSSHKey]: + """Get all SSH keys from the account + + :param name: str (optional) + Can be used to filter SSH keys by their name. The response will only contain the SSH key matching the specified name. + :param fingerprint: str (optional) + Can be used to filter SSH keys by their fingerprint. The response will only contain the SSH key matching the specified fingerprint. + :param label_selector: str (optional) + Can be used to filter SSH keys by labels. The response will only contain SSH keys matching the label selector. + :return: List[:class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>`] + """ + return self._iter_pages( + self.get_list, + name=name, + fingerprint=fingerprint, + label_selector=label_selector, + ) + + def get_by_name(self, name: str) -> BoundSSHKey | None: + """Get ssh key by name + + :param name: str + Used to get ssh key by name. + :return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` + """ + return self._get_first_by(name=name) + + def get_by_fingerprint(self, fingerprint: str) -> BoundSSHKey | None: + """Get ssh key by fingerprint + + :param fingerprint: str + Used to get ssh key by fingerprint. + :return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` + """ + return self._get_first_by(fingerprint=fingerprint) + + def create( + self, + name: str, + public_key: str, + labels: dict[str, str] | None = None, + ) -> BoundSSHKey: + """Creates a new SSH key with the given name and public_key. + + :param name: str + :param public_key: str + Public Key of the SSH Key you want create + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` + """ + data: dict[str, Any] = {"name": name, "public_key": public_key} + if labels is not None: + data["labels"] = labels + response = self._client.request(url="/ssh_keys", method="POST", json=data) + return BoundSSHKey(self, response["ssh_key"]) + + def update( + self, + ssh_key: SSHKey | BoundSSHKey, + name: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundSSHKey: + """Updates an SSH key. You can update an SSH key name and an SSH key labels. + + :param ssh_key: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` or :class:`SSHKey <hcloud.ssh_keys.domain.SSHKey>` + :param name: str (optional) + New Description to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` + """ + data: dict[str, Any] = {} + if name is not None: + data["name"] = name + if labels is not None: + data["labels"] = labels + response = self._client.request( + url=f"/ssh_keys/{ssh_key.id}", + method="PUT", + json=data, + ) + return BoundSSHKey(self, response["ssh_key"]) + + def delete(self, ssh_key: SSHKey | BoundSSHKey) -> bool: + """Deletes an SSH key. It cannot be used anymore. + + :param ssh_key: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` or :class:`SSHKey <hcloud.ssh_keys.domain.SSHKey>` + :return: True + """ + self._client.request(url=f"/ssh_keys/{ssh_key.id}", method="DELETE") + # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised + return True diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/ssh_keys/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/ssh_keys/domain.py new file mode 100644 index 000000000..3c880c4d8 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/ssh_keys/domain.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core import BaseDomain, DomainIdentityMixin + + +class SSHKey(BaseDomain, DomainIdentityMixin): + """SSHKey Domain + + :param id: int + ID of the SSH key + :param name: str + Name of the SSH key (must be unique per project) + :param fingerprint: str + Fingerprint of public key + :param public_key: str + Public Key + :param labels: Dict + User-defined labels (key-value pairs) + :param created: datetime + Point in time when the SSH Key was created + """ + + __slots__ = ("id", "name", "fingerprint", "public_key", "labels", "created") + + def __init__( + self, + id: int | None = None, + name: str | None = None, + fingerprint: str | None = None, + public_key: str | None = None, + labels: dict[str, str] | None = None, + created: str | None = None, + ): + self.id = id + self.name = name + self.fingerprint = fingerprint + self.public_key = public_key + self.labels = labels + self.created = isoparse(created) if created else None diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/volumes/__init__.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/volumes/__init__.py new file mode 100644 index 000000000..dc8ccbb61 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/volumes/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .client import BoundVolume, VolumesClient, VolumesPageResult # noqa: F401 +from .domain import CreateVolumeResponse, Volume # noqa: F401 diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/volumes/client.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/volumes/client.py new file mode 100644 index 000000000..a4709748a --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/volumes/client.py @@ -0,0 +1,458 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple + +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient +from ..core import BoundModelBase, ClientEntityBase, Meta +from ..locations import BoundLocation +from .domain import CreateVolumeResponse, Volume + +if TYPE_CHECKING: + from .._client import Client + from ..locations import Location + from ..servers import BoundServer, Server + + +class BoundVolume(BoundModelBase, Volume): + _client: VolumesClient + + model = Volume + + def __init__(self, client: VolumesClient, data: dict, complete: bool = True): + location = data.get("location") + if location is not None: + data["location"] = BoundLocation(client._client.locations, location) + + # pylint: disable=import-outside-toplevel + from ..servers import BoundServer + + server = data.get("server") + if server is not None: + data["server"] = BoundServer( + client._client.servers, {"id": server}, complete=False + ) + super().__init__(client, data, complete) + + def get_actions_list( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Returns all action objects for a volume. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + return self._client.get_actions_list(self, status, sort, page, per_page) + + def get_actions( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for a volume. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._client.get_actions(self, status, sort) + + def update( + self, + name: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundVolume: + """Updates the volume properties. + + :param name: str (optional) + New volume name + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.update(self, name, labels) + + def delete(self) -> bool: + """Deletes a volume. All volume data is irreversibly destroyed. The volume must not be attached to a server and it must not have delete protection enabled. + + :return: boolean + """ + return self._client.delete(self) + + def attach( + self, + server: Server | BoundServer, + automount: bool | None = None, + ) -> BoundAction: + """Attaches a volume to a server. Works only if the server is in the same location as the volume. + + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param automount: boolean + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.attach(self, server, automount) + + def detach(self) -> BoundAction: + """Detaches a volume from the server it’s attached to. You may attach it to a server again at a later time. + + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.detach(self) + + def resize(self, size: int) -> BoundAction: + """Changes the size of a volume. Note that downsizing a volume is not possible. + + :param size: int + New volume size in GB (must be greater than current size) + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.resize(self, size) + + def change_protection(self, delete: bool | None = None) -> BoundAction: + """Changes the protection configuration of a volume. + + :param delete: boolean + If True, prevents the volume from being deleted + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + return self._client.change_protection(self, delete) + + +class VolumesPageResult(NamedTuple): + volumes: list[BoundVolume] + meta: Meta | None + + +class VolumesClient(ClientEntityBase): + _client: Client + + actions: ResourceActionsClient + """Volumes scoped actions client + + :type: :class:`ResourceActionsClient <hcloud.actions.client.ResourceActionsClient>` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/volumes") + + def get_by_id(self, id: int) -> BoundVolume: + """Get a specific volume by its id + + :param id: int + :return: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` + """ + response = self._client.request(url=f"/volumes/{id}", method="GET") + return BoundVolume(self, response["volume"]) + + def get_list( + self, + name: str | None = None, + label_selector: str | None = None, + page: int | None = None, + per_page: int | None = None, + status: list[str] | None = None, + ) -> VolumesPageResult: + """Get a list of volumes from this account + + :param name: str (optional) + Can be used to filter volumes by their name. + :param label_selector: str (optional) + Can be used to filter volumes by labels. The response will only contain volumes matching the label selector. + :param status: List[str] (optional) + Can be used to filter volumes by their status. The response will only contain volumes matching the status. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundVolume <hcloud.volumes.client.BoundVolume>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if name is not None: + params["name"] = name + if label_selector is not None: + params["label_selector"] = label_selector + if status is not None: + params["status"] = status + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/volumes", method="GET", params=params) + volumes = [ + BoundVolume(self, volume_data) for volume_data in response["volumes"] + ] + return VolumesPageResult(volumes, Meta.parse_meta(response)) + + def get_all( + self, + label_selector: str | None = None, + status: list[str] | None = None, + ) -> list[BoundVolume]: + """Get all volumes from this account + + :param label_selector: + Can be used to filter volumes by labels. The response will only contain volumes matching the label selector. + :param status: List[str] (optional) + Can be used to filter volumes by their status. The response will only contain volumes matching the status. + :return: List[:class:`BoundVolume <hcloud.volumes.client.BoundVolume>`] + """ + return self._iter_pages( + self.get_list, + label_selector=label_selector, + status=status, + ) + + def get_by_name(self, name: str) -> BoundVolume | None: + """Get volume by name + + :param name: str + Used to get volume by name. + :return: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` + """ + return self._get_first_by(name=name) + + def create( + self, + size: int, + name: str, + labels: str | None = None, + location: Location | None = None, + server: Server | None = None, + automount: bool | None = None, + format: str | None = None, + ) -> CreateVolumeResponse: + """Creates a new volume attached to a server. + + :param size: int + Size of the volume in GB + :param name: str + Name of the volume + :param labels: Dict[str,str] (optional) + User-defined labels (key-value pairs) + :param location: :class:`BoundLocation <hcloud.locations.client.BoundLocation>` or :class:`Location <hcloud.locations.domain.Location>` + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param automount: boolean (optional) + Auto mount volumes after attach. + :param format: str (optional) + Format volume after creation. One of: xfs, ext4 + :return: :class:`CreateVolumeResponse <hcloud.volumes.domain.CreateVolumeResponse>` + """ + + if size <= 0: + raise ValueError("size must be greater than 0") + + if not bool(location) ^ bool(server): + raise ValueError("only one of server or location must be provided") + + data: dict[str, Any] = {"name": name, "size": size} + if labels is not None: + data["labels"] = labels + if location is not None: + data["location"] = location.id_or_name + + if server is not None: + data["server"] = server.id + if automount is not None: + data["automount"] = automount + if format is not None: + data["format"] = format + + response = self._client.request(url="/volumes", json=data, method="POST") + + result = CreateVolumeResponse( + volume=BoundVolume(self, response["volume"]), + action=BoundAction(self._client.actions, response["action"]), + next_actions=[ + BoundAction(self._client.actions, action) + for action in response["next_actions"] + ], + ) + return result + + def get_actions_list( + self, + volume: Volume | BoundVolume, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """Returns all action objects for a volume. + + :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) + """ + params: dict[str, Any] = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url=f"/volumes/{volume.id}/actions", + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return ActionsPageResult(actions, Meta.parse_meta(response)) + + def get_actions( + self, + volume: Volume | BoundVolume, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """Returns all action objects for a volume. + + :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + """ + return self._iter_pages( + self.get_actions_list, + volume, + status=status, + sort=sort, + ) + + def update( + self, + volume: Volume | BoundVolume, + name: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundVolume: + """Updates the volume properties. + + :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` + :param name: str (optional) + New volume name + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {} + if name is not None: + data.update({"name": name}) + if labels is not None: + data.update({"labels": labels}) + response = self._client.request( + url=f"/volumes/{volume.id}", + method="PUT", + json=data, + ) + return BoundVolume(self, response["volume"]) + + def delete(self, volume: Volume | BoundVolume) -> bool: + """Deletes a volume. All volume data is irreversibly destroyed. The volume must not be attached to a server and it must not have delete protection enabled. + + :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` + :return: boolean + """ + self._client.request(url=f"/volumes/{volume.id}", method="DELETE") + return True + + def resize(self, volume: Volume | BoundVolume, size: int) -> BoundAction: + """Changes the size of a volume. Note that downsizing a volume is not possible. + + :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` + :param size: int + New volume size in GB (must be greater than current size) + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data = self._client.request( + url=f"/volumes/{volume.id}/actions/resize", + json={"size": size}, + method="POST", + ) + return BoundAction(self._client.actions, data["action"]) + + def attach( + self, + volume: Volume | BoundVolume, + server: Server | BoundServer, + automount: bool | None = None, + ) -> BoundAction: + """Attaches a volume to a server. Works only if the server is in the same location as the volume. + + :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` + :param automount: boolean + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {"server": server.id} + if automount is not None: + data["automount"] = automount + + data = self._client.request( + url=f"/volumes/{volume.id}/actions/attach", + json=data, + method="POST", + ) + return BoundAction(self._client.actions, data["action"]) + + def detach(self, volume: Volume | BoundVolume) -> BoundAction: + """Detaches a volume from the server it’s attached to. You may attach it to a server again at a later time. + + :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data = self._client.request( + url=f"/volumes/{volume.id}/actions/detach", + method="POST", + ) + return BoundAction(self._client.actions, data["action"]) + + def change_protection( + self, + volume: Volume | BoundVolume, + delete: bool | None = None, + ) -> BoundAction: + """Changes the protection configuration of a volume. + + :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` + :param delete: boolean + If True, prevents the volume from being deleted + :return: :class:`BoundAction <hcloud.actions.client.BoundAction>` + """ + data: dict[str, Any] = {} + if delete is not None: + data.update({"delete": delete}) + + response = self._client.request( + url=f"/volumes/{volume.id}/actions/change_protection", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/volumes/domain.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/volumes/domain.py new file mode 100644 index 000000000..7eb544021 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/vendor/hcloud/volumes/domain.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core import BaseDomain, DomainIdentityMixin + +if TYPE_CHECKING: + from ..actions import BoundAction + from ..locations import BoundLocation, Location + from ..servers import BoundServer, Server + from .client import BoundVolume + + +class Volume(BaseDomain, DomainIdentityMixin): + """Volume Domain + + :param id: int + ID of the Volume + :param name: str + Name of the Volume + :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>`, None + Server the Volume is attached to, None if it is not attached at all. + :param created: datetime + Point in time when the Volume was created + :param location: :class:`BoundLocation <hcloud.locations.client.BoundLocation>` + Location of the Volume. Volume can only be attached to Servers in the same location. + :param size: int + Size in GB of the Volume + :param linux_device: str + Device path on the file system for the Volume + :param protection: dict + Protection configuration for the Volume + :param labels: dict + User-defined labels (key-value pairs) + :param status: str + Current status of the volume Choices: `creating`, `available` + :param format: str, None + Filesystem of the volume if formatted on creation, None if not formatted on creation. + """ + + STATUS_CREATING = "creating" + """Volume Status creating""" + STATUS_AVAILABLE = "available" + """Volume Status available""" + + __slots__ = ( + "id", + "name", + "server", + "location", + "size", + "linux_device", + "format", + "protection", + "labels", + "status", + "created", + ) + + def __init__( + self, + id: int, + name: str | None = None, + server: Server | BoundServer | None = None, + created: str | None = None, + location: Location | BoundLocation | None = None, + size: int | None = None, + linux_device: str | None = None, + format: str | None = None, + protection: dict | None = None, + labels: dict[str, str] | None = None, + status: str | None = None, + ): + self.id = id + self.name = name + self.server = server + self.created = isoparse(created) if created else None + self.location = location + self.size = size + self.linux_device = linux_device + self.format = format + self.protection = protection + self.labels = labels + self.status = status + + +class CreateVolumeResponse(BaseDomain): + """Create Volume Response Domain + + :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` + The created volume + :param action: :class:`BoundAction <hcloud.actions.client.BoundAction>` + The action that shows the progress of the Volume Creation + :param next_actions: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] + List of actions that are performed after the creation, like attaching to a server + """ + + __slots__ = ("volume", "action", "next_actions") + + def __init__( + self, + volume: BoundVolume, + action: BoundAction, + next_actions: list[BoundAction], + ): + self.volume = volume + self.action = action + self.next_actions = next_actions diff --git a/ansible_collections/hetzner/hcloud/plugins/module_utils/version.py b/ansible_collections/hetzner/hcloud/plugins/module_utils/version.py new file mode 100644 index 000000000..e78c320a2 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/module_utils/version.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +version = "2.5.0" # x-release-please-version diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/certificate.py b/ansible_collections/hetzner/hcloud/plugins/modules/certificate.py new file mode 100644 index 000000000..ea39be6ca --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/certificate.py @@ -0,0 +1,289 @@ +#!/usr/bin/python + +# Copyright: (c) 2020, 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 annotations + +DOCUMENTATION = """ +--- +module: certificate + +short_description: Create and manage certificates on the Hetzner Cloud. + + +description: + - Create, update and manage certificates on the Hetzner Cloud. + +author: + - Lukas Kaemmerling (@lkaemmerling) + +options: + id: + description: + - The ID of the Hetzner Cloud certificate to manage. + - Only required if no certificate I(name) is given + type: int + name: + description: + - The Name of the Hetzner Cloud certificate to manage. + - Only required if no certificate I(id) is given or a certificate does not exist. + type: str + labels: + description: + - User-defined labels (key-value pairs) + type: dict + certificate: + description: + - Certificate and chain in PEM format, in order so that each record directly certifies the one preceding. + - Required if certificate does not exist and I(type=uploaded). + type: str + private_key: + description: + - Certificate key in PEM format. + - Required if certificate does not exist and I(type=uploaded). + type: str + domain_names: + description: + - Domains and subdomains that should be contained in the Certificate issued by Let's Encrypt. + - Required if I(type=managed). + type: list + default: [ ] + elements: str + type: + description: + - Choose between uploading a Certificate in PEM format or requesting a managed Let's Encrypt Certificate. + default: uploaded + choices: [ uploaded, managed ] + type: str + state: + description: + - State of the certificate. + default: present + choices: [ absent, present ] + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Create a basic certificate + hetzner.hcloud.certificate: + name: my-certificate + certificate: -----BEGIN CERTIFICATE-----... + private_key: -----BEGIN PRIVATE KEY-----... + state: present + +- name: Create a certificate with labels + hetzner.hcloud.certificate: + name: my-certificate + certificate: -----BEGIN CERTIFICATE-----... + private_key: -----BEGIN PRIVATE KEY-----... + labels: + key: value + mylabel: 123 + state: present + +- name: Create a managed certificate + hetzner.hcloud.certificate: + name: my-certificate + type: managed + domain_names: + - example.com + - www.example.com + state: present + +- name: Ensure the certificate is absent (remove if needed) + hetzner.hcloud.certificate: + name: my-certificate + state: absent +""" + +RETURN = """ +hcloud_certificate: + description: The certificate instance + returned: Always + type: complex + contains: + id: + description: Numeric identifier of the certificate + returned: always + type: int + sample: 1937415 + name: + description: Name of the certificate + returned: always + type: str + sample: my website cert + fingerprint: + description: Fingerprint of the certificate + returned: always + type: str + sample: "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f" + certificate: + description: Certificate and chain in PEM format + returned: always + type: str + sample: "-----BEGIN CERTIFICATE-----..." + domain_names: + description: List of Domains and Subdomains covered by the Certificate + returned: always + type: dict + not_valid_before: + description: Point in time when the Certificate becomes valid (in ISO-8601 format) + returned: always + type: str + not_valid_after: + description: Point in time when the Certificate stops being valid (in ISO-8601 format) + returned: always + type: str + labels: + description: User-defined labels (key-value pairs) + returned: always + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.certificates import BoundCertificate + + +class AnsibleHCloudCertificate(AnsibleHCloud): + represent = "hcloud_certificate" + + hcloud_certificate: BoundCertificate | None = None + + def _prepare_result(self): + return { + "id": to_native(self.hcloud_certificate.id), + "name": to_native(self.hcloud_certificate.name), + "type": to_native(self.hcloud_certificate.type), + "fingerprint": to_native(self.hcloud_certificate.fingerprint), + "certificate": to_native(self.hcloud_certificate.certificate), + "not_valid_before": to_native(self.hcloud_certificate.not_valid_before), + "not_valid_after": to_native(self.hcloud_certificate.not_valid_after), + "domain_names": [to_native(domain) for domain in self.hcloud_certificate.domain_names], + "labels": self.hcloud_certificate.labels, + } + + def _get_certificate(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_certificate = self.client.certificates.get_by_id(self.module.params.get("id")) + elif self.module.params.get("name") is not None: + self.hcloud_certificate = self.client.certificates.get_by_name(self.module.params.get("name")) + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _create_certificate(self): + self.module.fail_on_missing_params(required_params=["name"]) + + params = { + "name": self.module.params.get("name"), + "labels": self.module.params.get("labels"), + } + if self.module.params.get("type") == "uploaded": + self.module.fail_on_missing_params(required_params=["certificate", "private_key"]) + params["certificate"] = self.module.params.get("certificate") + params["private_key"] = self.module.params.get("private_key") + if not self.module.check_mode: + try: + self.client.certificates.create(**params) + except HCloudException as exception: + self.fail_json_hcloud(exception) + else: + self.module.fail_on_missing_params(required_params=["domain_names"]) + params["domain_names"] = self.module.params.get("domain_names") + if not self.module.check_mode: + try: + resp = self.client.certificates.create_managed(**params) + resp.action.wait_until_finished(max_retries=1000) + except HCloudException as exception: + self.fail_json_hcloud(exception) + + self._mark_as_changed() + self._get_certificate() + + def _update_certificate(self): + try: + name = self.module.params.get("name") + if name is not None and self.hcloud_certificate.name != name: + self.module.fail_on_missing_params(required_params=["id"]) + if not self.module.check_mode: + self.hcloud_certificate.update(name=name) + self._mark_as_changed() + + labels = self.module.params.get("labels") + if labels is not None and self.hcloud_certificate.labels != labels: + if not self.module.check_mode: + self.hcloud_certificate.update(labels=labels) + self._mark_as_changed() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._get_certificate() + + def present_certificate(self): + self._get_certificate() + if self.hcloud_certificate is None: + self._create_certificate() + else: + self._update_certificate() + + def delete_certificate(self): + self._get_certificate() + if self.hcloud_certificate is not None: + if not self.module.check_mode: + try: + self.client.certificates.delete(self.hcloud_certificate) + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self.hcloud_certificate = None + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + type={ + "choices": ["uploaded", "managed"], + "default": "uploaded", + }, + domain_names={"type": "list", "elements": "str", "default": []}, + certificate={"type": "str"}, + private_key={"type": "str", "no_log": True}, + labels={"type": "dict"}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + required_one_of=[["id", "name"]], + required_if=[["state", "present", ["name"]]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudCertificate.define_module() + + hcloud = AnsibleHCloudCertificate(module) + state = module.params.get("state") + if state == "absent": + hcloud.delete_certificate() + elif state == "present": + hcloud.present_certificate() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/certificate_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/certificate_info.py new file mode 100644 index 000000000..e074046fd --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/certificate_info.py @@ -0,0 +1,162 @@ +#!/usr/bin/python + +# Copyright: (c) 2020, 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 annotations + +DOCUMENTATION = """ +--- +module: certificate_info +short_description: Gather infos about your Hetzner Cloud certificates. +description: + - Gather facts about your Hetzner Cloud certificates. +author: + - Lukas Kaemmerling (@LKaemmerling) +options: + id: + description: + - The ID of the certificate you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name of the certificate you want to get. + type: str + label_selector: + description: + - The label selector for the certificate you want to get. + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Gather hcloud certificate infos + hetzner.hcloud.certificate_info: + register: output +- name: Print the gathered infos + debug: + var: output.hcloud_certificate_info +""" + +RETURN = """ +hcloud_certificate_info: + description: The certificate instances + returned: Always + type: complex + contains: + id: + description: Numeric identifier of the certificate + returned: always + type: int + sample: 1937415 + name: + description: Name of the certificate + returned: always + type: str + sample: my website cert + fingerprint: + description: Fingerprint of the certificate + returned: always + type: str + sample: "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f" + certificate: + description: Certificate and chain in PEM format + returned: always + type: str + sample: "-----BEGIN CERTIFICATE-----..." + domain_names: + description: List of Domains and Subdomains covered by the Certificate + returned: always + type: dict + not_valid_before: + description: Point in time when the Certificate becomes valid (in ISO-8601 format) + returned: always + type: str + not_valid_after: + description: Point in time when the Certificate stops being valid (in ISO-8601 format) + returned: always + type: str + labels: + description: User-defined labels (key-value pairs) + returned: always + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.certificates import BoundCertificate + + +class AnsibleHCloudCertificateInfo(AnsibleHCloud): + represent = "hcloud_certificate_info" + + hcloud_certificate_info: list[BoundCertificate] | None = None + + def _prepare_result(self): + certificates = [] + + for certificate in self.hcloud_certificate_info: + if certificate: + certificates.append( + { + "id": to_native(certificate.id), + "name": to_native(certificate.name), + "fingerprint": to_native(certificate.fingerprint), + "certificate": to_native(certificate.certificate), + "not_valid_before": to_native(certificate.not_valid_before), + "not_valid_after": to_native(certificate.not_valid_after), + "domain_names": [to_native(domain) for domain in certificate.domain_names], + "labels": certificate.labels, + } + ) + return certificates + + def get_certificates(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_certificate_info = [self.client.certificates.get_by_id(self.module.params.get("id"))] + elif self.module.params.get("name") is not None: + self.hcloud_certificate_info = [self.client.certificates.get_by_name(self.module.params.get("name"))] + elif self.module.params.get("label_selector") is not None: + self.hcloud_certificate_info = self.client.certificates.get_all( + label_selector=self.module.params.get("label_selector") + ) + else: + self.hcloud_certificate_info = self.client.certificates.get_all() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + label_selector={"type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudCertificateInfo.define_module() + hcloud = AnsibleHCloudCertificateInfo(module) + + hcloud.get_certificates() + result = hcloud.get_result() + + ansible_info = {"hcloud_certificate_info": result["hcloud_certificate_info"]} + module.exit_json(**ansible_info) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/datacenter_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/datacenter_info.py new file mode 100644 index 000000000..f6665a6fb --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/datacenter_info.py @@ -0,0 +1,192 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: datacenter_info + +short_description: Gather info about the Hetzner Cloud datacenters. + +description: + - Gather info about your Hetzner Cloud datacenters. + +author: + - Lukas Kaemmerling (@LKaemmerling) + +options: + id: + description: + - The ID of the datacenter you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name of the datacenter you want to get. + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Gather hcloud datacenter info + hetzner.hcloud.datacenter_info: + register: output + +- name: Print the gathered info + debug: + var: output + +- name: List available server_types in a datacenter + block: + - name: Gather a hcloud datacenter + hetzner.hcloud.datacenter_info: + name: fsn1-dc14 + register: output + + - name: Gather a hcloud datacenter available server_types + hetzner.hcloud.server_type_info: + id: "{{ item }}" + loop: "{{ output.hcloud_datacenter_info[0].server_types.available }}" + register: available_server_types + + - name: Print a hcloud datacenter available server_types + ansible.builtin.debug: + var: available_server_types.results | map(attribute='hcloud_server_type_info') +""" + +RETURN = """ +hcloud_datacenter_info: + description: + - The datacenter info as list + returned: always + type: complex + contains: + id: + description: Numeric identifier of the datacenter + returned: always + type: int + sample: 1937415 + name: + description: Name of the datacenter + returned: always + type: str + sample: fsn1-dc8 + description: + description: Detail description of the datacenter + returned: always + type: str + sample: Falkenstein DC 8 + location: + description: Name of the location where the datacenter resides in + returned: always + type: str + sample: fsn1 + city: + description: City of the location + returned: always + type: str + sample: fsn1 + server_types: + description: The Server types the Datacenter can handle + returned: always + type: dict + contains: + available: + description: IDs of Server types that are supported and for which the Datacenter has enough resources left + returned: always + type: list + elements: int + sample: [1, 2, 3] + available_for_migration: + description: IDs of Server types that are supported and for which the Datacenter has enough resources left + returned: always + type: list + elements: int + sample: [1, 2, 3] + supported: + description: IDs of Server types that are supported in the Datacenter + returned: always + type: list + elements: int + sample: [1, 2, 3] +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.datacenters import BoundDatacenter + + +class AnsibleHCloudDatacenterInfo(AnsibleHCloud): + represent = "hcloud_datacenter_info" + + hcloud_datacenter_info: list[BoundDatacenter] | None = None + + def _prepare_result(self): + tmp = [] + + for datacenter in self.hcloud_datacenter_info: + if datacenter is None: + continue + + tmp.append( + { + "id": to_native(datacenter.id), + "name": to_native(datacenter.name), + "description": to_native(datacenter.description), + "location": to_native(datacenter.location.name), + "server_types": { + "available": [o.id for o in datacenter.server_types.available], + "available_for_migration": [o.id for o in datacenter.server_types.available_for_migration], + "supported": [o.id for o in datacenter.server_types.supported], + }, + } + ) + + return tmp + + def get_datacenters(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_datacenter_info = [self.client.datacenters.get_by_id(self.module.params.get("id"))] + elif self.module.params.get("name") is not None: + self.hcloud_datacenter_info = [self.client.datacenters.get_by_name(self.module.params.get("name"))] + else: + self.hcloud_datacenter_info = self.client.datacenters.get_all() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudDatacenterInfo.define_module() + hcloud = AnsibleHCloudDatacenterInfo(module) + + hcloud.get_datacenters() + result = hcloud.get_result() + + ansible_info = {"hcloud_datacenter_info": result["hcloud_datacenter_info"]} + module.exit_json(**ansible_info) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/firewall.py b/ansible_collections/hetzner/hcloud/plugins/modules/firewall.py new file mode 100644 index 000000000..3c51b5c0a --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/firewall.py @@ -0,0 +1,438 @@ +#!/usr/bin/python + +# Copyright: (c) 2020, 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 annotations + +DOCUMENTATION = """ +--- +module: firewall +short_description: Create and manage firewalls on the Hetzner Cloud. + +description: + - Create, update and manage firewalls on the Hetzner Cloud. + +author: + - Lukas Kaemmerling (@lkaemmerling) + +options: + id: + description: + - The ID of the Hetzner Cloud Firewall to manage. + - Only required if no firewall O(name) is given. + type: int + name: + description: + - The Name of the Hetzner Cloud Firewall to manage. + - Only required if no firewall O(id) is given, or the firewall does not exist. + type: str + labels: + description: + - User-defined labels (key-value pairs). + type: dict + rules: + description: + - List of rules the firewall contain. + type: list + elements: dict + suboptions: + description: + description: + - User defined description of this rule. + type: str + direction: + description: + - The direction of the firewall rule. + type: str + choices: [in, out] + protocol: + description: + - The protocol of the firewall rule. + type: str + choices: [icmp, tcp, udp, esp, gre] + port: + description: + - The port or port range allowed by this rule. + - A port range can be specified by separating two ports with a dash, e.g 1024-5000. + - Only used if O(rules[].protocol=tcp) or O(rules[].protocol=udp). + type: str + source_ips: + description: + - List of CIDRs that are allowed within this rule. + - Use 0.0.0.0/0 to allow all IPv4 addresses and ::/0 to allow all IPv6 addresses. + - Only used if O(rules[].direction=in). + type: list + elements: str + default: [] + destination_ips: + description: + - List of CIDRs that are allowed within this rule. + - Use 0.0.0.0/0 to allow all IPv4 addresses and ::/0 to allow all IPv6 addresses. + - Only used if O(rules[].direction=out). + type: list + elements: str + default: [] + force: + description: + - Force the deletion of the Firewall when still in use. + type: bool + default: false + state: + description: + - State of the firewall. + default: present + choices: [absent, present] + type: str + +extends_documentation_fragment: + - hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a basic firewall + hetzner.hcloud.firewall: + name: my-firewall + state: present + +- name: Create a firewall with rules + hetzner.hcloud.firewall: + name: my-firewall + rules: + - description: allow icmp from everywhere + direction: in + protocol: icmp + source_ips: + - 0.0.0.0/0 + - ::/0 + state: present + +- name: Create a firewall with labels + hetzner.hcloud.firewall: + name: my-firewall + labels: + key: value + mylabel: 123 + state: present + +- name: Ensure the firewall is absent (remove if needed) + hetzner.hcloud.firewall: + name: my-firewall + state: absent +""" + +RETURN = """ +hcloud_firewall: + description: The firewall instance. + returned: always + type: dict + contains: + id: + description: Numeric identifier of the firewall. + returned: always + type: int + sample: 1937415 + name: + description: Name of the firewall. + returned: always + type: str + sample: my-firewall + labels: + description: User-defined labels (key-value pairs). + returned: always + type: dict + rules: + description: List of rules the firewall contain. + returned: always + type: list + elements: dict + contains: + description: + description: User defined description of this rule. + type: str + returned: always + sample: allow http from anywhere + direction: + description: The direction of the firewall rule. + type: str + returned: always + sample: in + protocol: + description: The protocol of the firewall rule. + type: str + returned: always + sample: tcp + port: + description: The port or port range allowed by this rule. + type: str + returned: if RV(hcloud_firewall.rules[].protocol=tcp) or RV(hcloud_firewall.rules[].protocol=udp) + sample: "80" + source_ips: + description: List of source CIDRs that are allowed within this rule. + type: list + elements: str + returned: always + sample: ["0.0.0.0/0", "::/0"] + destination_ips: + description: List of destination CIDRs that are allowed within this rule. + type: list + elements: str + returned: always + sample: [] + applied_to: + description: List of Resources the Firewall is applied to. + returned: always + type: list + elements: dict + contains: + type: + description: Type of the resource. + type: str + choices: [server, label_selector] + sample: label_selector + server: + description: ID of the server. + type: int + sample: 12345 + label_selector: + description: Label selector value. + type: str + sample: env=prod + applied_to_resources: + description: List of Resources the Firewall label selector is applied to. + returned: if RV(hcloud_firewall.applied_to[].type=label_selector) + type: list + elements: dict + contains: + type: + description: Type of resource referenced. + type: str + choices: [server] + sample: server + server: + description: ID of the Server. + type: int + sample: 12345 +""" + +import time + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import APIException, HCloudException +from ..module_utils.vendor.hcloud.firewalls import ( + BoundFirewall, + FirewallResource, + FirewallRule, +) + + +class AnsibleHCloudFirewall(AnsibleHCloud): + represent = "hcloud_firewall" + + hcloud_firewall: BoundFirewall | None = None + + def _prepare_result(self): + return { + "id": to_native(self.hcloud_firewall.id), + "name": to_native(self.hcloud_firewall.name), + "rules": [self._prepare_result_rule(rule) for rule in self.hcloud_firewall.rules], + "labels": self.hcloud_firewall.labels, + "applied_to": [self._prepare_result_applied_to(resource) for resource in self.hcloud_firewall.applied_to], + } + + def _prepare_result_rule(self, rule: FirewallRule): + return { + "direction": to_native(rule.direction), + "protocol": to_native(rule.protocol), + "port": to_native(rule.port) if rule.port is not None else None, + "source_ips": [to_native(cidr) for cidr in rule.source_ips], + "destination_ips": [to_native(cidr) for cidr in rule.destination_ips], + "description": to_native(rule.description) if rule.description is not None else None, + } + + def _prepare_result_applied_to(self, resource: FirewallResource): + result = { + "type": to_native(resource.type), + "server": to_native(resource.server.id) if resource.server is not None else None, + "label_selector": ( + to_native(resource.label_selector.selector) if resource.label_selector is not None else None + ), + } + if resource.applied_to_resources is not None: + result["applied_to_resources"] = [ + { + "type": to_native(item.type), + "server": to_native(item.server.id) if item.server is not None else None, + } + for item in resource.applied_to_resources + ] + return result + + def _get_firewall(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_firewall = self.client.firewalls.get_by_id(self.module.params.get("id")) + elif self.module.params.get("name") is not None: + self.hcloud_firewall = self.client.firewalls.get_by_name(self.module.params.get("name")) + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _create_firewall(self): + self.module.fail_on_missing_params(required_params=["name"]) + params = { + "name": self.module.params.get("name"), + "labels": self.module.params.get("labels"), + } + rules = self.module.params.get("rules") + if rules is not None: + params["rules"] = [ + FirewallRule( + direction=rule["direction"], + protocol=rule["protocol"], + source_ips=rule["source_ips"] if rule["source_ips"] is not None else [], + destination_ips=rule["destination_ips"] if rule["destination_ips"] is not None else [], + port=rule["port"], + description=rule["description"], + ) + for rule in rules + ] + + if not self.module.check_mode: + try: + self.client.firewalls.create(**params) + except HCloudException as exception: + self.fail_json_hcloud(exception, params=params) + + self._mark_as_changed() + self._get_firewall() + + def _update_firewall(self): + name = self.module.params.get("name") + if name is not None and self.hcloud_firewall.name != name: + self.module.fail_on_missing_params(required_params=["id"]) + if not self.module.check_mode: + self.hcloud_firewall.update(name=name) + self._mark_as_changed() + + labels = self.module.params.get("labels") + if labels is not None and self.hcloud_firewall.labels != labels: + if not self.module.check_mode: + self.hcloud_firewall.update(labels=labels) + self._mark_as_changed() + + rules = self.module.params.get("rules") + if rules is not None and rules != [self._prepare_result_rule(rule) for rule in self.hcloud_firewall.rules]: + if not self.module.check_mode: + new_rules = [ + FirewallRule( + direction=rule["direction"], + protocol=rule["protocol"], + source_ips=rule["source_ips"] if rule["source_ips"] is not None else [], + destination_ips=rule["destination_ips"] if rule["destination_ips"] is not None else [], + port=rule["port"], + description=rule["description"], + ) + for rule in rules + ] + self.hcloud_firewall.set_rules(new_rules) + self._mark_as_changed() + + self._get_firewall() + + def present_firewall(self): + self._get_firewall() + if self.hcloud_firewall is None: + self._create_firewall() + else: + self._update_firewall() + + def delete_firewall(self): + self._get_firewall() + if self.hcloud_firewall is not None: + if not self.module.check_mode: + if self.hcloud_firewall.applied_to: + if self.module.params.get("force"): + actions = self.hcloud_firewall.remove_from_resources(self.hcloud_firewall.applied_to) + for action in actions: + action.wait_until_finished() + else: + self.module.warn( + f"Firewall {self.hcloud_firewall.name} is currently used by " + "other resources. You need to unassign the resources before " + "deleting the Firewall or use force=true." + ) + + retry_count = 0 + while True: + try: + self.hcloud_firewall.delete() + break + except APIException as exception: + if "is still in use" in exception.message and retry_count < 10: + retry_count += 1 + time.sleep(0.5 * retry_count) + continue + self.fail_json_hcloud(exception) + except HCloudException as exception: + self.fail_json_hcloud(exception) + + self._mark_as_changed() + self.hcloud_firewall = None + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + labels={"type": "dict"}, + rules=dict( + type="list", + elements="dict", + options=dict( + description={"type": "str"}, + direction={"type": "str", "choices": ["in", "out"]}, + protocol={"type": "str", "choices": ["icmp", "udp", "tcp", "esp", "gre"]}, + port={"type": "str"}, + source_ips={"type": "list", "elements": "str", "default": []}, + destination_ips={"type": "list", "elements": "str", "default": []}, + ), + required_together=[["direction", "protocol"]], + required_if=[ + ["protocol", "udp", ["port"]], + ["protocol", "tcp", ["port"]], + ], + ), + force={"type": "bool", "default": False}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + required_one_of=[["id", "name"]], + required_if=[["state", "present", ["name"]]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudFirewall.define_module() + + hcloud = AnsibleHCloudFirewall(module) + state = module.params.get("state") + if state == "absent": + hcloud.delete_firewall() + elif state == "present": + hcloud.present_firewall() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/firewall_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/firewall_info.py new file mode 100644 index 000000000..7e7a623d0 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/firewall_info.py @@ -0,0 +1,246 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: firewall_info +short_description: Gather infos about the Hetzner Cloud Firewalls. + +description: + - Gather facts about your Hetzner Cloud Firewalls. + +author: + - Jonas Lammler (@jooola) + +options: + id: + description: + - The ID of the Firewall you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name for the Firewall you want to get. + type: str + label_selector: + description: + - The label selector for the Firewalls you want to get. + type: str + +extends_documentation_fragment: + - hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Gather hcloud Firewall infos + hetzner.hcloud.firewall_info: + register: output + +- name: Print the gathered infos + debug: + var: output +""" + +RETURN = """ +hcloud_firewall_info: + description: List of Firewalls. + returned: always + type: list + elements: dict + contains: + id: + description: Numeric identifier of the firewall. + returned: always + type: int + sample: 1937415 + name: + description: Name of the firewall. + returned: always + type: str + sample: my-firewall + labels: + description: User-defined labels (key-value pairs). + returned: always + type: dict + rules: + description: List of rules the firewall contain. + returned: always + type: list + elements: dict + contains: + description: + description: User defined description of this rule. + type: str + returned: always + sample: allow http from anywhere + direction: + description: The direction of the firewall rule. + type: str + returned: always + sample: in + protocol: + description: The protocol of the firewall rule. + type: str + returned: always + sample: tcp + port: + description: The port or port range allowed by this rule. + type: str + returned: if RV(hcloud_firewall_info[].rules[].protocol=tcp) or RV(hcloud_firewall_info[].rules[].protocol=udp) + sample: "80" + source_ips: + description: List of source CIDRs that are allowed within this rule. + type: list + elements: str + returned: always + sample: ["0.0.0.0/0", "::/0"] + destination_ips: + description: List of destination CIDRs that are allowed within this rule. + type: list + elements: str + returned: always + sample: [] + applied_to: + description: List of Resources the Firewall is applied to. + returned: always + type: list + elements: dict + contains: + type: + description: Type of the resource. + type: str + choices: [server, label_selector] + sample: label_selector + server: + description: ID of the server. + type: int + sample: 12345 + label_selector: + description: Label selector value. + type: str + sample: env=prod + applied_to_resources: + description: List of Resources the Firewall label selector is applied to. + returned: if RV(hcloud_firewall_info[].applied_to[].type=label_selector) + type: list + elements: dict + contains: + type: + description: Type of resource referenced. + type: str + choices: [server] + sample: server + server: + description: ID of the Server. + type: int + sample: 12345 +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.firewalls import ( + BoundFirewall, + FirewallResource, + FirewallRule, +) + + +class AnsibleHCloudFirewallInfo(AnsibleHCloud): + represent = "hcloud_firewall_info" + + hcloud_firewall_info: list[BoundFirewall] | None = None + + def _prepare_result(self): + tmp = [] + + for firewall in self.hcloud_firewall_info: + if firewall is None: + continue + + tmp.append( + { + "id": to_native(firewall.id), + "name": to_native(firewall.name), + "labels": firewall.labels, + "rules": [self._prepare_result_rule(rule) for rule in firewall.rules], + "applied_to": [self._prepare_result_applied_to(resource) for resource in firewall.applied_to], + } + ) + + return tmp + + def _prepare_result_rule(self, rule: FirewallRule): + return { + "description": to_native(rule.description) if rule.description is not None else None, + "direction": to_native(rule.direction), + "protocol": to_native(rule.protocol), + "port": to_native(rule.port) if rule.port is not None else None, + "source_ips": [to_native(cidr) for cidr in rule.source_ips], + "destination_ips": [to_native(cidr) for cidr in rule.destination_ips], + } + + def _prepare_result_applied_to(self, resource: FirewallResource): + result = { + "type": to_native(resource.type), + "server": to_native(resource.server.id) if resource.server is not None else None, + "label_selector": ( + to_native(resource.label_selector.selector) if resource.label_selector is not None else None + ), + } + if resource.applied_to_resources is not None: + result["applied_to_resources"] = [ + { + "type": to_native(item.type), + "server": to_native(item.server.id) if item.server is not None else None, + } + for item in resource.applied_to_resources + ] + return result + + def get_firewalls(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_firewall_info = [self.client.firewalls.get_by_id(self.module.params.get("id"))] + elif self.module.params.get("name") is not None: + self.hcloud_firewall_info = [self.client.firewalls.get_by_name(self.module.params.get("name"))] + elif self.module.params.get("label_selector") is not None: + self.hcloud_firewall_info = self.client.firewalls.get_all( + label_selector=self.module.params.get("label_selector") + ) + else: + self.hcloud_firewall_info = self.client.firewalls.get_all() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + label_selector={"type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudFirewallInfo.define_module() + hcloud = AnsibleHCloudFirewallInfo(module) + + hcloud.get_firewalls() + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/firewall_resource.py b/ansible_collections/hetzner/hcloud/plugins/modules/firewall_resource.py new file mode 100644 index 000000000..207f27092 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/firewall_resource.py @@ -0,0 +1,243 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: firewall_resource +short_description: Manage Resources a Hetzner Cloud Firewall is applied to. + +description: + - Add and Remove Resources a Hetzner Cloud Firewall is applied to. + +author: + - Jonas Lammler (@jooola) + +version_added: 2.5.0 +options: + firewall: + description: + - Name or ID of the Hetzner Cloud Firewall. + type: str + required: true + servers: + description: + - List of Server Name or ID. + type: list + elements: str + label_selectors: + description: + - List of Label Selector. + type: list + elements: str + state: + description: + - State of the firewall resources. + default: present + choices: [absent, present] + type: str + +extends_documentation_fragment: + - hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Apply a firewall to a list of servers + hetzner.hcloud.firewall_resource: + name: my-firewall + servers: + - my-server + - 3456789 + state: present + +- name: Remove a firewall from a list of servers + hetzner.hcloud.firewall_resource: + name: my-firewall + servers: + - my-server + - 3456789 + state: absent + +- name: Apply a firewall to resources using label selectors + hetzner.hcloud.firewall_resource: + name: my-firewall + label_selectors: + - env=prod + state: present + +- name: Remove a firewall from resources using label selectors + hetzner.hcloud.firewall_resource: + name: my-firewall + label_selectors: + - env=prod + state: absent +""" + +RETURN = """ +hcloud_firewall_resource: + description: The Resources a Hetzner Cloud Firewall is applied to. + returned: always + type: dict + contains: + firewall: + description: + - Name of the Hetzner Cloud Firewall. + type: str + sample: my-firewall + servers: + description: + - List of Server Name. + type: list + elements: str + sample: [my-server1, my-server2] + label_selectors: + description: + - List of Label Selector. + type: list + elements: str + sample: [env=prod] +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.firewalls import ( + BoundFirewall, + FirewallResource, + FirewallResourceLabelSelector, +) +from ..module_utils.vendor.hcloud.servers import BoundServer + + +class AnsibleHCloudFirewallResource(AnsibleHCloud): + represent = "hcloud_firewall_resource" + + hcloud_firewall_resource: BoundFirewall | None = None + + def _prepare_result(self): + servers = [] + label_selectors = [] + for resource in self.hcloud_firewall_resource.applied_to: + if resource.type == FirewallResource.TYPE_SERVER: + servers.append(to_native(resource.server.name)) + elif resource.type == FirewallResource.TYPE_LABEL_SELECTOR: + label_selectors.append(to_native(resource.label_selector.selector)) + + return { + "firewall": to_native(self.hcloud_firewall_resource.name), + "servers": servers, + "label_selectors": label_selectors, + } + + def _get_firewall(self): + try: + self.hcloud_firewall_resource = self._client_get_by_name_or_id( + "firewalls", + self.module.params.get("firewall"), + ) + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _diff_firewall_resources(self, operator) -> list[FirewallResource]: + before = self._prepare_result() + + resources: list[FirewallResource] = [] + + servers: list[str] | None = self.module.params.get("servers") + if servers: + for server_param in servers: + try: + server: BoundServer = self._client_get_by_name_or_id("servers", server_param) + except HCloudException as exception: + self.fail_json_hcloud(exception) + + if operator(server.name, before["servers"]): + resources.append( + FirewallResource( + type=FirewallResource.TYPE_SERVER, + server=server, + ) + ) + + label_selectors = self.module.params.get("label_selectors") + if label_selectors: + for label_selector in label_selectors: + if operator(label_selector, before["label_selectors"]): + resources.append( + FirewallResource( + type=FirewallResource.TYPE_LABEL_SELECTOR, + label_selector=FirewallResourceLabelSelector(selector=label_selector), + ) + ) + + return resources + + def present_firewall_resources(self): + self._get_firewall() + resources = self._diff_firewall_resources( + lambda to_add, before: to_add not in before, + ) + if resources: + if not self.module.check_mode: + actions = self.hcloud_firewall_resource.apply_to_resources(resources=resources) + for action in actions: + action.wait_until_finished() + + self.hcloud_firewall_resource.reload() + + self._mark_as_changed() + + def absent_firewall_resources(self): + self._get_firewall() + resources = self._diff_firewall_resources( + lambda to_remove, before: to_remove in before, + ) + if resources: + if not self.module.check_mode: + actions = self.hcloud_firewall_resource.remove_from_resources(resources=resources) + for action in actions: + action.wait_until_finished() + + self.hcloud_firewall_resource.reload() + + self._mark_as_changed() + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec={ + "firewall": {"type": "str", "required": True}, + "servers": {"type": "list", "elements": "str"}, + "label_selectors": {"type": "list", "elements": "str"}, + "state": { + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + }, + required_one_of=[["servers", "label_selectors"]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudFirewallResource.define_module() + + hcloud = AnsibleHCloudFirewallResource(module) + state = module.params.get("state") + if state == "absent": + hcloud.absent_firewall_resources() + elif state == "present": + hcloud.present_firewall_resources() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/floating_ip.py b/ansible_collections/hetzner/hcloud/plugins/modules/floating_ip.py new file mode 100644 index 000000000..e037dd7a1 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/floating_ip.py @@ -0,0 +1,340 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: floating_ip + +short_description: Create and manage cloud Floating IPs on the Hetzner Cloud. + + +description: + - Create, update and manage cloud Floating IPs on the Hetzner Cloud. + +author: + - Lukas Kaemmerling (@lkaemmerling) +version_added: 0.1.0 +options: + id: + description: + - The ID of the Hetzner Cloud Floating IPs to manage. + - Only required if no Floating IP I(name) is given. + type: int + name: + description: + - The Name of the Hetzner Cloud Floating IPs to manage. + - Only required if no Floating IP I(id) is given or a Floating IP does not exist. + type: str + description: + description: + - The Description of the Hetzner Cloud Floating IPs. + type: str + home_location: + description: + - Home Location of the Hetzner Cloud Floating IP. + - Required if no I(server) is given and Floating IP does not exist. + type: str + server: + description: + - Server Name the Floating IP should be assigned to. + - Required if no I(home_location) is given and Floating IP does not exist. + type: str + type: + description: + - Type of the Floating IP. + - Required if Floating IP does not exist + choices: [ ipv4, ipv6 ] + type: str + force: + description: + - Force the assignment or deletion of the Floating IP. + type: bool + delete_protection: + description: + - Protect the Floating IP for deletion. + type: bool + labels: + description: + - User-defined labels (key-value pairs). + type: dict + state: + description: + - State of the Floating IP. + default: present + choices: [ absent, present ] + type: str + +extends_documentation_fragment: +- hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a basic IPv4 Floating IP + hetzner.hcloud.floating_ip: + name: my-floating-ip + home_location: fsn1 + type: ipv4 + state: present +- name: Create a basic IPv6 Floating IP + hetzner.hcloud.floating_ip: + name: my-floating-ip + home_location: fsn1 + type: ipv6 + state: present +- name: Assign a Floating IP to a server + hetzner.hcloud.floating_ip: + name: my-floating-ip + server: 1234 + state: present +- name: Assign a Floating IP to another server + hetzner.hcloud.floating_ip: + name: my-floating-ip + server: 1234 + force: true + state: present +- name: Floating IP should be absent + hetzner.hcloud.floating_ip: + name: my-floating-ip + state: absent +""" + +RETURN = """ +hcloud_floating_ip: + description: The Floating IP instance + returned: Always + type: complex + contains: + id: + description: ID of the Floating IP + type: int + returned: Always + sample: 12345 + name: + description: Name of the Floating IP + type: str + returned: Always + sample: my-floating-ip + description: + description: Description of the Floating IP + type: str + returned: Always + sample: my-floating-ip + ip: + description: IP Address of the Floating IP + type: str + returned: Always + sample: 116.203.104.109 + type: + description: Type of the Floating IP + type: str + returned: Always + sample: ipv4 + home_location: + description: Name of the home location of the Floating IP + type: str + returned: Always + sample: fsn1 + server: + description: Name of the server the Floating IP is assigned to. + type: str + returned: Always + sample: "my-server" + delete_protection: + description: True if Floating IP is protected for deletion + type: bool + returned: always + sample: false + version_added: "0.1.0" + labels: + description: User-defined labels (key-value pairs) + type: dict + returned: Always + sample: + key: value + mylabel: 123 +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.floating_ips import BoundFloatingIP + + +class AnsibleHCloudFloatingIP(AnsibleHCloud): + represent = "hcloud_floating_ip" + + hcloud_floating_ip: BoundFloatingIP | None = None + + def _prepare_result(self): + server = None + + if self.hcloud_floating_ip.server is not None: + server = to_native(self.hcloud_floating_ip.server.name) + return { + "id": to_native(self.hcloud_floating_ip.id), + "name": to_native(self.hcloud_floating_ip.name), + "description": to_native(self.hcloud_floating_ip.description), + "ip": to_native(self.hcloud_floating_ip.ip), + "type": to_native(self.hcloud_floating_ip.type), + "home_location": to_native(self.hcloud_floating_ip.home_location.name), + "labels": self.hcloud_floating_ip.labels, + "server": server, + "delete_protection": self.hcloud_floating_ip.protection["delete"], + } + + def _get_floating_ip(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_floating_ip = self.client.floating_ips.get_by_id(self.module.params.get("id")) + else: + self.hcloud_floating_ip = self.client.floating_ips.get_by_name(self.module.params.get("name")) + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _create_floating_ip(self): + self.module.fail_on_missing_params(required_params=["type"]) + try: + params = { + "description": self.module.params.get("description"), + "type": self.module.params.get("type"), + "name": self.module.params.get("name"), + } + if self.module.params.get("home_location") is not None: + params["home_location"] = self.client.locations.get_by_name(self.module.params.get("home_location")) + elif self.module.params.get("server") is not None: + params["server"] = self.client.servers.get_by_name(self.module.params.get("server")) + else: + self.module.fail_json(msg="one of the following is required: home_location, server") + + if self.module.params.get("labels") is not None: + params["labels"] = self.module.params.get("labels") + if not self.module.check_mode: + resp = self.client.floating_ips.create(**params) + self.hcloud_floating_ip = resp.floating_ip + + delete_protection = self.module.params.get("delete_protection") + if delete_protection is not None: + self.hcloud_floating_ip.change_protection(delete=delete_protection).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self._get_floating_ip() + + def _update_floating_ip(self): + try: + labels = self.module.params.get("labels") + if labels is not None and labels != self.hcloud_floating_ip.labels: + if not self.module.check_mode: + self.hcloud_floating_ip.update(labels=labels) + self._mark_as_changed() + + description = self.module.params.get("description") + if description is not None and description != self.hcloud_floating_ip.description: + if not self.module.check_mode: + self.hcloud_floating_ip.update(description=description) + self._mark_as_changed() + + server = self.module.params.get("server") + if server is not None and self.hcloud_floating_ip.server is not None: + if self.module.params.get("force") and server != self.hcloud_floating_ip.server.name: + if not self.module.check_mode: + self.hcloud_floating_ip.assign(self.client.servers.get_by_name(server)) + self._mark_as_changed() + elif server != self.hcloud_floating_ip.server.name: + self.module.warn( + "Floating IP is already assigned to another server " + f"{self.hcloud_floating_ip.server.name}. You need to " + "unassign the Floating IP or use force=true." + ) + self._mark_as_changed() + elif server is not None and self.hcloud_floating_ip.server is None: + if not self.module.check_mode: + self.hcloud_floating_ip.assign(self.client.servers.get_by_name(server)) + self._mark_as_changed() + elif server is None and self.hcloud_floating_ip.server is not None: + if not self.module.check_mode: + self.hcloud_floating_ip.unassign() + self._mark_as_changed() + + delete_protection = self.module.params.get("delete_protection") + if delete_protection is not None and delete_protection != self.hcloud_floating_ip.protection["delete"]: + if not self.module.check_mode: + self.hcloud_floating_ip.change_protection(delete=delete_protection).wait_until_finished() + self._mark_as_changed() + + self._get_floating_ip() + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def present_floating_ip(self): + self._get_floating_ip() + if self.hcloud_floating_ip is None: + self._create_floating_ip() + else: + self._update_floating_ip() + + def delete_floating_ip(self): + try: + self._get_floating_ip() + if self.hcloud_floating_ip is not None: + if self.module.params.get("force") or self.hcloud_floating_ip.server is None: + if not self.module.check_mode: + self.client.floating_ips.delete(self.hcloud_floating_ip) + else: + self.module.warn( + "Floating IP is currently assigned to server " + f"{self.hcloud_floating_ip.server.name}. You need to " + "unassign the Floating IP or use force=true." + ) + self._mark_as_changed() + self.hcloud_floating_ip = None + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + description={"type": "str"}, + server={"type": "str"}, + home_location={"type": "str"}, + force={"type": "bool"}, + type={"choices": ["ipv4", "ipv6"]}, + labels={"type": "dict"}, + delete_protection={"type": "bool"}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + required_one_of=[["id", "name"]], + mutually_exclusive=[["home_location", "server"]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudFloatingIP.define_module() + + hcloud = AnsibleHCloudFloatingIP(module) + state = module.params["state"] + if state == "absent": + hcloud.delete_floating_ip() + elif state == "present": + hcloud.present_floating_ip() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/floating_ip_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/floating_ip_info.py new file mode 100644 index 000000000..663d29622 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/floating_ip_info.py @@ -0,0 +1,180 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: floating_ip_info + +short_description: Gather infos about the Hetzner Cloud Floating IPs. + +description: + - Gather facts about your Hetzner Cloud Floating IPs. + +author: + - Lukas Kaemmerling (@LKaemmerling) + +options: + id: + description: + - The ID of the Floating IP you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name for the Floating IP you want to get. + type: str + label_selector: + description: + - The label selector for the Floating IP you want to get. + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Gather hcloud Floating ip infos + hetzner.hcloud.floating_ip_info: + register: output +- name: Print the gathered infos + debug: + var: output +""" + +RETURN = """ +hcloud_floating_ip_info: + description: The Floating ip infos as list + returned: always + type: complex + contains: + id: + description: Numeric identifier of the Floating IP + returned: always + type: int + sample: 1937415 + name: + description: Name of the Floating IP + returned: Always + type: str + sample: my-floating-ip + version_added: "0.1.0" + description: + description: Description of the Floating IP + returned: always + type: str + sample: Falkenstein DC 8 + ip: + description: IP address of the Floating IP + returned: always + type: str + sample: 131.232.99.1 + type: + description: Type of the Floating IP + returned: always + type: str + sample: ipv4 + server: + description: Name of the server where the Floating IP is assigned to. + returned: always + type: str + sample: my-server + home_location: + description: Location the Floating IP was created in + returned: always + type: str + sample: fsn1 + delete_protection: + description: True if the Floating IP is protected for deletion + returned: always + type: bool + version_added: "0.1.0" + labels: + description: User-defined labels (key-value pairs) + returned: always + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.floating_ips import BoundFloatingIP + + +class AnsibleHCloudFloatingIPInfo(AnsibleHCloud): + represent = "hcloud_floating_ip_info" + + hcloud_floating_ip_info: list[BoundFloatingIP] | None = None + + def _prepare_result(self): + tmp = [] + + for floating_ip in self.hcloud_floating_ip_info: + if floating_ip is not None: + server_name = None + if floating_ip.server is not None: + server_name = floating_ip.server.name + tmp.append( + { + "id": to_native(floating_ip.id), + "name": to_native(floating_ip.name), + "description": to_native(floating_ip.description), + "ip": to_native(floating_ip.ip), + "type": to_native(floating_ip.type), + "server": to_native(server_name), + "home_location": to_native(floating_ip.home_location.name), + "labels": floating_ip.labels, + "delete_protection": floating_ip.protection["delete"], + } + ) + + return tmp + + def get_floating_ips(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_floating_ip_info = [self.client.floating_ips.get_by_id(self.module.params.get("id"))] + elif self.module.params.get("name") is not None: + self.hcloud_floating_ip_info = [self.client.floating_ips.get_by_name(self.module.params.get("name"))] + elif self.module.params.get("label_selector") is not None: + self.hcloud_floating_ip_info = self.client.floating_ips.get_all( + label_selector=self.module.params.get("label_selector") + ) + else: + self.hcloud_floating_ip_info = self.client.floating_ips.get_all() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + label_selector={"type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudFloatingIPInfo.define_module() + hcloud = AnsibleHCloudFloatingIPInfo(module) + + hcloud.get_floating_ips() + result = hcloud.get_result() + + ansible_info = {"hcloud_floating_ip_info": result["hcloud_floating_ip_info"]} + module.exit_json(**ansible_info) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_certificate.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_certificate.py deleted file mode 100644 index 0f6dcf0f2..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_certificate.py +++ /dev/null @@ -1,291 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2020, 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 = ''' ---- -module: hcloud_certificate - -short_description: Create and manage certificates on the Hetzner Cloud. - - -description: - - Create, update and manage certificates on the Hetzner Cloud. - -author: - - Lukas Kaemmerling (@lkaemmerling) - -options: - id: - description: - - The ID of the Hetzner Cloud certificate to manage. - - Only required if no certificate I(name) is given - type: int - name: - description: - - The Name of the Hetzner Cloud certificate to manage. - - Only required if no certificate I(id) is given or a certificate does not exist. - type: str - labels: - description: - - User-defined labels (key-value pairs) - type: dict - certificate: - description: - - Certificate and chain in PEM format, in order so that each record directly certifies the one preceding. - - Required if certificate does not exist. - type: str - private_key: - description: - - Certificate key in PEM format. - - Required if certificate does not exist. - type: str - domain_names: - description: - - Certificate key in PEM format. - - Required if certificate does not exist. - type: list - default: [ ] - elements: str - type: - description: - - Choose between uploading a Certificate in PEM format or requesting a managed Let's Encrypt Certificate. - default: uploaded - choices: [ uploaded, managed ] - type: str - state: - description: - - State of the certificate. - default: present - choices: [ absent, present ] - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Create a basic certificate - hcloud_certificate: - name: my-certificate - certificate: "ssh-rsa AAAjjk76kgf...Xt" - private_key: "ssh-rsa AAAjjk76kgf...Xt" - state: present - -- name: Create a certificate with labels - hcloud_certificate: - name: my-certificate - certificate: "ssh-rsa AAAjjk76kgf...Xt" - private_key: "ssh-rsa AAAjjk76kgf...Xt" - labels: - key: value - mylabel: 123 - state: present - -- name: Ensure the certificate is absent (remove if needed) - hcloud_certificate: - name: my-certificate - state: absent -""" - -RETURN = """ -hcloud_certificate: - description: The certificate instance - returned: Always - type: complex - contains: - id: - description: Numeric identifier of the certificate - returned: always - type: int - sample: 1937415 - name: - description: Name of the certificate - returned: always - type: str - sample: my website cert - fingerprint: - description: Fingerprint of the certificate - returned: always - type: str - sample: "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f" - certificate: - description: Certificate and chain in PEM format - returned: always - type: str - sample: "-----BEGIN CERTIFICATE-----..." - domain_names: - description: List of Domains and Subdomains covered by the Certificate - returned: always - type: dict - not_valid_before: - description: Point in time when the Certificate becomes valid (in ISO-8601 format) - returned: always - type: str - not_valid_after: - description: Point in time when the Certificate stops being valid (in ISO-8601 format) - returned: always - type: str - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudCertificate(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_certificate") - self.hcloud_certificate = None - - def _prepare_result(self): - return { - "id": to_native(self.hcloud_certificate.id), - "name": to_native(self.hcloud_certificate.name), - "type": to_native(self.hcloud_certificate.type), - "fingerprint": to_native(self.hcloud_certificate.fingerprint), - "certificate": to_native(self.hcloud_certificate.certificate), - "not_valid_before": to_native(self.hcloud_certificate.not_valid_before), - "not_valid_after": to_native(self.hcloud_certificate.not_valid_after), - "domain_names": [to_native(domain) for domain in self.hcloud_certificate.domain_names], - "labels": self.hcloud_certificate.labels - } - - def _get_certificate(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_certificate = self.client.certificates.get_by_id( - self.module.params.get("id") - ) - elif self.module.params.get("name") is not None: - self.hcloud_certificate = self.client.certificates.get_by_name( - self.module.params.get("name") - ) - - except Exception as e: - self.module.fail_json(msg=e.message) - - def _create_certificate(self): - self.module.fail_on_missing_params( - required_params=["name"] - ) - - params = { - "name": self.module.params.get("name"), - "labels": self.module.params.get("labels") - } - if self.module.params.get('type') == 'uploaded': - self.module.fail_on_missing_params( - required_params=["certificate", "private_key"] - ) - params["certificate"] = self.module.params.get("certificate") - params["private_key"] = self.module.params.get("private_key") - if not self.module.check_mode: - try: - self.client.certificates.create(**params) - except Exception as e: - self.module.fail_json(msg=e.message) - else: - self.module.fail_on_missing_params( - required_params=["domain_names"] - ) - params["domain_names"] = self.module.params.get("domain_names") - if not self.module.check_mode: - try: - resp = self.client.certificates.create_managed(**params) - resp.action.wait_until_finished(max_retries=1000) - except Exception as e: - self.module.fail_json(msg=e.message) - - self._mark_as_changed() - self._get_certificate() - - def _update_certificate(self): - try: - name = self.module.params.get("name") - if name is not None and self.hcloud_certificate.name != name: - self.module.fail_on_missing_params( - required_params=["id"] - ) - if not self.module.check_mode: - self.hcloud_certificate.update(name=name) - self._mark_as_changed() - - labels = self.module.params.get("labels") - if labels is not None and self.hcloud_certificate.labels != labels: - if not self.module.check_mode: - self.hcloud_certificate.update(labels=labels) - self._mark_as_changed() - except Exception as e: - self.module.fail_json(msg=e.message) - self._get_certificate() - - def present_certificate(self): - self._get_certificate() - if self.hcloud_certificate is None: - self._create_certificate() - else: - self._update_certificate() - - def delete_certificate(self): - self._get_certificate() - if self.hcloud_certificate is not None: - if not self.module.check_mode: - try: - self.client.certificates.delete(self.hcloud_certificate) - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self.hcloud_certificate = None - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - type={ - "choices": ["uploaded", "managed"], - "default": "uploaded", - }, - domain_names={"type": "list", "elements": "str", "default": []}, - certificate={"type": "str"}, - private_key={"type": "str", "no_log": True}, - labels={"type": "dict"}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - required_one_of=[['id', 'name']], - required_if=[['state', 'present', ['name']]], - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudCertificate.define_module() - - hcloud = AnsibleHcloudCertificate(module) - state = module.params.get("state") - if state == "absent": - hcloud.delete_certificate() - elif state == "present": - hcloud.present_certificate() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_certificate_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_certificate_info.py deleted file mode 100644 index 855706f1f..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_certificate_info.py +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2020, 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 = ''' ---- -module: hcloud_certificate_info -short_description: Gather infos about your Hetzner Cloud certificates. -description: - - Gather facts about your Hetzner Cloud certificates. -author: - - Lukas Kaemmerling (@LKaemmerling) -options: - id: - description: - - The ID of the certificate you want to get. - type: int - name: - description: - - The name of the certificate you want to get. - type: str - label_selector: - description: - - The label selector for the certificate you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud certificate infos - hcloud_certificate_info: - register: output -- name: Print the gathered infos - debug: - var: output.hcloud_certificate_info -""" - -RETURN = """ -hcloud_certificate_info: - description: The certificate instances - returned: Always - type: complex - contains: - id: - description: Numeric identifier of the certificate - returned: always - type: int - sample: 1937415 - name: - description: Name of the certificate - returned: always - type: str - sample: my website cert - fingerprint: - description: Fingerprint of the certificate - returned: always - type: str - sample: "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f" - certificate: - description: Certificate and chain in PEM format - returned: always - type: str - sample: "-----BEGIN CERTIFICATE-----..." - domain_names: - description: List of Domains and Subdomains covered by the Certificate - returned: always - type: dict - not_valid_before: - description: Point in time when the Certificate becomes valid (in ISO-8601 format) - returned: always - type: str - not_valid_after: - description: Point in time when the Certificate stops being valid (in ISO-8601 format) - returned: always - type: str - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict -""" -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudCertificateInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_certificate_info") - self.hcloud_certificate_info = None - - def _prepare_result(self): - certificates = [] - - for certificate in self.hcloud_certificate_info: - if certificate: - certificates.append({ - "id": to_native(certificate.id), - "name": to_native(certificate.name), - "fingerprint": to_native(certificate.fingerprint), - "certificate": to_native(certificate.certificate), - "not_valid_before": to_native(certificate.not_valid_before), - "not_valid_after": to_native(certificate.not_valid_after), - "domain_names": [to_native(domain) for domain in certificate.domain_names], - "labels": certificate.labels - }) - return certificates - - def get_certificates(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_certificate_info = [self.client.certificates.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_certificate_info = [self.client.certificates.get_by_name( - self.module.params.get("name") - )] - elif self.module.params.get("label_selector") is not None: - self.hcloud_certificate_info = self.client.certificates.get_all( - label_selector=self.module.params.get("label_selector")) - else: - self.hcloud_certificate_info = self.client.certificates.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - label_selector={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudCertificateInfo.define_module() - - hcloud = AnsibleHcloudCertificateInfo(module) - hcloud.get_certificates() - result = hcloud.get_result() - - ansible_info = { - 'hcloud_certificate_info': result['hcloud_certificate_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_datacenter_facts.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_datacenter_facts.py deleted file mode 100644 index 8cebabf8c..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_datacenter_facts.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_datacenter_info - -short_description: Gather info about the Hetzner Cloud datacenters. - -description: - - Gather info about your Hetzner Cloud datacenters. - - This module was called C(hcloud_datacenter_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_datacenter_facts). - Note that the M(hetzner.hcloud.hcloud_datacenter_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_datacenter_info)! - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the datacenter you want to get. - type: int - name: - description: - - The name of the datacenter you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud datacenter info - hcloud_datacenter_info: - register: output -- name: Print the gathered info - debug: - var: output -""" - -RETURN = """ -hcloud_datacenter_info: - description: - - The datacenter info as list - - This module was called C(hcloud_datacenter_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_datacenter_facts). - Note that the M(hetzner.hcloud.hcloud_datacenter_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_datacenter_info)! - returned: always - type: complex - contains: - id: - description: Numeric identifier of the datacenter - returned: always - type: int - sample: 1937415 - name: - description: Name of the datacenter - returned: always - type: str - sample: fsn1-dc8 - description: - description: Detail description of the datacenter - returned: always - type: str - sample: Falkenstein DC 8 - location: - description: Name of the location where the datacenter resides in - returned: always - type: str - sample: fsn1 - city: - description: City of the location - returned: always - type: str - sample: fsn1 -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudDatacenterInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_datacenter_info") - self.hcloud_datacenter_info = None - - def _prepare_result(self): - tmp = [] - - for datacenter in self.hcloud_datacenter_info: - if datacenter is not None: - tmp.append({ - "id": to_native(datacenter.id), - "name": to_native(datacenter.name), - "description": to_native(datacenter.description), - "location": to_native(datacenter.location.name) - }) - - return tmp - - def get_datacenters(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_datacenter_info = [self.client.datacenters.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_datacenter_info = [self.client.datacenters.get_by_name( - self.module.params.get("name") - )] - else: - self.hcloud_datacenter_info = self.client.datacenters.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudDatacenterInfo.define_module() - - is_old_facts = module._name == 'hcloud_datacenter_facts' - if is_old_facts: - module.deprecate("The 'hcloud_datacenter_facts' module has been renamed to 'hcloud_datacenter_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - hcloud = AnsibleHcloudDatacenterInfo(module) - - hcloud.get_datacenters() - result = hcloud.get_result() - if is_old_facts: - ansible_info = { - 'hcloud_datacenter_facts': result['hcloud_datacenter_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_datacenter_info': result['hcloud_datacenter_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_datacenter_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_datacenter_info.py deleted file mode 100644 index 8cebabf8c..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_datacenter_info.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_datacenter_info - -short_description: Gather info about the Hetzner Cloud datacenters. - -description: - - Gather info about your Hetzner Cloud datacenters. - - This module was called C(hcloud_datacenter_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_datacenter_facts). - Note that the M(hetzner.hcloud.hcloud_datacenter_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_datacenter_info)! - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the datacenter you want to get. - type: int - name: - description: - - The name of the datacenter you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud datacenter info - hcloud_datacenter_info: - register: output -- name: Print the gathered info - debug: - var: output -""" - -RETURN = """ -hcloud_datacenter_info: - description: - - The datacenter info as list - - This module was called C(hcloud_datacenter_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_datacenter_facts). - Note that the M(hetzner.hcloud.hcloud_datacenter_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_datacenter_info)! - returned: always - type: complex - contains: - id: - description: Numeric identifier of the datacenter - returned: always - type: int - sample: 1937415 - name: - description: Name of the datacenter - returned: always - type: str - sample: fsn1-dc8 - description: - description: Detail description of the datacenter - returned: always - type: str - sample: Falkenstein DC 8 - location: - description: Name of the location where the datacenter resides in - returned: always - type: str - sample: fsn1 - city: - description: City of the location - returned: always - type: str - sample: fsn1 -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudDatacenterInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_datacenter_info") - self.hcloud_datacenter_info = None - - def _prepare_result(self): - tmp = [] - - for datacenter in self.hcloud_datacenter_info: - if datacenter is not None: - tmp.append({ - "id": to_native(datacenter.id), - "name": to_native(datacenter.name), - "description": to_native(datacenter.description), - "location": to_native(datacenter.location.name) - }) - - return tmp - - def get_datacenters(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_datacenter_info = [self.client.datacenters.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_datacenter_info = [self.client.datacenters.get_by_name( - self.module.params.get("name") - )] - else: - self.hcloud_datacenter_info = self.client.datacenters.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudDatacenterInfo.define_module() - - is_old_facts = module._name == 'hcloud_datacenter_facts' - if is_old_facts: - module.deprecate("The 'hcloud_datacenter_facts' module has been renamed to 'hcloud_datacenter_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - hcloud = AnsibleHcloudDatacenterInfo(module) - - hcloud.get_datacenters() - result = hcloud.get_result() - if is_old_facts: - ansible_info = { - 'hcloud_datacenter_facts': result['hcloud_datacenter_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_datacenter_info': result['hcloud_datacenter_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_firewall.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_firewall.py deleted file mode 100644 index 34608977e..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_firewall.py +++ /dev/null @@ -1,359 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2020, 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 = ''' ---- -module: hcloud_firewall - -short_description: Create and manage firewalls on the Hetzner Cloud. - - -description: - - Create, update and manage firewalls on the Hetzner Cloud. - -author: - - Lukas Kaemmerling (@lkaemmerling) - -options: - id: - description: - - The ID of the Hetzner Cloud firewall to manage. - - Only required if no firewall I(name) is given - type: int - name: - description: - - The Name of the Hetzner Cloud firewall to manage. - - Only required if no firewall I(id) is given, or a firewall does not exist. - type: str - labels: - description: - - User-defined labels (key-value pairs) - type: dict - rules: - description: - - List of rules the firewall should contain. - type: list - elements: dict - suboptions: - direction: - description: - - The direction of the firewall rule. - type: str - choices: [ in, out ] - port: - description: - - The port of the firewall rule. - type: str - protocol: - description: - - The protocol of the firewall rule. - type: str - choices: [ icmp, tcp, udp, esp, gre ] - source_ips: - description: - - List of CIDRs that are allowed within this rule - type: list - elements: str - default: [ ] - destination_ips: - description: - - List of CIDRs that are allowed within this rule - type: list - elements: str - default: [ ] - description: - description: - - User defined description of this rule. - type: str - state: - description: - - State of the firewall. - default: present - choices: [ absent, present ] - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud -''' - -EXAMPLES = """ -- name: Create a basic firewall - hcloud_firewall: - name: my-firewall - state: present - -- name: Create a firewall with rules - hcloud_firewall: - name: my-firewall - rules: - - direction: in - protocol: icmp - source_ips: - - 0.0.0.0/0 - - ::/0 - description: allow icmp in - state: present - -- name: Create a firewall with labels - hcloud_firewall: - name: my-firewall - labels: - key: value - mylabel: 123 - state: present - -- name: Ensure the firewall is absent (remove if needed) - hcloud_firewall: - name: my-firewall - state: absent -""" - -RETURN = """ -hcloud_firewall: - description: The firewall instance - returned: Always - type: complex - contains: - id: - description: Numeric identifier of the firewall - returned: always - type: int - sample: 1937415 - name: - description: Name of the firewall - returned: always - type: str - sample: my firewall - rules: - description: List of Rules within this Firewall - returned: always - type: complex - contains: - direction: - description: Direction of the Firewall Rule - type: str - returned: always - sample: in - protocol: - description: Protocol of the Firewall Rule - type: str - returned: always - sample: icmp - port: - description: Port of the Firewall Rule, None/Null if protocol is icmp - type: str - returned: always - sample: in - source_ips: - description: Source IPs of the Firewall - type: list - elements: str - returned: always - destination_ips: - description: Source IPs of the Firewall - type: list - elements: str - returned: always - description: - description: User defined description of the Firewall Rule - type: str - returned: always - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud -import time - -try: - from hcloud.firewalls.domain import FirewallRule - from hcloud import APIException -except ImportError: - APIException = None - FirewallRule = None - - -class AnsibleHcloudFirewall(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_firewall") - self.hcloud_firewall = None - - def _prepare_result(self): - return { - "id": to_native(self.hcloud_firewall.id), - "name": to_native(self.hcloud_firewall.name), - "rules": [self._prepare_result_rule(rule) for rule in self.hcloud_firewall.rules], - "labels": self.hcloud_firewall.labels - } - - def _prepare_result_rule(self, rule): - return { - "direction": rule.direction, - "protocol": to_native(rule.protocol), - "port": to_native(rule.port) if rule.port is not None else None, - "source_ips": [to_native(cidr) for cidr in rule.source_ips], - "destination_ips": [to_native(cidr) for cidr in rule.destination_ips], - "description": to_native(rule.description) if rule.description is not None else None, - } - - def _get_firewall(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_firewall = self.client.firewalls.get_by_id( - self.module.params.get("id") - ) - elif self.module.params.get("name") is not None: - self.hcloud_firewall = self.client.firewalls.get_by_name( - self.module.params.get("name") - ) - - except Exception as e: - self.module.fail_json(msg=e.message) - - def _create_firewall(self): - self.module.fail_on_missing_params( - required_params=["name"] - ) - params = { - "name": self.module.params.get("name"), - "labels": self.module.params.get("labels") - } - rules = self.module.params.get("rules") - if rules is not None: - params["rules"] = [ - FirewallRule( - direction=rule["direction"], - protocol=rule["protocol"], - source_ips=rule["source_ips"] if rule["source_ips"] is not None else [], - destination_ips=rule["destination_ips"] if rule["destination_ips"] is not None else [], - port=rule["port"], - description=rule["description"], - ) - for rule in rules - ] - if not self.module.check_mode: - try: - self.client.firewalls.create(**params) - except Exception as e: - self.module.fail_json(msg=e.message, **params) - self._mark_as_changed() - self._get_firewall() - - def _update_firewall(self): - name = self.module.params.get("name") - if name is not None and self.hcloud_firewall.name != name: - self.module.fail_on_missing_params( - required_params=["id"] - ) - if not self.module.check_mode: - self.hcloud_firewall.update(name=name) - self._mark_as_changed() - - labels = self.module.params.get("labels") - if labels is not None and self.hcloud_firewall.labels != labels: - if not self.module.check_mode: - self.hcloud_firewall.update(labels=labels) - self._mark_as_changed() - - rules = self.module.params.get("rules") - if rules is not None and rules != [self._prepare_result_rule(rule) for rule in self.hcloud_firewall.rules]: - if not self.module.check_mode: - new_rules = [ - FirewallRule( - direction=rule["direction"], - protocol=rule["protocol"], - source_ips=rule["source_ips"] if rule["source_ips"] is not None else [], - destination_ips=rule["destination_ips"] if rule["destination_ips"] is not None else [], - port=rule["port"], - description=rule["description"], - ) - for rule in rules - ] - self.hcloud_firewall.set_rules(new_rules) - self._mark_as_changed() - self._get_firewall() - - def present_firewall(self): - self._get_firewall() - if self.hcloud_firewall is None: - self._create_firewall() - else: - self._update_firewall() - - def delete_firewall(self): - self._get_firewall() - if self.hcloud_firewall is not None: - if not self.module.check_mode: - retry_count = 0 - while retry_count < 10: - try: - self.client.firewalls.delete(self.hcloud_firewall) - break - except APIException as e: - if "is still in use" in e.message: - retry_count = retry_count + 1 - time.sleep(0.5 * retry_count) - else: - self.module.fail_json(msg=e.message) - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self.hcloud_firewall = None - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - rules=dict( - type="list", - elements="dict", - options=dict( - direction={"type": "str", "choices": ["in", "out"]}, - protocol={"type": "str", "choices": ["icmp", "udp", "tcp", "esp", "gre"]}, - port={"type": "str"}, - source_ips={"type": "list", "elements": "str", "default": []}, - destination_ips={"type": "list", "elements": "str", "default": []}, - description={"type": "str"}, - ), - required_together=[["direction", "protocol"]], - ), - labels={"type": "dict"}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - required_one_of=[['id', 'name']], - required_if=[['state', 'present', ['name']]], - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudFirewall.define_module() - - hcloud = AnsibleHcloudFirewall(module) - state = module.params.get("state") - if state == "absent": - hcloud.delete_firewall() - elif state == "present": - hcloud.present_firewall() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_floating_ip.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_floating_ip.py deleted file mode 100644 index 1ee61ea13..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_floating_ip.py +++ /dev/null @@ -1,355 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_floating_ip - -short_description: Create and manage cloud Floating IPs on the Hetzner Cloud. - - -description: - - Create, update and manage cloud Floating IPs on the Hetzner Cloud. - -author: - - Lukas Kaemmerling (@lkaemmerling) -version_added: 0.1.0 -options: - id: - description: - - The ID of the Hetzner Cloud Floating IPs to manage. - - Only required if no Floating IP I(name) is given. - type: int - name: - description: - - The Name of the Hetzner Cloud Floating IPs to manage. - - Only required if no Floating IP I(id) is given or a Floating IP does not exist. - type: str - description: - description: - - The Description of the Hetzner Cloud Floating IPs. - type: str - home_location: - description: - - Home Location of the Hetzner Cloud Floating IP. - - Required if no I(server) is given and Floating IP does not exist. - type: str - server: - description: - - Server Name the Floating IP should be assigned to. - - Required if no I(home_location) is given and Floating IP does not exist. - type: str - type: - description: - - Type of the Floating IP. - - Required if Floating IP does not exist - choices: [ ipv4, ipv6 ] - type: str - force: - description: - - Force the assignment or deletion of the Floating IP. - type: bool - delete_protection: - description: - - Protect the Floating IP for deletion. - type: bool - labels: - description: - - User-defined labels (key-value pairs). - type: dict - state: - description: - - State of the Floating IP. - default: present - choices: [ absent, present ] - type: str - -requirements: - - hcloud-python >= 1.6.0 - -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Create a basic IPv4 Floating IP - hcloud_floating_ip: - name: my-floating-ip - home_location: fsn1 - type: ipv4 - state: present -- name: Create a basic IPv6 Floating IP - hcloud_floating_ip: - name: my-floating-ip - home_location: fsn1 - type: ipv6 - state: present -- name: Assign a Floating IP to a server - hcloud_floating_ip: - name: my-floating-ip - server: 1234 - state: present -- name: Assign a Floating IP to another server - hcloud_floating_ip: - name: my-floating-ip - server: 1234 - force: yes - state: present -- name: Floating IP should be absent - hcloud_floating_ip: - name: my-floating-ip - state: absent -""" - -RETURN = """ -hcloud_floating_ip: - description: The Floating IP instance - returned: Always - type: complex - contains: - id: - description: ID of the Floating IP - type: int - returned: Always - sample: 12345 - name: - description: Name of the Floating IP - type: str - returned: Always - sample: my-floating-ip - description: - description: Description of the Floating IP - type: str - returned: Always - sample: my-floating-ip - ip: - description: IP Address of the Floating IP - type: str - returned: Always - sample: 116.203.104.109 - type: - description: Type of the Floating IP - type: str - returned: Always - sample: ipv4 - home_location: - description: Name of the home location of the Floating IP - type: str - returned: Always - sample: fsn1 - server: - description: Name of the server the Floating IP is assigned to. - type: str - returned: Always - sample: "my-server" - delete_protection: - description: True if Floating IP is protected for deletion - type: bool - returned: always - sample: false - version_added: "0.1.0" - labels: - description: User-defined labels (key-value pairs) - type: dict - returned: Always - sample: - key: value - mylabel: 123 -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudFloatingIP(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_floating_ip") - self.hcloud_floating_ip = None - - def _prepare_result(self): - server = None - - if self.hcloud_floating_ip.server is not None: - server = to_native(self.hcloud_floating_ip.server.name) - return { - "id": to_native(self.hcloud_floating_ip.id), - "name": to_native(self.hcloud_floating_ip.name), - "description": to_native(self.hcloud_floating_ip.description), - "ip": to_native(self.hcloud_floating_ip.ip), - "type": to_native(self.hcloud_floating_ip.type), - "home_location": to_native(self.hcloud_floating_ip.home_location.name), - "labels": self.hcloud_floating_ip.labels, - "server": server, - "delete_protection": self.hcloud_floating_ip.protection["delete"], - } - - def _get_floating_ip(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_floating_ip = self.client.floating_ips.get_by_id( - self.module.params.get("id") - ) - else: - self.hcloud_floating_ip = self.client.floating_ips.get_by_name( - self.module.params.get("name") - ) - except Exception as e: - self.module.fail_json(msg=e.message) - - def _create_floating_ip(self): - self.module.fail_on_missing_params( - required_params=["type"] - ) - try: - params = { - "description": self.module.params.get("description"), - "type": self.module.params.get("type"), - "name": self.module.params.get("name"), - } - if self.module.params.get("home_location") is not None: - params["home_location"] = self.client.locations.get_by_name( - self.module.params.get("home_location") - ) - elif self.module.params.get("server") is not None: - params["server"] = self.client.servers.get_by_name( - self.module.params.get("server") - ) - else: - self.module.fail_json(msg="one of the following is required: home_location, server") - - if self.module.params.get("labels") is not None: - params["labels"] = self.module.params.get("labels") - if not self.module.check_mode: - resp = self.client.floating_ips.create(**params) - self.hcloud_floating_ip = resp.floating_ip - - delete_protection = self.module.params.get("delete_protection") - if delete_protection is not None: - self.hcloud_floating_ip.change_protection(delete=delete_protection).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self._get_floating_ip() - - def _update_floating_ip(self): - try: - labels = self.module.params.get("labels") - if labels is not None and labels != self.hcloud_floating_ip.labels: - if not self.module.check_mode: - self.hcloud_floating_ip.update(labels=labels) - self._mark_as_changed() - - description = self.module.params.get("description") - if description is not None and description != self.hcloud_floating_ip.description: - if not self.module.check_mode: - self.hcloud_floating_ip.update(description=description) - self._mark_as_changed() - - server = self.module.params.get("server") - if server is not None and self.hcloud_floating_ip.server is not None: - if self.module.params.get("force") and server != self.hcloud_floating_ip.server.name: - if not self.module.check_mode: - self.hcloud_floating_ip.assign( - self.client.servers.get_by_name(server) - ) - self._mark_as_changed() - elif server != self.hcloud_floating_ip.server.name: - self.module.warn( - "Floating IP is already assigned to another server %s. You need to unassign the Floating IP or use force=yes." - % self.hcloud_floating_ip.server.name - ) - self._mark_as_changed() - elif server is not None and self.hcloud_floating_ip.server is None: - if not self.module.check_mode: - self.hcloud_floating_ip.assign( - self.client.servers.get_by_name(server) - ) - self._mark_as_changed() - elif server is None and self.hcloud_floating_ip.server is not None: - if not self.module.check_mode: - self.hcloud_floating_ip.unassign() - self._mark_as_changed() - - delete_protection = self.module.params.get("delete_protection") - if delete_protection is not None and delete_protection != self.hcloud_floating_ip.protection["delete"]: - if not self.module.check_mode: - self.hcloud_floating_ip.change_protection(delete=delete_protection).wait_until_finished() - self._mark_as_changed() - - self._get_floating_ip() - except Exception as e: - self.module.fail_json(msg=e.message) - - def present_floating_ip(self): - self._get_floating_ip() - if self.hcloud_floating_ip is None: - self._create_floating_ip() - else: - self._update_floating_ip() - - def delete_floating_ip(self): - try: - self._get_floating_ip() - if self.hcloud_floating_ip is not None: - if self.module.params.get("force") or self.hcloud_floating_ip.server is None: - if not self.module.check_mode: - self.client.floating_ips.delete(self.hcloud_floating_ip) - else: - self.module.warn( - "Floating IP is currently assigned to server %s. You need to unassign the Floating IP or use force=yes." - % self.hcloud_floating_ip.server.name - ) - self._mark_as_changed() - self.hcloud_floating_ip = None - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - description={"type": "str"}, - server={"type": "str"}, - home_location={"type": "str"}, - force={"type": "bool"}, - type={"choices": ["ipv4", "ipv6"]}, - labels={"type": "dict"}, - delete_protection={"type": "bool"}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - required_one_of=[['id', 'name']], - mutually_exclusive=[['home_location', 'server']], - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudFloatingIP.define_module() - - hcloud = AnsibleHcloudFloatingIP(module) - state = module.params["state"] - if state == "absent": - hcloud.delete_floating_ip() - elif state == "present": - hcloud.present_floating_ip() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_floating_ip_facts.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_floating_ip_facts.py deleted file mode 100644 index 2ec359600..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_floating_ip_facts.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_floating_ip_info - -short_description: Gather infos about the Hetzner Cloud Floating IPs. - -description: - - Gather facts about your Hetzner Cloud Floating IPs. - - This module was called C(hcloud_floating_ip_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_floating_ip_facts). - Note that the M(hetzner.hcloud.hcloud_floating_ip_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_floating_ip_info)! - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the Floating IP you want to get. - type: int - label_selector: - description: - - The label selector for the Floating IP you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud Floating ip infos - hcloud_floating_ip_info: - register: output -- name: Print the gathered infos - debug: - var: output -""" - -RETURN = """ -hcloud_floating_ip_info: - description: The Floating ip infos as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the Floating IP - returned: always - type: int - sample: 1937415 - name: - description: Name of the Floating IP - returned: Always - type: str - sample: my-floating-ip - version_added: "0.1.0" - description: - description: Description of the Floating IP - returned: always - type: str - sample: Falkenstein DC 8 - ip: - description: IP address of the Floating IP - returned: always - type: str - sample: 131.232.99.1 - type: - description: Type of the Floating IP - returned: always - type: str - sample: ipv4 - server: - description: Name of the server where the Floating IP is assigned to. - returned: always - type: str - sample: my-server - home_location: - description: Location the Floating IP was created in - returned: always - type: str - sample: fsn1 - delete_protection: - description: True if the Floating IP is protected for deletion - returned: always - type: bool - version_added: "0.1.0" - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudFloatingIPInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_floating_ip_info") - self.hcloud_floating_ip_info = None - - def _prepare_result(self): - tmp = [] - - for floating_ip in self.hcloud_floating_ip_info: - if floating_ip is not None: - server_name = None - if floating_ip.server is not None: - server_name = floating_ip.server.name - tmp.append({ - "id": to_native(floating_ip.id), - "name": to_native(floating_ip.name), - "description": to_native(floating_ip.description), - "ip": to_native(floating_ip.ip), - "type": to_native(floating_ip.type), - "server": to_native(server_name), - "home_location": to_native(floating_ip.home_location.name), - "labels": floating_ip.labels, - "delete_protection": floating_ip.protection["delete"], - }) - - return tmp - - def get_floating_ips(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_floating_ip_info = [self.client.floating_ips.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("label_selector") is not None: - self.hcloud_floating_ip_info = self.client.floating_ips.get_all( - label_selector=self.module.params.get("label_selector")) - else: - self.hcloud_floating_ip_info = self.client.floating_ips.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - label_selector={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudFloatingIPInfo.define_module() - - is_old_facts = module._name == 'hcloud_floating_ip_facts' - if is_old_facts: - module.deprecate("The 'hcloud_floating_ip_facts' module has been renamed to 'hcloud_floating_ip_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - - hcloud = AnsibleHcloudFloatingIPInfo(module) - - hcloud.get_floating_ips() - result = hcloud.get_result() - if is_old_facts: - ansible_info = { - 'hcloud_floating_ip_facts': result['hcloud_floating_ip_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_floating_ip_info': result['hcloud_floating_ip_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_floating_ip_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_floating_ip_info.py deleted file mode 100644 index 2ec359600..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_floating_ip_info.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_floating_ip_info - -short_description: Gather infos about the Hetzner Cloud Floating IPs. - -description: - - Gather facts about your Hetzner Cloud Floating IPs. - - This module was called C(hcloud_floating_ip_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_floating_ip_facts). - Note that the M(hetzner.hcloud.hcloud_floating_ip_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_floating_ip_info)! - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the Floating IP you want to get. - type: int - label_selector: - description: - - The label selector for the Floating IP you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud Floating ip infos - hcloud_floating_ip_info: - register: output -- name: Print the gathered infos - debug: - var: output -""" - -RETURN = """ -hcloud_floating_ip_info: - description: The Floating ip infos as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the Floating IP - returned: always - type: int - sample: 1937415 - name: - description: Name of the Floating IP - returned: Always - type: str - sample: my-floating-ip - version_added: "0.1.0" - description: - description: Description of the Floating IP - returned: always - type: str - sample: Falkenstein DC 8 - ip: - description: IP address of the Floating IP - returned: always - type: str - sample: 131.232.99.1 - type: - description: Type of the Floating IP - returned: always - type: str - sample: ipv4 - server: - description: Name of the server where the Floating IP is assigned to. - returned: always - type: str - sample: my-server - home_location: - description: Location the Floating IP was created in - returned: always - type: str - sample: fsn1 - delete_protection: - description: True if the Floating IP is protected for deletion - returned: always - type: bool - version_added: "0.1.0" - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudFloatingIPInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_floating_ip_info") - self.hcloud_floating_ip_info = None - - def _prepare_result(self): - tmp = [] - - for floating_ip in self.hcloud_floating_ip_info: - if floating_ip is not None: - server_name = None - if floating_ip.server is not None: - server_name = floating_ip.server.name - tmp.append({ - "id": to_native(floating_ip.id), - "name": to_native(floating_ip.name), - "description": to_native(floating_ip.description), - "ip": to_native(floating_ip.ip), - "type": to_native(floating_ip.type), - "server": to_native(server_name), - "home_location": to_native(floating_ip.home_location.name), - "labels": floating_ip.labels, - "delete_protection": floating_ip.protection["delete"], - }) - - return tmp - - def get_floating_ips(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_floating_ip_info = [self.client.floating_ips.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("label_selector") is not None: - self.hcloud_floating_ip_info = self.client.floating_ips.get_all( - label_selector=self.module.params.get("label_selector")) - else: - self.hcloud_floating_ip_info = self.client.floating_ips.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - label_selector={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudFloatingIPInfo.define_module() - - is_old_facts = module._name == 'hcloud_floating_ip_facts' - if is_old_facts: - module.deprecate("The 'hcloud_floating_ip_facts' module has been renamed to 'hcloud_floating_ip_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - - hcloud = AnsibleHcloudFloatingIPInfo(module) - - hcloud.get_floating_ips() - result = hcloud.get_result() - if is_old_facts: - ansible_info = { - 'hcloud_floating_ip_facts': result['hcloud_floating_ip_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_floating_ip_info': result['hcloud_floating_ip_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_image_facts.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_image_facts.py deleted file mode 100644 index 8acd8846a..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_image_facts.py +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_image_info - -short_description: Gather infos about your Hetzner Cloud images. - - -description: - - Gather infos about your Hetzner Cloud images. - - This module was called C(hcloud_location_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_location_facts). - Note that the M(hetzner.hcloud.hcloud_image_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_image_info)! - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the image you want to get. - type: int - name: - description: - - The name of the image you want to get. - type: str - label_selector: - description: - - The label selector for the images you want to get. - type: str - type: - description: - - The type for the images you want to get. - default: system - choices: [ system, snapshot, backup ] - type: str - architecture: - description: - - The architecture for the images you want to get. - type: str - choices: [ x86, arm ] -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud image infos - hcloud_image_info: - register: output - -- name: Print the gathered infos - debug: - var: output -""" - -RETURN = """ -hcloud_image_info: - description: The image infos as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the image - returned: always - type: int - sample: 1937415 - type: - description: Type of the image - returned: always - type: str - sample: system - status: - description: Status of the image - returned: always - type: str - sample: available - name: - description: Name of the image - returned: always - type: str - sample: ubuntu-18.04 - description: - description: Detail description of the image - returned: always - type: str - sample: Ubuntu 18.04 Standard 64 bit - os_flavor: - description: OS flavor of the image - returned: always - type: str - sample: ubuntu - os_version: - description: OS version of the image - returned: always - type: str - sample: 18.04 - architecture: - description: Image is compatible with this architecture - returned: always - type: str - sample: x86 - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudImageInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_image_info") - self.hcloud_image_info = None - - def _prepare_result(self): - tmp = [] - - for image in self.hcloud_image_info: - if image is not None: - tmp.append({ - "id": to_native(image.id), - "status": to_native(image.status), - "type": to_native(image.type), - "name": to_native(image.name), - "description": to_native(image.description), - "os_flavor": to_native(image.os_flavor), - "os_version": to_native(image.os_version), - "architecture": to_native(image.architecture), - "labels": image.labels, - }) - return tmp - - def get_images(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_image_info = [self.client.images.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None and self.module.params.get("architecture") is not None: - self.hcloud_image_info = [self.client.images.get_by_name_and_architecture( - self.module.params.get("name"), - self.module.params.get("architecture") - )] - elif self.module.params.get("name") is not None: - self.hcloud_image_info = [self.client.images.get_by_name( - self.module.params.get("name") - )] - else: - params = {} - label_selector = self.module.params.get("label_selector") - if label_selector: - params["label_selector"] = label_selector - - image_type = self.module.params.get("type") - if image_type: - params["type"] = image_type - - architecture = self.module.params.get("architecture") - if architecture: - params["architecture"] = architecture - - self.hcloud_image_info = self.client.images.get_all(**params) - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - label_selector={"type": "str"}, - type={"choices": ["system", "snapshot", "backup"], "default": "system", "type": "str"}, - architecture={"choices": ["x86", "arm"], "type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudImageInfo.define_module() - - is_old_facts = module._name == 'hcloud_image_facts' - if is_old_facts: - module.deprecate("The 'hcloud_image_facts' module has been renamed to 'hcloud_image_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - - hcloud = AnsibleHcloudImageInfo(module) - hcloud.get_images() - result = hcloud.get_result() - - if is_old_facts: - ansible_info = { - 'hcloud_imagen_facts': result['hcloud_image_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_image_info': result['hcloud_image_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_image_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_image_info.py deleted file mode 100644 index 8acd8846a..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_image_info.py +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_image_info - -short_description: Gather infos about your Hetzner Cloud images. - - -description: - - Gather infos about your Hetzner Cloud images. - - This module was called C(hcloud_location_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_location_facts). - Note that the M(hetzner.hcloud.hcloud_image_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_image_info)! - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the image you want to get. - type: int - name: - description: - - The name of the image you want to get. - type: str - label_selector: - description: - - The label selector for the images you want to get. - type: str - type: - description: - - The type for the images you want to get. - default: system - choices: [ system, snapshot, backup ] - type: str - architecture: - description: - - The architecture for the images you want to get. - type: str - choices: [ x86, arm ] -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud image infos - hcloud_image_info: - register: output - -- name: Print the gathered infos - debug: - var: output -""" - -RETURN = """ -hcloud_image_info: - description: The image infos as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the image - returned: always - type: int - sample: 1937415 - type: - description: Type of the image - returned: always - type: str - sample: system - status: - description: Status of the image - returned: always - type: str - sample: available - name: - description: Name of the image - returned: always - type: str - sample: ubuntu-18.04 - description: - description: Detail description of the image - returned: always - type: str - sample: Ubuntu 18.04 Standard 64 bit - os_flavor: - description: OS flavor of the image - returned: always - type: str - sample: ubuntu - os_version: - description: OS version of the image - returned: always - type: str - sample: 18.04 - architecture: - description: Image is compatible with this architecture - returned: always - type: str - sample: x86 - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudImageInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_image_info") - self.hcloud_image_info = None - - def _prepare_result(self): - tmp = [] - - for image in self.hcloud_image_info: - if image is not None: - tmp.append({ - "id": to_native(image.id), - "status": to_native(image.status), - "type": to_native(image.type), - "name": to_native(image.name), - "description": to_native(image.description), - "os_flavor": to_native(image.os_flavor), - "os_version": to_native(image.os_version), - "architecture": to_native(image.architecture), - "labels": image.labels, - }) - return tmp - - def get_images(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_image_info = [self.client.images.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None and self.module.params.get("architecture") is not None: - self.hcloud_image_info = [self.client.images.get_by_name_and_architecture( - self.module.params.get("name"), - self.module.params.get("architecture") - )] - elif self.module.params.get("name") is not None: - self.hcloud_image_info = [self.client.images.get_by_name( - self.module.params.get("name") - )] - else: - params = {} - label_selector = self.module.params.get("label_selector") - if label_selector: - params["label_selector"] = label_selector - - image_type = self.module.params.get("type") - if image_type: - params["type"] = image_type - - architecture = self.module.params.get("architecture") - if architecture: - params["architecture"] = architecture - - self.hcloud_image_info = self.client.images.get_all(**params) - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - label_selector={"type": "str"}, - type={"choices": ["system", "snapshot", "backup"], "default": "system", "type": "str"}, - architecture={"choices": ["x86", "arm"], "type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudImageInfo.define_module() - - is_old_facts = module._name == 'hcloud_image_facts' - if is_old_facts: - module.deprecate("The 'hcloud_image_facts' module has been renamed to 'hcloud_image_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - - hcloud = AnsibleHcloudImageInfo(module) - hcloud.get_images() - result = hcloud.get_result() - - if is_old_facts: - ansible_info = { - 'hcloud_imagen_facts': result['hcloud_image_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_image_info': result['hcloud_image_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer.py deleted file mode 100644 index 9c6c2bbaf..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer.py +++ /dev/null @@ -1,318 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2020, 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 = ''' ---- -module: hcloud_load_balancer - -short_description: Create and manage cloud Load Balancers on the Hetzner Cloud. - - -description: - - Create, update and manage cloud Load Balancers on the Hetzner Cloud. - -author: - - Lukas Kaemmerling (@LKaemmerling) -version_added: 0.1.0 -options: - id: - description: - - The ID of the Hetzner Cloud Load Balancer to manage. - - Only required if no Load Balancer I(name) is given - type: int - name: - description: - - The Name of the Hetzner Cloud Load Balancer to manage. - - Only required if no Load Balancer I(id) is given or a Load Balancer does not exist. - type: str - load_balancer_type: - description: - - The Load Balancer Type of the Hetzner Cloud Load Balancer to manage. - - Required if Load Balancer does not exist. - type: str - location: - description: - - Location of Load Balancer. - - Required if no I(network_zone) is given and Load Balancer does not exist. - type: str - network_zone: - description: - - Network Zone of Load Balancer. - - Required of no I(location) is given and Load Balancer does not exist. - type: str - labels: - description: - - User-defined labels (key-value pairs). - type: dict - disable_public_interface: - description: - - Disables the public interface. - type: bool - default: False - delete_protection: - description: - - Protect the Load Balancer for deletion. - type: bool - state: - description: - - State of the Load Balancer. - default: present - choices: [ absent, present ] - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -requirements: - - hcloud-python >= 1.8.0 -''' - -EXAMPLES = """ -- name: Create a basic Load Balancer - hcloud_load_balancer: - name: my-Load Balancer - load_balancer_type: lb11 - location: fsn1 - state: present - -- name: Ensure the Load Balancer is absent (remove if needed) - hcloud_load_balancer: - name: my-Load Balancer - state: absent - -""" - -RETURN = """ -hcloud_load_balancer: - description: The Load Balancer instance - returned: Always - type: complex - contains: - id: - description: Numeric identifier of the Load Balancer - returned: always - type: int - sample: 1937415 - name: - description: Name of the Load Balancer - returned: always - type: str - sample: my-Load-Balancer - status: - description: Status of the Load Balancer - returned: always - type: str - sample: running - load_balancer_type: - description: Name of the Load Balancer type of the Load Balancer - returned: always - type: str - sample: cx11 - ipv4_address: - description: Public IPv4 address of the Load Balancer - returned: always - type: str - sample: 116.203.104.109 - ipv6_address: - description: Public IPv6 address of the Load Balancer - returned: always - type: str - sample: 2a01:4f8:1c1c:c140::1 - location: - description: Name of the location of the Load Balancer - returned: always - type: str - sample: fsn1 - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict - delete_protection: - description: True if Load Balancer is protected for deletion - type: bool - returned: always - sample: false - disable_public_interface: - description: True if Load Balancer public interface is disabled - type: bool - returned: always - sample: false -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudLoadBalancer(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_load_balancer") - self.hcloud_load_balancer = None - - def _prepare_result(self): - private_ipv4_address = None if len(self.hcloud_load_balancer.private_net) == 0 else to_native( - self.hcloud_load_balancer.private_net[0].ip) - return { - "id": to_native(self.hcloud_load_balancer.id), - "name": to_native(self.hcloud_load_balancer.name), - "ipv4_address": to_native(self.hcloud_load_balancer.public_net.ipv4.ip), - "ipv6_address": to_native(self.hcloud_load_balancer.public_net.ipv6.ip), - "private_ipv4_address": private_ipv4_address, - "load_balancer_type": to_native(self.hcloud_load_balancer.load_balancer_type.name), - "location": to_native(self.hcloud_load_balancer.location.name), - "labels": self.hcloud_load_balancer.labels, - "delete_protection": self.hcloud_load_balancer.protection["delete"], - "disable_public_interface": False if self.hcloud_load_balancer.public_net.enabled else True, - } - - def _get_load_balancer(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_load_balancer = self.client.load_balancers.get_by_id( - self.module.params.get("id") - ) - else: - self.hcloud_load_balancer = self.client.load_balancers.get_by_name( - self.module.params.get("name") - ) - except Exception as e: - self.module.fail_json(msg=e.message) - - def _create_load_balancer(self): - - self.module.fail_on_missing_params( - required_params=["name", "load_balancer_type"] - ) - try: - params = { - "name": self.module.params.get("name"), - "load_balancer_type": self.client.load_balancer_types.get_by_name( - self.module.params.get("load_balancer_type") - ), - "labels": self.module.params.get("labels"), - } - - if self.module.params.get("location") is None and self.module.params.get("network_zone") is None: - self.module.fail_json(msg="one of the following is required: location, network_zone") - elif self.module.params.get("location") is not None and self.module.params.get("network_zone") is None: - params["location"] = self.client.locations.get_by_name( - self.module.params.get("location") - ) - elif self.module.params.get("location") is None and self.module.params.get("network_zone") is not None: - params["network_zone"] = self.module.params.get("network_zone") - - if not self.module.check_mode: - resp = self.client.load_balancers.create(**params) - resp.action.wait_until_finished(max_retries=1000) - - delete_protection = self.module.params.get("delete_protection") - if delete_protection is not None: - self._get_load_balancer() - self.hcloud_load_balancer.change_protection(delete=delete_protection).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self._get_load_balancer() - - def _update_load_balancer(self): - try: - labels = self.module.params.get("labels") - if labels is not None and labels != self.hcloud_load_balancer.labels: - if not self.module.check_mode: - self.hcloud_load_balancer.update(labels=labels) - self._mark_as_changed() - - delete_protection = self.module.params.get("delete_protection") - if delete_protection is not None and delete_protection != self.hcloud_load_balancer.protection["delete"]: - if not self.module.check_mode: - self.hcloud_load_balancer.change_protection(delete=delete_protection).wait_until_finished() - self._mark_as_changed() - self._get_load_balancer() - - disable_public_interface = self.module.params.get("disable_public_interface") - if disable_public_interface is not None and disable_public_interface != (not self.hcloud_load_balancer.public_net.enabled): - if not self.module.check_mode: - if disable_public_interface is True: - self.hcloud_load_balancer.disable_public_interface().wait_until_finished() - else: - self.hcloud_load_balancer.enable_public_interface().wait_until_finished() - self._mark_as_changed() - - load_balancer_type = self.module.params.get("load_balancer_type") - if load_balancer_type is not None and self.hcloud_load_balancer.load_balancer_type.name != load_balancer_type: - new_load_balancer_type = self.client.load_balancer_types.get_by_name(load_balancer_type) - if not new_load_balancer_type: - self.module.fail_json(msg="unknown load balancer type") - if not self.module.check_mode: - self.hcloud_load_balancer.change_type( - load_balancer_type=new_load_balancer_type, - ).wait_until_finished(max_retries=1000) - - self._mark_as_changed() - self._get_load_balancer() - except Exception as e: - self.module.fail_json(msg=e.message) - - def present_load_balancer(self): - self._get_load_balancer() - if self.hcloud_load_balancer is None: - self._create_load_balancer() - else: - self._update_load_balancer() - - def delete_load_balancer(self): - try: - self._get_load_balancer() - if self.hcloud_load_balancer is not None: - if not self.module.check_mode: - self.client.load_balancers.delete(self.hcloud_load_balancer) - self._mark_as_changed() - self.hcloud_load_balancer = None - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - load_balancer_type={"type": "str"}, - location={"type": "str"}, - network_zone={"type": "str"}, - labels={"type": "dict"}, - delete_protection={"type": "bool"}, - disable_public_interface={"type": "bool", "default": False}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - required_one_of=[['id', 'name']], - mutually_exclusive=[["location", "network_zone"]], - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudLoadBalancer.define_module() - - hcloud = AnsibleHcloudLoadBalancer(module) - state = module.params.get("state") - if state == "absent": - hcloud.delete_load_balancer() - elif state == "present": - hcloud.present_load_balancer() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_info.py deleted file mode 100644 index 159dad258..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_info.py +++ /dev/null @@ -1,398 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2020, 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 = ''' ---- -module: hcloud_load_balancer_info - -short_description: Gather infos about your Hetzner Cloud Load Balancers. - - -description: - - Gather infos about your Hetzner Cloud Load Balancers.. - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the Load Balancers you want to get. - type: int - name: - description: - - The name of the Load Balancers you want to get. - type: str - label_selector: - description: - - The label selector for the Load Balancers you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud load_balancer infos - hcloud_load_balancer_info: - register: output - -- name: Print the gathered infos - debug: - var: output -""" - -RETURN = """ -hcloud_load_balancer_info: - description: The load_balancer infos as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the Load Balancer - returned: always - type: int - sample: 1937415 - name: - description: Name of the Load Balancer - returned: always - type: str - sample: my-Load-Balancer - status: - description: Status of the Load Balancer - returned: always - type: str - sample: running - load_balancer_type: - description: Name of the Load Balancer type of the Load Balancer - returned: always - type: str - sample: cx11 - ipv4_address: - description: Public IPv4 address of the Load Balancer - returned: always - type: str - sample: 116.203.104.109 - ipv6_address: - description: Public IPv6 address of the Load Balancer - returned: always - type: str - sample: 2a01:4f8:1c1c:c140::1 - location: - description: Name of the location of the Load Balancer - returned: always - type: str - sample: fsn1 - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict - delete_protection: - description: True if Load Balancer is protected for deletion - type: bool - returned: always - sample: false - disable_public_interface: - description: True if Load Balancer public interface is disabled - type: bool - returned: always - sample: false - targets: - description: The targets of the Load Balancer - returned: always - type: complex - contains: - type: - description: Type of the Load Balancer Target - type: str - returned: always - sample: server - load_balancer: - description: Name of the Load Balancer - type: str - returned: always - sample: my-LoadBalancer - server: - description: Name of the Server - type: str - returned: if I(type) is server - sample: my-server - label_selector: - description: Label Selector - type: str - returned: if I(type) is label_selector - sample: application=backend - ip: - description: IP of the dedicated server - type: str - returned: if I(type) is ip - sample: 127.0.0.1 - use_private_ip: - description: - - Route the traffic over the private IP of the Load Balancer through a Hetzner Cloud Network. - - Load Balancer needs to be attached to a network. See M(hetzner.hcloud.hcloud.hcloud_load_balancer_network) - type: bool - sample: true - returned: always - services: - description: all services from this Load Balancer - returned: Always - type: complex - contains: - listen_port: - description: The port the service listens on, i.e. the port users can connect to. - returned: always - type: int - sample: 443 - protocol: - description: Protocol of the service - returned: always - type: str - sample: http - destination_port: - description: - - The port traffic is forwarded to, i.e. the port the targets are listening and accepting connections on. - returned: always - type: int - sample: 80 - proxyprotocol: - description: - - Enable the PROXY protocol. - returned: always - type: bool - sample: false - http: - description: Configuration for HTTP and HTTPS services - returned: always - type: complex - contains: - cookie_name: - description: Name of the cookie which will be set when you enable sticky sessions - returned: always - type: str - sample: HCLBSTICKY - cookie_lifetime: - description: Lifetime of the cookie which will be set when you enable sticky sessions, in seconds - returned: always - type: int - sample: 3600 - certificates: - description: List of Names or IDs of certificates - returned: always - type: list - elements: str - sticky_sessions: - description: Enable or disable sticky_sessions - returned: always - type: bool - sample: true - redirect_http: - description: Redirect Traffic from Port 80 to Port 443, only available if protocol is https - returned: always - type: bool - sample: false - health_check: - description: Configuration for health checks - returned: always - type: complex - contains: - protocol: - description: Protocol the health checks will be performed over - returned: always - type: str - sample: http - port: - description: Port the health check will be performed on - returned: always - type: int - sample: 80 - interval: - description: Interval of health checks, in seconds - returned: always - type: int - sample: 15 - timeout: - description: Timeout of health checks, in seconds - returned: always - type: int - sample: 10 - retries: - description: Number of retries until a target is marked as unhealthy - returned: always - type: int - sample: 3 - http: - description: Additional Configuration of health checks with protocol http/https - returned: always - type: complex - contains: - domain: - description: Domain we will set within the HTTP HOST header - returned: always - type: str - sample: example.com - path: - description: Path we will try to access - returned: always - type: str - sample: / - response: - description: Response we expect, if response is not within the health check response the target is unhealthy - returned: always - type: str - status_codes: - description: List of HTTP status codes we expect to get when we perform the health check. - returned: always - type: list - elements: str - sample: ["2??","3??"] - tls: - description: Verify the TLS certificate, only available if health check protocol is https - returned: always - type: bool - sample: false -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudLoadBalancerInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_load_balancer_info") - self.hcloud_load_balancer_info = None - - def _prepare_result(self): - tmp = [] - - for load_balancer in self.hcloud_load_balancer_info: - if load_balancer is not None: - services = [self._prepare_service_result(service) for service in load_balancer.services] - targets = [self._prepare_target_result(target) for target in load_balancer.targets] - - private_ipv4_address = None if len(load_balancer.private_net) == 0 else to_native( - load_balancer.private_net[0].ip) - tmp.append({ - "id": to_native(load_balancer.id), - "name": to_native(load_balancer.name), - "ipv4_address": to_native(load_balancer.public_net.ipv4.ip), - "ipv6_address": to_native(load_balancer.public_net.ipv6.ip), - "private_ipv4_address": private_ipv4_address, - "load_balancer_type": to_native(load_balancer.load_balancer_type.name), - "location": to_native(load_balancer.location.name), - "labels": load_balancer.labels, - "delete_protection": load_balancer.protection["delete"], - "disable_public_interface": False if load_balancer.public_net.enabled else True, - "targets": targets, - "services": services - }) - return tmp - - @staticmethod - def _prepare_service_result(service): - http = None - if service.protocol != "tcp": - http = { - "cookie_name": to_native(service.http.cookie_name), - "cookie_lifetime": service.http.cookie_name, - "redirect_http": service.http.redirect_http, - "sticky_sessions": service.http.sticky_sessions, - "certificates": [to_native(certificate.name) for certificate in - service.http.certificates], - } - health_check = { - "protocol": to_native(service.health_check.protocol), - "port": service.health_check.port, - "interval": service.health_check.interval, - "timeout": service.health_check.timeout, - "retries": service.health_check.retries, - } - if service.health_check.protocol != "tcp": - health_check["http"] = { - "domain": to_native(service.health_check.http.domain), - "path": to_native(service.health_check.http.path), - "response": to_native(service.health_check.http.response), - "certificates": [to_native(status_code) for status_code in - service.health_check.http.status_codes], - "tls": service.health_check.http.tls, - } - return { - "protocol": to_native(service.protocol), - "listen_port": service.listen_port, - "destination_port": service.destination_port, - "proxyprotocol": service.proxyprotocol, - "http": http, - "health_check": health_check, - } - - @staticmethod - def _prepare_target_result(target): - result = { - "type": to_native(target.type), - "use_private_ip": target.use_private_ip - } - if target.type == "server": - result["server"] = to_native(target.server.name) - elif target.type == "label_selector": - result["label_selector"] = to_native(target.label_selector.selector) - elif target.type == "ip": - result["ip"] = to_native(target.ip.ip) - return result - - def get_load_balancers(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_load_balancer_info = [self.client.load_balancers.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_load_balancer_info = [self.client.load_balancers.get_by_name( - self.module.params.get("name") - )] - else: - params = {} - label_selector = self.module.params.get("label_selector") - if label_selector: - params["label_selector"] = label_selector - - self.hcloud_load_balancer_info = self.client.load_balancers.get_all(**params) - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - label_selector={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudLoadBalancerInfo.define_module() - - hcloud = AnsibleHcloudLoadBalancerInfo(module) - hcloud.get_load_balancers() - result = hcloud.get_result() - - ansible_info = { - 'hcloud_load_balancer_info': result['hcloud_load_balancer_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_network.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_network.py deleted file mode 100644 index 63a7c5471..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_network.py +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2020, 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 = ''' ---- -module: hcloud_load_balancer_network - -short_description: Manage the relationship between Hetzner Cloud Networks and Load Balancers - - -description: - - Create and delete the relationship Hetzner Cloud Networks and Load Balancers - -author: - - Lukas Kaemmerling (@lkaemmerling) -version_added: 0.1.0 -options: - network: - description: - - The name of the Hetzner Cloud Networks. - type: str - required: true - load_balancer: - description: - - The name of the Hetzner Cloud Load Balancer. - type: str - required: true - ip: - description: - - The IP the Load Balancer should have. - type: str - state: - description: - - State of the load_balancer_network. - default: present - choices: [ absent, present ] - type: str - -requirements: - - hcloud-python >= 1.8.1 - -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Create a basic Load Balancer network - hcloud_load_balancer_network: - network: my-network - load_balancer: my-LoadBalancer - state: present - -- name: Create a Load Balancer network and specify the ip address - hcloud_load_balancer_network: - network: my-network - load_balancer: my-LoadBalancer - ip: 10.0.0.1 - state: present - -- name: Ensure the Load Balancer network is absent (remove if needed) - hcloud_load_balancer_network: - network: my-network - load_balancer: my-LoadBalancer - state: absent -""" - -RETURN = """ -hcloud_load_balancer_network: - description: The relationship between a Load Balancer and a network - returned: always - type: complex - contains: - network: - description: Name of the Network - type: str - returned: always - sample: my-network - load_balancer: - description: Name of the Load Balancer - type: str - returned: always - sample: my-LoadBalancer - ip: - description: IP of the Load Balancer within the Network ip range - type: str - returned: always - sample: 10.0.0.8 -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudLoadBalancerNetwork(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_load_balancer_network") - self.hcloud_network = None - self.hcloud_load_balancer = None - self.hcloud_load_balancer_network = None - - def _prepare_result(self): - return { - "network": to_native(self.hcloud_network.name), - "load_balancer": to_native(self.hcloud_load_balancer.name), - "ip": to_native(self.hcloud_load_balancer_network.ip), - } - - def _get_load_balancer_and_network(self): - try: - network = self.module.params.get("network") - self.hcloud_network = self.client.networks.get_by_name(network) - if not self.hcloud_network: - self.module.fail_json(msg="Network does not exist: %s" % network) - - load_balancer_name = self.module.params.get("load_balancer") - self.hcloud_load_balancer = self.client.load_balancers.get_by_name( - load_balancer_name - ) - if not self.hcloud_load_balancer: - self.module.fail_json(msg="Load balancer does not exist: %s" % load_balancer_name) - - self.hcloud_load_balancer_network = None - except Exception as e: - self.module.fail_json(msg=e.message) - - def _get_load_balancer_network(self): - for privateNet in self.hcloud_load_balancer.private_net: - if privateNet.network.id == self.hcloud_network.id: - self.hcloud_load_balancer_network = privateNet - - def _create_load_balancer_network(self): - params = { - "network": self.hcloud_network - } - - if self.module.params.get("ip") is not None: - params["ip"] = self.module.params.get("ip") - - if not self.module.check_mode: - try: - self.hcloud_load_balancer.attach_to_network(**params).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - - self._mark_as_changed() - self._get_load_balancer_and_network() - self._get_load_balancer_network() - - def present_load_balancer_network(self): - self._get_load_balancer_and_network() - self._get_load_balancer_network() - if self.hcloud_load_balancer_network is None: - self._create_load_balancer_network() - - def delete_load_balancer_network(self): - self._get_load_balancer_and_network() - self._get_load_balancer_network() - if self.hcloud_load_balancer_network is not None and self.hcloud_load_balancer is not None: - if not self.module.check_mode: - try: - self.hcloud_load_balancer.detach_from_network( - self.hcloud_load_balancer_network.network).wait_until_finished() - self._mark_as_changed() - except Exception as e: - self.module.fail_json(msg=e.message) - - self.hcloud_load_balancer_network = None - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - network={"type": "str", "required": True}, - load_balancer={"type": "str", "required": True}, - ip={"type": "str"}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudLoadBalancerNetwork.define_module() - - hcloud = AnsibleHcloudLoadBalancerNetwork(module) - state = module.params["state"] - if state == "absent": - hcloud.delete_load_balancer_network() - elif state == "present": - hcloud.present_load_balancer_network() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_service.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_service.py deleted file mode 100644 index b5edcc6b5..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_service.py +++ /dev/null @@ -1,620 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2020, 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 = ''' ---- -module: hcloud_load_balancer_service - -short_description: Create and manage the services of cloud Load Balancers on the Hetzner Cloud. - - -description: - - Create, update and manage the services of cloud Load Balancers on the Hetzner Cloud. - -author: - - Lukas Kaemmerling (@LKaemmerling) -version_added: 0.1.0 -options: - load_balancer: - description: - - The Name of the Hetzner Cloud Load Balancer the service belongs to - type: str - required: true - listen_port: - description: - - The port the service listens on, i.e. the port users can connect to. - type: int - required: true - destination_port: - description: - - The port traffic is forwarded to, i.e. the port the targets are listening and accepting connections on. - - Required if services does not exist and protocol is tcp. - type: int - protocol: - description: - - Protocol of the service. - - Required if Load Balancer does not exist. - type: str - choices: [ http, https, tcp ] - proxyprotocol: - description: - - Enable the PROXY protocol. - type: bool - default: False - http: - description: - - Configuration for HTTP and HTTPS services - type: dict - suboptions: - cookie_name: - description: - - Name of the cookie which will be set when you enable sticky sessions - type: str - cookie_lifetime: - description: - - Lifetime of the cookie which will be set when you enable sticky sessions, in seconds - type: int - certificates: - description: - - List of Names or IDs of certificates - type: list - elements: str - sticky_sessions: - description: - - Enable or disable sticky_sessions - type: bool - default: False - redirect_http: - description: - - Redirect Traffic from Port 80 to Port 443, only available if protocol is https - type: bool - default: False - health_check: - description: - - Configuration for health checks - type: dict - suboptions: - protocol: - description: - - Protocol the health checks will be performed over - type: str - choices: [ http, https, tcp ] - port: - description: - - Port the health check will be performed on - type: int - interval: - description: - - Interval of health checks, in seconds - type: int - timeout: - description: - - Timeout of health checks, in seconds - type: int - retries: - description: - - Number of retries until a target is marked as unhealthy - type: int - http: - description: - - Additional Configuration of health checks with protocol http/https - type: dict - suboptions: - domain: - description: - - Domain we will set within the HTTP HOST header - type: str - path: - description: - - Path we will try to access - type: str - response: - description: - - Response we expect, if response is not within the health check response the target is unhealthy - type: str - status_codes: - description: - - List of HTTP status codes we expect to get when we perform the health check. - type: list - elements: str - tls: - description: - - Verify the TLS certificate, only available if health check protocol is https - type: bool - default: False - state: - description: - - State of the Load Balancer. - default: present - choices: [ absent, present ] - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -requirements: - - hcloud-python >= 1.8.1 -''' - -EXAMPLES = """ -- name: Create a basic Load Balancer service with Port 80 - hcloud_load_balancer_service: - load_balancer: my-load-balancer - protocol: http - listen_port: 80 - state: present - -- name: Ensure the Load Balancer is absent (remove if needed) - hcloud_load_balancer_service: - load_balancer: my-Load Balancer - protocol: http - listen_port: 80 - state: absent -""" - -RETURN = """ -hcloud_load_balancer_service: - description: The Load Balancer service instance - returned: Always - type: complex - contains: - load_balancer: - description: The name of the Load Balancer where the service belongs to - returned: always - type: str - sample: my-load-balancer - listen_port: - description: The port the service listens on, i.e. the port users can connect to. - returned: always - type: int - sample: 443 - protocol: - description: Protocol of the service - returned: always - type: str - sample: http - destination_port: - description: - - The port traffic is forwarded to, i.e. the port the targets are listening and accepting connections on. - returned: always - type: int - sample: 80 - proxyprotocol: - description: - - Enable the PROXY protocol. - returned: always - type: bool - sample: false - http: - description: Configuration for HTTP and HTTPS services - returned: always - type: complex - contains: - cookie_name: - description: Name of the cookie which will be set when you enable sticky sessions - returned: always - type: str - sample: HCLBSTICKY - cookie_lifetime: - description: Lifetime of the cookie which will be set when you enable sticky sessions, in seconds - returned: always - type: int - sample: 3600 - certificates: - description: List of Names or IDs of certificates - returned: always - type: list - elements: str - sticky_sessions: - description: Enable or disable sticky_sessions - returned: always - type: bool - sample: true - redirect_http: - description: Redirect Traffic from Port 80 to Port 443, only available if protocol is https - returned: always - type: bool - sample: false - health_check: - description: Configuration for health checks - returned: always - type: complex - contains: - protocol: - description: Protocol the health checks will be performed over - returned: always - type: str - sample: http - port: - description: Port the health check will be performed on - returned: always - type: int - sample: 80 - interval: - description: Interval of health checks, in seconds - returned: always - type: int - sample: 15 - timeout: - description: Timeout of health checks, in seconds - returned: always - type: int - sample: 10 - retries: - description: Number of retries until a target is marked as unhealthy - returned: always - type: int - sample: 3 - http: - description: Additional Configuration of health checks with protocol http/https - returned: always - type: complex - contains: - domain: - description: Domain we will set within the HTTP HOST header - returned: always - type: str - sample: example.com - path: - description: Path we will try to access - returned: always - type: str - sample: / - response: - description: Response we expect, if response is not within the health check response the target is unhealthy - returned: always - type: str - status_codes: - description: List of HTTP status codes we expect to get when we perform the health check. - returned: always - type: list - elements: str - sample: ["2??","3??"] - tls: - description: Verify the TLS certificate, only available if health check protocol is https - returned: always - type: bool - sample: false -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - -try: - from hcloud.load_balancers.domain import LoadBalancerService, LoadBalancerServiceHttp, \ - LoadBalancerHealthCheck, LoadBalancerHealtCheckHttp - from hcloud import APIException -except ImportError: - APIException = None - - -class AnsibleHcloudLoadBalancerService(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_load_balancer_service") - self.hcloud_load_balancer = None - self.hcloud_load_balancer_service = None - - def _prepare_result(self): - http = None - if self.hcloud_load_balancer_service.protocol != "tcp": - http = { - "cookie_name": to_native(self.hcloud_load_balancer_service.http.cookie_name), - "cookie_lifetime": self.hcloud_load_balancer_service.http.cookie_name, - "redirect_http": self.hcloud_load_balancer_service.http.redirect_http, - "sticky_sessions": self.hcloud_load_balancer_service.http.sticky_sessions, - "certificates": [to_native(certificate.name) for certificate in - self.hcloud_load_balancer_service.http.certificates], - } - health_check = { - "protocol": to_native(self.hcloud_load_balancer_service.health_check.protocol), - "port": self.hcloud_load_balancer_service.health_check.port, - "interval": self.hcloud_load_balancer_service.health_check.interval, - "timeout": self.hcloud_load_balancer_service.health_check.timeout, - "retries": self.hcloud_load_balancer_service.health_check.retries, - } - if self.hcloud_load_balancer_service.health_check.protocol != "tcp": - health_check["http"] = { - "domain": to_native(self.hcloud_load_balancer_service.health_check.http.domain), - "path": to_native(self.hcloud_load_balancer_service.health_check.http.path), - "response": to_native(self.hcloud_load_balancer_service.health_check.http.response), - "certificates": [to_native(status_code) for status_code in - self.hcloud_load_balancer_service.health_check.http.status_codes], - "tls": self.hcloud_load_balancer_service.health_check.http.tls, - } - return { - "load_balancer": to_native(self.hcloud_load_balancer.name), - "protocol": to_native(self.hcloud_load_balancer_service.protocol), - "listen_port": self.hcloud_load_balancer_service.listen_port, - "destination_port": self.hcloud_load_balancer_service.destination_port, - "proxyprotocol": self.hcloud_load_balancer_service.proxyprotocol, - "http": http, - "health_check": health_check, - } - - def _get_load_balancer(self): - try: - load_balancer_name = self.module.params.get("load_balancer") - self.hcloud_load_balancer = self.client.load_balancers.get_by_name( - load_balancer_name - ) - if not self.hcloud_load_balancer: - self.module.fail_json(msg="Load balancer does not exist: %s" % load_balancer_name) - - self._get_load_balancer_service() - except Exception as e: - self.module.fail_json(msg=e.message) - - def _create_load_balancer_service(self): - - self.module.fail_on_missing_params( - required_params=["protocol"] - ) - if self.module.params.get("protocol") == "tcp": - self.module.fail_on_missing_params( - required_params=["destination_port"] - ) - - params = { - "protocol": self.module.params.get("protocol"), - "listen_port": self.module.params.get("listen_port"), - "proxyprotocol": self.module.params.get("proxyprotocol") - } - - if self.module.params.get("destination_port"): - params["destination_port"] = self.module.params.get("destination_port") - - if self.module.params.get("http"): - params["http"] = self.__get_service_http(http_arg=self.module.params.get("http")) - - if self.module.params.get("health_check"): - params["health_check"] = self.__get_service_health_checks( - health_check=self.module.params.get("health_check")) - - if not self.module.check_mode: - try: - self.hcloud_load_balancer.add_service(LoadBalancerService(**params)).wait_until_finished( - max_retries=1000) - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self._get_load_balancer() - self._get_load_balancer_service() - - def __get_service_http(self, http_arg): - service_http = LoadBalancerServiceHttp(certificates=[]) - if http_arg.get("cookie_name") is not None: - service_http.cookie_name = http_arg.get("cookie_name") - if http_arg.get("cookie_lifetime") is not None: - service_http.cookie_lifetime = http_arg.get("cookie_lifetime") - if http_arg.get("sticky_sessions") is not None: - service_http.sticky_sessions = http_arg.get("sticky_sessions") - if http_arg.get("redirect_http") is not None: - service_http.redirect_http = http_arg.get("redirect_http") - if http_arg.get("certificates") is not None: - certificates = http_arg.get("certificates") - if certificates is not None: - for certificate in certificates: - hcloud_cert = None - try: - try: - hcloud_cert = self.client.certificates.get_by_name( - certificate - ) - except Exception: - hcloud_cert = self.client.certificates.get_by_id( - certificate - ) - except Exception as e: - self.module.fail_json(msg=e.message) - service_http.certificates.append(hcloud_cert) - - return service_http - - def __get_service_health_checks(self, health_check): - service_health_check = LoadBalancerHealthCheck() - if health_check.get("protocol") is not None: - service_health_check.protocol = health_check.get("protocol") - if health_check.get("port") is not None: - service_health_check.port = health_check.get("port") - if health_check.get("interval") is not None: - service_health_check.interval = health_check.get("interval") - if health_check.get("timeout") is not None: - service_health_check.timeout = health_check.get("timeout") - if health_check.get("retries") is not None: - service_health_check.retries = health_check.get("retries") - if health_check.get("http") is not None: - health_check_http = health_check.get("http") - service_health_check.http = LoadBalancerHealtCheckHttp() - if health_check_http.get("domain") is not None: - service_health_check.http.domain = health_check_http.get("domain") - if health_check_http.get("path") is not None: - service_health_check.http.path = health_check_http.get("path") - if health_check_http.get("response") is not None: - service_health_check.http.response = health_check_http.get("response") - if health_check_http.get("status_codes") is not None: - service_health_check.http.status_codes = health_check_http.get("status_codes") - if health_check_http.get("tls") is not None: - service_health_check.http.tls = health_check_http.get("tls") - - return service_health_check - - def _update_load_balancer_service(self): - changed = False - try: - params = { - "listen_port": self.module.params.get("listen_port"), - } - - if self.module.params.get("destination_port") is not None: - if self.hcloud_load_balancer_service.destination_port != self.module.params.get("destination_port"): - params["destination_port"] = self.module.params.get("destination_port") - changed = True - - if self.module.params.get("protocol") is not None: - if self.hcloud_load_balancer_service.protocol != self.module.params.get("protocol"): - params["protocol"] = self.module.params.get("protocol") - changed = True - - if self.module.params.get("proxyprotocol") is not None: - if self.hcloud_load_balancer_service.proxyprotocol != self.module.params.get("proxyprotocol"): - params["proxyprotocol"] = self.module.params.get("proxyprotocol") - changed = True - - if self.module.params.get("http") is not None: - params["http"] = self.__get_service_http(http_arg=self.module.params.get("http")) - changed = True - - if self.module.params.get("health_check") is not None: - params["health_check"] = self.__get_service_health_checks( - health_check=self.module.params.get("health_check")) - changed = True - - if not self.module.check_mode: - self.hcloud_load_balancer.update_service(LoadBalancerService(**params)).wait_until_finished( - max_retries=1000) - except Exception as e: - self.module.fail_json(msg=e.message) - self._get_load_balancer() - - if changed: - self._mark_as_changed() - - def _get_load_balancer_service(self): - for service in self.hcloud_load_balancer.services: - if self.module.params.get("listen_port") == service.listen_port: - self.hcloud_load_balancer_service = service - - def present_load_balancer_service(self): - self._get_load_balancer() - if self.hcloud_load_balancer_service is None: - self._create_load_balancer_service() - else: - self._update_load_balancer_service() - - def delete_load_balancer_service(self): - try: - self._get_load_balancer() - if self.hcloud_load_balancer_service is not None: - if not self.module.check_mode: - try: - self.hcloud_load_balancer.delete_service(self.hcloud_load_balancer_service).wait_until_finished( - max_retries=1000) - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self.hcloud_load_balancer_service = None - except APIException as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - load_balancer={"type": "str", "required": True}, - listen_port={"type": "int", "required": True}, - destination_port={"type": "int"}, - protocol={ - "type": "str", - "choices": ["http", "https", "tcp"], - }, - proxyprotocol={"type": "bool", "default": False}, - http={ - "type": "dict", - "options": dict( - cookie_name={ - "type": "str" - }, - cookie_lifetime={ - "type": "int" - }, - sticky_sessions={ - "type": "bool", - "default": False - }, - redirect_http={ - "type": "bool", - "default": False - }, - certificates={ - "type": "list", - "elements": "str" - }, - - ) - }, - health_check={ - "type": "dict", - "options": dict( - protocol={ - "type": "str", - "choices": ["http", "https", "tcp"], - }, - port={ - "type": "int" - }, - interval={ - "type": "int" - }, - timeout={ - "type": "int" - }, - retries={ - "type": "int" - }, - http={ - "type": "dict", - "options": dict( - domain={ - "type": "str" - }, - path={ - "type": "str" - }, - response={ - "type": "str" - }, - status_codes={ - "type": "list", - "elements": "str" - }, - tls={ - "type": "bool", - "default": False - }, - ) - } - ) - - }, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudLoadBalancerService.define_module() - - hcloud = AnsibleHcloudLoadBalancerService(module) - state = module.params.get("state") - if state == "absent": - hcloud.delete_load_balancer_service() - elif state == "present": - hcloud.present_load_balancer_service() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_target.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_target.py deleted file mode 100644 index 760884466..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_target.py +++ /dev/null @@ -1,321 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_load_balancer_target - -short_description: Manage Hetzner Cloud Load Balancer targets - - -description: - - Create and delete Hetzner Cloud Load Balancer targets - -author: - - Lukas Kaemmerling (@lkaemmerling) -version_added: 0.1.0 -options: - type: - description: - - The type of the target. - type: str - choices: [ server, label_selector, ip ] - required: true - load_balancer: - description: - - The name of the Hetzner Cloud Load Balancer. - type: str - required: true - server: - description: - - The name of the Hetzner Cloud Server. - - Required if I(type) is server - type: str - label_selector: - description: - - A Label Selector that will be used to determine the targets dynamically - - Required if I(type) is label_selector - type: str - ip: - description: - - An IP from a Hetzner Dedicated Server, needs to belongs to the same user as the project. - - Required if I(type) is ip - type: str - use_private_ip: - description: - - Route the traffic over the private IP of the Load Balancer through a Hetzner Cloud Network. - - Load Balancer needs to be attached to a network. See M(hetzner.hcloud.hcloud.hcloud_load_balancer_network) - type: bool - default: False - state: - description: - - State of the load_balancer_network. - default: present - choices: [ absent, present ] - type: str - -requirements: - - hcloud-python >= 1.8.1 - -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Create a server Load Balancer target - hetzner.hcloud.hcloud_load_balancer_target: - type: server - load_balancer: my-LoadBalancer - server: my-server - state: present - -- name: Create a label_selector Load Balancer target - hetzner.hcloud.hcloud_load_balancer_target: - type: label_selector - load_balancer: my-LoadBalancer - label_selector: application=backend - state: present - -- name: Create an IP Load Balancer target - hetzner.hcloud.hcloud_load_balancer_target: - type: ip - load_balancer: my-LoadBalancer - ip: 127.0.0.1 - state: present - -- name: Ensure the Load Balancer target is absent (remove if needed) - hetzner.hcloud.hcloud_load_balancer_target: - type: server - load_balancer: my-LoadBalancer - server: my-server - state: absent -""" - -RETURN = """ -hcloud_load_balancer_target: - description: The relationship between a Load Balancer and a network - returned: always - type: complex - contains: - type: - description: Type of the Load Balancer Target - type: str - returned: always - sample: server - load_balancer: - description: Name of the Load Balancer - type: str - returned: always - sample: my-LoadBalancer - server: - description: Name of the Server - type: str - returned: if I(type) is server - sample: my-server - label_selector: - description: Label Selector - type: str - returned: if I(type) is label_selector - sample: application=backend - ip: - description: IP of the dedicated server - type: str - returned: if I(type) is ip - sample: 127.0.0.1 - use_private_ip: - description: - - Route the traffic over the private IP of the Load Balancer through a Hetzner Cloud Network. - - Load Balancer needs to be attached to a network. See M(hetzner.hcloud.hcloud.hcloud_load_balancer_network) - type: bool - sample: true - returned: always -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - -try: - from hcloud.load_balancers.domain import LoadBalancerTarget, LoadBalancerTargetLabelSelector, LoadBalancerTargetIP -except ImportError: - LoadBalancerTarget = None - LoadBalancerTargetLabelSelector = None - LoadBalancerTargetIP = None - - -class AnsibleHcloudLoadBalancerTarget(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_load_balancer_target") - self.hcloud_load_balancer = None - self.hcloud_load_balancer_target = None - self.hcloud_server = None - - def _prepare_result(self): - result = { - "type": to_native(self.hcloud_load_balancer_target.type), - "load_balancer": to_native(self.hcloud_load_balancer.name), - "use_private_ip": self.hcloud_load_balancer_target.use_private_ip - } - - if self.hcloud_load_balancer_target.type == "server": - result["server"] = to_native(self.hcloud_load_balancer_target.server.name) - elif self.hcloud_load_balancer_target.type == "label_selector": - result["label_selector"] = to_native(self.hcloud_load_balancer_target.label_selector.selector) - elif self.hcloud_load_balancer_target.type == "ip": - result["ip"] = to_native(self.hcloud_load_balancer_target.ip.ip) - return result - - def _get_load_balancer_and_target(self): - try: - load_balancer_name = self.module.params.get("load_balancer") - self.hcloud_load_balancer = self.client.load_balancers.get_by_name( - load_balancer_name - ) - if not self.hcloud_load_balancer: - self.module.fail_json(msg="Load balancer does not exist: %s" % load_balancer_name) - - if self.module.params.get("type") == "server": - server_name = self.module.params.get("server") - self.hcloud_server = self.client.servers.get_by_name(server_name) - if not self.hcloud_server: - self.module.fail_json(msg="Server not found: %s" % server_name) - - self.hcloud_load_balancer_target = None - except Exception as e: - self.module.fail_json(msg=e.message) - - def _get_load_balancer_target(self): - for target in self.hcloud_load_balancer.targets: - if self.module.params.get("type") == "server" and target.type == "server": - if target.server.id == self.hcloud_server.id: - self.hcloud_load_balancer_target = target - elif self.module.params.get("type") == "label_selector" and target.type == "label_selector": - if target.label_selector.selector == self.module.params.get("label_selector"): - self.hcloud_load_balancer_target = target - elif self.module.params.get("type") == "ip" and target.type == "ip": - if target.ip.ip == self.module.params.get("ip"): - self.hcloud_load_balancer_target = target - - def _create_load_balancer_target(self): - params = { - "target": None - } - - if self.module.params.get("type") == "server": - self.module.fail_on_missing_params( - required_params=["server"] - ) - params["target"] = LoadBalancerTarget(type=self.module.params.get("type"), server=self.hcloud_server, - use_private_ip=self.module.params.get("use_private_ip")) - elif self.module.params.get("type") == "label_selector": - self.module.fail_on_missing_params( - required_params=["label_selector"] - ) - params["target"] = LoadBalancerTarget(type=self.module.params.get("type"), - label_selector=LoadBalancerTargetLabelSelector( - selector=self.module.params.get("label_selector")), - use_private_ip=self.module.params.get("use_private_ip")) - elif self.module.params.get("type") == "ip": - self.module.fail_on_missing_params( - required_params=["ip"] - ) - params["target"] = LoadBalancerTarget(type=self.module.params.get("type"), - ip=LoadBalancerTargetIP(ip=self.module.params.get("ip")), - use_private_ip=False) - - if not self.module.check_mode: - try: - self.hcloud_load_balancer.add_target(**params).wait_until_finished() - except Exception as e: - if e.code == "locked" or e.code == "conflict": - self._create_load_balancer_target() - else: - self.module.fail_json(msg=e.message) - - self._mark_as_changed() - self._get_load_balancer_and_target() - self._get_load_balancer_target() - - def present_load_balancer_target(self): - self._get_load_balancer_and_target() - self._get_load_balancer_target() - if self.hcloud_load_balancer_target is None: - self._create_load_balancer_target() - - def delete_load_balancer_target(self): - self._get_load_balancer_and_target() - self._get_load_balancer_target() - if self.hcloud_load_balancer_target is not None and self.hcloud_load_balancer is not None: - if not self.module.check_mode: - target = None - if self.module.params.get("type") == "server": - self.module.fail_on_missing_params( - required_params=["server"] - ) - target = LoadBalancerTarget(type=self.module.params.get("type"), - server=self.hcloud_server) - elif self.module.params.get("type") == "label_selector": - self.module.fail_on_missing_params( - required_params=["label_selector"] - ) - target = LoadBalancerTarget(type=self.module.params.get("type"), - label_selector=LoadBalancerTargetLabelSelector( - selector=self.module.params.get("label_selector")), - use_private_ip=self.module.params.get("use_private_ip")) - elif self.module.params.get("type") == "ip": - self.module.fail_on_missing_params( - required_params=["ip"] - ) - target = LoadBalancerTarget(type=self.module.params.get("type"), - ip=LoadBalancerTargetIP(ip=self.module.params.get("ip")), - use_private_ip=False) - try: - self.hcloud_load_balancer.remove_target(target).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self.hcloud_load_balancer_target = None - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - type={"type": "str", "required": True, "choices": ["server", "label_selector", "ip"]}, - load_balancer={"type": "str", "required": True}, - server={"type": "str"}, - label_selector={"type": "str"}, - ip={"type": "str"}, - use_private_ip={"type": "bool", "default": False}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudLoadBalancerTarget.define_module() - - hcloud = AnsibleHcloudLoadBalancerTarget(module) - state = module.params["state"] - if state == "absent": - hcloud.delete_load_balancer_target() - elif state == "present": - hcloud.present_load_balancer_target() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_type_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_type_info.py deleted file mode 100644 index a481ea9c9..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_type_info.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_load_balancer_type_info - -short_description: Gather infos about the Hetzner Cloud Load Balancer types. - - -description: - - Gather infos about your Hetzner Cloud Load Balancer types. - -author: - - Lukas Kaemmerling (@LKaemmerling) -version_added: 0.1.0 -options: - id: - description: - - The ID of the Load Balancer type you want to get. - type: int - name: - description: - - The name of the Load Balancer type you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud Load Balancer type infos - hcloud_load_balancer_type_info: - register: output - -- name: Print the gathered infos - debug: - var: output.hcloud_load_balancer_type_info -""" - -RETURN = """ -hcloud_load_balancer_type_info: - description: The Load Balancer type infos as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the Load Balancer type - returned: always - type: int - sample: 1937415 - name: - description: Name of the Load Balancer type - returned: always - type: str - sample: lb11 - description: - description: Description of the Load Balancer type - returned: always - type: str - sample: LB11 - max_connections: - description: Number of maximum simultaneous open connections - returned: always - type: int - sample: 1 - max_services: - description: Number of services a Load Balancer of this type can have - returned: always - type: int - sample: 1 - max_targets: - description: Number of targets a single Load Balancer can have - returned: always - type: int - sample: 25 - max_assigned_certificates: - description: Number of SSL Certificates that can be assigned to a single Load Balancer - returned: always - type: int - sample: 5 -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudLoadBalancerTypeInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_load_balancer_type_info") - self.hcloud_load_balancer_type_info = None - - def _prepare_result(self): - tmp = [] - - for load_balancer_type in self.hcloud_load_balancer_type_info: - if load_balancer_type is not None: - tmp.append({ - "id": to_native(load_balancer_type.id), - "name": to_native(load_balancer_type.name), - "description": to_native(load_balancer_type.description), - "max_connections": load_balancer_type.max_connections, - "max_services": load_balancer_type.max_services, - "max_targets": load_balancer_type.max_targets, - "max_assigned_certificates": load_balancer_type.max_assigned_certificates - }) - return tmp - - def get_load_balancer_types(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_load_balancer_type_info = [self.client.load_balancer_types.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_load_balancer_type_info = [self.client.load_balancer_types.get_by_name( - self.module.params.get("name") - )] - else: - self.hcloud_load_balancer_type_info = self.client.load_balancer_types.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudLoadBalancerTypeInfo.define_module() - - hcloud = AnsibleHcloudLoadBalancerTypeInfo(module) - hcloud.get_load_balancer_types() - result = hcloud.get_result() - ansible_info = { - 'hcloud_load_balancer_type_info': result['hcloud_load_balancer_type_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_location_facts.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_location_facts.py deleted file mode 100644 index 623c6ab68..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_location_facts.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_location_info - -short_description: Gather infos about your Hetzner Cloud locations. - - -description: - - Gather infos about your Hetzner Cloud locations. - - This module was called C(hcloud_location_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_location_facts). - Note that the M(hetzner.hcloud.hcloud_location_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_location_info)! - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the location you want to get. - type: int - name: - description: - - The name of the location you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud location infos - hcloud_location_info: - register: output - -- name: Print the gathered infos - debug: - var: output -""" - -RETURN = """ -hcloud_location_info: - description: The location infos as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the location - returned: always - type: int - sample: 1937415 - name: - description: Name of the location - returned: always - type: str - sample: fsn1 - description: - description: Detail description of the location - returned: always - type: str - sample: Falkenstein DC Park 1 - country: - description: Country code of the location - returned: always - type: str - sample: DE - city: - description: City of the location - returned: always - type: str - sample: Falkenstein -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudLocationInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_location_info") - self.hcloud_location_info = None - - def _prepare_result(self): - tmp = [] - - for location in self.hcloud_location_info: - if location is not None: - tmp.append({ - "id": to_native(location.id), - "name": to_native(location.name), - "description": to_native(location.description), - "city": to_native(location.city), - "country": to_native(location.country) - }) - return tmp - - def get_locations(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_location_info = [self.client.locations.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_location_info = [self.client.locations.get_by_name( - self.module.params.get("name") - )] - else: - self.hcloud_location_info = self.client.locations.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudLocationInfo.define_module() - - is_old_facts = module._name == 'hcloud_location_facts' - if is_old_facts: - module.deprecate("The 'hcloud_location_info' module has been renamed to 'hcloud_location_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - - hcloud = AnsibleHcloudLocationInfo(module) - hcloud.get_locations() - result = hcloud.get_result() - if is_old_facts: - ansible_info = { - 'hcloud_location_facts': result['hcloud_location_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_location_info': result['hcloud_location_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_location_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_location_info.py deleted file mode 100644 index 623c6ab68..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_location_info.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_location_info - -short_description: Gather infos about your Hetzner Cloud locations. - - -description: - - Gather infos about your Hetzner Cloud locations. - - This module was called C(hcloud_location_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_location_facts). - Note that the M(hetzner.hcloud.hcloud_location_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_location_info)! - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the location you want to get. - type: int - name: - description: - - The name of the location you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud location infos - hcloud_location_info: - register: output - -- name: Print the gathered infos - debug: - var: output -""" - -RETURN = """ -hcloud_location_info: - description: The location infos as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the location - returned: always - type: int - sample: 1937415 - name: - description: Name of the location - returned: always - type: str - sample: fsn1 - description: - description: Detail description of the location - returned: always - type: str - sample: Falkenstein DC Park 1 - country: - description: Country code of the location - returned: always - type: str - sample: DE - city: - description: City of the location - returned: always - type: str - sample: Falkenstein -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudLocationInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_location_info") - self.hcloud_location_info = None - - def _prepare_result(self): - tmp = [] - - for location in self.hcloud_location_info: - if location is not None: - tmp.append({ - "id": to_native(location.id), - "name": to_native(location.name), - "description": to_native(location.description), - "city": to_native(location.city), - "country": to_native(location.country) - }) - return tmp - - def get_locations(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_location_info = [self.client.locations.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_location_info = [self.client.locations.get_by_name( - self.module.params.get("name") - )] - else: - self.hcloud_location_info = self.client.locations.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudLocationInfo.define_module() - - is_old_facts = module._name == 'hcloud_location_facts' - if is_old_facts: - module.deprecate("The 'hcloud_location_info' module has been renamed to 'hcloud_location_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - - hcloud = AnsibleHcloudLocationInfo(module) - hcloud.get_locations() - result = hcloud.get_result() - if is_old_facts: - ansible_info = { - 'hcloud_location_facts': result['hcloud_location_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_location_info': result['hcloud_location_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_network.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_network.py deleted file mode 100644 index 9c005d29f..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_network.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_network - -short_description: Create and manage cloud Networks on the Hetzner Cloud. - - -description: - - Create, update and manage cloud Networks on the Hetzner Cloud. - - You need at least hcloud-python 1.3.0. - -author: - - Lukas Kaemmerling (@lkaemmerling) - -options: - id: - description: - - The ID of the Hetzner Cloud Networks to manage. - - Only required if no Network I(name) is given. - type: int - name: - description: - - The Name of the Hetzner Cloud Network to manage. - - Only required if no Network I(id) is given or a Network does not exist. - type: str - ip_range: - description: - - IP range of the Network. - - Required if Network does not exist. - type: str - labels: - description: - - User-defined labels (key-value pairs). - type: dict - delete_protection: - description: - - Protect the Network for deletion. - type: bool - state: - description: - - State of the Network. - default: present - choices: [ absent, present ] - type: str - -requirements: - - hcloud-python >= 1.3.0 - -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Create a basic network - hcloud_network: - name: my-network - ip_range: 10.0.0.0/8 - state: present - -- name: Ensure the Network is absent (remove if needed) - hcloud_network: - name: my-network - state: absent -""" - -RETURN = """ -hcloud_network: - description: The Network - returned: always - type: complex - contains: - id: - description: ID of the Network - type: int - returned: always - sample: 12345 - name: - description: Name of the Network - type: str - returned: always - sample: my-volume - ip_range: - description: IP range of the Network - type: str - returned: always - sample: 10.0.0.0/8 - delete_protection: - description: True if Network is protected for deletion - type: bool - returned: always - sample: false - version_added: "0.1.0" - labels: - description: User-defined labels (key-value pairs) - type: dict - returned: always - sample: - key: value - mylabel: 123 -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudNetwork(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_network") - self.hcloud_network = None - - def _prepare_result(self): - return { - "id": to_native(self.hcloud_network.id), - "name": to_native(self.hcloud_network.name), - "ip_range": to_native(self.hcloud_network.ip_range), - "delete_protection": self.hcloud_network.protection["delete"], - "labels": self.hcloud_network.labels, - } - - def _get_network(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_network = self.client.networks.get_by_id( - self.module.params.get("id") - ) - else: - self.hcloud_network = self.client.networks.get_by_name( - self.module.params.get("name") - ) - except Exception as e: - self.module.fail_json(msg=e.message) - - def _create_network(self): - - self.module.fail_on_missing_params( - required_params=["name", "ip_range"] - ) - params = { - "name": self.module.params.get("name"), - "ip_range": self.module.params.get("ip_range"), - "labels": self.module.params.get("labels"), - } - try: - if not self.module.check_mode: - self.client.networks.create(**params) - - delete_protection = self.module.params.get("delete_protection") - if delete_protection is not None: - self._get_network() - self.hcloud_network.change_protection(delete=delete_protection).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self._get_network() - - def _update_network(self): - try: - labels = self.module.params.get("labels") - if labels is not None and labels != self.hcloud_network.labels: - if not self.module.check_mode: - self.hcloud_network.update(labels=labels) - self._mark_as_changed() - - ip_range = self.module.params.get("ip_range") - if ip_range is not None and ip_range != self.hcloud_network.ip_range: - if not self.module.check_mode: - self.hcloud_network.change_ip_range(ip_range=ip_range).wait_until_finished() - self._mark_as_changed() - - delete_protection = self.module.params.get("delete_protection") - if delete_protection is not None and delete_protection != self.hcloud_network.protection["delete"]: - if not self.module.check_mode: - self.hcloud_network.change_protection(delete=delete_protection).wait_until_finished() - self._mark_as_changed() - except Exception as e: - self.module.fail_json(msg=e.message) - self._get_network() - - def present_network(self): - self._get_network() - if self.hcloud_network is None: - self._create_network() - else: - self._update_network() - - def delete_network(self): - try: - self._get_network() - if self.hcloud_network is not None: - if not self.module.check_mode: - self.client.networks.delete(self.hcloud_network) - self._mark_as_changed() - except Exception as e: - self.module.fail_json(msg=e.message) - self.hcloud_network = None - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - ip_range={"type": "str"}, - labels={"type": "dict"}, - delete_protection={"type": "bool"}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - required_one_of=[['id', 'name']], - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudNetwork.define_module() - - hcloud = AnsibleHcloudNetwork(module) - state = module.params["state"] - if state == "absent": - hcloud.delete_network() - elif state == "present": - hcloud.present_network() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_network_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_network_info.py deleted file mode 100644 index 382e447aa..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_network_info.py +++ /dev/null @@ -1,293 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_network_info - -short_description: Gather info about your Hetzner Cloud networks. - - -description: - - Gather info about your Hetzner Cloud networks. - -author: - - Christopher Schmitt (@cschmitt-hcloud) - -options: - id: - description: - - The ID of the network you want to get. - type: int - name: - description: - - The name of the network you want to get. - type: str - label_selector: - description: - - The label selector for the network you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud network info - local_action: - module: hcloud_network_info - -- name: Print the gathered info - debug: - var: hcloud_network_info -""" - -RETURN = """ -hcloud_network_info: - description: The network info as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the network - returned: always - type: int - sample: 1937415 - name: - description: Name of the network - returned: always - type: str - sample: awesome-network - ip_range: - description: IP range of the network - returned: always - type: str - sample: 10.0.0.0/16 - subnetworks: - description: Subnetworks belonging to the network - returned: always - type: complex - contains: - type: - description: Type of the subnetwork. - returned: always - type: str - sample: cloud - network_zone: - description: Network of the subnetwork. - returned: always - type: str - sample: eu-central - ip_range: - description: IP range of the subnetwork - returned: always - type: str - sample: 10.0.0.0/24 - gateway: - description: Gateway of this subnetwork - returned: always - type: str - sample: 10.0.0.1 - routes: - description: Routes belonging to the network - returned: always - type: complex - contains: - ip_range: - description: Destination network or host of this route. - returned: always - type: str - sample: 10.0.0.0/16 - gateway: - description: Gateway of this route - returned: always - type: str - sample: 10.0.0.1 - servers: - description: Servers attached to the network - returned: always - type: complex - contains: - id: - description: Numeric identifier of the server - returned: always - type: int - sample: 1937415 - name: - description: Name of the server - returned: always - type: str - sample: my-server - status: - description: Status of the server - returned: always - type: str - sample: running - server_type: - description: Name of the server type of the server - returned: always - type: str - sample: cx11 - ipv4_address: - description: Public IPv4 address of the server, None if not existing - returned: always - type: str - sample: 116.203.104.109 - ipv6: - description: IPv6 network of the server, None if not existing - returned: always - type: str - sample: 2a01:4f8:1c1c:c140::/64 - location: - description: Name of the location of the server - returned: always - type: str - sample: fsn1 - datacenter: - description: Name of the datacenter of the server - returned: always - type: str - sample: fsn1-dc14 - rescue_enabled: - description: True if rescue mode is enabled, Server will then boot into rescue system on next reboot - returned: always - type: bool - sample: false - backup_window: - description: Time window (UTC) in which the backup will run, or null if the backups are not enabled - returned: always - type: bool - sample: 22-02 - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict - delete_protection: - description: True if the network is protected for deletion - returned: always - type: bool - version_added: "0.1.0" - labels: - description: Labels of the network - returned: always - type: dict -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudNetworkInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_network_info") - self.hcloud_network_info = None - - def _prepare_result(self): - tmp = [] - - for network in self.hcloud_network_info: - if network is not None: - subnets = [] - for subnet in network.subnets: - prepared_subnet = { - "type": subnet.type, - "ip_range": subnet.ip_range, - "network_zone": subnet.network_zone, - "gateway": subnet.gateway, - } - subnets.append(prepared_subnet) - routes = [] - for route in network.routes: - prepared_route = { - "destination": route.destination, - "gateway": route.gateway - } - routes.append(prepared_route) - - servers = [] - for server in network.servers: - image = None if server.image is None else to_native(server.image.name) - ipv4_address = None if server.public_net.ipv4 is None else to_native(server.public_net.ipv4.ip) - ipv6 = None if server.public_net.ipv6 is None else to_native(server.public_net.ipv6.ip) - prepared_server = { - "id": to_native(server.id), - "name": to_native(server.name), - "ipv4_address": ipv4_address, - "ipv6": ipv6, - "image": image, - "server_type": to_native(server.server_type.name), - "datacenter": to_native(server.datacenter.name), - "location": to_native(server.datacenter.location.name), - "rescue_enabled": server.rescue_enabled, - "backup_window": to_native(server.backup_window), - "labels": server.labels, - "status": to_native(server.status), - } - servers.append(prepared_server) - - tmp.append({ - "id": to_native(network.id), - "name": to_native(network.name), - "ip_range": to_native(network.ip_range), - "subnetworks": subnets, - "routes": routes, - "servers": servers, - "labels": network.labels, - "delete_protection": network.protection["delete"], - }) - return tmp - - def get_networks(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_network_info = [self.client.networks.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_network_info = [self.client.networks.get_by_name( - self.module.params.get("name") - )] - elif self.module.params.get("label_selector") is not None: - self.hcloud_network_info = self.client.networks.get_all( - label_selector=self.module.params.get("label_selector")) - else: - self.hcloud_network_info = self.client.networks.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - label_selector={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudNetworkInfo.define_module() - - hcloud = AnsibleHcloudNetworkInfo(module) - hcloud.get_networks() - result = hcloud.get_result() - info = { - 'hcloud_network_info': result['hcloud_network_info'] - } - module.exit_json(**info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_placement_group.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_placement_group.py deleted file mode 100644 index 522bb679d..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_placement_group.py +++ /dev/null @@ -1,230 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2020, 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 = """ ---- -module: hcloud_placement_group - -short_description: Create and manage placement groups on the Hetzner Cloud. - - -description: - - Create, update and manage placement groups on the Hetzner Cloud. - -author: - - Adrian Huber (@Adi146) - -options: - id: - description: - - The ID of the Hetzner Cloud placement group to manage. - - Only required if no placement group I(name) is given - type: int - name: - description: - - The Name of the Hetzner Cloud placement group to manage. - - Only required if no placement group I(id) is given, or a placement group does not exist. - type: str - labels: - description: - - User-defined labels (key-value pairs) - type: dict - type: - description: - - The Type of the Hetzner Cloud placement group. - type: str - state: - description: - - State of the placement group. - default: present - choices: [ absent, present ] - type: str - -requirements: - - hcloud-python >= 1.15.0 - -extends_documentation_fragment: -- hetzner.hcloud.hcloud -""" - -EXAMPLES = """ -- name: Create a basic placement group - hcloud_placement_group: - name: my-placement-group - state: present - type: spread - -- name: Create a placement group with labels - hcloud_placement_group: - name: my-placement-group - type: spread - labels: - key: value - mylabel: 123 - state: present - -- name: Ensure the placement group is absent (remove if needed) - hcloud_placement_group: - name: my-placement-group - state: absent -""" - -RETURN = """ -hcloud_placement_group: - description: The placement group instance - returned: Always - type: complex - contains: - id: - description: Numeric identifier of the placement group - returned: always - type: int - sample: 1937415 - name: - description: Name of the placement group - returned: always - type: str - sample: my placement group - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict - type: - description: Type of the placement group - returned: always - type: str - sample: spread - servers: - description: Server IDs of the placement group - returned: always - type: list - elements: int - sample: - - 4711 - - 4712 -""" - -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native - - -class AnsibleHcloudPlacementGroup(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_placement_group") - self.hcloud_placement_group = None - - def _prepare_result(self): - return { - "id": to_native(self.hcloud_placement_group.id), - "name": to_native(self.hcloud_placement_group.name), - "labels": self.hcloud_placement_group.labels, - "type": to_native(self.hcloud_placement_group.type), - "servers": self.hcloud_placement_group.servers, - } - - def _get_placement_group(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_placement_group = self.client.placement_groups.get_by_id( - self.module.params.get("id") - ) - elif self.module.params.get("name") is not None: - self.hcloud_placement_group = self.client.placement_groups.get_by_name( - self.module.params.get("name") - ) - except Exception as e: - self.module.fail_json(msg=e.message) - - def _create_placement_group(self): - self.module.fail_on_missing_params( - required_params=["name"] - ) - params = { - "name": self.module.params.get("name"), - "type": self.module.params.get("type"), - "labels": self.module.params.get("labels"), - } - if not self.module.check_mode: - try: - self.client.placement_groups.create(**params) - except Exception as e: - self.module.fail_json(msg=e.message, **params) - self._mark_as_changed() - self._get_placement_group() - - def _update_placement_group(self): - name = self.module.params.get("name") - if name is not None and self.hcloud_placement_group.name != name: - self.module.fail_on_missing_params( - required_params=["id"] - ) - if not self.module.check_mode: - self.hcloud_placement_group.update(name=name) - self._mark_as_changed() - - labels = self.module.params.get("labels") - if labels is not None and self.hcloud_placement_group.labels != labels: - if not self.module.check_mode: - self.hcloud_placement_group.update(labels=labels) - self._mark_as_changed() - - self._get_placement_group() - - def present_placement_group(self): - self._get_placement_group() - if self.hcloud_placement_group is None: - self._create_placement_group() - else: - self._update_placement_group() - - def delete_placement_group(self): - self._get_placement_group() - if self.hcloud_placement_group is not None: - if not self.module.check_mode: - self.client.placement_groups.delete(self.hcloud_placement_group) - self._mark_as_changed() - self.hcloud_placement_group = None - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - labels={"type": "dict"}, - type={"type": "str"}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - required_one_of=[['id', 'name']], - required_if=[['state', 'present', ['name']]], - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudPlacementGroup.define_module() - - hcloud = AnsibleHcloudPlacementGroup(module) - state = module.params.get("state") - if state == "absent": - hcloud.delete_placement_group() - elif state == "present": - hcloud.present_placement_group() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_primary_ip.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_primary_ip.py deleted file mode 100644 index c192d5fec..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_primary_ip.py +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2022, 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 = ''' ---- -module: hcloud_primary_ip - -short_description: Create and manage cloud Primary IPs on the Hetzner Cloud. - - -description: - - Create, update and manage cloud Primary IPs on the Hetzner Cloud. - -author: - - Lukas Kaemmerling (@lkaemmerling) -version_added: 1.8.0 -options: - id: - description: - - The ID of the Hetzner Cloud Primary IPs to manage. - - Only required if no Primary IP I(name) is given. - type: int - name: - description: - - The Name of the Hetzner Cloud Primary IPs to manage. - - Only required if no Primary IP I(id) is given or a Primary IP does not exist. - type: str - datacenter: - description: - - Home Location of the Hetzner Cloud Primary IP. - - Required if no I(server) is given and Primary IP does not exist. - type: str - type: - description: - - Type of the Primary IP. - - Required if Primary IP does not exist - choices: [ ipv4, ipv6 ] - type: str - auto_delete: - description: - - Delete this Primary IP when the resource it is assigned to is deleted - type: bool - default: no - delete_protection: - description: - - Protect the Primary IP for deletion. - type: bool - labels: - description: - - User-defined labels (key-value pairs). - type: dict - state: - description: - - State of the Primary IP. - default: present - choices: [ absent, present ] - type: str - -requirements: - - hcloud-python >= 1.9.0 - -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Create a basic IPv4 Primary IP - hcloud_primary_ip: - name: my-primary-ip - datacenter: fsn1-dc14 - type: ipv4 - state: present -- name: Create a basic IPv6 Primary IP - hcloud_primary_ip: - name: my-primary-ip - datacenter: fsn1-dc14 - type: ipv6 - state: present -- name: Primary IP should be absent - hcloud_primary_ip: - name: my-primary-ip - state: absent -""" - -RETURN = """ -hcloud_primary_ip: - description: The Primary IP instance - returned: Always - type: complex - contains: - id: - description: ID of the Primary IP - type: int - returned: Always - sample: 12345 - name: - description: Name of the Primary IP - type: str - returned: Always - sample: my-primary-ip - ip: - description: IP Address of the Primary IP - type: str - returned: Always - sample: 116.203.104.109 - type: - description: Type of the Primary IP - type: str - returned: Always - sample: ipv4 - datacenter: - description: Name of the datacenter of the Primary IP - type: str - returned: Always - sample: fsn1-dc14 - delete_protection: - description: True if Primary IP is protected for deletion - type: bool - returned: always - sample: false - labels: - description: User-defined labels (key-value pairs) - type: dict - returned: Always - sample: - key: value - mylabel: 123 -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudPrimaryIP(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_primary_ip") - self.hcloud_primary_ip = None - - def _prepare_result(self): - return { - "id": to_native(self.hcloud_primary_ip.id), - "name": to_native(self.hcloud_primary_ip.name), - "ip": to_native(self.hcloud_primary_ip.ip), - "type": to_native(self.hcloud_primary_ip.type), - "datacenter": to_native(self.hcloud_primary_ip.datacenter.name), - "labels": self.hcloud_primary_ip.labels, - "delete_protection": self.hcloud_primary_ip.protection["delete"], - } - - def _get_primary_ip(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_primary_ip = self.client.primary_ips.get_by_id( - self.module.params.get("id") - ) - else: - self.hcloud_primary_ip = self.client.primary_ips.get_by_name( - self.module.params.get("name") - ) - except Exception as e: - self.module.fail_json(msg=e.message) - - def _create_primary_ip(self): - self.module.fail_on_missing_params( - required_params=["type", "datacenter"] - ) - try: - params = { - "type": self.module.params.get("type"), - "name": self.module.params.get("name"), - "datacenter": self.client.datacenters.get_by_name( - self.module.params.get("datacenter") - ) - } - - if self.module.params.get("labels") is not None: - params["labels"] = self.module.params.get("labels") - if not self.module.check_mode: - resp = self.client.primary_ips.create(**params) - self.hcloud_primary_ip = resp.primary_ip - - delete_protection = self.module.params.get("delete_protection") - if delete_protection is not None: - self.hcloud_primary_ip.change_protection(delete=delete_protection).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e) - self._mark_as_changed() - self._get_primary_ip() - - def _update_primary_ip(self): - try: - labels = self.module.params.get("labels") - if labels is not None and labels != self.hcloud_primary_ip.labels: - if not self.module.check_mode: - self.hcloud_primary_ip.update(labels=labels) - self._mark_as_changed() - - delete_protection = self.module.params.get("delete_protection") - if delete_protection is not None and delete_protection != self.hcloud_primary_ip.protection["delete"]: - if not self.module.check_mode: - self.hcloud_primary_ip.change_protection(delete=delete_protection).wait_until_finished() - self._mark_as_changed() - - self._get_primary_ip() - except Exception as e: - self.module.fail_json(msg=e.message) - - def present_primary_ip(self): - self._get_primary_ip() - if self.hcloud_primary_ip is None: - self._create_primary_ip() - else: - self._update_primary_ip() - - def delete_primary_ip(self): - try: - self._get_primary_ip() - if self.hcloud_primary_ip is not None: - if not self.module.check_mode: - self.client.primary_ips.delete(self.hcloud_primary_ip) - self._mark_as_changed() - self.hcloud_primary_ip = None - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - datacenter={"type": "str"}, - auto_delete={"type": "bool", "default": False}, - type={"choices": ["ipv4", "ipv6"]}, - labels={"type": "dict"}, - delete_protection={"type": "bool"}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - required_one_of=[['id', 'name']], - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudPrimaryIP.define_module() - - hcloud = AnsibleHcloudPrimaryIP(module) - state = module.params["state"] - if state == "absent": - hcloud.delete_primary_ip() - elif state == "present": - hcloud.present_primary_ip() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_rdns.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_rdns.py deleted file mode 100644 index 9f79fbe70..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_rdns.py +++ /dev/null @@ -1,360 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_rdns - -short_description: Create and manage reverse DNS entries on the Hetzner Cloud. - - -description: - - Create, update and delete reverse DNS entries on the Hetzner Cloud. - -author: - - Lukas Kaemmerling (@lkaemmerling) - -options: - server: - description: - - The name of the Hetzner Cloud server you want to add the reverse DNS entry to. - type: str - floating_ip: - description: - - The name of the Hetzner Cloud Floating IP you want to add the reverse DNS entry to. - type: str - primary_ip: - description: - - The name of the Hetzner Cloud Primary IP you want to add the reverse DNS entry to. - type: str - load_balancer: - description: - - The name of the Hetzner Cloud Load Balancer you want to add the reverse DNS entry to. - type: str - ip_address: - description: - - The IP address that should point to I(dns_ptr). - type: str - required: true - dns_ptr: - description: - - The DNS address the I(ip_address) should resolve to. - - Omit the param to reset the reverse DNS entry to the default value. - type: str - state: - description: - - State of the reverse DNS entry. - default: present - choices: [ absent, present ] - type: str - -requirements: - - hcloud-python >= 1.3.0 - -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Create a reverse DNS entry for a server - hcloud_rdns: - server: my-server - ip_address: 123.123.123.123 - dns_ptr: example.com - state: present - -- name: Create a reverse DNS entry for a Floating IP - hcloud_rdns: - floating_ip: my-floating-ip - ip_address: 123.123.123.123 - dns_ptr: example.com - state: present - -- name: Create a reverse DNS entry for a Primary IP - hcloud_rdns: - primary_ip: my-primary-ip - ip_address: 123.123.123.123 - dns_ptr: example.com - state: present - -- name: Create a reverse DNS entry for a Load Balancer - hcloud_rdns: - load_balancer: my-load-balancer - ip_address: 123.123.123.123 - dns_ptr: example.com - state: present - -- name: Ensure the reverse DNS entry is absent (remove if needed) - hcloud_rdns: - server: my-server - ip_address: 123.123.123.123 - dns_ptr: example.com - state: absent -""" - -RETURN = """ -hcloud_rdns: - description: The reverse DNS entry - returned: always - type: complex - contains: - server: - description: Name of the server - type: str - returned: always - sample: my-server - floating_ip: - description: Name of the Floating IP - type: str - returned: always - sample: my-floating-ip - primary_ip: - description: Name of the Primary IP - type: str - returned: always - sample: my-primary-ip - load_balancer: - description: Name of the Load Balancer - type: str - returned: always - sample: my-load-balancer - ip_address: - description: The IP address that point to the DNS ptr - type: str - returned: always - sample: 123.123.123.123 - dns_ptr: - description: The DNS that resolves to the IP - type: str - returned: always - sample: example.com -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud -from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils - - -class AnsibleHcloudReverseDNS(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_rdns") - self.hcloud_resource = None - self.hcloud_rdns = None - - def _prepare_result(self): - result = { - "server": None, - "floating_ip": None, - "load_balancer": None, - "ip_address": to_native(self.hcloud_rdns["ip_address"]), - "dns_ptr": to_native(self.hcloud_rdns["dns_ptr"]), - } - - if self.module.params.get("server"): - result["server"] = to_native(self.hcloud_resource.name) - elif self.module.params.get("floating_ip"): - result["floating_ip"] = to_native(self.hcloud_resource.name) - elif self.module.params.get("load_balancer"): - result["load_balancer"] = to_native(self.hcloud_resource.name) - elif self.module.params.get("primary_ip"): - result["primary_ip"] = to_native(self.hcloud_resource.name) - return result - - def _get_resource(self): - try: - if self.module.params.get("server"): - self.hcloud_resource = self.client.servers.get_by_name( - self.module.params.get("server") - ) - if self.hcloud_resource is None: - self.module.fail_json(msg="The selected server does not exist") - elif self.module.params.get("floating_ip"): - self.hcloud_resource = self.client.floating_ips.get_by_name( - self.module.params.get("floating_ip") - ) - if self.hcloud_resource is None: - self.module.fail_json(msg="The selected Floating IP does not exist") - elif self.module.params.get("primary_ip"): - self.hcloud_resource = self.client.primary_ips.get_by_name( - self.module.params.get("primary_ip") - ) - if self.hcloud_resource is None: - self.module.fail_json(msg="The selected Floating IP does not exist") - elif self.module.params.get("load_balancer"): - self.hcloud_resource = self.client.load_balancers.get_by_name( - self.module.params.get("load_balancer") - ) - if self.hcloud_resource is None: - self.module.fail_json(msg="The selected Load Balancer does not exist") - except Exception as e: - self.module.fail_json(msg=e.message) - - def _get_rdns(self): - ip_address = self.module.params.get("ip_address") - if utils.validate_ip_address(ip_address): - if self.module.params.get("server"): - if self.hcloud_resource.public_net.ipv4.ip == ip_address: - self.hcloud_rdns = { - "ip_address": self.hcloud_resource.public_net.ipv4.ip, - "dns_ptr": self.hcloud_resource.public_net.ipv4.dns_ptr, - } - else: - self.module.fail_json(msg="The selected server does not have this IP address") - elif self.module.params.get("floating_ip"): - if self.hcloud_resource.ip == ip_address: - self.hcloud_rdns = { - "ip_address": self.hcloud_resource.ip, - "dns_ptr": self.hcloud_resource.dns_ptr[0]["dns_ptr"], - } - else: - self.module.fail_json(msg="The selected Floating IP does not have this IP address") - elif self.module.params.get("primary_ip"): - if self.hcloud_resource.ip == ip_address: - self.hcloud_rdns = { - "ip_address": self.hcloud_resource.ip, - "dns_ptr": self.hcloud_resource.dns_ptr[0]["dns_ptr"], - } - else: - self.module.fail_json(msg="The selected Primary IP does not have this IP address") - elif self.module.params.get("load_balancer"): - if self.hcloud_resource.public_net.ipv4.ip == ip_address: - self.hcloud_rdns = { - "ip_address": self.hcloud_resource.public_net.ipv4.ip, - "dns_ptr": self.hcloud_resource.public_net.ipv4.dns_ptr, - } - else: - self.module.fail_json(msg="The selected Load Balancer does not have this IP address") - - elif utils.validate_ip_v6_address(ip_address): - if self.module.params.get("server"): - for ipv6_address_dns_ptr in self.hcloud_resource.public_net.ipv6.dns_ptr: - if ipv6_address_dns_ptr["ip"] == ip_address: - self.hcloud_rdns = { - "ip_address": ipv6_address_dns_ptr["ip"], - "dns_ptr": ipv6_address_dns_ptr["dns_ptr"], - } - elif self.module.params.get("floating_ip"): - for ipv6_address_dns_ptr in self.hcloud_resource.dns_ptr: - if ipv6_address_dns_ptr["ip"] == ip_address: - self.hcloud_rdns = { - "ip_address": ipv6_address_dns_ptr["ip"], - "dns_ptr": ipv6_address_dns_ptr["dns_ptr"], - } - elif self.module.params.get("primary_ip"): - for ipv6_address_dns_ptr in self.hcloud_resource.dns_ptr: - if ipv6_address_dns_ptr["ip"] == ip_address: - self.hcloud_rdns = { - "ip_address": ipv6_address_dns_ptr["ip"], - "dns_ptr": ipv6_address_dns_ptr["dns_ptr"], - } - elif self.module.params.get("load_balancer"): - for ipv6_address_dns_ptr in self.hcloud_resource.public_net.ipv6.dns_ptr: - if ipv6_address_dns_ptr["ip"] == ip_address: - self.hcloud_rdns = { - "ip_address": ipv6_address_dns_ptr["ip"], - "dns_ptr": ipv6_address_dns_ptr["dns_ptr"], - } - else: - self.module.fail_json(msg="The given IP address is not valid") - - def _create_rdns(self): - self.module.fail_on_missing_params( - required_params=["dns_ptr"] - ) - params = { - "ip": self.module.params.get("ip_address"), - "dns_ptr": self.module.params.get("dns_ptr"), - } - - if not self.module.check_mode: - try: - self.hcloud_resource.change_dns_ptr(**params).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self._get_resource() - self._get_rdns() - - def _update_rdns(self): - dns_ptr = self.module.params.get("dns_ptr") - if dns_ptr != self.hcloud_rdns["dns_ptr"]: - params = { - "ip": self.module.params.get("ip_address"), - "dns_ptr": dns_ptr, - } - - if not self.module.check_mode: - try: - self.hcloud_resource.change_dns_ptr(**params).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self._get_resource() - self._get_rdns() - - def present_rdns(self): - self._get_resource() - self._get_rdns() - if self.hcloud_rdns is None: - self._create_rdns() - else: - self._update_rdns() - - def delete_rdns(self): - self._get_resource() - self._get_rdns() - if self.hcloud_rdns is not None: - if not self.module.check_mode: - try: - self.hcloud_resource.change_dns_ptr(ip=self.hcloud_rdns['ip_address'], dns_ptr=None) - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self.hcloud_rdns = None - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - server={"type": "str"}, - floating_ip={"type": "str"}, - load_balancer={"type": "str"}, - primary_ip={"type": "str"}, - ip_address={"type": "str", "required": True}, - dns_ptr={"type": "str"}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - required_one_of=[['server', 'floating_ip', 'load_balancer', 'primary_ip']], - mutually_exclusive=[["server", "floating_ip", 'load_balancer', 'primary_ip']], - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudReverseDNS.define_module() - - hcloud = AnsibleHcloudReverseDNS(module) - state = module.params["state"] - if state == "absent": - hcloud.delete_rdns() - elif state == "present": - hcloud.present_rdns() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_route.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_route.py deleted file mode 100644 index c75177953..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_route.py +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_route - -short_description: Create and delete cloud routes on the Hetzner Cloud. - - -description: - - Create, update and delete cloud routes on the Hetzner Cloud. - -author: - - Lukas Kaemmerling (@lkaemmerling) - -options: - network: - description: - - The name of the Hetzner Cloud Network. - type: str - required: true - destination: - description: - - Destination network or host of this route. - type: str - required: true - gateway: - description: - - Gateway for the route. - type: str - required: true - state: - description: - - State of the route. - default: present - choices: [ absent, present ] - type: str - -requirements: - - hcloud-python >= 1.3.0 - -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Create a basic route - hcloud_route: - network: my-network - destination: 10.100.1.0/24 - gateway: 10.0.1.1 - state: present - -- name: Ensure the route is absent - hcloud_route: - network: my-network - destination: 10.100.1.0/24 - gateway: 10.0.1.1 - state: absent -""" - -RETURN = """ -hcloud_route: - description: One Route of a Network - returned: always - type: complex - contains: - network: - description: Name of the Network - type: str - returned: always - sample: my-network - destination: - description: Destination network or host of this route - type: str - returned: always - sample: 10.0.0.0/8 - gateway: - description: Gateway of the route - type: str - returned: always - sample: 10.0.0.1 -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - -try: - from hcloud.networks.domain import NetworkRoute -except ImportError: - NetworkRoute = None - - -class AnsibleHcloudRoute(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_route") - self.hcloud_network = None - self.hcloud_route = None - - def _prepare_result(self): - return { - "network": to_native(self.hcloud_network.name), - "destination": to_native(self.hcloud_route.destination), - "gateway": self.hcloud_route.gateway, - } - - def _get_network(self): - try: - self.hcloud_network = self.client.networks.get_by_name(self.module.params.get("network")) - self.hcloud_route = None - except Exception as e: - self.module.fail_json(msg=e.message) - - def _get_route(self): - destination = self.module.params.get("destination") - gateway = self.module.params.get("gateway") - for route in self.hcloud_network.routes: - if route.destination == destination and route.gateway == gateway: - self.hcloud_route = route - - def _create_route(self): - route = NetworkRoute( - destination=self.module.params.get("destination"), - gateway=self.module.params.get('gateway') - ) - - if not self.module.check_mode: - try: - self.hcloud_network.add_route(route=route).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - - self._mark_as_changed() - self._get_network() - self._get_route() - - def present_route(self): - self._get_network() - self._get_route() - if self.hcloud_route is None: - self._create_route() - - def delete_route(self): - self._get_network() - self._get_route() - if self.hcloud_route is not None and self.hcloud_network is not None: - if not self.module.check_mode: - try: - self.hcloud_network.delete_route(self.hcloud_route).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self.hcloud_route = None - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - network={"type": "str", "required": True}, - gateway={"type": "str", "required": True}, - destination={"type": "str", "required": True}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudRoute.define_module() - - hcloud = AnsibleHcloudRoute(module) - state = module.params["state"] - if state == "absent": - hcloud.delete_route() - elif state == "present": - hcloud.present_route() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server.py deleted file mode 100644 index 3a77da695..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server.py +++ /dev/null @@ -1,928 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_server - -short_description: Create and manage cloud servers on the Hetzner Cloud. - - -description: - - Create, update and manage cloud servers on the Hetzner Cloud. - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the Hetzner Cloud server to manage. - - Only required if no server I(name) is given - type: int - name: - description: - - The Name of the Hetzner Cloud server to manage. - - Only required if no server I(id) is given or a server does not exist. - type: str - server_type: - description: - - The Server Type of the Hetzner Cloud server to manage. - - Required if server does not exist. - type: str - ssh_keys: - description: - - List of SSH key names - - The key names correspond to the SSH keys configured for your - Hetzner Cloud account access. - type: list - elements: str - volumes: - description: - - List of Volumes IDs that should be attached to the server on server creation. - type: list - elements: str - firewalls: - description: - - List of Firewall IDs that should be attached to the server on server creation. - type: list - elements: str - image: - description: - - Image the server should be created from. - - Required if server does not exist. - type: str - location: - description: - - Location of Server. - - Required if no I(datacenter) is given and server does not exist. - type: str - datacenter: - description: - - Datacenter of Server. - - Required of no I(location) is given and server does not exist. - type: str - backups: - description: - - Enable or disable Backups for the given Server. - type: bool - upgrade_disk: - description: - - Resize the disk size, when resizing a server. - - If you want to downgrade the server later, this value should be False. - type: bool - default: no - enable_ipv4: - description: - - Enables the public ipv4 address - type: bool - default: yes - enable_ipv6: - description: - - Enables the public ipv6 address - type: bool - default: yes - ipv4: - description: - - ID of the ipv4 Primary IP to use. If omitted and enable_ipv4 is true, a new ipv4 Primary IP will automatically be created - type: str - ipv6: - description: - - ID of the ipv6 Primary IP to use. If omitted and enable_ipv6 is true, a new ipv6 Primary IP will automatically be created. - type: str - private_networks: - description: - - List of private networks the server is attached to (name or ID) - - If None, private networks are left as they are (e.g. if previously added by hcloud_server_network), - if it has any other value (including []), only those networks are attached to the server. - type: list - elements: str - force_upgrade: - description: - - Deprecated - - Force the upgrade of the server. - - Power off the server if it is running on upgrade. - type: bool - default: no - force: - description: - - Force the update of the server. - - May power off the server if update. - type: bool - default: no - allow_deprecated_image: - description: - - Allows the creation of servers with deprecated images. - type: bool - default: no - user_data: - description: - - User Data to be passed to the server on creation. - - Only used if server does not exist. - type: str - rescue_mode: - description: - - Add the Hetzner rescue system type you want the server to be booted into. - type: str - labels: - description: - - User-defined labels (key-value pairs). - type: dict - delete_protection: - description: - - Protect the Server for deletion. - - Needs to be the same as I(rebuild_protection). - type: bool - rebuild_protection: - description: - - Protect the Server for rebuild. - - Needs to be the same as I(delete_protection). - type: bool - placement_group: - description: - - Placement Group of the server. - type: str - state: - description: - - State of the server. - default: present - choices: [ absent, present, restarted, started, stopped, rebuild ] - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Create a basic server - hcloud_server: - name: my-server - server_type: cx11 - image: ubuntu-18.04 - state: present - -- name: Create a basic server with ssh key - hcloud_server: - name: my-server - server_type: cx11 - image: ubuntu-18.04 - location: fsn1 - ssh_keys: - - me@myorganisation - state: present - -- name: Resize an existing server - hcloud_server: - name: my-server - server_type: cx21 - upgrade_disk: yes - state: present - -- name: Ensure the server is absent (remove if needed) - hcloud_server: - name: my-server - state: absent - -- name: Ensure the server is started - hcloud_server: - name: my-server - state: started - -- name: Ensure the server is stopped - hcloud_server: - name: my-server - state: stopped - -- name: Ensure the server is restarted - hcloud_server: - name: my-server - state: restarted - -- name: Ensure the server is will be booted in rescue mode and therefore restarted - hcloud_server: - name: my-server - rescue_mode: linux64 - state: restarted - -- name: Ensure the server is rebuild - hcloud_server: - name: my-server - image: ubuntu-18.04 - state: rebuild - -- name: Add server to placement group - hcloud_server: - name: my-server - placement_group: my-placement-group - force: True - state: present - -- name: Remove server from placement group - hcloud_server: - name: my-server - placement_group: null - state: present - -- name: Add server with private network only - hcloud_server: - name: my-server - enable_ipv4: false - enable_ipv6: false - private_networks: - - my-network - - 4711 - state: present -""" - -RETURN = """ -hcloud_server: - description: The server instance - returned: Always - type: complex - contains: - id: - description: Numeric identifier of the server - returned: always - type: int - sample: 1937415 - name: - description: Name of the server - returned: always - type: str - sample: my-server - status: - description: Status of the server - returned: always - type: str - sample: running - server_type: - description: Name of the server type of the server - returned: always - type: str - sample: cx11 - ipv4_address: - description: Public IPv4 address of the server - returned: always - type: str - sample: 116.203.104.109 - ipv6: - description: IPv6 network of the server - returned: always - type: str - sample: 2a01:4f8:1c1c:c140::/64 - private_networks: - description: List of private networks the server is attached to (name or ID) - returned: always - type: list - elements: str - sample: ['my-network', 'another-network', '4711'] - private_networks_info: - description: List of private networks the server is attached to (dict with name and ip) - returned: always - type: list - elements: dict - sample: [{'name': 'my-network', 'ip': '192.168.1.1'}, {'name': 'another-network', 'ip': '10.185.50.40'}] - location: - description: Name of the location of the server - returned: always - type: str - sample: fsn1 - placement_group: - description: Placement Group of the server - type: str - returned: always - sample: 4711 - version_added: "1.5.0" - datacenter: - description: Name of the datacenter of the server - returned: always - type: str - sample: fsn1-dc14 - rescue_enabled: - description: True if rescue mode is enabled, Server will then boot into rescue system on next reboot - returned: always - type: bool - sample: false - backup_window: - description: Time window (UTC) in which the backup will run, or null if the backups are not enabled - returned: always - type: bool - sample: 22-02 - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict - delete_protection: - description: True if server is protected for deletion - type: bool - returned: always - sample: false - version_added: "0.1.0" - rebuild_protection: - description: True if server is protected for rebuild - type: bool - returned: always - sample: false - version_added: "0.1.0" -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud -from datetime import timedelta - -try: - from hcloud.volumes.domain import Volume - from hcloud.ssh_keys.domain import SSHKey - from hcloud.servers.domain import Server, ServerCreatePublicNetwork - from hcloud.firewalls.domain import FirewallResource -except ImportError: - Volume = None - SSHKey = None - Server = None - ServerCreatePublicNetwork = None - FirewallResource = None - - -class AnsibleHcloudServer(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_server") - self.hcloud_server = None - - def _prepare_result(self): - image = None if self.hcloud_server.image is None else to_native(self.hcloud_server.image.name) - placement_group = None if self.hcloud_server.placement_group is None else to_native( - self.hcloud_server.placement_group.name) - ipv4_address = None if self.hcloud_server.public_net.ipv4 is None else to_native( - self.hcloud_server.public_net.ipv4.ip) - ipv6 = None if self.hcloud_server.public_net.ipv6 is None else to_native(self.hcloud_server.public_net.ipv6.ip) - backup_window = None if self.hcloud_server.backup_window is None else to_native(self.hcloud_server.backup_window) - return { - "id": to_native(self.hcloud_server.id), - "name": to_native(self.hcloud_server.name), - "ipv4_address": ipv4_address, - "ipv6": ipv6, - "private_networks": [to_native(net.network.name) for net in self.hcloud_server.private_net], - "private_networks_info": [{"name": to_native(net.network.name), "ip": net.ip} for net in self.hcloud_server.private_net], - "image": image, - "server_type": to_native(self.hcloud_server.server_type.name), - "datacenter": to_native(self.hcloud_server.datacenter.name), - "location": to_native(self.hcloud_server.datacenter.location.name), - "placement_group": placement_group, - "rescue_enabled": self.hcloud_server.rescue_enabled, - "backup_window": backup_window, - "labels": self.hcloud_server.labels, - "delete_protection": self.hcloud_server.protection["delete"], - "rebuild_protection": self.hcloud_server.protection["rebuild"], - "status": to_native(self.hcloud_server.status), - } - - def _get_server(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_server = self.client.servers.get_by_id( - self.module.params.get("id") - ) - else: - self.hcloud_server = self.client.servers.get_by_name( - self.module.params.get("name") - ) - except Exception as e: - self.module.fail_json(msg=e.message) - - def _create_server(self): - self.module.fail_on_missing_params( - required_params=["name", "server_type", "image"] - ) - - server_type = self._get_server_type() - - params = { - "name": self.module.params.get("name"), - "server_type": server_type, - "user_data": self.module.params.get("user_data"), - "labels": self.module.params.get("labels"), - "image": self._get_image(server_type), - "placement_group": self._get_placement_group(), - "public_net": ServerCreatePublicNetwork( - enable_ipv4=self.module.params.get("enable_ipv4"), - enable_ipv6=self.module.params.get("enable_ipv6") - ) - } - - if self.module.params.get("ipv4") is not None: - p = self.client.primary_ips.get_by_name(self.module.params.get("ipv4")) - if not p: - p = self.client.primary_ips.get_by_id(self.module.params.get("ipv4")) - params["public_net"].ipv4 = p - - if self.module.params.get("ipv6") is not None: - p = self.client.primary_ips.get_by_name(self.module.params.get("ipv6")) - if not p: - p = self.client.primary_ips.get_by_id(self.module.params.get("ipv6")) - params["public_net"].ipv6 = p - - if self.module.params.get("private_networks") is not None: - _networks = [] - for network_name_or_id in self.module.params.get("private_networks"): - _networks.append( - self.client.networks.get_by_name(network_name_or_id) - or self.client.networks.get_by_id(network_name_or_id) - ) - params["networks"] = _networks - - if self.module.params.get("ssh_keys") is not None: - params["ssh_keys"] = [ - SSHKey(name=ssh_key_name) - for ssh_key_name in self.module.params.get("ssh_keys") - ] - - if self.module.params.get("volumes") is not None: - params["volumes"] = [ - Volume(id=volume_id) for volume_id in self.module.params.get("volumes") - ] - if self.module.params.get("firewalls") is not None: - params["firewalls"] = [] - for fw in self.module.params.get("firewalls"): - f = self.client.firewalls.get_by_name(fw) - if f is not None: - # When firewall name is not available look for id instead - params["firewalls"].append(f) - else: - params["firewalls"].append(self.client.firewalls.get_by_id(fw)) - - if self.module.params.get("location") is None and self.module.params.get("datacenter") is None: - # When not given, the API will choose the location. - params["location"] = None - params["datacenter"] = None - elif self.module.params.get("location") is not None and self.module.params.get("datacenter") is None: - params["location"] = self.client.locations.get_by_name( - self.module.params.get("location") - ) - elif self.module.params.get("location") is None and self.module.params.get("datacenter") is not None: - params["datacenter"] = self.client.datacenters.get_by_name( - self.module.params.get("datacenter") - ) - - if self.module.params.get("state") == "stopped": - params["start_after_create"] = False - if not self.module.check_mode: - try: - resp = self.client.servers.create(**params) - self.result["root_password"] = resp.root_password - resp.action.wait_until_finished(max_retries=1000) - [action.wait_until_finished() for action in resp.next_actions] - - rescue_mode = self.module.params.get("rescue_mode") - if rescue_mode: - self._get_server() - self._set_rescue_mode(rescue_mode) - - backups = self.module.params.get("backups") - if backups: - self._get_server() - self.hcloud_server.enable_backup().wait_until_finished() - - delete_protection = self.module.params.get("delete_protection") - rebuild_protection = self.module.params.get("rebuild_protection") - if delete_protection is not None and rebuild_protection is not None: - self._get_server() - self.hcloud_server.change_protection(delete=delete_protection, - rebuild=rebuild_protection).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self._get_server() - - def _get_image(self, server_type): - image_resp = self.client.images.get_list(name=self.module.params.get("image"), architecture=server_type.architecture, include_deprecated=True) - images = getattr(image_resp, 'images') - image = None - if images is not None and len(images) > 0: - # If image name is not available look for id instead - image = images[0] - else: - try: - image = self.client.images.get_by_id(self.module.params.get("image")) - except Exception: - self.module.fail_json(msg="Image %s was not found" % self.module.params.get('image')) - if image.deprecated is not None: - available_until = image.deprecated + timedelta(days=90) - if self.module.params.get("allow_deprecated_image"): - self.module.warn( - "You try to use a deprecated image. The image %s will continue to be available until %s.") % ( - image.name, available_until.strftime('%Y-%m-%d')) - else: - self.module.fail_json( - msg=("You try to use a deprecated image. The image %s will continue to be available until %s." + - " If you want to use this image use allow_deprecated_image=yes." - ) % (image.name, available_until.strftime('%Y-%m-%d'))) - return image - - def _get_server_type(self): - server_type = self.client.server_types.get_by_name( - self.module.params.get("server_type") - ) - if server_type is None: - try: - server_type = self.client.server_types.get_by_id(self.module.params.get("server_type")) - except Exception: - self.module.fail_json(msg="server_type %s was not found" % self.module.params.get('server_type')) - - return server_type - - def _get_placement_group(self): - if self.module.params.get("placement_group") is None: - return None - - placement_group = self.client.placement_groups.get_by_name( - self.module.params.get("placement_group") - ) - if placement_group is None: - try: - placement_group = self.client.placement_groups.get_by_id(self.module.params.get("placement_group")) - except Exception: - self.module.fail_json( - msg="placement_group %s was not found" % self.module.params.get("placement_group")) - - return placement_group - - def _get_primary_ip(self, field): - if self.module.params.get(field) is None: - return None - - primary_ip = self.client.primary_ips.get_by_name( - self.module.params.get(field) - ) - if primary_ip is None: - try: - primary_ip = self.client.primary_ips.get_by_id(self.module.params.get(field)) - except Exception as e: - self.module.fail_json( - msg="primary_ip %s was not found" % self.module.params.get(field)) - - return primary_ip - - def _update_server(self): - if "force_upgrade" in self.module.params: - self.module.warn("force_upgrade is deprecated, use force instead") - - try: - previous_server_status = self.hcloud_server.status - - rescue_mode = self.module.params.get("rescue_mode") - if rescue_mode and self.hcloud_server.rescue_enabled is False: - if not self.module.check_mode: - self._set_rescue_mode(rescue_mode) - self._mark_as_changed() - elif not rescue_mode and self.hcloud_server.rescue_enabled is True: - if not self.module.check_mode: - self.hcloud_server.disable_rescue().wait_until_finished() - self._mark_as_changed() - - backups = self.module.params.get("backups") - if backups and self.hcloud_server.backup_window is None: - if not self.module.check_mode: - self.hcloud_server.enable_backup().wait_until_finished() - self._mark_as_changed() - elif backups is not None and not backups and self.hcloud_server.backup_window is not None: - if not self.module.check_mode: - self.hcloud_server.disable_backup().wait_until_finished() - self._mark_as_changed() - - labels = self.module.params.get("labels") - if labels is not None and labels != self.hcloud_server.labels: - if not self.module.check_mode: - self.hcloud_server.update(labels=labels) - self._mark_as_changed() - - wanted_firewalls = self.module.params.get("firewalls") - if wanted_firewalls is not None: - # Removing existing but not wanted firewalls - for current_firewall in self.hcloud_server.public_net.firewalls: - if current_firewall.firewall.name not in wanted_firewalls: - self._mark_as_changed() - if not self.module.check_mode: - r = FirewallResource(type="server", server=self.hcloud_server) - actions = self.client.firewalls.remove_from_resources(current_firewall.firewall, [r]) - for a in actions: - a.wait_until_finished() - - # Adding wanted firewalls that doesn't exist yet - for fname in wanted_firewalls: - found = False - for f in self.hcloud_server.public_net.firewalls: - if f.firewall.name == fname: - found = True - break - - if not found: - self._mark_as_changed() - if not self.module.check_mode: - fw = self.client.firewalls.get_by_name(fname) - if fw is None: - self.module.fail_json(msg="firewall %s was not found" % fname) - r = FirewallResource(type="server", server=self.hcloud_server) - actions = self.client.firewalls.apply_to_resources(fw, [r]) - for a in actions: - a.wait_until_finished() - - if "placement_group" in self.module.params: - if self.module.params["placement_group"] is None and self.hcloud_server.placement_group is not None: - if not self.module.check_mode: - self.hcloud_server.remove_from_placement_group().wait_until_finished() - self._mark_as_changed() - else: - placement_group = self._get_placement_group() - if ( - placement_group is not None and - ( - self.hcloud_server.placement_group is None or - self.hcloud_server.placement_group.id != placement_group.id - ) - ): - self.stop_server_if_forced() - if not self.module.check_mode: - self.hcloud_server.add_to_placement_group(placement_group).wait_until_finished() - self._mark_as_changed() - - if "ipv4" in self.module.params: - if ( - self.module.params["ipv4"] is None and - self.hcloud_server.public_net.primary_ipv4 is not None and - not self.module.params.get("enable_ipv4") - ): - self.stop_server_if_forced() - if not self.module.check_mode: - self.hcloud_server.public_net.primary_ipv4.unassign().wait_until_finished() - self._mark_as_changed() - else: - primary_ip = self._get_primary_ip("ipv4") - if ( - primary_ip is not None and - ( - self.hcloud_server.public_net.primary_ipv4 is None or - self.hcloud_server.public_net.primary_ipv4.id != primary_ip.id - ) - ): - self.stop_server_if_forced() - if not self.module.check_mode: - if self.hcloud_server.public_net.primary_ipv4: - self.hcloud_server.public_net.primary_ipv4.unassign().wait_until_finished() - primary_ip.assign(self.hcloud_server.id, "server").wait_until_finished() - self._mark_as_changed() - if "ipv6" in self.module.params: - if ( - (self.module.params["ipv6"] is None or self.module.params["ipv6"] == "") and - self.hcloud_server.public_net.primary_ipv6 is not None and - not self.module.params.get("enable_ipv6") - ): - self.stop_server_if_forced() - if not self.module.check_mode: - self.hcloud_server.public_net.primary_ipv6.unassign().wait_until_finished() - self._mark_as_changed() - else: - primary_ip = self._get_primary_ip("ipv6") - if ( - primary_ip is not None and - ( - self.hcloud_server.public_net.primary_ipv6 is None or - self.hcloud_server.public_net.primary_ipv6.id != primary_ip.id - ) - ): - self.stop_server_if_forced() - if not self.module.check_mode: - if self.hcloud_server.public_net.primary_ipv6 is not None: - self.hcloud_server.public_net.primary_ipv6.unassign().wait_until_finished() - primary_ip.assign(self.hcloud_server.id, "server").wait_until_finished() - self._mark_as_changed() - if "private_networks" in self.module.params and self.module.params["private_networks"] is not None: - if not bool(self.module.params["private_networks"]): - # This handles None, "" and [] - networks_target = {} - else: - _networks = {} - for network_name_or_id in self.module.params.get("private_networks"): - _found_network = self.client.networks.get_by_name(network_name_or_id) \ - or self.client.networks.get_by_id(network_name_or_id) - _networks.update( - {_found_network.id: _found_network} - ) - networks_target = _networks - networks_is = dict() - for p_network in self.hcloud_server.private_net: - networks_is.update({p_network.network.id: p_network.network}) - for network_id in set(list(networks_is) + list(networks_target)): - if network_id in networks_is and network_id not in networks_target: - self.stop_server_if_forced() - if not self.module.check_mode: - self.hcloud_server.detach_from_network(networks_is[network_id]).wait_until_finished() - self._mark_as_changed() - elif network_id in networks_target and network_id not in networks_is: - self.stop_server_if_forced() - if not self.module.check_mode: - self.hcloud_server.attach_to_network(networks_target[network_id]).wait_until_finished() - self._mark_as_changed() - - server_type = self.module.params.get("server_type") - if server_type is not None and self.hcloud_server.server_type.name != server_type: - self.stop_server_if_forced() - - timeout = 100 - if self.module.params.get("upgrade_disk"): - timeout = ( - 1000 - ) # When we upgrade the disk to the resize progress takes some more time. - if not self.module.check_mode: - self.hcloud_server.change_type( - server_type=self._get_server_type(), - upgrade_disk=self.module.params.get("upgrade_disk"), - ).wait_until_finished(timeout) - self._mark_as_changed() - - if ( - not self.module.check_mode and - ( - ( - self.module.params.get("state") == "present" and - previous_server_status == Server.STATUS_RUNNING - ) or - self.module.params.get("state") == "started" - ) - ): - self.start_server() - - delete_protection = self.module.params.get("delete_protection") - rebuild_protection = self.module.params.get("rebuild_protection") - if (delete_protection is not None and rebuild_protection is not None) and ( - delete_protection != self.hcloud_server.protection["delete"] or rebuild_protection != - self.hcloud_server.protection["rebuild"]): - if not self.module.check_mode: - self.hcloud_server.change_protection(delete=delete_protection, - rebuild=rebuild_protection).wait_until_finished() - self._mark_as_changed() - self._get_server() - except Exception as e: - self.module.fail_json(msg=e) - - def _set_rescue_mode(self, rescue_mode): - if self.module.params.get("ssh_keys"): - resp = self.hcloud_server.enable_rescue(type=rescue_mode, - ssh_keys=[self.client.ssh_keys.get_by_name(ssh_key_name).id - for - ssh_key_name in - self.module.params.get("ssh_keys")]) - else: - resp = self.hcloud_server.enable_rescue(type=rescue_mode) - resp.action.wait_until_finished() - self.result["root_password"] = resp.root_password - - def start_server(self): - try: - if self.hcloud_server: - if self.hcloud_server.status != Server.STATUS_RUNNING: - if not self.module.check_mode: - self.client.servers.power_on(self.hcloud_server).wait_until_finished() - self._mark_as_changed() - self._get_server() - except Exception as e: - self.module.fail_json(msg=e.message) - - def stop_server(self): - try: - if self.hcloud_server: - if self.hcloud_server.status != Server.STATUS_OFF: - if not self.module.check_mode: - self.client.servers.power_off(self.hcloud_server).wait_until_finished() - self._mark_as_changed() - self._get_server() - except Exception as e: - self.module.fail_json(msg=e.message) - - def stop_server_if_forced(self): - previous_server_status = self.hcloud_server.status - if previous_server_status == Server.STATUS_RUNNING and not self.module.check_mode: - if ( - self.module.params.get("force_upgrade") or - self.module.params.get("force") or - self.module.params.get("state") == "stopped" - ): - self.stop_server() # Only stopped server can be upgraded - return previous_server_status - else: - self.module.warn( - "You can not upgrade a running instance %s. You need to stop the instance or use force=yes." - % self.hcloud_server.name - ) - - return None - - def rebuild_server(self): - self.module.fail_on_missing_params( - required_params=["image"] - ) - try: - if not self.module.check_mode: - image = self._get_image(self.hcloud_server.server_type) - self.client.servers.rebuild(self.hcloud_server, image).wait_until_finished(1000) # When we rebuild the server progress takes some more time. - self._mark_as_changed() - - self._get_server() - except Exception as e: - self.module.fail_json(msg=e.message) - - def present_server(self): - self._get_server() - if self.hcloud_server is None: - self._create_server() - else: - self._update_server() - - def delete_server(self): - try: - self._get_server() - if self.hcloud_server is not None: - if not self.module.check_mode: - self.client.servers.delete(self.hcloud_server).wait_until_finished() - self._mark_as_changed() - self.hcloud_server = None - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - image={"type": "str"}, - server_type={"type": "str"}, - location={"type": "str"}, - datacenter={"type": "str"}, - user_data={"type": "str"}, - ssh_keys={"type": "list", "elements": "str", "no_log": False}, - volumes={"type": "list", "elements": "str"}, - firewalls={"type": "list", "elements": "str"}, - labels={"type": "dict"}, - backups={"type": "bool"}, - upgrade_disk={"type": "bool", "default": False}, - enable_ipv4={"type": "bool", "default": True}, - enable_ipv6={"type": "bool", "default": True}, - ipv4={"type": "str"}, - ipv6={"type": "str"}, - private_networks={"type": "list", "elements": "str", "default": None}, - force={"type": "bool", "default": False}, - force_upgrade={"type": "bool", "default": False}, - allow_deprecated_image={"type": "bool", "default": False}, - rescue_mode={"type": "str"}, - delete_protection={"type": "bool"}, - rebuild_protection={"type": "bool"}, - placement_group={"type": "str"}, - state={ - "choices": ["absent", "present", "restarted", "started", "stopped", "rebuild"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - required_one_of=[['id', 'name']], - mutually_exclusive=[["location", "datacenter"]], - required_together=[["delete_protection", "rebuild_protection"]], - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudServer.define_module() - - hcloud = AnsibleHcloudServer(module) - state = module.params.get("state") - if state == "absent": - hcloud.delete_server() - elif state == "present": - hcloud.present_server() - elif state == "started": - hcloud.present_server() - hcloud.start_server() - elif state == "stopped": - hcloud.present_server() - hcloud.stop_server() - elif state == "restarted": - hcloud.present_server() - hcloud.stop_server() - hcloud.start_server() - elif state == "rebuild": - hcloud.present_server() - hcloud.rebuild_server() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_facts.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_facts.py deleted file mode 100644 index 102ceec0d..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_facts.py +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_server_info - -short_description: Gather infos about your Hetzner Cloud servers. - - -description: - - Gather infos about your Hetzner Cloud servers. - - This module was called C(hcloud_server_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_server_facts). - Note that the M(hetzner.hcloud.hcloud_server_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_server_info)! - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the server you want to get. - type: int - name: - description: - - The name of the server you want to get. - type: str - label_selector: - description: - - The label selector for the server you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud server infos - hcloud_server_info: - register: output - -- name: Print the gathered infos - debug: - var: output.hcloud_server_info -""" - -RETURN = """ -hcloud_server_info: - description: The server infos as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the server - returned: always - type: int - sample: 1937415 - name: - description: Name of the server - returned: always - type: str - sample: my-server - status: - description: Status of the server - returned: always - type: str - sample: running - server_type: - description: Name of the server type of the server - returned: always - type: str - sample: cx11 - ipv4_address: - description: Public IPv4 address of the server - returned: always - type: str - sample: 116.203.104.109 - ipv6: - description: IPv6 network of the server - returned: always - type: str - sample: 2a01:4f8:1c1c:c140::/64 - private_networks: - description: List of private networks the server is attached to (name) - returned: always - type: list - elements: str - sample: ['my-network', 'another-network'] - private_networks_info: - description: List of private networks the server is attached to (dict with name and ip) - returned: always - type: list - elements: dict - sample: [{'name': 'my-network', 'ip': '192.168.1.1'}, {'name': 'another-network', 'ip': '10.185.50.40'}] - location: - description: Name of the location of the server - returned: always - type: str - sample: fsn1 - placement_group: - description: Placement Group of the server - type: str - returned: always - sample: 4711 - version_added: "1.5.0" - datacenter: - description: Name of the datacenter of the server - returned: always - type: str - sample: fsn1-dc14 - rescue_enabled: - description: True if rescue mode is enabled, Server will then boot into rescue system on next reboot - returned: always - type: bool - sample: false - backup_window: - description: Time window (UTC) in which the backup will run, or null if the backups are not enabled - returned: always - type: bool - sample: 22-02 - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict - delete_protection: - description: True if server is protected for deletion - type: bool - returned: always - sample: false - version_added: "0.1.0" - rebuild_protection: - description: True if server is protected for rebuild - type: bool - returned: always - sample: false - version_added: "0.1.0" -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudServerInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_server_info") - self.hcloud_server_info = None - - def _prepare_result(self): - tmp = [] - - for server in self.hcloud_server_info: - if server is not None: - image = None if server.image is None else to_native(server.image.name) - placement_group = None if server.placement_group is None else to_native(server.placement_group.name) - ipv4_address = None if server.public_net.ipv4 is None else to_native(server.public_net.ipv4.ip) - ipv6 = None if server.public_net.ipv6 is None else to_native(server.public_net.ipv6.ip) - backup_window = None if server.backup_window is None else to_native(server.backup_window) - tmp.append({ - "id": to_native(server.id), - "name": to_native(server.name), - "ipv4_address": ipv4_address, - "ipv6": ipv6, - "private_networks": [to_native(net.network.name) for net in server.private_net], - "private_networks_info": [{"name": to_native(net.network.name), "ip": net.ip} for net in server.private_net], - "image": image, - "server_type": to_native(server.server_type.name), - "datacenter": to_native(server.datacenter.name), - "location": to_native(server.datacenter.location.name), - "placement_group": placement_group, - "rescue_enabled": server.rescue_enabled, - "backup_window": backup_window, - "labels": server.labels, - "status": to_native(server.status), - "delete_protection": server.protection["delete"], - "rebuild_protection": server.protection["rebuild"], - }) - return tmp - - def get_servers(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_server_info = [self.client.servers.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_server_info = [self.client.servers.get_by_name( - self.module.params.get("name") - )] - elif self.module.params.get("label_selector") is not None: - self.hcloud_server_info = self.client.servers.get_all( - label_selector=self.module.params.get("label_selector")) - else: - self.hcloud_server_info = self.client.servers.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - label_selector={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudServerInfo.define_module() - - is_old_facts = module._name == 'hcloud_server_facts' - if is_old_facts: - module.deprecate("The 'hcloud_server_facts' module has been renamed to 'hcloud_server_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - - hcloud = AnsibleHcloudServerInfo(module) - hcloud.get_servers() - result = hcloud.get_result() - - if is_old_facts: - ansible_info = { - 'hcloud_server_facts': result['hcloud_server_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_server_info': result['hcloud_server_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_info.py deleted file mode 100644 index 102ceec0d..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_info.py +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_server_info - -short_description: Gather infos about your Hetzner Cloud servers. - - -description: - - Gather infos about your Hetzner Cloud servers. - - This module was called C(hcloud_server_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_server_facts). - Note that the M(hetzner.hcloud.hcloud_server_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_server_info)! - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the server you want to get. - type: int - name: - description: - - The name of the server you want to get. - type: str - label_selector: - description: - - The label selector for the server you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud server infos - hcloud_server_info: - register: output - -- name: Print the gathered infos - debug: - var: output.hcloud_server_info -""" - -RETURN = """ -hcloud_server_info: - description: The server infos as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the server - returned: always - type: int - sample: 1937415 - name: - description: Name of the server - returned: always - type: str - sample: my-server - status: - description: Status of the server - returned: always - type: str - sample: running - server_type: - description: Name of the server type of the server - returned: always - type: str - sample: cx11 - ipv4_address: - description: Public IPv4 address of the server - returned: always - type: str - sample: 116.203.104.109 - ipv6: - description: IPv6 network of the server - returned: always - type: str - sample: 2a01:4f8:1c1c:c140::/64 - private_networks: - description: List of private networks the server is attached to (name) - returned: always - type: list - elements: str - sample: ['my-network', 'another-network'] - private_networks_info: - description: List of private networks the server is attached to (dict with name and ip) - returned: always - type: list - elements: dict - sample: [{'name': 'my-network', 'ip': '192.168.1.1'}, {'name': 'another-network', 'ip': '10.185.50.40'}] - location: - description: Name of the location of the server - returned: always - type: str - sample: fsn1 - placement_group: - description: Placement Group of the server - type: str - returned: always - sample: 4711 - version_added: "1.5.0" - datacenter: - description: Name of the datacenter of the server - returned: always - type: str - sample: fsn1-dc14 - rescue_enabled: - description: True if rescue mode is enabled, Server will then boot into rescue system on next reboot - returned: always - type: bool - sample: false - backup_window: - description: Time window (UTC) in which the backup will run, or null if the backups are not enabled - returned: always - type: bool - sample: 22-02 - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict - delete_protection: - description: True if server is protected for deletion - type: bool - returned: always - sample: false - version_added: "0.1.0" - rebuild_protection: - description: True if server is protected for rebuild - type: bool - returned: always - sample: false - version_added: "0.1.0" -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudServerInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_server_info") - self.hcloud_server_info = None - - def _prepare_result(self): - tmp = [] - - for server in self.hcloud_server_info: - if server is not None: - image = None if server.image is None else to_native(server.image.name) - placement_group = None if server.placement_group is None else to_native(server.placement_group.name) - ipv4_address = None if server.public_net.ipv4 is None else to_native(server.public_net.ipv4.ip) - ipv6 = None if server.public_net.ipv6 is None else to_native(server.public_net.ipv6.ip) - backup_window = None if server.backup_window is None else to_native(server.backup_window) - tmp.append({ - "id": to_native(server.id), - "name": to_native(server.name), - "ipv4_address": ipv4_address, - "ipv6": ipv6, - "private_networks": [to_native(net.network.name) for net in server.private_net], - "private_networks_info": [{"name": to_native(net.network.name), "ip": net.ip} for net in server.private_net], - "image": image, - "server_type": to_native(server.server_type.name), - "datacenter": to_native(server.datacenter.name), - "location": to_native(server.datacenter.location.name), - "placement_group": placement_group, - "rescue_enabled": server.rescue_enabled, - "backup_window": backup_window, - "labels": server.labels, - "status": to_native(server.status), - "delete_protection": server.protection["delete"], - "rebuild_protection": server.protection["rebuild"], - }) - return tmp - - def get_servers(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_server_info = [self.client.servers.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_server_info = [self.client.servers.get_by_name( - self.module.params.get("name") - )] - elif self.module.params.get("label_selector") is not None: - self.hcloud_server_info = self.client.servers.get_all( - label_selector=self.module.params.get("label_selector")) - else: - self.hcloud_server_info = self.client.servers.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - label_selector={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudServerInfo.define_module() - - is_old_facts = module._name == 'hcloud_server_facts' - if is_old_facts: - module.deprecate("The 'hcloud_server_facts' module has been renamed to 'hcloud_server_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - - hcloud = AnsibleHcloudServerInfo(module) - hcloud.get_servers() - result = hcloud.get_result() - - if is_old_facts: - ansible_info = { - 'hcloud_server_facts': result['hcloud_server_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_server_info': result['hcloud_server_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_network.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_network.py deleted file mode 100644 index 79f6838fd..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_network.py +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_server_network - -short_description: Manage the relationship between Hetzner Cloud Networks and servers - - -description: - - Create and delete the relationship Hetzner Cloud Networks and servers - -author: - - Lukas Kaemmerling (@lkaemmerling) - -options: - network: - description: - - The name of the Hetzner Cloud Networks. - type: str - required: true - server: - description: - - The name of the Hetzner Cloud server. - type: str - required: true - ip: - description: - - The IP the server should have. - type: str - alias_ips: - description: - - Alias IPs the server has. - type: list - elements: str - state: - description: - - State of the server_network. - default: present - choices: [ absent, present ] - type: str - -requirements: - - hcloud-python >= 1.3.0 - -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Create a basic server network - hcloud_server_network: - network: my-network - server: my-server - state: present - -- name: Create a server network and specify the ip address - hcloud_server_network: - network: my-network - server: my-server - ip: 10.0.0.1 - state: present - -- name: Create a server network and add alias ips - hcloud_server_network: - network: my-network - server: my-server - ip: 10.0.0.1 - alias_ips: - - 10.1.0.1 - - 10.2.0.1 - state: present - -- name: Ensure the server network is absent (remove if needed) - hcloud_server_network: - network: my-network - server: my-server - state: absent -""" - -RETURN = """ -hcloud_server_network: - description: The relationship between a server and a network - returned: always - type: complex - contains: - network: - description: Name of the Network - type: str - returned: always - sample: my-network - server: - description: Name of the server - type: str - returned: always - sample: my-server - ip: - description: IP of the server within the Network ip range - type: str - returned: always - sample: 10.0.0.8 - alias_ips: - description: Alias IPs of the server within the Network ip range - type: str - returned: always - sample: [10.1.0.1, ...] -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - -try: - from hcloud import APIException -except ImportError: - APIException = None - - -class AnsibleHcloudServerNetwork(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_server_network") - self.hcloud_network = None - self.hcloud_server = None - self.hcloud_server_network = None - - def _prepare_result(self): - return { - "network": to_native(self.hcloud_network.name), - "server": to_native(self.hcloud_server.name), - "ip": to_native(self.hcloud_server_network.ip), - "alias_ips": self.hcloud_server_network.alias_ips, - } - - def _get_server_and_network(self): - try: - self.hcloud_network = self.client.networks.get_by_name(self.module.params.get("network")) - self.hcloud_server = self.client.servers.get_by_name(self.module.params.get("server")) - self.hcloud_server_network = None - except Exception as e: - self.module.fail_json(msg=e.message) - - def _get_server_network(self): - for privateNet in self.hcloud_server.private_net: - if privateNet.network.id == self.hcloud_network.id: - self.hcloud_server_network = privateNet - - def _create_server_network(self): - params = { - "network": self.hcloud_network - } - - if self.module.params.get("ip") is not None: - params["ip"] = self.module.params.get("ip") - if self.module.params.get("alias_ips") is not None: - params["alias_ips"] = self.module.params.get("alias_ips") - - if not self.module.check_mode: - try: - self.hcloud_server.attach_to_network(**params).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - - self._mark_as_changed() - self._get_server_and_network() - self._get_server_network() - - def _update_server_network(self): - params = { - "network": self.hcloud_network - } - alias_ips = self.module.params.get("alias_ips") - if alias_ips is not None and sorted(self.hcloud_server_network.alias_ips) != sorted(alias_ips): - params["alias_ips"] = alias_ips - - if not self.module.check_mode: - try: - self.hcloud_server.change_alias_ips(**params).wait_until_finished() - except APIException as e: - self.module.fail_json(msg=e.message) - - self._mark_as_changed() - self._get_server_and_network() - self._get_server_network() - - def present_server_network(self): - self._get_server_and_network() - self._get_server_network() - if self.hcloud_server_network is None: - self._create_server_network() - else: - self._update_server_network() - - def delete_server_network(self): - self._get_server_and_network() - self._get_server_network() - if self.hcloud_server_network is not None and self.hcloud_server is not None: - if not self.module.check_mode: - try: - self.hcloud_server.detach_from_network(self.hcloud_server_network.network).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self.hcloud_server_network = None - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - network={"type": "str", "required": True}, - server={"type": "str", "required": True}, - ip={"type": "str"}, - alias_ips={"type": "list", "elements": "str"}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudServerNetwork.define_module() - - hcloud = AnsibleHcloudServerNetwork(module) - state = module.params["state"] - if state == "absent": - hcloud.delete_server_network() - elif state == "present": - hcloud.present_server_network() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_type_facts.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_type_facts.py deleted file mode 100644 index a84067c32..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_type_facts.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_server_type_info - -short_description: Gather infos about the Hetzner Cloud server types. - - -description: - - Gather infos about your Hetzner Cloud server types. - - This module was called C(hcloud_server_type_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_server_type_facts). - Note that the M(hetzner.hcloud.hcloud_server_type_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_server_type_info)! - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the server type you want to get. - type: int - name: - description: - - The name of the server type you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud server type infos - hcloud_server_type_info: - register: output - -- name: Print the gathered infos - debug: - var: output.hcloud_server_type_info -""" - -RETURN = """ -hcloud_server_type_info: - description: The server type infos as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the server type - returned: always - type: int - sample: 1937415 - name: - description: Name of the server type - returned: always - type: str - sample: fsn1 - description: - description: Detail description of the server type - returned: always - type: str - sample: Falkenstein DC Park 1 - cores: - description: Number of cpu cores a server of this type will have - returned: always - type: int - sample: 1 - memory: - description: Memory a server of this type will have in GB - returned: always - type: int - sample: 1 - disk: - description: Disk size a server of this type will have in GB - returned: always - type: int - sample: 25 - storage_type: - description: Type of server boot drive - returned: always - type: str - sample: local - cpu_type: - description: Type of cpu - returned: always - type: str - sample: shared - architecture: - description: Architecture of cpu - returned: always - type: str - sample: x86 -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudServerTypeInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_server_type_info") - self.hcloud_server_type_info = None - - def _prepare_result(self): - tmp = [] - - for server_type in self.hcloud_server_type_info: - if server_type is not None: - tmp.append({ - "id": to_native(server_type.id), - "name": to_native(server_type.name), - "description": to_native(server_type.description), - "cores": server_type.cores, - "memory": server_type.memory, - "disk": server_type.disk, - "storage_type": to_native(server_type.storage_type), - "cpu_type": to_native(server_type.cpu_type), - "architecture": to_native(server_type.architecture) - }) - return tmp - - def get_server_types(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_server_type_info = [self.client.server_types.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_server_type_info = [self.client.server_types.get_by_name( - self.module.params.get("name") - )] - else: - self.hcloud_server_type_info = self.client.server_types.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudServerTypeInfo.define_module() - - is_old_facts = module._name == 'hcloud_server_type_facts' - if is_old_facts: - module.deprecate("The 'hcloud_server_type_info' module has been renamed to 'hcloud_server_type_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - - hcloud = AnsibleHcloudServerTypeInfo(module) - hcloud.get_server_types() - result = hcloud.get_result() - if is_old_facts: - ansible_info = { - 'hcloud_server_type_info': result['hcloud_server_type_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_server_type_info': result['hcloud_server_type_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_type_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_type_info.py deleted file mode 100644 index a84067c32..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_type_info.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_server_type_info - -short_description: Gather infos about the Hetzner Cloud server types. - - -description: - - Gather infos about your Hetzner Cloud server types. - - This module was called C(hcloud_server_type_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_server_type_facts). - Note that the M(hetzner.hcloud.hcloud_server_type_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_server_type_info)! - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the server type you want to get. - type: int - name: - description: - - The name of the server type you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud server type infos - hcloud_server_type_info: - register: output - -- name: Print the gathered infos - debug: - var: output.hcloud_server_type_info -""" - -RETURN = """ -hcloud_server_type_info: - description: The server type infos as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the server type - returned: always - type: int - sample: 1937415 - name: - description: Name of the server type - returned: always - type: str - sample: fsn1 - description: - description: Detail description of the server type - returned: always - type: str - sample: Falkenstein DC Park 1 - cores: - description: Number of cpu cores a server of this type will have - returned: always - type: int - sample: 1 - memory: - description: Memory a server of this type will have in GB - returned: always - type: int - sample: 1 - disk: - description: Disk size a server of this type will have in GB - returned: always - type: int - sample: 25 - storage_type: - description: Type of server boot drive - returned: always - type: str - sample: local - cpu_type: - description: Type of cpu - returned: always - type: str - sample: shared - architecture: - description: Architecture of cpu - returned: always - type: str - sample: x86 -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudServerTypeInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_server_type_info") - self.hcloud_server_type_info = None - - def _prepare_result(self): - tmp = [] - - for server_type in self.hcloud_server_type_info: - if server_type is not None: - tmp.append({ - "id": to_native(server_type.id), - "name": to_native(server_type.name), - "description": to_native(server_type.description), - "cores": server_type.cores, - "memory": server_type.memory, - "disk": server_type.disk, - "storage_type": to_native(server_type.storage_type), - "cpu_type": to_native(server_type.cpu_type), - "architecture": to_native(server_type.architecture) - }) - return tmp - - def get_server_types(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_server_type_info = [self.client.server_types.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_server_type_info = [self.client.server_types.get_by_name( - self.module.params.get("name") - )] - else: - self.hcloud_server_type_info = self.client.server_types.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudServerTypeInfo.define_module() - - is_old_facts = module._name == 'hcloud_server_type_facts' - if is_old_facts: - module.deprecate("The 'hcloud_server_type_info' module has been renamed to 'hcloud_server_type_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - - hcloud = AnsibleHcloudServerTypeInfo(module) - hcloud.get_server_types() - result = hcloud.get_result() - if is_old_facts: - ansible_info = { - 'hcloud_server_type_info': result['hcloud_server_type_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_server_type_info': result['hcloud_server_type_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_ssh_key.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_ssh_key.py deleted file mode 100644 index 59a5197f5..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_ssh_key.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_ssh_key - -short_description: Create and manage ssh keys on the Hetzner Cloud. - - -description: - - Create, update and manage ssh keys on the Hetzner Cloud. - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the Hetzner Cloud ssh_key to manage. - - Only required if no ssh_key I(name) is given - type: int - name: - description: - - The Name of the Hetzner Cloud ssh_key to manage. - - Only required if no ssh_key I(id) is given or a ssh_key does not exist. - type: str - fingerprint: - description: - - The Fingerprint of the Hetzner Cloud ssh_key to manage. - - Only required if no ssh_key I(id) or I(name) is given. - type: str - labels: - description: - - User-defined labels (key-value pairs) - type: dict - public_key: - description: - - The Public Key to add. - - Required if ssh_key does not exist. - type: str - state: - description: - - State of the ssh_key. - default: present - choices: [ absent, present ] - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Create a basic ssh_key - hcloud_ssh_key: - name: my-ssh_key - public_key: "ssh-rsa AAAjjk76kgf...Xt" - state: present - -- name: Create a ssh_key with labels - hcloud_ssh_key: - name: my-ssh_key - public_key: "ssh-rsa AAAjjk76kgf...Xt" - labels: - key: value - mylabel: 123 - state: present - -- name: Ensure the ssh_key is absent (remove if needed) - hcloud_ssh_key: - name: my-ssh_key - state: absent -""" - -RETURN = """ -hcloud_ssh_key: - description: The ssh_key instance - returned: Always - type: complex - contains: - id: - description: ID of the ssh_key - type: int - returned: Always - sample: 12345 - name: - description: Name of the ssh_key - type: str - returned: Always - sample: my-ssh-key - fingerprint: - description: Fingerprint of the ssh_key - type: str - returned: Always - sample: b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f - public_key: - description: Public key of the ssh_key - type: str - returned: Always - sample: "ssh-rsa AAAjjk76kgf...Xt" - labels: - description: User-defined labels (key-value pairs) - type: dict - returned: Always - sample: - key: value - mylabel: 123 -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudSSHKey(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_ssh_key") - self.hcloud_ssh_key = None - - def _prepare_result(self): - return { - "id": to_native(self.hcloud_ssh_key.id), - "name": to_native(self.hcloud_ssh_key.name), - "fingerprint": to_native(self.hcloud_ssh_key.fingerprint), - "public_key": to_native(self.hcloud_ssh_key.public_key), - "labels": self.hcloud_ssh_key.labels, - } - - def _get_ssh_key(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_ssh_key = self.client.ssh_keys.get_by_id( - self.module.params.get("id") - ) - elif self.module.params.get("fingerprint") is not None: - self.hcloud_ssh_key = self.client.ssh_keys.get_by_fingerprint( - self.module.params.get("fingerprint") - ) - elif self.module.params.get("name") is not None: - self.hcloud_ssh_key = self.client.ssh_keys.get_by_name( - self.module.params.get("name") - ) - - except Exception as e: - self.module.fail_json(msg=e.message) - - def _create_ssh_key(self): - self.module.fail_on_missing_params( - required_params=["name", "public_key"] - ) - params = { - "name": self.module.params.get("name"), - "public_key": self.module.params.get("public_key"), - "labels": self.module.params.get("labels") - } - - if not self.module.check_mode: - try: - self.client.ssh_keys.create(**params) - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self._get_ssh_key() - - def _update_ssh_key(self): - name = self.module.params.get("name") - if name is not None and self.hcloud_ssh_key.name != name: - self.module.fail_on_missing_params( - required_params=["id"] - ) - if not self.module.check_mode: - self.hcloud_ssh_key.update(name=name) - self._mark_as_changed() - - labels = self.module.params.get("labels") - if labels is not None and self.hcloud_ssh_key.labels != labels: - if not self.module.check_mode: - self.hcloud_ssh_key.update(labels=labels) - self._mark_as_changed() - - self._get_ssh_key() - - def present_ssh_key(self): - self._get_ssh_key() - if self.hcloud_ssh_key is None: - self._create_ssh_key() - else: - self._update_ssh_key() - - def delete_ssh_key(self): - self._get_ssh_key() - if self.hcloud_ssh_key is not None: - if not self.module.check_mode: - try: - self.client.ssh_keys.delete(self.hcloud_ssh_key) - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self.hcloud_ssh_key = None - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - public_key={"type": "str"}, - fingerprint={"type": "str"}, - labels={"type": "dict"}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - required_one_of=[['id', 'name', 'fingerprint']], - required_if=[['state', 'present', ['name']]], - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudSSHKey.define_module() - - hcloud = AnsibleHcloudSSHKey(module) - state = module.params.get("state") - if state == "absent": - hcloud.delete_ssh_key() - elif state == "present": - hcloud.present_ssh_key() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_ssh_key_facts.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_ssh_key_facts.py deleted file mode 100644 index aab98ed60..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_ssh_key_facts.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_ssh_key_info -short_description: Gather infos about your Hetzner Cloud ssh_keys. -description: - - Gather facts about your Hetzner Cloud ssh_keys. - - This module was called C(hcloud_ssh_key_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_ssh_key_facts). - Note that the M(hetzner.hcloud.hcloud_ssh_key_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_ssh_key_info)! -author: - - Christopher Schmitt (@cschmitt-hcloud) -options: - id: - description: - - The ID of the ssh key you want to get. - type: int - name: - description: - - The name of the ssh key you want to get. - type: str - fingerprint: - description: - - The fingerprint of the ssh key you want to get. - type: str - label_selector: - description: - - The label selector for the ssh key you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud sshkey infos - hcloud_ssh_key_info: - register: output -- name: Print the gathered infos - debug: - var: output.hcloud_ssh_key_info -""" - -RETURN = """ -hcloud_ssh_key_info: - description: The ssh key instances - returned: Always - type: complex - contains: - id: - description: Numeric identifier of the ssh_key - returned: always - type: int - sample: 1937415 - name: - description: Name of the ssh_key - returned: always - type: str - sample: my-ssh-key - fingerprint: - description: Fingerprint of the ssh key - returned: always - type: str - sample: 0e:e0:bd:c7:2d:1f:69:49:94:44:91:f1:19:fd:35:f3 - public_key: - description: The actual public key - returned: always - type: str - sample: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGpl/tnk74nnQJxxLAtutUApUZMRJxryKh7VXkNbd4g9 john@example.com" - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict -""" -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudSSHKeyInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_ssh_key_info") - self.hcloud_ssh_key_info = None - - def _prepare_result(self): - ssh_keys = [] - - for ssh_key in self.hcloud_ssh_key_info: - if ssh_key: - ssh_keys.append({ - "id": to_native(ssh_key.id), - "name": to_native(ssh_key.name), - "fingerprint": to_native(ssh_key.fingerprint), - "public_key": to_native(ssh_key.public_key), - "labels": ssh_key.labels - }) - return ssh_keys - - def get_ssh_keys(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_ssh_key_info = [self.client.ssh_keys.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_ssh_key_info = [self.client.ssh_keys.get_by_name( - self.module.params.get("name") - )] - elif self.module.params.get("fingerprint") is not None: - self.hcloud_ssh_key_info = [self.client.ssh_keys.get_by_fingerprint( - self.module.params.get("fingerprint") - )] - elif self.module.params.get("label_selector") is not None: - self.hcloud_ssh_key_info = self.client.ssh_keys.get_all( - label_selector=self.module.params.get("label_selector")) - else: - self.hcloud_ssh_key_info = self.client.ssh_keys.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - fingerprint={"type": "str"}, - label_selector={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudSSHKeyInfo.define_module() - - is_old_facts = module._name == 'hcloud_ssh_key_facts' - if is_old_facts: - module.deprecate("The 'hcloud_ssh_key_facts' module has been renamed to 'hcloud_ssh_key_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - - hcloud = AnsibleHcloudSSHKeyInfo(module) - hcloud.get_ssh_keys() - result = hcloud.get_result() - - if is_old_facts: - ansible_info = { - 'hcloud_ssh_key_facts': result['hcloud_ssh_key_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_ssh_key_info': result['hcloud_ssh_key_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_ssh_key_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_ssh_key_info.py deleted file mode 100644 index aab98ed60..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_ssh_key_info.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_ssh_key_info -short_description: Gather infos about your Hetzner Cloud ssh_keys. -description: - - Gather facts about your Hetzner Cloud ssh_keys. - - This module was called C(hcloud_ssh_key_facts) before Ansible 2.9, returning C(ansible_facts) and C(hcloud_ssh_key_facts). - Note that the M(hetzner.hcloud.hcloud_ssh_key_info) module no longer returns C(ansible_facts) and the value was renamed to C(hcloud_ssh_key_info)! -author: - - Christopher Schmitt (@cschmitt-hcloud) -options: - id: - description: - - The ID of the ssh key you want to get. - type: int - name: - description: - - The name of the ssh key you want to get. - type: str - fingerprint: - description: - - The fingerprint of the ssh key you want to get. - type: str - label_selector: - description: - - The label selector for the ssh key you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud sshkey infos - hcloud_ssh_key_info: - register: output -- name: Print the gathered infos - debug: - var: output.hcloud_ssh_key_info -""" - -RETURN = """ -hcloud_ssh_key_info: - description: The ssh key instances - returned: Always - type: complex - contains: - id: - description: Numeric identifier of the ssh_key - returned: always - type: int - sample: 1937415 - name: - description: Name of the ssh_key - returned: always - type: str - sample: my-ssh-key - fingerprint: - description: Fingerprint of the ssh key - returned: always - type: str - sample: 0e:e0:bd:c7:2d:1f:69:49:94:44:91:f1:19:fd:35:f3 - public_key: - description: The actual public key - returned: always - type: str - sample: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGpl/tnk74nnQJxxLAtutUApUZMRJxryKh7VXkNbd4g9 john@example.com" - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict -""" -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudSSHKeyInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_ssh_key_info") - self.hcloud_ssh_key_info = None - - def _prepare_result(self): - ssh_keys = [] - - for ssh_key in self.hcloud_ssh_key_info: - if ssh_key: - ssh_keys.append({ - "id": to_native(ssh_key.id), - "name": to_native(ssh_key.name), - "fingerprint": to_native(ssh_key.fingerprint), - "public_key": to_native(ssh_key.public_key), - "labels": ssh_key.labels - }) - return ssh_keys - - def get_ssh_keys(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_ssh_key_info = [self.client.ssh_keys.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_ssh_key_info = [self.client.ssh_keys.get_by_name( - self.module.params.get("name") - )] - elif self.module.params.get("fingerprint") is not None: - self.hcloud_ssh_key_info = [self.client.ssh_keys.get_by_fingerprint( - self.module.params.get("fingerprint") - )] - elif self.module.params.get("label_selector") is not None: - self.hcloud_ssh_key_info = self.client.ssh_keys.get_all( - label_selector=self.module.params.get("label_selector")) - else: - self.hcloud_ssh_key_info = self.client.ssh_keys.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - fingerprint={"type": "str"}, - label_selector={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudSSHKeyInfo.define_module() - - is_old_facts = module._name == 'hcloud_ssh_key_facts' - if is_old_facts: - module.deprecate("The 'hcloud_ssh_key_facts' module has been renamed to 'hcloud_ssh_key_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - - hcloud = AnsibleHcloudSSHKeyInfo(module) - hcloud.get_ssh_keys() - result = hcloud.get_result() - - if is_old_facts: - ansible_info = { - 'hcloud_ssh_key_facts': result['hcloud_ssh_key_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_ssh_key_info': result['hcloud_ssh_key_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_subnetwork.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_subnetwork.py deleted file mode 100644 index c2ba66d80..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_subnetwork.py +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_subnetwork - -short_description: Manage cloud subnetworks on the Hetzner Cloud. - - -description: - - Create, update and delete cloud subnetworks on the Hetzner Cloud. - -author: - - Lukas Kaemmerling (@lkaemmerling) - -options: - network: - description: - - The ID or Name of the Hetzner Cloud Networks. - type: str - required: true - ip_range: - description: - - IP range of the subnetwork. - type: str - required: true - type: - description: - - Type of subnetwork. - type: str - choices: [ server, cloud, vswitch ] - required: true - network_zone: - description: - - Name of network zone. - type: str - required: true - vswitch_id: - description: - - ID of the vSwitch you want to couple with your Network. - - Required if type == vswitch - type: int - state: - description: - - State of the subnetwork. - default: present - choices: [ absent, present ] - type: str - -requirements: - - hcloud-python >= 1.10.0 - -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Create a basic subnetwork - hcloud_subnetwork: - network: my-network - ip_range: 10.0.0.0/16 - network_zone: eu-central - type: cloud - state: present - -- name: Create a basic subnetwork - hcloud_subnetwork: - network: my-vswitch-network - ip_range: 10.0.0.0/24 - network_zone: eu-central - type: vswitch - vswitch_id: 123 - state: present - -- name: Ensure the subnetwork is absent (remove if needed) - hcloud_subnetwork: - network: my-network - ip_range: 10.0.0.0/8 - network_zone: eu-central - type: cloud - state: absent -""" - -RETURN = """ -hcloud_subnetwork: - description: One Subnet of a Network - returned: always - type: complex - contains: - network: - description: Name of the Network - type: str - returned: always - sample: my-network - ip_range: - description: IP range of the Network - type: str - returned: always - sample: 10.0.0.0/8 - type: - description: Type of subnetwork - type: str - returned: always - sample: server - network_zone: - description: Name of network zone - type: str - returned: always - sample: eu-central - vswitch_id: - description: ID of the vswitch, null if not type vswitch - type: int - returned: always - sample: 123 - gateway: - description: Gateway of the subnetwork - type: str - returned: always - sample: 10.0.0.1 -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - -try: - from hcloud.networks.domain import NetworkSubnet -except ImportError: - NetworkSubnet = None - - -class AnsibleHcloudSubnetwork(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_subnetwork") - self.hcloud_network = None - self.hcloud_subnetwork = None - - def _prepare_result(self): - return { - "network": to_native(self.hcloud_network.name), - "ip_range": to_native(self.hcloud_subnetwork.ip_range), - "type": to_native(self.hcloud_subnetwork.type), - "network_zone": to_native(self.hcloud_subnetwork.network_zone), - "gateway": self.hcloud_subnetwork.gateway, - "vswitch_id": self.hcloud_subnetwork.vswitch_id, - } - - def _get_network(self): - try: - self.hcloud_network = self.client.networks.get_by_name(self.module.params.get("network")) - self.hcloud_subnetwork = None - except Exception as e: - self.module.fail_json(msg=e.message) - - def _get_subnetwork(self): - subnet_ip_range = self.module.params.get("ip_range") - for subnetwork in self.hcloud_network.subnets: - if subnetwork.ip_range == subnet_ip_range: - self.hcloud_subnetwork = subnetwork - - def _create_subnetwork(self): - params = { - "ip_range": self.module.params.get("ip_range"), - "type": self.module.params.get('type'), - "network_zone": self.module.params.get('network_zone') - } - if self.module.params.get('type') == NetworkSubnet.TYPE_VSWITCH: - self.module.fail_on_missing_params( - required_params=["vswitch_id"] - ) - params["vswitch_id"] = self.module.params.get('vswitch_id') - - if not self.module.check_mode: - try: - self.hcloud_network.add_subnet(subnet=NetworkSubnet(**params)).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - - self._mark_as_changed() - self._get_network() - self._get_subnetwork() - - def present_subnetwork(self): - self._get_network() - self._get_subnetwork() - if self.hcloud_subnetwork is None: - self._create_subnetwork() - - def delete_subnetwork(self): - self._get_network() - self._get_subnetwork() - if self.hcloud_subnetwork is not None and self.hcloud_network is not None: - if not self.module.check_mode: - try: - self.hcloud_network.delete_subnet(self.hcloud_subnetwork).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self.hcloud_subnetwork = None - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - network={"type": "str", "required": True}, - network_zone={"type": "str", "required": True}, - type={ - "type": "str", - "required": True, - "choices": ["server", "cloud", "vswitch"] - }, - ip_range={"type": "str", "required": True}, - vswitch_id={"type": "int"}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudSubnetwork.define_module() - - hcloud = AnsibleHcloudSubnetwork(module) - state = module.params["state"] - if state == "absent": - hcloud.delete_subnetwork() - elif state == "present": - hcloud.present_subnetwork() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_volume.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_volume.py deleted file mode 100644 index 623a399b4..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_volume.py +++ /dev/null @@ -1,340 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_volume - -short_description: Create and manage block Volume on the Hetzner Cloud. - - -description: - - Create, update and attach/detach block Volume on the Hetzner Cloud. - -author: - - Christopher Schmitt (@cschmitt-hcloud) - -options: - id: - description: - - The ID of the Hetzner Cloud Block Volume to manage. - - Only required if no volume I(name) is given - type: int - name: - description: - - The Name of the Hetzner Cloud Block Volume to manage. - - Only required if no volume I(id) is given or a volume does not exist. - type: str - size: - description: - - The size of the Block Volume in GB. - - Required if volume does not yet exists. - type: int - automount: - description: - - Automatically mount the Volume. - type: bool - default: False - format: - description: - - Automatically Format the volume on creation - - Can only be used in case the Volume does not exist. - type: str - choices: [xfs, ext4] - location: - description: - - Location of the Hetzner Cloud Volume. - - Required if no I(server) is given and Volume does not exist. - type: str - server: - description: - - Server Name the Volume should be assigned to. - - Required if no I(location) is given and Volume does not exist. - type: str - delete_protection: - description: - - Protect the Volume for deletion. - type: bool - labels: - description: - - User-defined key-value pairs. - type: dict - state: - description: - - State of the Volume. - default: present - choices: [absent, present] - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Create a Volume - hcloud_volume: - name: my-volume - location: fsn1 - size: 100 - state: present -- name: Create a Volume and format it with ext4 - hcloud_volume: - name: my-volume - location: fsn - format: ext4 - size: 100 - state: present -- name: Mount a existing Volume and automount - hcloud_volume: - name: my-volume - server: my-server - automount: yes - state: present -- name: Mount a existing Volume and automount - hcloud_volume: - name: my-volume - server: my-server - automount: yes - state: present -- name: Ensure the Volume is absent (remove if needed) - hcloud_volume: - name: my-volume - state: absent -""" - -RETURN = """ -hcloud_volume: - description: The block Volume - returned: Always - type: complex - contains: - id: - description: ID of the Volume - type: int - returned: Always - sample: 12345 - name: - description: Name of the Volume - type: str - returned: Always - sample: my-volume - size: - description: Size in GB of the Volume - type: int - returned: Always - sample: 1337 - linux_device: - description: Path to the device that contains the Volume. - returned: always - type: str - sample: /dev/disk/by-id/scsi-0HC_Volume_12345 - version_added: "0.1.0" - location: - description: Location name where the Volume is located at - type: str - returned: Always - sample: "fsn1" - labels: - description: User-defined labels (key-value pairs) - type: dict - returned: Always - sample: - key: value - mylabel: 123 - server: - description: Server name where the Volume is attached to - type: str - returned: Always - sample: "my-server" - delete_protection: - description: True if Volume is protected for deletion - type: bool - returned: always - sample: false - version_added: "0.1.0" -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudVolume(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_volume") - self.hcloud_volume = None - - def _prepare_result(self): - server_name = None - if self.hcloud_volume.server is not None: - server_name = to_native(self.hcloud_volume.server.name) - - return { - "id": to_native(self.hcloud_volume.id), - "name": to_native(self.hcloud_volume.name), - "size": self.hcloud_volume.size, - "location": to_native(self.hcloud_volume.location.name), - "labels": self.hcloud_volume.labels, - "server": server_name, - "linux_device": to_native(self.hcloud_volume.linux_device), - "delete_protection": self.hcloud_volume.protection["delete"], - } - - def _get_volume(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_volume = self.client.volumes.get_by_id( - self.module.params.get("id") - ) - else: - self.hcloud_volume = self.client.volumes.get_by_name( - self.module.params.get("name") - ) - except Exception as e: - self.module.fail_json(msg=e.message) - - def _create_volume(self): - self.module.fail_on_missing_params( - required_params=["name", "size"] - ) - params = { - "name": self.module.params.get("name"), - "size": self.module.params.get("size"), - "automount": self.module.params.get("automount"), - "format": self.module.params.get("format"), - "labels": self.module.params.get("labels") - } - if self.module.params.get("server") is not None: - params['server'] = self.client.servers.get_by_name(self.module.params.get("server")) - elif self.module.params.get("location") is not None: - params['location'] = self.client.locations.get_by_name(self.module.params.get("location")) - else: - self.module.fail_json(msg="server or location is required") - - if not self.module.check_mode: - try: - resp = self.client.volumes.create(**params) - resp.action.wait_until_finished() - [action.wait_until_finished() for action in resp.next_actions] - delete_protection = self.module.params.get("delete_protection") - if delete_protection is not None: - self._get_volume() - self.hcloud_volume.change_protection(delete=delete_protection).wait_until_finished() - except Exception as e: - self.module.fail_json(msg=e.message) - self._mark_as_changed() - self._get_volume() - - def _update_volume(self): - try: - size = self.module.params.get("size") - if size: - if self.hcloud_volume.size < size: - if not self.module.check_mode: - self.hcloud_volume.resize(size).wait_until_finished() - self._mark_as_changed() - elif self.hcloud_volume.size > size: - self.module.warn("Shrinking of volumes is not supported") - - server_name = self.module.params.get("server") - if server_name: - server = self.client.servers.get_by_name(server_name) - if self.hcloud_volume.server is None or self.hcloud_volume.server.name != server.name: - if not self.module.check_mode: - automount = self.module.params.get("automount", False) - self.hcloud_volume.attach(server, automount=automount).wait_until_finished() - self._mark_as_changed() - else: - if self.hcloud_volume.server is not None: - if not self.module.check_mode: - self.hcloud_volume.detach().wait_until_finished() - self._mark_as_changed() - - labels = self.module.params.get("labels") - if labels is not None and labels != self.hcloud_volume.labels: - if not self.module.check_mode: - self.hcloud_volume.update(labels=labels) - self._mark_as_changed() - - delete_protection = self.module.params.get("delete_protection") - if delete_protection is not None and delete_protection != self.hcloud_volume.protection["delete"]: - if not self.module.check_mode: - self.hcloud_volume.change_protection(delete=delete_protection).wait_until_finished() - self._mark_as_changed() - - self._get_volume() - except Exception as e: - self.module.fail_json(msg=e.message) - - def present_volume(self): - self._get_volume() - if self.hcloud_volume is None: - self._create_volume() - else: - self._update_volume() - - def delete_volume(self): - try: - self._get_volume() - if self.hcloud_volume is not None: - if not self.module.check_mode: - if self.hcloud_volume.server is not None: - self.hcloud_volume.detach().wait_until_finished() - self.client.volumes.delete(self.hcloud_volume) - self._mark_as_changed() - self.hcloud_volume = None - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - size={"type": "int"}, - location={"type": "str"}, - server={"type": "str"}, - labels={"type": "dict"}, - automount={"type": "bool", "default": False}, - format={"type": "str", - "choices": ['xfs', 'ext4'], - }, - delete_protection={"type": "bool"}, - state={ - "choices": ["absent", "present"], - "default": "present", - }, - **Hcloud.base_module_arguments() - ), - required_one_of=[['id', 'name']], - mutually_exclusive=[["location", "server"]], - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudVolume.define_module() - - hcloud = AnsibleHcloudVolume(module) - state = module.params.get("state") - if state == "absent": - module.fail_on_missing_params( - required_params=["name"] - ) - hcloud.delete_volume() - else: - hcloud.present_volume() - - module.exit_json(**hcloud.get_result()) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_volume_facts.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_volume_facts.py deleted file mode 100644 index 9520bfa14..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_volume_facts.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_volume_info - -short_description: Gather infos about your Hetzner Cloud Volumes. - -description: - - Gather infos about your Hetzner Cloud Volumes. - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the Volume you want to get. - type: int - name: - description: - - The name of the Volume you want to get. - type: str - label_selector: - description: - - The label selector for the Volume you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud Volume infos - hcloud_volume_info: - register: output -- name: Print the gathered infos - debug: - var: output.hcloud_volume_info -""" - -RETURN = """ -hcloud_volume_info: - description: The Volume infos as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the Volume - returned: always - type: int - sample: 1937415 - name: - description: Name of the Volume - returned: always - type: str - sample: my-volume - size: - description: Size of the Volume - returned: always - type: str - sample: 10 - linux_device: - description: Path to the device that contains the Volume. - returned: always - type: str - sample: /dev/disk/by-id/scsi-0HC_Volume_12345 - version_added: "0.1.0" - location: - description: Name of the location where the Volume resides in - returned: always - type: str - sample: fsn1 - server: - description: Name of the server where the Volume is attached to - returned: always - type: str - sample: my-server - delete_protection: - description: True if the Volume is protected for deletion - returned: always - type: bool - version_added: "0.1.0" - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudVolumeInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_volume_info") - self.hcloud_volume_info = None - - def _prepare_result(self): - tmp = [] - - for volume in self.hcloud_volume_info: - if volume is not None: - server_name = None - if volume.server is not None: - server_name = to_native(volume.server.name) - tmp.append({ - "id": to_native(volume.id), - "name": to_native(volume.name), - "size": volume.size, - "location": to_native(volume.location.name), - "labels": volume.labels, - "server": server_name, - "linux_device": to_native(volume.linux_device), - "delete_protection": volume.protection["delete"], - }) - - return tmp - - def get_volumes(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_volume_info = [self.client.volumes.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_volume_info = [self.client.volumes.get_by_name( - self.module.params.get("name") - )] - elif self.module.params.get("label_selector") is not None: - self.hcloud_volume_info = self.client.volumes.get_all( - label_selector=self.module.params.get("label_selector")) - else: - self.hcloud_volume_info = self.client.volumes.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - label_selector={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudVolumeInfo.define_module() - - is_old_facts = module._name == 'hcloud_volume_facts' - if is_old_facts: - module.deprecate("The 'hcloud_volume_facts' module has been renamed to 'hcloud_volume_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - - hcloud = AnsibleHcloudVolumeInfo(module) - - hcloud.get_volumes() - result = hcloud.get_result() - if is_old_facts: - ansible_info = { - 'hcloud_volume_facts': result['hcloud_volume_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_volume_info': result['hcloud_volume_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_volume_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_volume_info.py deleted file mode 100644 index 9520bfa14..000000000 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_volume_info.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 = ''' ---- -module: hcloud_volume_info - -short_description: Gather infos about your Hetzner Cloud Volumes. - -description: - - Gather infos about your Hetzner Cloud Volumes. - -author: - - Lukas Kaemmerling (@LKaemmerling) - -options: - id: - description: - - The ID of the Volume you want to get. - type: int - name: - description: - - The name of the Volume you want to get. - type: str - label_selector: - description: - - The label selector for the Volume you want to get. - type: str -extends_documentation_fragment: -- hetzner.hcloud.hcloud - -''' - -EXAMPLES = """ -- name: Gather hcloud Volume infos - hcloud_volume_info: - register: output -- name: Print the gathered infos - debug: - var: output.hcloud_volume_info -""" - -RETURN = """ -hcloud_volume_info: - description: The Volume infos as list - returned: always - type: complex - contains: - id: - description: Numeric identifier of the Volume - returned: always - type: int - sample: 1937415 - name: - description: Name of the Volume - returned: always - type: str - sample: my-volume - size: - description: Size of the Volume - returned: always - type: str - sample: 10 - linux_device: - description: Path to the device that contains the Volume. - returned: always - type: str - sample: /dev/disk/by-id/scsi-0HC_Volume_12345 - version_added: "0.1.0" - location: - description: Name of the location where the Volume resides in - returned: always - type: str - sample: fsn1 - server: - description: Name of the server where the Volume is attached to - returned: always - type: str - sample: my-server - delete_protection: - description: True if the Volume is protected for deletion - returned: always - type: bool - version_added: "0.1.0" - labels: - description: User-defined labels (key-value pairs) - returned: always - type: dict -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - - -class AnsibleHcloudVolumeInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_volume_info") - self.hcloud_volume_info = None - - def _prepare_result(self): - tmp = [] - - for volume in self.hcloud_volume_info: - if volume is not None: - server_name = None - if volume.server is not None: - server_name = to_native(volume.server.name) - tmp.append({ - "id": to_native(volume.id), - "name": to_native(volume.name), - "size": volume.size, - "location": to_native(volume.location.name), - "labels": volume.labels, - "server": server_name, - "linux_device": to_native(volume.linux_device), - "delete_protection": volume.protection["delete"], - }) - - return tmp - - def get_volumes(self): - try: - if self.module.params.get("id") is not None: - self.hcloud_volume_info = [self.client.volumes.get_by_id( - self.module.params.get("id") - )] - elif self.module.params.get("name") is not None: - self.hcloud_volume_info = [self.client.volumes.get_by_name( - self.module.params.get("name") - )] - elif self.module.params.get("label_selector") is not None: - self.hcloud_volume_info = self.client.volumes.get_all( - label_selector=self.module.params.get("label_selector")) - else: - self.hcloud_volume_info = self.client.volumes.get_all() - - except Exception as e: - self.module.fail_json(msg=e.message) - - @staticmethod - def define_module(): - return AnsibleModule( - argument_spec=dict( - id={"type": "int"}, - name={"type": "str"}, - label_selector={"type": "str"}, - **Hcloud.base_module_arguments() - ), - supports_check_mode=True, - ) - - -def main(): - module = AnsibleHcloudVolumeInfo.define_module() - - is_old_facts = module._name == 'hcloud_volume_facts' - if is_old_facts: - module.deprecate("The 'hcloud_volume_facts' module has been renamed to 'hcloud_volume_info', " - "and the renamed one no longer returns ansible_facts", version='2.0.0', collection_name="hetzner.hcloud") - - hcloud = AnsibleHcloudVolumeInfo(module) - - hcloud.get_volumes() - result = hcloud.get_result() - if is_old_facts: - ansible_info = { - 'hcloud_volume_facts': result['hcloud_volume_info'] - } - module.exit_json(ansible_facts=ansible_info) - else: - ansible_info = { - 'hcloud_volume_info': result['hcloud_volume_info'] - } - module.exit_json(**ansible_info) - - -if __name__ == "__main__": - main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/image_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/image_info.py new file mode 100644 index 000000000..b0d7fc482 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/image_info.py @@ -0,0 +1,209 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: image_info + +short_description: Gather infos about your Hetzner Cloud images. + + +description: + - Gather infos about your Hetzner Cloud images. + +author: + - Lukas Kaemmerling (@LKaemmerling) + +options: + id: + description: + - The ID of the image you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name of the image you want to get. + type: str + label_selector: + description: + - The label selector for the images you want to get. + type: str + type: + description: + - The type for the images you want to get. + default: system + choices: [ system, snapshot, backup ] + type: str + architecture: + description: + - The architecture for the images you want to get. + type: str + choices: [ x86, arm ] +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Gather hcloud image infos + hetzner.hcloud.image_info: + register: output + +- name: Print the gathered infos + debug: + var: output +""" + +RETURN = """ +hcloud_image_info: + description: The image infos as list + returned: always + type: complex + contains: + id: + description: Numeric identifier of the image + returned: always + type: int + sample: 1937415 + type: + description: Type of the image + returned: always + type: str + sample: system + status: + description: Status of the image + returned: always + type: str + sample: available + name: + description: Name of the image + returned: always + type: str + sample: ubuntu-22.04 + description: + description: Detail description of the image + returned: always + type: str + sample: Ubuntu 18.04 Standard 64 bit + os_flavor: + description: OS flavor of the image + returned: always + type: str + sample: ubuntu + os_version: + description: OS version of the image + returned: always + type: str + sample: 18.04 + architecture: + description: Image is compatible with this architecture + returned: always + type: str + sample: x86 + labels: + description: User-defined labels (key-value pairs) + returned: always + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.images import BoundImage + + +class AnsibleHCloudImageInfo(AnsibleHCloud): + represent = "hcloud_image_info" + + hcloud_image_info: list[BoundImage] | None = None + + def _prepare_result(self): + tmp = [] + + for image in self.hcloud_image_info: + if image is not None: + tmp.append( + { + "id": to_native(image.id), + "status": to_native(image.status), + "type": to_native(image.type), + "name": to_native(image.name), + "description": to_native(image.description), + "os_flavor": to_native(image.os_flavor), + "os_version": to_native(image.os_version), + "architecture": to_native(image.architecture), + "labels": image.labels, + } + ) + return tmp + + def get_images(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_image_info = [self.client.images.get_by_id(self.module.params.get("id"))] + elif self.module.params.get("name") is not None and self.module.params.get("architecture") is not None: + self.hcloud_image_info = [ + self.client.images.get_by_name_and_architecture( + self.module.params.get("name"), + self.module.params.get("architecture"), + ) + ] + elif self.module.params.get("name") is not None: + self.module.warn( + "This module only returns x86 images by default. Please set architecture:x86|arm to hide this message." + ) + self.hcloud_image_info = [self.client.images.get_by_name(self.module.params.get("name"))] + else: + params = {} + label_selector = self.module.params.get("label_selector") + if label_selector: + params["label_selector"] = label_selector + + image_type = self.module.params.get("type") + if image_type: + params["type"] = image_type + + architecture = self.module.params.get("architecture") + if architecture: + params["architecture"] = architecture + + self.hcloud_image_info = self.client.images.get_all(**params) + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + label_selector={"type": "str"}, + type={"choices": ["system", "snapshot", "backup"], "default": "system", "type": "str"}, + architecture={"choices": ["x86", "arm"], "type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudImageInfo.define_module() + hcloud = AnsibleHCloudImageInfo(module) + + hcloud.get_images() + result = hcloud.get_result() + + ansible_info = {"hcloud_image_info": result["hcloud_image_info"]} + module.exit_json(**ansible_info) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/iso_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/iso_info.py new file mode 100644 index 000000000..e623d1714 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/iso_info.py @@ -0,0 +1,206 @@ +#!/usr/bin/python + +# Copyright: (c) 2022, Patrice Le Guyader +# heavily inspired by the work of @LKaemmerling +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +from __future__ import annotations + +DOCUMENTATION = """ +--- +module: iso_info + +short_description: Gather infos about the Hetzner Cloud ISO list. + +description: + - Gather infos about the Hetzner Cloud ISO list. + +author: + - Patrice Le Guyader (@patlegu) + - Lukas Kaemmerling (@LKaemmerling) + +options: + id: + description: + - The ID of the ISO image you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name of the ISO you want to get. + type: str + architecture: + description: + - Filter ISOs with compatible architecture. + type: str + choices: [x86, arm] + include_architecture_wildcard: + description: + - Include ISOs with wildcard architecture (architecture is null). + - Works only if architecture filter is specified. + type: bool + +extends_documentation_fragment: + - hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Gather hcloud ISO type infos + hetzner.hcloud.iso_info: + register: output + +- name: Print the gathered infos + debug: + var: output.hcloud_iso_info +""" + +RETURN = """ +hcloud_iso_info: + description: The ISO type infos as list + returned: always + type: complex + contains: + id: + description: ID of the ISO + returned: always + type: int + sample: 22110 + name: + description: Unique identifier of the ISO. Only set for public ISOs + returned: always + type: str + sample: debian-12.0.0-amd64-netinst.iso + description: + description: Description of the ISO + returned: always + type: str + sample: Debian 12.0 (amd64/netinstall) + architecture: + description: > + Type of cpu architecture this ISO is compatible with. + None indicates no restriction on the architecture (wildcard). + returned: when supported + type: str + sample: x86 + type: + description: Type of the ISO, can be one of `public`, `private`. + returned: always + type: str + sample: public + deprecated: + description: > + ISO 8601 timestamp of deprecation, None if ISO is still available. + After the deprecation time it will no longer be possible to attach the + ISO to servers. This field is deprecated. Use `deprecation` instead. + returned: always + type: str + sample: "2024-12-01T00:00:00+00:00" + deprecation: + description: > + Describes if, when & how the resources was deprecated. If this field is + set to None the resource is not deprecated. If it has a value, it is + considered deprecated. + returned: if the resource is deprecated + type: dict + contains: + announced: + description: Date of when the deprecation was announced. + returned: always + type: str + sample: "2021-11-01T00:00:00+00:00" + unavailable_after: + description: > + After the time in this field, the resource will not be available + from the general listing endpoint of the resource type, and it can + not be used in new resources. For example, if this is an image, + you can not create new servers with this image after the mentioned + date. + returned: always + type: str + sample: "2021-12-01T00:00:00+00:00" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.isos import BoundIso + + +class AnsibleHCloudIsoInfo(AnsibleHCloud): + represent = "hcloud_iso_info" + + hcloud_iso_info: list[BoundIso] | None = None + + def _prepare_result(self): + tmp = [] + + for iso_info in self.hcloud_iso_info: + if iso_info is None: + continue + + tmp.append( + { + "id": to_native(iso_info.id), + "name": to_native(iso_info.name), + "description": to_native(iso_info.description), + "type": iso_info.type, + "architecture": iso_info.architecture, + "deprecated": ( + iso_info.deprecation.unavailable_after if iso_info.deprecation is not None else None + ), + "deprecation": ( + { + "announced": iso_info.deprecation.announced.isoformat(), + "unavailable_after": iso_info.deprecation.unavailable_after.isoformat(), + } + if iso_info.deprecation is not None + else None + ), + } + ) + + return tmp + + def get_iso_infos(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_iso_info = [self.client.isos.get_by_id(self.module.params.get("id"))] + elif self.module.params.get("name") is not None: + self.hcloud_iso_info = [self.client.isos.get_by_name(self.module.params.get("name"))] + else: + self.hcloud_iso_info = self.client.isos.get_all( + architecture=self.module.params.get("architecture"), + include_wildcard_architecture=self.module.params.get("include_wildcard_architecture"), + ) + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + architecture={"type": "str", "choices": ["x86", "arm"]}, + include_architecture_wildcard={"type": "bool"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudIsoInfo.define_module() + hcloud = AnsibleHCloudIsoInfo(module) + hcloud.get_iso_infos() + result = hcloud.get_result() + ansible_info = {"hcloud_iso_info": result["hcloud_iso_info"]} + module.exit_json(**ansible_info) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer.py b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer.py new file mode 100644 index 000000000..1a0d8712a --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer.py @@ -0,0 +1,340 @@ +#!/usr/bin/python + +# Copyright: (c) 2020, 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 annotations + +DOCUMENTATION = """ +--- +module: load_balancer + +short_description: Create and manage cloud Load Balancers on the Hetzner Cloud. + +description: + - Create, update and manage cloud Load Balancers on the Hetzner Cloud. + +author: + - Lukas Kaemmerling (@LKaemmerling) +version_added: 0.1.0 +options: + id: + description: + - The ID of the Hetzner Cloud Load Balancer to manage. + - Only required if no Load Balancer I(name) is given + type: int + name: + description: + - The Name of the Hetzner Cloud Load Balancer to manage. + - Only required if no Load Balancer I(id) is given or a Load Balancer does not exist. + type: str + load_balancer_type: + description: + - The Load Balancer Type of the Hetzner Cloud Load Balancer to manage. + - Required if Load Balancer does not exist. + type: str + algorithm: + description: + - Algorithm of the Load Balancer. + type: str + default: round_robin + choices: [round_robin, least_connections] + location: + description: + - Location of Load Balancer. + - Required if no I(network_zone) is given and Load Balancer does not exist. + type: str + network_zone: + description: + - Network Zone of Load Balancer. + - Required of no I(location) is given and Load Balancer does not exist. + type: str + labels: + description: + - User-defined labels (key-value pairs). + type: dict + disable_public_interface: + description: + - Disables the public interface. + type: bool + default: False + delete_protection: + description: + - Protect the Load Balancer for deletion. + type: bool + state: + description: + - State of the Load Balancer. + default: present + choices: [ absent, present ] + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a basic Load Balancer + hetzner.hcloud.load_balancer: + name: my-Load Balancer + load_balancer_type: lb11 + algorithm: round_robin + location: fsn1 + state: present + +- name: Ensure the Load Balancer is absent (remove if needed) + hetzner.hcloud.load_balancer: + name: my-Load Balancer + state: absent +""" + +RETURN = """ +hcloud_load_balancer: + description: The Load Balancer instance + returned: Always + type: complex + contains: + id: + description: Numeric identifier of the Load Balancer + returned: always + type: int + sample: 1937415 + name: + description: Name of the Load Balancer + returned: always + type: str + sample: my-Load-Balancer + status: + description: Status of the Load Balancer + returned: always + type: str + sample: running + load_balancer_type: + description: Name of the Load Balancer type of the Load Balancer + returned: always + type: str + sample: cx11 + algorithm: + description: Algorithm of the Load Balancer. + returned: always + type: str + choices: [round_robin, least_connections] + sample: round_robin + ipv4_address: + description: Public IPv4 address of the Load Balancer + returned: always + type: str + sample: 116.203.104.109 + ipv6_address: + description: Public IPv6 address of the Load Balancer + returned: always + type: str + sample: 2a01:4f8:1c1c:c140::1 + location: + description: Name of the location of the Load Balancer + returned: always + type: str + sample: fsn1 + labels: + description: User-defined labels (key-value pairs) + returned: always + type: dict + delete_protection: + description: True if Load Balancer is protected for deletion + type: bool + returned: always + sample: false + disable_public_interface: + description: True if Load Balancer public interface is disabled + type: bool + returned: always + sample: false +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.load_balancers import ( + BoundLoadBalancer, + LoadBalancerAlgorithm, +) + + +class AnsibleHCloudLoadBalancer(AnsibleHCloud): + represent = "hcloud_load_balancer" + + hcloud_load_balancer: BoundLoadBalancer | None = None + + def _prepare_result(self): + private_ipv4_address = ( + None + if len(self.hcloud_load_balancer.private_net) == 0 + else to_native(self.hcloud_load_balancer.private_net[0].ip) + ) + return { + "id": to_native(self.hcloud_load_balancer.id), + "name": to_native(self.hcloud_load_balancer.name), + "ipv4_address": to_native(self.hcloud_load_balancer.public_net.ipv4.ip), + "ipv6_address": to_native(self.hcloud_load_balancer.public_net.ipv6.ip), + "private_ipv4_address": private_ipv4_address, + "load_balancer_type": to_native(self.hcloud_load_balancer.load_balancer_type.name), + "algorithm": to_native(self.hcloud_load_balancer.algorithm.type), + "location": to_native(self.hcloud_load_balancer.location.name), + "labels": self.hcloud_load_balancer.labels, + "delete_protection": self.hcloud_load_balancer.protection["delete"], + "disable_public_interface": False if self.hcloud_load_balancer.public_net.enabled else True, + } + + def _get_load_balancer(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_load_balancer = self.client.load_balancers.get_by_id(self.module.params.get("id")) + else: + self.hcloud_load_balancer = self.client.load_balancers.get_by_name(self.module.params.get("name")) + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _create_load_balancer(self): + self.module.fail_on_missing_params(required_params=["name", "load_balancer_type"]) + try: + params = { + "name": self.module.params.get("name"), + "algorithm": LoadBalancerAlgorithm(type=self.module.params.get("algorithm", "round_robin")), + "load_balancer_type": self.client.load_balancer_types.get_by_name( + self.module.params.get("load_balancer_type") + ), + "labels": self.module.params.get("labels"), + } + + if self.module.params.get("location") is None and self.module.params.get("network_zone") is None: + self.module.fail_json(msg="one of the following is required: location, network_zone") + elif self.module.params.get("location") is not None and self.module.params.get("network_zone") is None: + params["location"] = self.client.locations.get_by_name(self.module.params.get("location")) + elif self.module.params.get("location") is None and self.module.params.get("network_zone") is not None: + params["network_zone"] = self.module.params.get("network_zone") + + if not self.module.check_mode: + resp = self.client.load_balancers.create(**params) + resp.action.wait_until_finished(max_retries=1000) + + delete_protection = self.module.params.get("delete_protection") + if delete_protection is not None: + self._get_load_balancer() + self.hcloud_load_balancer.change_protection(delete=delete_protection).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self._get_load_balancer() + + def _update_load_balancer(self): + try: + labels = self.module.params.get("labels") + if labels is not None and labels != self.hcloud_load_balancer.labels: + if not self.module.check_mode: + self.hcloud_load_balancer.update(labels=labels) + self._mark_as_changed() + + delete_protection = self.module.params.get("delete_protection") + if delete_protection is not None and delete_protection != self.hcloud_load_balancer.protection["delete"]: + if not self.module.check_mode: + self.hcloud_load_balancer.change_protection(delete=delete_protection).wait_until_finished() + self._mark_as_changed() + self._get_load_balancer() + + disable_public_interface = self.module.params.get("disable_public_interface") + if disable_public_interface is not None and disable_public_interface != ( + not self.hcloud_load_balancer.public_net.enabled + ): + if not self.module.check_mode: + if disable_public_interface is True: + self.hcloud_load_balancer.disable_public_interface().wait_until_finished() + else: + self.hcloud_load_balancer.enable_public_interface().wait_until_finished() + self._mark_as_changed() + + load_balancer_type = self.module.params.get("load_balancer_type") + if ( + load_balancer_type is not None + and self.hcloud_load_balancer.load_balancer_type.name != load_balancer_type + ): + new_load_balancer_type = self.client.load_balancer_types.get_by_name(load_balancer_type) + if not new_load_balancer_type: + self.module.fail_json(msg="unknown load balancer type") + if not self.module.check_mode: + self.hcloud_load_balancer.change_type( + load_balancer_type=new_load_balancer_type, + ).wait_until_finished(max_retries=1000) + + self._mark_as_changed() + + algorithm = self.module.params.get("algorithm") + if algorithm is not None and self.hcloud_load_balancer.algorithm.type != algorithm: + self.hcloud_load_balancer.change_algorithm( + algorithm=LoadBalancerAlgorithm(type=algorithm) + ).wait_until_finished() + self._mark_as_changed() + + self._get_load_balancer() + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def present_load_balancer(self): + self._get_load_balancer() + if self.hcloud_load_balancer is None: + self._create_load_balancer() + else: + self._update_load_balancer() + + def delete_load_balancer(self): + try: + self._get_load_balancer() + if self.hcloud_load_balancer is not None: + if not self.module.check_mode: + self.client.load_balancers.delete(self.hcloud_load_balancer) + self._mark_as_changed() + self.hcloud_load_balancer = None + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + load_balancer_type={"type": "str"}, + algorithm={"choices": ["round_robin", "least_connections"], "default": "round_robin"}, + location={"type": "str"}, + network_zone={"type": "str"}, + labels={"type": "dict"}, + delete_protection={"type": "bool"}, + disable_public_interface={"type": "bool", "default": False}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + required_one_of=[["id", "name"]], + mutually_exclusive=[["location", "network_zone"]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudLoadBalancer.define_module() + + hcloud = AnsibleHCloudLoadBalancer(module) + state = module.params.get("state") + if state == "absent": + hcloud.delete_load_balancer() + elif state == "present": + hcloud.present_load_balancer() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_info.py new file mode 100644 index 000000000..19ead98c2 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_info.py @@ -0,0 +1,424 @@ +#!/usr/bin/python + +# Copyright: (c) 2020, 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 annotations + +DOCUMENTATION = """ +--- +module: load_balancer_info + +short_description: Gather infos about your Hetzner Cloud Load Balancers. + + +description: + - Gather infos about your Hetzner Cloud Load Balancers.. + +author: + - Lukas Kaemmerling (@LKaemmerling) + +options: + id: + description: + - The ID of the Load Balancers you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name of the Load Balancers you want to get. + type: str + label_selector: + description: + - The label selector for the Load Balancers you want to get. + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Gather hcloud load_balancer infos + hetzner.hcloud.load_balancer_info: + register: output + +- name: Print the gathered infos + debug: + var: output +""" + +RETURN = """ +hcloud_load_balancer_info: + description: The load_balancer infos as list + returned: always + type: complex + contains: + id: + description: Numeric identifier of the Load Balancer + returned: always + type: int + sample: 1937415 + name: + description: Name of the Load Balancer + returned: always + type: str + sample: my-Load-Balancer + status: + description: Status of the Load Balancer + returned: always + type: str + sample: running + load_balancer_type: + description: Name of the Load Balancer type of the Load Balancer + returned: always + type: str + sample: cx11 + ipv4_address: + description: Public IPv4 address of the Load Balancer + returned: always + type: str + sample: 116.203.104.109 + ipv6_address: + description: Public IPv6 address of the Load Balancer + returned: always + type: str + sample: 2a01:4f8:1c1c:c140::1 + location: + description: Name of the location of the Load Balancer + returned: always + type: str + sample: fsn1 + labels: + description: User-defined labels (key-value pairs) + returned: always + type: dict + delete_protection: + description: True if Load Balancer is protected for deletion + type: bool + returned: always + sample: false + disable_public_interface: + description: True if Load Balancer public interface is disabled + type: bool + returned: always + sample: false + targets: + description: The targets of the Load Balancer + returned: always + type: complex + contains: + type: + description: Type of the Load Balancer Target + type: str + returned: always + sample: server + load_balancer: + description: Name of the Load Balancer + type: str + returned: always + sample: my-LoadBalancer + server: + description: Name of the Server + type: str + returned: if I(type) is server + sample: my-server + label_selector: + description: Label Selector + type: str + returned: if I(type) is label_selector + sample: application=backend + ip: + description: IP of the dedicated server + type: str + returned: if I(type) is ip + sample: 127.0.0.1 + use_private_ip: + description: + - Route the traffic over the private IP of the Load Balancer through a Hetzner Cloud Network. + - Load Balancer needs to be attached to a network. See M(hetzner.hcloud.load_balancer_network) + type: bool + sample: true + returned: always + health_status: + description: + - List of health statuses of the services on this target. Only present for target types "server" and "ip". + type: list + returned: if I(type) is server or ip + contains: + listen_port: + description: Load Balancer Target listen port + type: int + returned: always + sample: 80 + status: + description: Load Balancer Target status + type: str + choices: [healthy, unhealthy, unknown] + returned: always + sample: healthy + services: + description: all services from this Load Balancer + returned: Always + type: complex + contains: + listen_port: + description: The port the service listens on, i.e. the port users can connect to. + returned: always + type: int + sample: 443 + protocol: + description: Protocol of the service + returned: always + type: str + sample: http + destination_port: + description: + - The port traffic is forwarded to, i.e. the port the targets are listening and accepting connections on. + returned: always + type: int + sample: 80 + proxyprotocol: + description: + - Enable the PROXY protocol. + returned: always + type: bool + sample: false + http: + description: Configuration for HTTP and HTTPS services + returned: always + type: complex + contains: + cookie_name: + description: Name of the cookie which will be set when you enable sticky sessions + returned: always + type: str + sample: HCLBSTICKY + cookie_lifetime: + description: Lifetime of the cookie which will be set when you enable sticky sessions, in seconds + returned: always + type: int + sample: 3600 + certificates: + description: List of Names or IDs of certificates + returned: always + type: list + elements: str + sticky_sessions: + description: Enable or disable sticky_sessions + returned: always + type: bool + sample: true + redirect_http: + description: Redirect Traffic from Port 80 to Port 443, only available if protocol is https + returned: always + type: bool + sample: false + health_check: + description: Configuration for health checks + returned: always + type: complex + contains: + protocol: + description: Protocol the health checks will be performed over + returned: always + type: str + sample: http + port: + description: Port the health check will be performed on + returned: always + type: int + sample: 80 + interval: + description: Interval of health checks, in seconds + returned: always + type: int + sample: 15 + timeout: + description: Timeout of health checks, in seconds + returned: always + type: int + sample: 10 + retries: + description: Number of retries until a target is marked as unhealthy + returned: always + type: int + sample: 3 + http: + description: Additional Configuration of health checks with protocol http/https + returned: always + type: complex + contains: + domain: + description: Domain we will set within the HTTP HOST header + returned: always + type: str + sample: example.com + path: + description: Path we will try to access + returned: always + type: str + sample: / + response: + description: Response we expect, if response is not within the health check response the target is unhealthy + returned: always + type: str + status_codes: + description: List of HTTP status codes we expect to get when we perform the health check. + returned: always + type: list + elements: str + sample: ["2??","3??"] + tls: + description: Verify the TLS certificate, only available if health check protocol is https + returned: always + type: bool + sample: false +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.load_balancers import BoundLoadBalancer + + +class AnsibleHCloudLoadBalancerInfo(AnsibleHCloud): + represent = "hcloud_load_balancer_info" + + hcloud_load_balancer_info: list[BoundLoadBalancer] | None = None + + def _prepare_result(self): + tmp = [] + + for load_balancer in self.hcloud_load_balancer_info: + if load_balancer is not None: + services = [self._prepare_service_result(service) for service in load_balancer.services] + targets = [self._prepare_target_result(target) for target in load_balancer.targets] + + private_ipv4_address = ( + None if len(load_balancer.private_net) == 0 else to_native(load_balancer.private_net[0].ip) + ) + tmp.append( + { + "id": to_native(load_balancer.id), + "name": to_native(load_balancer.name), + "ipv4_address": to_native(load_balancer.public_net.ipv4.ip), + "ipv6_address": to_native(load_balancer.public_net.ipv6.ip), + "private_ipv4_address": private_ipv4_address, + "load_balancer_type": to_native(load_balancer.load_balancer_type.name), + "location": to_native(load_balancer.location.name), + "labels": load_balancer.labels, + "delete_protection": load_balancer.protection["delete"], + "disable_public_interface": False if load_balancer.public_net.enabled else True, + "targets": targets, + "services": services, + } + ) + return tmp + + @staticmethod + def _prepare_service_result(service): + http = None + if service.protocol != "tcp": + http = { + "cookie_name": to_native(service.http.cookie_name), + "cookie_lifetime": service.http.cookie_name, + "redirect_http": service.http.redirect_http, + "sticky_sessions": service.http.sticky_sessions, + "certificates": [to_native(certificate.name) for certificate in service.http.certificates], + } + health_check = { + "protocol": to_native(service.health_check.protocol), + "port": service.health_check.port, + "interval": service.health_check.interval, + "timeout": service.health_check.timeout, + "retries": service.health_check.retries, + } + if service.health_check.protocol != "tcp": + health_check["http"] = { + "domain": to_native(service.health_check.http.domain), + "path": to_native(service.health_check.http.path), + "response": to_native(service.health_check.http.response), + "certificates": [to_native(status_code) for status_code in service.health_check.http.status_codes], + "tls": service.health_check.http.tls, + } + return { + "protocol": to_native(service.protocol), + "listen_port": service.listen_port, + "destination_port": service.destination_port, + "proxyprotocol": service.proxyprotocol, + "http": http, + "health_check": health_check, + } + + @staticmethod + def _prepare_target_result(target): + result = { + "type": to_native(target.type), + "use_private_ip": target.use_private_ip, + } + if target.type == "server": + result["server"] = to_native(target.server.name) + elif target.type == "label_selector": + result["label_selector"] = to_native(target.label_selector.selector) + elif target.type == "ip": + result["ip"] = to_native(target.ip.ip) + + if target.health_status is not None: + result["health_status"] = [ + { + "listen_port": item.listen_port, + "status": item.status, + } + for item in target.health_status + ] + + return result + + def get_load_balancers(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_load_balancer_info = [self.client.load_balancers.get_by_id(self.module.params.get("id"))] + elif self.module.params.get("name") is not None: + self.hcloud_load_balancer_info = [ + self.client.load_balancers.get_by_name(self.module.params.get("name")) + ] + else: + params = {} + label_selector = self.module.params.get("label_selector") + if label_selector: + params["label_selector"] = label_selector + + self.hcloud_load_balancer_info = self.client.load_balancers.get_all(**params) + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + label_selector={"type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudLoadBalancerInfo.define_module() + hcloud = AnsibleHCloudLoadBalancerInfo(module) + + hcloud.get_load_balancers() + result = hcloud.get_result() + + ansible_info = {"hcloud_load_balancer_info": result["hcloud_load_balancer_info"]} + module.exit_json(**ansible_info) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_network.py b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_network.py new file mode 100644 index 000000000..4560f8735 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_network.py @@ -0,0 +1,203 @@ +#!/usr/bin/python + +# Copyright: (c) 2020, 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 annotations + +DOCUMENTATION = """ +--- +module: load_balancer_network + +short_description: Manage the relationship between Hetzner Cloud Networks and Load Balancers + + +description: + - Create and delete the relationship Hetzner Cloud Networks and Load Balancers + +author: + - Lukas Kaemmerling (@lkaemmerling) +version_added: 0.1.0 +options: + network: + description: + - Name or ID of the Hetzner Cloud Networks. + type: str + required: true + load_balancer: + description: + - Name or ID of the Hetzner Cloud Load Balancer. + type: str + required: true + ip: + description: + - The IP the Load Balancer should have. + type: str + state: + description: + - State of the load_balancer_network. + default: present + choices: [ absent, present ] + type: str + +extends_documentation_fragment: +- hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a basic Load Balancer network + hetzner.hcloud.load_balancer_network: + network: my-network + load_balancer: my-LoadBalancer + state: present + +- name: Create a Load Balancer network and specify the ip address + hetzner.hcloud.load_balancer_network: + network: my-network + load_balancer: my-LoadBalancer + ip: 10.0.0.1 + state: present + +- name: Ensure the Load Balancer network is absent (remove if needed) + hetzner.hcloud.load_balancer_network: + network: my-network + load_balancer: my-LoadBalancer + state: absent +""" + +RETURN = """ +hcloud_load_balancer_network: + description: The relationship between a Load Balancer and a network + returned: always + type: complex + contains: + network: + description: Name of the Network + type: str + returned: always + sample: my-network + load_balancer: + description: Name of the Load Balancer + type: str + returned: always + sample: my-LoadBalancer + ip: + description: IP of the Load Balancer within the Network ip range + type: str + returned: always + sample: 10.0.0.8 +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.load_balancers import BoundLoadBalancer, PrivateNet +from ..module_utils.vendor.hcloud.networks import BoundNetwork + + +class AnsibleHCloudLoadBalancerNetwork(AnsibleHCloud): + represent = "hcloud_load_balancer_network" + + hcloud_network: BoundNetwork | None = None + hcloud_load_balancer: BoundLoadBalancer | None = None + hcloud_load_balancer_network: PrivateNet | None = None + + def _prepare_result(self): + return { + "network": to_native(self.hcloud_network.name), + "load_balancer": to_native(self.hcloud_load_balancer.name), + "ip": to_native(self.hcloud_load_balancer_network.ip), + } + + def _get_load_balancer_and_network(self): + try: + self.hcloud_network = self._client_get_by_name_or_id( + "networks", + self.module.params.get("network"), + ) + self.hcloud_load_balancer = self._client_get_by_name_or_id( + "load_balancers", + self.module.params.get("load_balancer"), + ) + + self.hcloud_load_balancer_network = None + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _get_load_balancer_network(self): + for private_net in self.hcloud_load_balancer.private_net: + if private_net.network.id == self.hcloud_network.id: + self.hcloud_load_balancer_network = private_net + + def _create_load_balancer_network(self): + params = {"network": self.hcloud_network} + + if self.module.params.get("ip") is not None: + params["ip"] = self.module.params.get("ip") + + if not self.module.check_mode: + try: + self.hcloud_load_balancer.attach_to_network(**params).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + + self._mark_as_changed() + self._get_load_balancer_and_network() + self._get_load_balancer_network() + + def present_load_balancer_network(self): + self._get_load_balancer_and_network() + self._get_load_balancer_network() + if self.hcloud_load_balancer_network is None: + self._create_load_balancer_network() + + def delete_load_balancer_network(self): + self._get_load_balancer_and_network() + self._get_load_balancer_network() + if self.hcloud_load_balancer_network is not None and self.hcloud_load_balancer is not None: + if not self.module.check_mode: + try: + self.hcloud_load_balancer.detach_from_network( + self.hcloud_load_balancer_network.network + ).wait_until_finished() + self._mark_as_changed() + except HCloudException as exception: + self.fail_json_hcloud(exception) + + self.hcloud_load_balancer_network = None + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + network={"type": "str", "required": True}, + load_balancer={"type": "str", "required": True}, + ip={"type": "str"}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudLoadBalancerNetwork.define_module() + + hcloud = AnsibleHCloudLoadBalancerNetwork(module) + state = module.params["state"] + if state == "absent": + hcloud.delete_load_balancer_network() + elif state == "present": + hcloud.present_load_balancer_network() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_service.py b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_service.py new file mode 100644 index 000000000..1fc18deef --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_service.py @@ -0,0 +1,578 @@ +#!/usr/bin/python + +# Copyright: (c) 2020, 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 annotations + +DOCUMENTATION = """ +--- +module: load_balancer_service + +short_description: Create and manage the services of cloud Load Balancers on the Hetzner Cloud. + + +description: + - Create, update and manage the services of cloud Load Balancers on the Hetzner Cloud. + +author: + - Lukas Kaemmerling (@LKaemmerling) +version_added: 0.1.0 +options: + load_balancer: + description: + - Name or ID of the Hetzner Cloud Load Balancer the service belongs to + type: str + required: true + listen_port: + description: + - The port the service listens on, i.e. the port users can connect to. + type: int + required: true + destination_port: + description: + - The port traffic is forwarded to, i.e. the port the targets are listening and accepting connections on. + - Required if services does not exist and protocol is tcp. + type: int + protocol: + description: + - Protocol of the service. + - Required if Load Balancer does not exist. + type: str + choices: [ http, https, tcp ] + proxyprotocol: + description: + - Enable the PROXY protocol. + type: bool + default: False + http: + description: + - Configuration for HTTP and HTTPS services + type: dict + suboptions: + cookie_name: + description: + - Name of the cookie which will be set when you enable sticky sessions + type: str + cookie_lifetime: + description: + - Lifetime of the cookie which will be set when you enable sticky sessions, in seconds + type: int + certificates: + description: + - List of Names or IDs of certificates + type: list + elements: str + sticky_sessions: + description: + - Enable or disable sticky_sessions + type: bool + default: False + redirect_http: + description: + - Redirect Traffic from Port 80 to Port 443, only available if protocol is https + type: bool + default: False + health_check: + description: + - Configuration for health checks + type: dict + suboptions: + protocol: + description: + - Protocol the health checks will be performed over + type: str + choices: [ http, https, tcp ] + port: + description: + - Port the health check will be performed on + type: int + interval: + description: + - Interval of health checks, in seconds + type: int + timeout: + description: + - Timeout of health checks, in seconds + type: int + retries: + description: + - Number of retries until a target is marked as unhealthy + type: int + http: + description: + - Additional Configuration of health checks with protocol http/https + type: dict + suboptions: + domain: + description: + - Domain we will set within the HTTP HOST header + type: str + path: + description: + - Path we will try to access + type: str + response: + description: + - Response we expect, if response is not within the health check response the target is unhealthy + type: str + status_codes: + description: + - List of HTTP status codes we expect to get when we perform the health check. + type: list + elements: str + tls: + description: + - Verify the TLS certificate, only available if health check protocol is https + type: bool + default: False + state: + description: + - State of the Load Balancer. + default: present + choices: [ absent, present ] + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a basic Load Balancer service with Port 80 + hetzner.hcloud.load_balancer_service: + load_balancer: my-load-balancer + protocol: http + listen_port: 80 + state: present + +- name: Ensure the Load Balancer is absent (remove if needed) + hetzner.hcloud.load_balancer_service: + load_balancer: my-Load Balancer + protocol: http + listen_port: 80 + state: absent +""" + +RETURN = """ +hcloud_load_balancer_service: + description: The Load Balancer service instance + returned: Always + type: complex + contains: + load_balancer: + description: The name of the Load Balancer where the service belongs to + returned: always + type: str + sample: my-load-balancer + listen_port: + description: The port the service listens on, i.e. the port users can connect to. + returned: always + type: int + sample: 443 + protocol: + description: Protocol of the service + returned: always + type: str + sample: http + destination_port: + description: + - The port traffic is forwarded to, i.e. the port the targets are listening and accepting connections on. + returned: always + type: int + sample: 80 + proxyprotocol: + description: + - Enable the PROXY protocol. + returned: always + type: bool + sample: false + http: + description: Configuration for HTTP and HTTPS services + returned: always + type: complex + contains: + cookie_name: + description: Name of the cookie which will be set when you enable sticky sessions + returned: always + type: str + sample: HCLBSTICKY + cookie_lifetime: + description: Lifetime of the cookie which will be set when you enable sticky sessions, in seconds + returned: always + type: int + sample: 3600 + certificates: + description: List of Names or IDs of certificates + returned: always + type: list + elements: str + sticky_sessions: + description: Enable or disable sticky_sessions + returned: always + type: bool + sample: true + redirect_http: + description: Redirect Traffic from Port 80 to Port 443, only available if protocol is https + returned: always + type: bool + sample: false + health_check: + description: Configuration for health checks + returned: always + type: complex + contains: + protocol: + description: Protocol the health checks will be performed over + returned: always + type: str + sample: http + port: + description: Port the health check will be performed on + returned: always + type: int + sample: 80 + interval: + description: Interval of health checks, in seconds + returned: always + type: int + sample: 15 + timeout: + description: Timeout of health checks, in seconds + returned: always + type: int + sample: 10 + retries: + description: Number of retries until a target is marked as unhealthy + returned: always + type: int + sample: 3 + http: + description: Additional Configuration of health checks with protocol http/https + returned: always + type: complex + contains: + domain: + description: Domain we will set within the HTTP HOST header + returned: always + type: str + sample: example.com + path: + description: Path we will try to access + returned: always + type: str + sample: / + response: + description: Response we expect, if response is not within the health check response the target is unhealthy + returned: always + type: str + status_codes: + description: List of HTTP status codes we expect to get when we perform the health check. + returned: always + type: list + elements: str + sample: ["2??","3??"] + tls: + description: Verify the TLS certificate, only available if health check protocol is https + returned: always + type: bool + sample: false +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import APIException, HCloudException +from ..module_utils.vendor.hcloud.load_balancers import ( + BoundLoadBalancer, + LoadBalancerHealtCheckHttp, + LoadBalancerHealthCheck, + LoadBalancerService, + LoadBalancerServiceHttp, +) + + +class AnsibleHCloudLoadBalancerService(AnsibleHCloud): + represent = "hcloud_load_balancer_service" + + hcloud_load_balancer: BoundLoadBalancer | None = None + hcloud_load_balancer_service: LoadBalancerService | None = None + + def _prepare_result(self): + http = None + if self.hcloud_load_balancer_service.protocol != "tcp": + http = { + "cookie_name": to_native(self.hcloud_load_balancer_service.http.cookie_name), + "cookie_lifetime": self.hcloud_load_balancer_service.http.cookie_name, + "redirect_http": self.hcloud_load_balancer_service.http.redirect_http, + "sticky_sessions": self.hcloud_load_balancer_service.http.sticky_sessions, + "certificates": [ + to_native(certificate.name) for certificate in self.hcloud_load_balancer_service.http.certificates + ], + } + health_check = { + "protocol": to_native(self.hcloud_load_balancer_service.health_check.protocol), + "port": self.hcloud_load_balancer_service.health_check.port, + "interval": self.hcloud_load_balancer_service.health_check.interval, + "timeout": self.hcloud_load_balancer_service.health_check.timeout, + "retries": self.hcloud_load_balancer_service.health_check.retries, + } + if self.hcloud_load_balancer_service.health_check.protocol != "tcp": + health_check["http"] = { + "domain": to_native(self.hcloud_load_balancer_service.health_check.http.domain), + "path": to_native(self.hcloud_load_balancer_service.health_check.http.path), + "response": to_native(self.hcloud_load_balancer_service.health_check.http.response), + "status_codes": [ + to_native(status_code) + for status_code in self.hcloud_load_balancer_service.health_check.http.status_codes + ], + "tls": self.hcloud_load_balancer_service.health_check.http.tls, + } + return { + "load_balancer": to_native(self.hcloud_load_balancer.name), + "protocol": to_native(self.hcloud_load_balancer_service.protocol), + "listen_port": self.hcloud_load_balancer_service.listen_port, + "destination_port": self.hcloud_load_balancer_service.destination_port, + "proxyprotocol": self.hcloud_load_balancer_service.proxyprotocol, + "http": http, + "health_check": health_check, + } + + def _get_load_balancer(self): + try: + self.hcloud_load_balancer = self._client_get_by_name_or_id( + "load_balancers", + self.module.params.get("load_balancer"), + ) + self._get_load_balancer_service() + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _create_load_balancer_service(self): + self.module.fail_on_missing_params(required_params=["protocol"]) + if self.module.params.get("protocol") == "tcp": + self.module.fail_on_missing_params(required_params=["destination_port"]) + + params = { + "protocol": self.module.params.get("protocol"), + "listen_port": self.module.params.get("listen_port"), + "proxyprotocol": self.module.params.get("proxyprotocol"), + } + + if self.module.params.get("destination_port"): + params["destination_port"] = self.module.params.get("destination_port") + + if self.module.params.get("http"): + params["http"] = self.__get_service_http(http_arg=self.module.params.get("http")) + + if self.module.params.get("health_check"): + params["health_check"] = self.__get_service_health_checks( + health_check=self.module.params.get("health_check") + ) + + if not self.module.check_mode: + try: + self.hcloud_load_balancer.add_service(LoadBalancerService(**params)).wait_until_finished( + max_retries=1000 + ) + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self._get_load_balancer() + self._get_load_balancer_service() + + def __get_service_http(self, http_arg): + service_http = LoadBalancerServiceHttp(certificates=[]) + if http_arg.get("cookie_name") is not None: + service_http.cookie_name = http_arg.get("cookie_name") + if http_arg.get("cookie_lifetime") is not None: + service_http.cookie_lifetime = http_arg.get("cookie_lifetime") + if http_arg.get("sticky_sessions") is not None: + service_http.sticky_sessions = http_arg.get("sticky_sessions") + if http_arg.get("redirect_http") is not None: + service_http.redirect_http = http_arg.get("redirect_http") + if http_arg.get("certificates") is not None: + certificates = http_arg.get("certificates") + if certificates is not None: + for certificate in certificates: + hcloud_cert = None + try: + try: + hcloud_cert = self.client.certificates.get_by_name(certificate) + except Exception: + hcloud_cert = self.client.certificates.get_by_id(certificate) + except HCloudException as exception: + self.fail_json_hcloud(exception) + service_http.certificates.append(hcloud_cert) + + return service_http + + def __get_service_health_checks(self, health_check): + service_health_check = LoadBalancerHealthCheck() + if health_check.get("protocol") is not None: + service_health_check.protocol = health_check.get("protocol") + if health_check.get("port") is not None: + service_health_check.port = health_check.get("port") + if health_check.get("interval") is not None: + service_health_check.interval = health_check.get("interval") + if health_check.get("timeout") is not None: + service_health_check.timeout = health_check.get("timeout") + if health_check.get("retries") is not None: + service_health_check.retries = health_check.get("retries") + if health_check.get("http") is not None: + health_check_http = health_check.get("http") + service_health_check.http = LoadBalancerHealtCheckHttp() + if health_check_http.get("domain") is not None: + service_health_check.http.domain = health_check_http.get("domain") + if health_check_http.get("path") is not None: + service_health_check.http.path = health_check_http.get("path") + if health_check_http.get("response") is not None: + service_health_check.http.response = health_check_http.get("response") + if health_check_http.get("status_codes") is not None: + service_health_check.http.status_codes = health_check_http.get("status_codes") + if health_check_http.get("tls") is not None: + service_health_check.http.tls = health_check_http.get("tls") + + return service_health_check + + def _update_load_balancer_service(self): + changed = False + try: + params = { + "listen_port": self.module.params.get("listen_port"), + } + + if self.module.params.get("destination_port") is not None: + if self.hcloud_load_balancer_service.destination_port != self.module.params.get("destination_port"): + params["destination_port"] = self.module.params.get("destination_port") + changed = True + + if self.module.params.get("protocol") is not None: + if self.hcloud_load_balancer_service.protocol != self.module.params.get("protocol"): + params["protocol"] = self.module.params.get("protocol") + changed = True + + if self.module.params.get("proxyprotocol") is not None: + if self.hcloud_load_balancer_service.proxyprotocol != self.module.params.get("proxyprotocol"): + params["proxyprotocol"] = self.module.params.get("proxyprotocol") + changed = True + + if self.module.params.get("http") is not None: + params["http"] = self.__get_service_http(http_arg=self.module.params.get("http")) + changed = True + + if self.module.params.get("health_check") is not None: + params["health_check"] = self.__get_service_health_checks( + health_check=self.module.params.get("health_check") + ) + changed = True + + if not self.module.check_mode: + self.hcloud_load_balancer.update_service(LoadBalancerService(**params)).wait_until_finished( + max_retries=1000 + ) + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._get_load_balancer() + + if changed: + self._mark_as_changed() + + def _get_load_balancer_service(self): + for service in self.hcloud_load_balancer.services: + if self.module.params.get("listen_port") == service.listen_port: + self.hcloud_load_balancer_service = service + + def present_load_balancer_service(self): + self._get_load_balancer() + if self.hcloud_load_balancer_service is None: + self._create_load_balancer_service() + else: + self._update_load_balancer_service() + + def delete_load_balancer_service(self): + try: + self._get_load_balancer() + if self.hcloud_load_balancer_service is not None: + if not self.module.check_mode: + try: + self.hcloud_load_balancer.delete_service(self.hcloud_load_balancer_service).wait_until_finished( + max_retries=1000 + ) + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self.hcloud_load_balancer_service = None + except APIException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + load_balancer={"type": "str", "required": True}, + listen_port={"type": "int", "required": True}, + destination_port={"type": "int"}, + protocol={ + "type": "str", + "choices": ["http", "https", "tcp"], + }, + proxyprotocol={"type": "bool", "default": False}, + http={ + "type": "dict", + "options": dict( + cookie_name={"type": "str"}, + cookie_lifetime={"type": "int"}, + sticky_sessions={"type": "bool", "default": False}, + redirect_http={"type": "bool", "default": False}, + certificates={"type": "list", "elements": "str"}, + ), + }, + health_check={ + "type": "dict", + "options": dict( + protocol={ + "type": "str", + "choices": ["http", "https", "tcp"], + }, + port={"type": "int"}, + interval={"type": "int"}, + timeout={"type": "int"}, + retries={"type": "int"}, + http={ + "type": "dict", + "options": dict( + domain={"type": "str"}, + path={"type": "str"}, + response={"type": "str"}, + status_codes={"type": "list", "elements": "str"}, + tls={"type": "bool", "default": False}, + ), + }, + ), + }, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudLoadBalancerService.define_module() + + hcloud = AnsibleHCloudLoadBalancerService(module) + state = module.params.get("state") + if state == "absent": + hcloud.delete_load_balancer_service() + elif state == "present": + hcloud.present_load_balancer_service() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_target.py b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_target.py new file mode 100644 index 000000000..36e7f608f --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_target.py @@ -0,0 +1,313 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: load_balancer_target + +short_description: Manage Hetzner Cloud Load Balancer targets + + +description: + - Create and delete Hetzner Cloud Load Balancer targets + +author: + - Lukas Kaemmerling (@lkaemmerling) +version_added: 0.1.0 +options: + type: + description: + - The type of the target. + type: str + choices: [ server, label_selector, ip ] + required: true + load_balancer: + description: + - Name or ID of the Hetzner Cloud Load Balancer. + type: str + required: true + server: + description: + - Name or ID of the Hetzner Cloud Server. + - Required if I(type) is server + type: str + label_selector: + description: + - A Label Selector that will be used to determine the targets dynamically + - Required if I(type) is label_selector + type: str + ip: + description: + - An IP from a Hetzner Dedicated Server, needs to belongs to the same user as the project. + - Required if I(type) is ip + type: str + use_private_ip: + description: + - Route the traffic over the private IP of the Load Balancer through a Hetzner Cloud Network. + - Load Balancer needs to be attached to a network. See M(hetzner.hcloud.load_balancer_network) + type: bool + default: False + state: + description: + - State of the load_balancer_network. + default: present + choices: [ absent, present ] + type: str + +extends_documentation_fragment: +- hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a server Load Balancer target + hetzner.hcloud.load_balancer_target: + type: server + load_balancer: my-LoadBalancer + server: my-server + state: present + +- name: Create a label_selector Load Balancer target + hetzner.hcloud.load_balancer_target: + type: label_selector + load_balancer: my-LoadBalancer + label_selector: application=backend + state: present + +- name: Create an IP Load Balancer target + hetzner.hcloud.load_balancer_target: + type: ip + load_balancer: my-LoadBalancer + ip: 127.0.0.1 + state: present + +- name: Ensure the Load Balancer target is absent (remove if needed) + hetzner.hcloud.load_balancer_target: + type: server + load_balancer: my-LoadBalancer + server: my-server + state: absent +""" + +RETURN = """ +hcloud_load_balancer_target: + description: The relationship between a Load Balancer and a network + returned: always + type: complex + contains: + type: + description: Type of the Load Balancer Target + type: str + returned: always + sample: server + load_balancer: + description: Name of the Load Balancer + type: str + returned: always + sample: my-LoadBalancer + server: + description: Name of the Server + type: str + returned: if I(type) is server + sample: my-server + label_selector: + description: Label Selector + type: str + returned: if I(type) is label_selector + sample: application=backend + ip: + description: IP of the dedicated server + type: str + returned: if I(type) is ip + sample: 127.0.0.1 + use_private_ip: + description: + - Route the traffic over the private IP of the Load Balancer through a Hetzner Cloud Network. + - Load Balancer needs to be attached to a network. See M(hetzner.hcloud.load_balancer_network) + type: bool + sample: true + returned: always +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import APIException, HCloudException +from ..module_utils.vendor.hcloud.load_balancers import ( + BoundLoadBalancer, + LoadBalancerTarget, + LoadBalancerTargetIP, + LoadBalancerTargetLabelSelector, +) +from ..module_utils.vendor.hcloud.servers import BoundServer + + +class AnsibleHCloudLoadBalancerTarget(AnsibleHCloud): + represent = "hcloud_load_balancer_target" + + hcloud_load_balancer: BoundLoadBalancer | None = None + hcloud_load_balancer_target: LoadBalancerTarget | None = None + hcloud_server: BoundServer | None = None + + def _prepare_result(self): + result = { + "type": to_native(self.hcloud_load_balancer_target.type), + "load_balancer": to_native(self.hcloud_load_balancer.name), + "use_private_ip": self.hcloud_load_balancer_target.use_private_ip, + } + + if self.hcloud_load_balancer_target.type == "server": + result["server"] = to_native(self.hcloud_load_balancer_target.server.name) + elif self.hcloud_load_balancer_target.type == "label_selector": + result["label_selector"] = to_native(self.hcloud_load_balancer_target.label_selector.selector) + elif self.hcloud_load_balancer_target.type == "ip": + result["ip"] = to_native(self.hcloud_load_balancer_target.ip.ip) + return result + + def _get_load_balancer_and_target(self): + try: + self.hcloud_load_balancer = self._client_get_by_name_or_id( + "load_balancers", + self.module.params.get("load_balancer"), + ) + + if self.module.params.get("type") == "server": + self.hcloud_server = self._client_get_by_name_or_id( + "servers", + self.module.params.get("server"), + ) + + self.hcloud_load_balancer_target = None + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _get_load_balancer_target(self): + for target in self.hcloud_load_balancer.targets: + if self.module.params.get("type") == "server" and target.type == "server": + if target.server.id == self.hcloud_server.id: + self.hcloud_load_balancer_target = target + elif self.module.params.get("type") == "label_selector" and target.type == "label_selector": + if target.label_selector.selector == self.module.params.get("label_selector"): + self.hcloud_load_balancer_target = target + elif self.module.params.get("type") == "ip" and target.type == "ip": + if target.ip.ip == self.module.params.get("ip"): + self.hcloud_load_balancer_target = target + + def _create_load_balancer_target(self): + params = {"target": None} + + if self.module.params.get("type") == "server": + self.module.fail_on_missing_params(required_params=["server"]) + params["target"] = LoadBalancerTarget( + type=self.module.params.get("type"), + server=self.hcloud_server, + use_private_ip=self.module.params.get("use_private_ip"), + ) + elif self.module.params.get("type") == "label_selector": + self.module.fail_on_missing_params(required_params=["label_selector"]) + params["target"] = LoadBalancerTarget( + type=self.module.params.get("type"), + label_selector=LoadBalancerTargetLabelSelector(selector=self.module.params.get("label_selector")), + use_private_ip=self.module.params.get("use_private_ip"), + ) + elif self.module.params.get("type") == "ip": + self.module.fail_on_missing_params(required_params=["ip"]) + params["target"] = LoadBalancerTarget( + type=self.module.params.get("type"), + ip=LoadBalancerTargetIP(ip=self.module.params.get("ip")), + use_private_ip=False, + ) + + if not self.module.check_mode: + try: + self.hcloud_load_balancer.add_target(**params).wait_until_finished() + except APIException as exception: + if exception.code == "locked" or exception.code == "conflict": + self._create_load_balancer_target() + else: + self.fail_json_hcloud(exception) + except HCloudException as exception: + self.fail_json_hcloud(exception) + + self._mark_as_changed() + self._get_load_balancer_and_target() + self._get_load_balancer_target() + + def present_load_balancer_target(self): + self._get_load_balancer_and_target() + self._get_load_balancer_target() + if self.hcloud_load_balancer_target is None: + self._create_load_balancer_target() + + def delete_load_balancer_target(self): + self._get_load_balancer_and_target() + self._get_load_balancer_target() + if self.hcloud_load_balancer_target is not None and self.hcloud_load_balancer is not None: + if not self.module.check_mode: + target = None + if self.module.params.get("type") == "server": + self.module.fail_on_missing_params(required_params=["server"]) + target = LoadBalancerTarget(type=self.module.params.get("type"), server=self.hcloud_server) + elif self.module.params.get("type") == "label_selector": + self.module.fail_on_missing_params(required_params=["label_selector"]) + target = LoadBalancerTarget( + type=self.module.params.get("type"), + label_selector=LoadBalancerTargetLabelSelector( + selector=self.module.params.get("label_selector") + ), + use_private_ip=self.module.params.get("use_private_ip"), + ) + elif self.module.params.get("type") == "ip": + self.module.fail_on_missing_params(required_params=["ip"]) + target = LoadBalancerTarget( + type=self.module.params.get("type"), + ip=LoadBalancerTargetIP(ip=self.module.params.get("ip")), + use_private_ip=False, + ) + try: + self.hcloud_load_balancer.remove_target(target).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self.hcloud_load_balancer_target = None + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + type={"type": "str", "required": True, "choices": ["server", "label_selector", "ip"]}, + load_balancer={"type": "str", "required": True}, + server={"type": "str"}, + label_selector={"type": "str"}, + ip={"type": "str"}, + use_private_ip={"type": "bool", "default": False}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudLoadBalancerTarget.define_module() + + hcloud = AnsibleHCloudLoadBalancerTarget(module) + state = module.params["state"] + if state == "absent": + hcloud.delete_load_balancer_target() + elif state == "present": + hcloud.present_load_balancer_target() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_type_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_type_info.py new file mode 100644 index 000000000..67feafd59 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_type_info.py @@ -0,0 +1,161 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: load_balancer_type_info + +short_description: Gather infos about the Hetzner Cloud Load Balancer types. + + +description: + - Gather infos about your Hetzner Cloud Load Balancer types. + +author: + - Lukas Kaemmerling (@LKaemmerling) +version_added: 0.1.0 +options: + id: + description: + - The ID of the Load Balancer type you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name of the Load Balancer type you want to get. + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Gather hcloud Load Balancer type infos + hetzner.hcloud.load_balancer_type_info: + register: output + +- name: Print the gathered infos + debug: + var: output.hcloud_load_balancer_type_info +""" + +RETURN = """ +hcloud_load_balancer_type_info: + description: The Load Balancer type infos as list + returned: always + type: complex + contains: + id: + description: Numeric identifier of the Load Balancer type + returned: always + type: int + sample: 1937415 + name: + description: Name of the Load Balancer type + returned: always + type: str + sample: lb11 + description: + description: Description of the Load Balancer type + returned: always + type: str + sample: LB11 + max_connections: + description: Number of maximum simultaneous open connections + returned: always + type: int + sample: 1 + max_services: + description: Number of services a Load Balancer of this type can have + returned: always + type: int + sample: 1 + max_targets: + description: Number of targets a single Load Balancer can have + returned: always + type: int + sample: 25 + max_assigned_certificates: + description: Number of SSL Certificates that can be assigned to a single Load Balancer + returned: always + type: int + sample: 5 +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.load_balancer_types import BoundLoadBalancerType + + +class AnsibleHCloudLoadBalancerTypeInfo(AnsibleHCloud): + represent = "hcloud_load_balancer_type_info" + + hcloud_load_balancer_type_info: list[BoundLoadBalancerType] | None = None + + def _prepare_result(self): + tmp = [] + + for load_balancer_type in self.hcloud_load_balancer_type_info: + if load_balancer_type is not None: + tmp.append( + { + "id": to_native(load_balancer_type.id), + "name": to_native(load_balancer_type.name), + "description": to_native(load_balancer_type.description), + "max_connections": load_balancer_type.max_connections, + "max_services": load_balancer_type.max_services, + "max_targets": load_balancer_type.max_targets, + "max_assigned_certificates": load_balancer_type.max_assigned_certificates, + } + ) + return tmp + + def get_load_balancer_types(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_load_balancer_type_info = [ + self.client.load_balancer_types.get_by_id(self.module.params.get("id")) + ] + elif self.module.params.get("name") is not None: + self.hcloud_load_balancer_type_info = [ + self.client.load_balancer_types.get_by_name(self.module.params.get("name")) + ] + else: + self.hcloud_load_balancer_type_info = self.client.load_balancer_types.get_all() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudLoadBalancerTypeInfo.define_module() + hcloud = AnsibleHCloudLoadBalancerTypeInfo(module) + + hcloud.get_load_balancer_types() + result = hcloud.get_result() + + ansible_info = {"hcloud_load_balancer_type_info": result["hcloud_load_balancer_type_info"]} + module.exit_json(**ansible_info) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/location_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/location_info.py new file mode 100644 index 000000000..ac495c6c8 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/location_info.py @@ -0,0 +1,145 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: location_info + +short_description: Gather infos about your Hetzner Cloud locations. + + +description: + - Gather infos about your Hetzner Cloud locations. + +author: + - Lukas Kaemmerling (@LKaemmerling) + +options: + id: + description: + - The ID of the location you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name of the location you want to get. + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Gather hcloud location infos + hetzner.hcloud.location_info: + register: output + +- name: Print the gathered infos + debug: + var: output +""" + +RETURN = """ +hcloud_location_info: + description: The location infos as list + returned: always + type: complex + contains: + id: + description: Numeric identifier of the location + returned: always + type: int + sample: 1937415 + name: + description: Name of the location + returned: always + type: str + sample: fsn1 + description: + description: Detail description of the location + returned: always + type: str + sample: Falkenstein DC Park 1 + country: + description: Country code of the location + returned: always + type: str + sample: DE + city: + description: City of the location + returned: always + type: str + sample: Falkenstein +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.locations import BoundLocation + + +class AnsibleHCloudLocationInfo(AnsibleHCloud): + represent = "hcloud_location_info" + + hcloud_location_info: list[BoundLocation] | None = None + + def _prepare_result(self): + tmp = [] + + for location in self.hcloud_location_info: + if location is not None: + tmp.append( + { + "id": to_native(location.id), + "name": to_native(location.name), + "description": to_native(location.description), + "city": to_native(location.city), + "country": to_native(location.country), + } + ) + return tmp + + def get_locations(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_location_info = [self.client.locations.get_by_id(self.module.params.get("id"))] + elif self.module.params.get("name") is not None: + self.hcloud_location_info = [self.client.locations.get_by_name(self.module.params.get("name"))] + else: + self.hcloud_location_info = self.client.locations.get_all() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudLocationInfo.define_module() + hcloud = AnsibleHCloudLocationInfo(module) + + hcloud.get_locations() + result = hcloud.get_result() + + ansible_info = {"hcloud_location_info": result["hcloud_location_info"]} + module.exit_json(**ansible_info) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/network.py b/ansible_collections/hetzner/hcloud/plugins/modules/network.py new file mode 100644 index 000000000..24e45a48d --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/network.py @@ -0,0 +1,259 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: network + +short_description: Create and manage cloud Networks on the Hetzner Cloud. + + +description: + - Create, update and manage cloud Networks on the Hetzner Cloud. + - You need at least hcloud-python 1.3.0. + +author: + - Lukas Kaemmerling (@lkaemmerling) + +options: + id: + description: + - The ID of the Hetzner Cloud Networks to manage. + - Only required if no Network I(name) is given. + type: int + name: + description: + - The Name of the Hetzner Cloud Network to manage. + - Only required if no Network I(id) is given or a Network does not exist. + type: str + ip_range: + description: + - IP range of the Network. + - Required if Network does not exist. + type: str + expose_routes_to_vswitch: + description: + - Indicates if the routes from this network should be exposed to the vSwitch connection. + - The exposing only takes effect if a vSwitch connection is active. + type: bool + labels: + description: + - User-defined labels (key-value pairs). + type: dict + delete_protection: + description: + - Protect the Network for deletion. + type: bool + state: + description: + - State of the Network. + default: present + choices: [ absent, present ] + type: str + +extends_documentation_fragment: +- hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a basic network + hetzner.hcloud.network: + name: my-network + ip_range: 10.0.0.0/8 + state: present + +- name: Ensure the Network is absent (remove if needed) + hetzner.hcloud.network: + name: my-network + state: absent +""" + +RETURN = """ +hcloud_network: + description: The Network + returned: always + type: complex + contains: + id: + description: ID of the Network + type: int + returned: always + sample: 12345 + name: + description: Name of the Network + type: str + returned: always + sample: my-volume + ip_range: + description: IP range of the Network + type: str + returned: always + sample: 10.0.0.0/8 + expose_routes_to_vswitch: + description: Indicates if the routes from this network should be exposed to the vSwitch connection. + type: bool + returned: always + sample: false + delete_protection: + description: True if Network is protected for deletion + type: bool + returned: always + sample: false + version_added: "0.1.0" + labels: + description: User-defined labels (key-value pairs) + type: dict + returned: always + sample: + key: value + mylabel: 123 +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.networks import BoundNetwork + + +class AnsibleHCloudNetwork(AnsibleHCloud): + represent = "hcloud_network" + + hcloud_network: BoundNetwork | None = None + + def _prepare_result(self): + return { + "id": to_native(self.hcloud_network.id), + "name": to_native(self.hcloud_network.name), + "ip_range": to_native(self.hcloud_network.ip_range), + "expose_routes_to_vswitch": self.hcloud_network.expose_routes_to_vswitch, + "delete_protection": self.hcloud_network.protection["delete"], + "labels": self.hcloud_network.labels, + } + + def _get_network(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_network = self.client.networks.get_by_id(self.module.params.get("id")) + else: + self.hcloud_network = self.client.networks.get_by_name(self.module.params.get("name")) + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _create_network(self): + self.module.fail_on_missing_params(required_params=["name", "ip_range"]) + params = { + "name": self.module.params.get("name"), + "ip_range": self.module.params.get("ip_range"), + "labels": self.module.params.get("labels"), + } + + expose_routes_to_vswitch = self.module.params.get("expose_routes_to_vswitch") + if expose_routes_to_vswitch is not None: + params["expose_routes_to_vswitch"] = expose_routes_to_vswitch + + try: + if not self.module.check_mode: + self.client.networks.create(**params) + + delete_protection = self.module.params.get("delete_protection") + if delete_protection is not None: + self._get_network() + self.hcloud_network.change_protection(delete=delete_protection).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self._get_network() + + def _update_network(self): + try: + labels = self.module.params.get("labels") + if labels is not None and labels != self.hcloud_network.labels: + if not self.module.check_mode: + self.hcloud_network.update(labels=labels) + self._mark_as_changed() + + ip_range = self.module.params.get("ip_range") + if ip_range is not None and ip_range != self.hcloud_network.ip_range: + if not self.module.check_mode: + self.hcloud_network.change_ip_range(ip_range=ip_range).wait_until_finished() + self._mark_as_changed() + + expose_routes_to_vswitch = self.module.params.get("expose_routes_to_vswitch") + if ( + expose_routes_to_vswitch is not None + and expose_routes_to_vswitch != self.hcloud_network.expose_routes_to_vswitch + ): + if not self.module.check_mode: + self.hcloud_network.update(expose_routes_to_vswitch=expose_routes_to_vswitch) + self._mark_as_changed() + + delete_protection = self.module.params.get("delete_protection") + if delete_protection is not None and delete_protection != self.hcloud_network.protection["delete"]: + if not self.module.check_mode: + self.hcloud_network.change_protection(delete=delete_protection).wait_until_finished() + self._mark_as_changed() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._get_network() + + def present_network(self): + self._get_network() + if self.hcloud_network is None: + self._create_network() + else: + self._update_network() + + def delete_network(self): + try: + self._get_network() + if self.hcloud_network is not None: + if not self.module.check_mode: + self.client.networks.delete(self.hcloud_network) + self._mark_as_changed() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self.hcloud_network = None + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + ip_range={"type": "str"}, + expose_routes_to_vswitch={"type": "bool"}, + labels={"type": "dict"}, + delete_protection={"type": "bool"}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + required_one_of=[["id", "name"]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudNetwork.define_module() + + hcloud = AnsibleHCloudNetwork(module) + state = module.params["state"] + if state == "absent": + hcloud.delete_network() + elif state == "present": + hcloud.present_network() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/network_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/network_info.py new file mode 100644 index 000000000..4008352b4 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/network_info.py @@ -0,0 +1,296 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: network_info + +short_description: Gather info about your Hetzner Cloud networks. + + +description: + - Gather info about your Hetzner Cloud networks. + +author: + - Christopher Schmitt (@cschmitt-hcloud) + +options: + id: + description: + - The ID of the network you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name of the network you want to get. + type: str + label_selector: + description: + - The label selector for the network you want to get. + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Gather hcloud network info + local_action: + module: hcloud_network_info + +- name: Print the gathered info + debug: + var: hcloud_network_info +""" + +RETURN = """ +hcloud_network_info: + description: The network info as list + returned: always + type: complex + contains: + id: + description: Numeric identifier of the network + returned: always + type: int + sample: 1937415 + name: + description: Name of the network + returned: always + type: str + sample: awesome-network + ip_range: + description: IP range of the network + returned: always + type: str + sample: 10.0.0.0/16 + subnetworks: + description: Subnetworks belonging to the network + returned: always + type: complex + contains: + type: + description: Type of the subnetwork. + returned: always + type: str + sample: cloud + network_zone: + description: Network of the subnetwork. + returned: always + type: str + sample: eu-central + ip_range: + description: IP range of the subnetwork + returned: always + type: str + sample: 10.0.0.0/24 + gateway: + description: Gateway of this subnetwork + returned: always + type: str + sample: 10.0.0.1 + routes: + description: Routes belonging to the network + returned: always + type: complex + contains: + ip_range: + description: Destination network or host of this route. + returned: always + type: str + sample: 10.0.0.0/16 + gateway: + description: Gateway of this route + returned: always + type: str + sample: 10.0.0.1 + expose_routes_to_vswitch: + description: Indicates if the routes from this network should be exposed to the vSwitch connection. + returned: always + type: bool + sample: false + servers: + description: Servers attached to the network + returned: always + type: complex + contains: + id: + description: Numeric identifier of the server + returned: always + type: int + sample: 1937415 + name: + description: Name of the server + returned: always + type: str + sample: my-server + status: + description: Status of the server + returned: always + type: str + sample: running + server_type: + description: Name of the server type of the server + returned: always + type: str + sample: cx11 + ipv4_address: + description: Public IPv4 address of the server, None if not existing + returned: always + type: str + sample: 116.203.104.109 + ipv6: + description: IPv6 network of the server, None if not existing + returned: always + type: str + sample: 2a01:4f8:1c1c:c140::/64 + location: + description: Name of the location of the server + returned: always + type: str + sample: fsn1 + datacenter: + description: Name of the datacenter of the server + returned: always + type: str + sample: fsn1-dc14 + rescue_enabled: + description: True if rescue mode is enabled, Server will then boot into rescue system on next reboot + returned: always + type: bool + sample: false + backup_window: + description: Time window (UTC) in which the backup will run, or null if the backups are not enabled + returned: always + type: bool + sample: 22-02 + labels: + description: User-defined labels (key-value pairs) + returned: always + type: dict + delete_protection: + description: True if the network is protected for deletion + returned: always + type: bool + version_added: "0.1.0" + labels: + description: Labels of the network + returned: always + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.networks import BoundNetwork + + +class AnsibleHCloudNetworkInfo(AnsibleHCloud): + represent = "hcloud_network_info" + + hcloud_network_info: list[BoundNetwork] | None = None + + def _prepare_result(self): + tmp = [] + + for network in self.hcloud_network_info: + if network is not None: + subnets = [] + for subnet in network.subnets: + prepared_subnet = { + "type": subnet.type, + "ip_range": subnet.ip_range, + "network_zone": subnet.network_zone, + "gateway": subnet.gateway, + } + subnets.append(prepared_subnet) + routes = [] + for route in network.routes: + prepared_route = {"destination": route.destination, "gateway": route.gateway} + routes.append(prepared_route) + + servers = [] + for server in network.servers: + image = None if server.image is None else to_native(server.image.name) + ipv4_address = None if server.public_net.ipv4 is None else to_native(server.public_net.ipv4.ip) + ipv6 = None if server.public_net.ipv6 is None else to_native(server.public_net.ipv6.ip) + prepared_server = { + "id": to_native(server.id), + "name": to_native(server.name), + "ipv4_address": ipv4_address, + "ipv6": ipv6, + "image": image, + "server_type": to_native(server.server_type.name), + "datacenter": to_native(server.datacenter.name), + "location": to_native(server.datacenter.location.name), + "rescue_enabled": server.rescue_enabled, + "backup_window": to_native(server.backup_window), + "labels": server.labels, + "status": to_native(server.status), + } + servers.append(prepared_server) + + tmp.append( + { + "id": to_native(network.id), + "name": to_native(network.name), + "ip_range": to_native(network.ip_range), + "subnetworks": subnets, + "routes": routes, + "expose_routes_to_vswitch": network.expose_routes_to_vswitch, + "servers": servers, + "labels": network.labels, + "delete_protection": network.protection["delete"], + } + ) + return tmp + + def get_networks(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_network_info = [self.client.networks.get_by_id(self.module.params.get("id"))] + elif self.module.params.get("name") is not None: + self.hcloud_network_info = [self.client.networks.get_by_name(self.module.params.get("name"))] + elif self.module.params.get("label_selector") is not None: + self.hcloud_network_info = self.client.networks.get_all( + label_selector=self.module.params.get("label_selector") + ) + else: + self.hcloud_network_info = self.client.networks.get_all() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + label_selector={"type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudNetworkInfo.define_module() + hcloud = AnsibleHCloudNetworkInfo(module) + + hcloud.get_networks() + result = hcloud.get_result() + + info = {"hcloud_network_info": result["hcloud_network_info"]} + module.exit_json(**info) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/placement_group.py b/ansible_collections/hetzner/hcloud/plugins/modules/placement_group.py new file mode 100644 index 000000000..ba26fad22 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/placement_group.py @@ -0,0 +1,220 @@ +#!/usr/bin/python + +# Copyright: (c) 2020, 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 annotations + +DOCUMENTATION = """ +--- +module: placement_group + +short_description: Create and manage placement groups on the Hetzner Cloud. + + +description: + - Create, update and manage placement groups on the Hetzner Cloud. + +author: + - Adrian Huber (@Adi146) + +options: + id: + description: + - The ID of the Hetzner Cloud placement group to manage. + - Only required if no placement group I(name) is given + type: int + name: + description: + - The Name of the Hetzner Cloud placement group to manage. + - Only required if no placement group I(id) is given, or a placement group does not exist. + type: str + labels: + description: + - User-defined labels (key-value pairs) + type: dict + type: + description: + - The Type of the Hetzner Cloud placement group. + type: str + state: + description: + - State of the placement group. + default: present + choices: [ absent, present ] + type: str + +extends_documentation_fragment: +- hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a basic placement group + hetzner.hcloud.placement_group: + name: my-placement-group + state: present + type: spread + +- name: Create a placement group with labels + hetzner.hcloud.placement_group: + name: my-placement-group + type: spread + labels: + key: value + mylabel: 123 + state: present + +- name: Ensure the placement group is absent (remove if needed) + hetzner.hcloud.placement_group: + name: my-placement-group + state: absent +""" + +RETURN = """ +hcloud_placement_group: + description: The placement group instance + returned: Always + type: complex + contains: + id: + description: Numeric identifier of the placement group + returned: always + type: int + sample: 1937415 + name: + description: Name of the placement group + returned: always + type: str + sample: my placement group + labels: + description: User-defined labels (key-value pairs) + returned: always + type: dict + type: + description: Type of the placement group + returned: always + type: str + sample: spread + servers: + description: Server IDs of the placement group + returned: always + type: list + elements: int + sample: + - 4711 + - 4712 +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.placement_groups import BoundPlacementGroup + + +class AnsibleHCloudPlacementGroup(AnsibleHCloud): + represent = "hcloud_placement_group" + + hcloud_placement_group: BoundPlacementGroup | None = None + + def _prepare_result(self): + return { + "id": to_native(self.hcloud_placement_group.id), + "name": to_native(self.hcloud_placement_group.name), + "labels": self.hcloud_placement_group.labels, + "type": to_native(self.hcloud_placement_group.type), + "servers": self.hcloud_placement_group.servers, + } + + def _get_placement_group(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_placement_group = self.client.placement_groups.get_by_id(self.module.params.get("id")) + elif self.module.params.get("name") is not None: + self.hcloud_placement_group = self.client.placement_groups.get_by_name(self.module.params.get("name")) + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _create_placement_group(self): + self.module.fail_on_missing_params(required_params=["name"]) + params = { + "name": self.module.params.get("name"), + "type": self.module.params.get("type"), + "labels": self.module.params.get("labels"), + } + if not self.module.check_mode: + try: + self.client.placement_groups.create(**params) + except HCloudException as exception: + self.fail_json_hcloud(exception, params=params) + self._mark_as_changed() + self._get_placement_group() + + def _update_placement_group(self): + name = self.module.params.get("name") + if name is not None and self.hcloud_placement_group.name != name: + self.module.fail_on_missing_params(required_params=["id"]) + if not self.module.check_mode: + self.hcloud_placement_group.update(name=name) + self._mark_as_changed() + + labels = self.module.params.get("labels") + if labels is not None and self.hcloud_placement_group.labels != labels: + if not self.module.check_mode: + self.hcloud_placement_group.update(labels=labels) + self._mark_as_changed() + + self._get_placement_group() + + def present_placement_group(self): + self._get_placement_group() + if self.hcloud_placement_group is None: + self._create_placement_group() + else: + self._update_placement_group() + + def delete_placement_group(self): + self._get_placement_group() + if self.hcloud_placement_group is not None: + if not self.module.check_mode: + self.client.placement_groups.delete(self.hcloud_placement_group) + self._mark_as_changed() + self.hcloud_placement_group = None + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + labels={"type": "dict"}, + type={"type": "str"}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + required_one_of=[["id", "name"]], + required_if=[["state", "present", ["name"]]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudPlacementGroup.define_module() + + hcloud = AnsibleHCloudPlacementGroup(module) + state = module.params.get("state") + if state == "absent": + hcloud.delete_placement_group() + elif state == "present": + hcloud.present_placement_group() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/primary_ip.py b/ansible_collections/hetzner/hcloud/plugins/modules/primary_ip.py new file mode 100644 index 000000000..607f6c7e1 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/primary_ip.py @@ -0,0 +1,260 @@ +#!/usr/bin/python + +# Copyright: (c) 2022, 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 annotations + +DOCUMENTATION = """ +--- +module: primary_ip + +short_description: Create and manage cloud Primary IPs on the Hetzner Cloud. + + +description: + - Create, update and manage cloud Primary IPs on the Hetzner Cloud. + +author: + - Lukas Kaemmerling (@lkaemmerling) +version_added: 1.8.0 +options: + id: + description: + - The ID of the Hetzner Cloud Primary IPs to manage. + - Only required if no Primary IP I(name) is given. + type: int + name: + description: + - The Name of the Hetzner Cloud Primary IPs to manage. + - Only required if no Primary IP I(id) is given or a Primary IP does not exist. + type: str + datacenter: + description: + - Home Location of the Hetzner Cloud Primary IP. + - Required if no I(server) is given and Primary IP does not exist. + type: str + type: + description: + - Type of the Primary IP. + - Required if Primary IP does not exist + choices: [ ipv4, ipv6 ] + type: str + auto_delete: + description: + - Delete this Primary IP when the resource it is assigned to is deleted + type: bool + default: no + delete_protection: + description: + - Protect the Primary IP for deletion. + type: bool + labels: + description: + - User-defined labels (key-value pairs). + type: dict + state: + description: + - State of the Primary IP. + default: present + choices: [ absent, present ] + type: str + +extends_documentation_fragment: +- hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a basic IPv4 Primary IP + hetzner.hcloud.primary_ip: + name: my-primary-ip + datacenter: fsn1-dc14 + type: ipv4 + state: present +- name: Create a basic IPv6 Primary IP + hetzner.hcloud.primary_ip: + name: my-primary-ip + datacenter: fsn1-dc14 + type: ipv6 + state: present +- name: Primary IP should be absent + hetzner.hcloud.primary_ip: + name: my-primary-ip + state: absent +""" + +RETURN = """ +hcloud_primary_ip: + description: The Primary IP instance + returned: Always + type: complex + contains: + id: + description: ID of the Primary IP + type: int + returned: Always + sample: 12345 + name: + description: Name of the Primary IP + type: str + returned: Always + sample: my-primary-ip + ip: + description: IP Address of the Primary IP + type: str + returned: Always + sample: 116.203.104.109 + type: + description: Type of the Primary IP + type: str + returned: Always + sample: ipv4 + datacenter: + description: Name of the datacenter of the Primary IP + type: str + returned: Always + sample: fsn1-dc14 + delete_protection: + description: True if Primary IP is protected for deletion + type: bool + returned: always + sample: false + labels: + description: User-defined labels (key-value pairs) + type: dict + returned: Always + sample: + key: value + mylabel: 123 +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.primary_ips import BoundPrimaryIP + + +class AnsibleHCloudPrimaryIP(AnsibleHCloud): + represent = "hcloud_primary_ip" + + hcloud_primary_ip: BoundPrimaryIP | None = None + + def _prepare_result(self): + return { + "id": to_native(self.hcloud_primary_ip.id), + "name": to_native(self.hcloud_primary_ip.name), + "ip": to_native(self.hcloud_primary_ip.ip), + "type": to_native(self.hcloud_primary_ip.type), + "datacenter": to_native(self.hcloud_primary_ip.datacenter.name), + "labels": self.hcloud_primary_ip.labels, + "delete_protection": self.hcloud_primary_ip.protection["delete"], + } + + def _get_primary_ip(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_primary_ip = self.client.primary_ips.get_by_id(self.module.params.get("id")) + else: + self.hcloud_primary_ip = self.client.primary_ips.get_by_name(self.module.params.get("name")) + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _create_primary_ip(self): + self.module.fail_on_missing_params(required_params=["type", "datacenter"]) + try: + params = { + "type": self.module.params.get("type"), + "name": self.module.params.get("name"), + "datacenter": self.client.datacenters.get_by_name(self.module.params.get("datacenter")), + } + + if self.module.params.get("labels") is not None: + params["labels"] = self.module.params.get("labels") + if not self.module.check_mode: + resp = self.client.primary_ips.create(**params) + self.hcloud_primary_ip = resp.primary_ip + + delete_protection = self.module.params.get("delete_protection") + if delete_protection is not None: + self.hcloud_primary_ip.change_protection(delete=delete_protection).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self._get_primary_ip() + + def _update_primary_ip(self): + try: + labels = self.module.params.get("labels") + if labels is not None and labels != self.hcloud_primary_ip.labels: + if not self.module.check_mode: + self.hcloud_primary_ip.update(labels=labels) + self._mark_as_changed() + + delete_protection = self.module.params.get("delete_protection") + if delete_protection is not None and delete_protection != self.hcloud_primary_ip.protection["delete"]: + if not self.module.check_mode: + self.hcloud_primary_ip.change_protection(delete=delete_protection).wait_until_finished() + self._mark_as_changed() + + self._get_primary_ip() + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def present_primary_ip(self): + self._get_primary_ip() + if self.hcloud_primary_ip is None: + self._create_primary_ip() + else: + self._update_primary_ip() + + def delete_primary_ip(self): + try: + self._get_primary_ip() + if self.hcloud_primary_ip is not None: + if not self.module.check_mode: + self.client.primary_ips.delete(self.hcloud_primary_ip) + self._mark_as_changed() + self.hcloud_primary_ip = None + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + datacenter={"type": "str"}, + auto_delete={"type": "bool", "default": False}, + type={"choices": ["ipv4", "ipv6"]}, + labels={"type": "dict"}, + delete_protection={"type": "bool"}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + required_one_of=[["id", "name"]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudPrimaryIP.define_module() + + hcloud = AnsibleHCloudPrimaryIP(module) + state = module.params["state"] + if state == "absent": + hcloud.delete_primary_ip() + elif state == "present": + hcloud.present_primary_ip() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/primary_ip_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/primary_ip_info.py new file mode 100644 index 000000000..c0bfdbb35 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/primary_ip_info.py @@ -0,0 +1,203 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: primary_ip_info + +short_description: Gather infos about the Hetzner Cloud Primary IPs. + +description: + - Gather facts about your Hetzner Cloud Primary IPs. + +author: + - Lukas Kaemmerling (@LKaemmerling) + - Kevin Castner (@kcastner) + +options: + id: + description: + - The ID of the Primary IP you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name for the Primary IP you want to get. + type: str + label_selector: + description: + - The label selector for the Primary IP you want to get. + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Gather hcloud Primary IP infos + hetzner.hcloud.primary_ip_info: + register: output + +- name: Gather hcloud Primary IP infos by id + hetzner.hcloud.primary_ip_info: + id: 673954 + register: output + +- name: Gather hcloud Primary IP infos by name + hetzner.hcloud.primary_ip_info: + name: srv1-v4 + register: output + +- name: Gather hcloud Primary IP infos by label + hetzner.hcloud.primary_ip_info: + label_selector: srv03-ips + register: output + +- name: Print the gathered infos + debug: + var: output +""" + +RETURN = """ +hcloud_primary_ip_info: + description: The Primary IP infos as list + returned: always + type: complex + contains: + id: + description: Numeric identifier of the Primary IP + returned: always + type: int + sample: 1937415 + name: + description: Name of the Primary IP + returned: always + type: str + sample: my-primary-ip + ip: + description: IP address of the Primary IP + returned: always + type: str + sample: 131.232.99.1 + type: + description: Type of the Primary IP + returned: always + type: str + sample: ipv4 + assignee_id: + description: Numeric identifier of the ressource where the Primary IP is assigned to. + returned: always + type: int + sample: 19584637 + assignee_type: + description: Name of the type where the Primary IP is assigned to. + returned: always + type: str + sample: server + home_location: + description: Location with datacenter where the Primary IP was created in + returned: always + type: str + sample: fsn1-dc1 + dns_ptr: + description: Shows the DNS PTR Record for Primary IP. + returned: always + type: str + sample: srv01.example.com + labels: + description: User-defined labels (key-value pairs) + returned: always + type: dict + delete_protection: + description: True if the Primary IP is protected for deletion + returned: always + type: bool +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.primary_ips import BoundPrimaryIP + + +class AnsibleHCloudPrimaryIPInfo(AnsibleHCloud): + represent = "hcloud_primary_ip_info" + + hcloud_primary_ip_info: list[BoundPrimaryIP] | None = None + + def _prepare_result(self): + tmp = [] + + for primary_ip in self.hcloud_primary_ip_info: + if primary_ip is not None: + dns_ptr = None + if len(primary_ip.dns_ptr) > 0: + dns_ptr = primary_ip.dns_ptr[0]["dns_ptr"] + tmp.append( + { + "id": to_native(primary_ip.id), + "name": to_native(primary_ip.name), + "ip": to_native(primary_ip.ip), + "type": to_native(primary_ip.type), + "assignee_id": ( + to_native(primary_ip.assignee_id) if primary_ip.assignee_id is not None else None + ), + "assignee_type": to_native(primary_ip.assignee_type), + "home_location": to_native(primary_ip.datacenter.name), + "dns_ptr": to_native(dns_ptr) if dns_ptr is not None else None, + "labels": primary_ip.labels, + "delete_protection": primary_ip.protection["delete"], + } + ) + + return tmp + + def get_primary_ips(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_primary_ip_info = [self.client.primary_ips.get_by_id(self.module.params.get("id"))] + elif self.module.params.get("name") is not None: + self.hcloud_primary_ip_info = [self.client.primary_ips.get_by_name(self.module.params.get("name"))] + elif self.module.params.get("label_selector") is not None: + self.hcloud_primary_ip_info = self.client.primary_ips.get_all( + label_selector=self.module.params.get("label_selector") + ) + else: + self.hcloud_primary_ip_info = self.client.primary_ips.get_all() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + label_selector={"type": "str"}, + name={"type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudPrimaryIPInfo.define_module() + hcloud = AnsibleHCloudPrimaryIPInfo(module) + + hcloud.get_primary_ips() + result = hcloud.get_result() + + ansible_info = {"hcloud_primary_ip_info": result["hcloud_primary_ip_info"]} + module.exit_json(**ansible_info) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/rdns.py b/ansible_collections/hetzner/hcloud/plugins/modules/rdns.py new file mode 100644 index 000000000..b2decdec8 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/rdns.py @@ -0,0 +1,360 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: rdns + +short_description: Create and manage reverse DNS entries on the Hetzner Cloud. + + +description: + - Create, update and delete reverse DNS entries on the Hetzner Cloud. + +author: + - Lukas Kaemmerling (@lkaemmerling) + +options: + server: + description: + - Name or ID of the Hetzner Cloud server you want to add the reverse DNS entry to. + type: str + floating_ip: + description: + - Name or ID of the Hetzner Cloud Floating IP you want to add the reverse DNS entry to. + type: str + primary_ip: + description: + - Name or ID of the Hetzner Cloud Primary IP you want to add the reverse DNS entry to. + type: str + load_balancer: + description: + - Name or ID of the Hetzner Cloud Load Balancer you want to add the reverse DNS entry to. + type: str + ip_address: + description: + - The IP address that should point to I(dns_ptr). + type: str + required: true + dns_ptr: + description: + - The DNS address the I(ip_address) should resolve to. + - Omit the param to reset the reverse DNS entry to the default value. + type: str + state: + description: + - State of the reverse DNS entry. + default: present + choices: [ absent, present ] + type: str + +extends_documentation_fragment: +- hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a reverse DNS entry for a server + hetzner.hcloud.rdns: + server: my-server + ip_address: 123.123.123.123 + dns_ptr: example.com + state: present + +- name: Create a reverse DNS entry for a Floating IP + hetzner.hcloud.rdns: + floating_ip: my-floating-ip + ip_address: 123.123.123.123 + dns_ptr: example.com + state: present + +- name: Create a reverse DNS entry for a Primary IP + hetzner.hcloud.rdns: + primary_ip: my-primary-ip + ip_address: 123.123.123.123 + dns_ptr: example.com + state: present + +- name: Create a reverse DNS entry for a Load Balancer + hetzner.hcloud.rdns: + load_balancer: my-load-balancer + ip_address: 123.123.123.123 + dns_ptr: example.com + state: present + +- name: Ensure the reverse DNS entry is absent (remove if needed) + hetzner.hcloud.rdns: + server: my-server + ip_address: 123.123.123.123 + dns_ptr: example.com + state: absent +""" + +RETURN = """ +hcloud_rdns: + description: The reverse DNS entry + returned: always + type: complex + contains: + server: + description: Name of the server + type: str + returned: always + sample: my-server + floating_ip: + description: Name of the Floating IP + type: str + returned: always + sample: my-floating-ip + primary_ip: + description: Name of the Primary IP + type: str + returned: always + sample: my-primary-ip + load_balancer: + description: Name of the Load Balancer + type: str + returned: always + sample: my-load-balancer + ip_address: + description: The IP address that point to the DNS ptr + type: str + returned: always + sample: 123.123.123.123 + dns_ptr: + description: The DNS that resolves to the IP + type: str + returned: always + sample: example.com +""" + +import ipaddress +from typing import Any + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.floating_ips import BoundFloatingIP +from ..module_utils.vendor.hcloud.load_balancers import BoundLoadBalancer +from ..module_utils.vendor.hcloud.primary_ips import BoundPrimaryIP +from ..module_utils.vendor.hcloud.servers import BoundServer + + +class AnsibleHCloudReverseDNS(AnsibleHCloud): + represent = "hcloud_rdns" + + hcloud_resource: BoundServer | BoundFloatingIP | BoundLoadBalancer | BoundPrimaryIP | None = None + hcloud_rdns: dict[str, Any] | None = None + + def _prepare_result(self): + result = { + "server": None, + "floating_ip": None, + "load_balancer": None, + "ip_address": to_native(self.hcloud_rdns["ip_address"]), + "dns_ptr": to_native(self.hcloud_rdns["dns_ptr"]), + } + + if self.module.params.get("server"): + result["server"] = to_native(self.hcloud_resource.name) + elif self.module.params.get("floating_ip"): + result["floating_ip"] = to_native(self.hcloud_resource.name) + elif self.module.params.get("load_balancer"): + result["load_balancer"] = to_native(self.hcloud_resource.name) + elif self.module.params.get("primary_ip"): + result["primary_ip"] = to_native(self.hcloud_resource.name) + return result + + def _get_resource(self): + try: + if self.module.params.get("server"): + self.hcloud_resource = self._client_get_by_name_or_id( + "servers", + self.module.params.get("server"), + ) + elif self.module.params.get("floating_ip"): + self.hcloud_resource = self._client_get_by_name_or_id( + "floating_ips", + self.module.params.get("floating_ip"), + ) + elif self.module.params.get("primary_ip"): + self.hcloud_resource = self._client_get_by_name_or_id( + "primary_ips", + self.module.params.get("primary_ip"), + ) + elif self.module.params.get("load_balancer"): + self.hcloud_resource = self._client_get_by_name_or_id( + "load_balancers", + self.module.params.get("load_balancer"), + ) + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _get_rdns(self): + ip_address = self.module.params.get("ip_address") + + try: + ip_address_obj = ipaddress.ip_address(ip_address) + except ValueError: + self.module.fail_json(msg=f"The given IP address is not valid: {ip_address}") + + if ip_address_obj.version == 4: + if self.module.params.get("server"): + if self.hcloud_resource.public_net.ipv4.ip == ip_address: + self.hcloud_rdns = { + "ip_address": self.hcloud_resource.public_net.ipv4.ip, + "dns_ptr": self.hcloud_resource.public_net.ipv4.dns_ptr, + } + else: + self.module.fail_json(msg="The selected server does not have this IP address") + elif self.module.params.get("floating_ip"): + if self.hcloud_resource.ip == ip_address: + self.hcloud_rdns = { + "ip_address": self.hcloud_resource.ip, + "dns_ptr": self.hcloud_resource.dns_ptr[0]["dns_ptr"], + } + else: + self.module.fail_json(msg="The selected Floating IP does not have this IP address") + elif self.module.params.get("primary_ip"): + if self.hcloud_resource.ip == ip_address: + self.hcloud_rdns = { + "ip_address": self.hcloud_resource.ip, + "dns_ptr": self.hcloud_resource.dns_ptr[0]["dns_ptr"], + } + else: + self.module.fail_json(msg="The selected Primary IP does not have this IP address") + elif self.module.params.get("load_balancer"): + if self.hcloud_resource.public_net.ipv4.ip == ip_address: + self.hcloud_rdns = { + "ip_address": self.hcloud_resource.public_net.ipv4.ip, + "dns_ptr": self.hcloud_resource.public_net.ipv4.dns_ptr, + } + else: + self.module.fail_json(msg="The selected Load Balancer does not have this IP address") + + elif ip_address_obj.version == 6: + if self.module.params.get("server"): + for ipv6_address_dns_ptr in self.hcloud_resource.public_net.ipv6.dns_ptr: + if ipv6_address_dns_ptr["ip"] == ip_address: + self.hcloud_rdns = { + "ip_address": ipv6_address_dns_ptr["ip"], + "dns_ptr": ipv6_address_dns_ptr["dns_ptr"], + } + elif self.module.params.get("floating_ip"): + for ipv6_address_dns_ptr in self.hcloud_resource.dns_ptr: + if ipv6_address_dns_ptr["ip"] == ip_address: + self.hcloud_rdns = { + "ip_address": ipv6_address_dns_ptr["ip"], + "dns_ptr": ipv6_address_dns_ptr["dns_ptr"], + } + elif self.module.params.get("primary_ip"): + for ipv6_address_dns_ptr in self.hcloud_resource.dns_ptr: + if ipv6_address_dns_ptr["ip"] == ip_address: + self.hcloud_rdns = { + "ip_address": ipv6_address_dns_ptr["ip"], + "dns_ptr": ipv6_address_dns_ptr["dns_ptr"], + } + elif self.module.params.get("load_balancer"): + for ipv6_address_dns_ptr in self.hcloud_resource.public_net.ipv6.dns_ptr: + if ipv6_address_dns_ptr["ip"] == ip_address: + self.hcloud_rdns = { + "ip_address": ipv6_address_dns_ptr["ip"], + "dns_ptr": ipv6_address_dns_ptr["dns_ptr"], + } + + def _create_rdns(self): + self.module.fail_on_missing_params(required_params=["dns_ptr"]) + params = { + "ip": self.module.params.get("ip_address"), + "dns_ptr": self.module.params.get("dns_ptr"), + } + + if not self.module.check_mode: + try: + self.hcloud_resource.change_dns_ptr(**params).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self._get_resource() + self._get_rdns() + + def _update_rdns(self): + dns_ptr = self.module.params.get("dns_ptr") + if dns_ptr != self.hcloud_rdns["dns_ptr"]: + params = { + "ip": self.module.params.get("ip_address"), + "dns_ptr": dns_ptr, + } + + if not self.module.check_mode: + try: + self.hcloud_resource.change_dns_ptr(**params).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self._get_resource() + self._get_rdns() + + def present_rdns(self): + self._get_resource() + self._get_rdns() + if self.hcloud_rdns is None: + self._create_rdns() + else: + self._update_rdns() + + def delete_rdns(self): + self._get_resource() + self._get_rdns() + if self.hcloud_rdns is not None: + if not self.module.check_mode: + try: + self.hcloud_resource.change_dns_ptr(ip=self.hcloud_rdns["ip_address"], dns_ptr=None) + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self.hcloud_rdns = None + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + server={"type": "str"}, + floating_ip={"type": "str"}, + load_balancer={"type": "str"}, + primary_ip={"type": "str"}, + ip_address={"type": "str", "required": True}, + dns_ptr={"type": "str"}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + required_one_of=[["server", "floating_ip", "load_balancer", "primary_ip"]], + mutually_exclusive=[["server", "floating_ip", "load_balancer", "primary_ip"]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudReverseDNS.define_module() + + hcloud = AnsibleHCloudReverseDNS(module) + state = module.params["state"] + if state == "absent": + hcloud.delete_rdns() + elif state == "present": + hcloud.present_rdns() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/route.py b/ansible_collections/hetzner/hcloud/plugins/modules/route.py new file mode 100644 index 000000000..3c96a7382 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/route.py @@ -0,0 +1,190 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: route + +short_description: Create and delete cloud routes on the Hetzner Cloud. + + +description: + - Create, update and delete cloud routes on the Hetzner Cloud. + +author: + - Lukas Kaemmerling (@lkaemmerling) + +options: + network: + description: + - Name or ID of the Hetzner Cloud Network. + type: str + required: true + destination: + description: + - Destination network or host of this route. + type: str + required: true + gateway: + description: + - Gateway for the route. + type: str + required: true + state: + description: + - State of the route. + default: present + choices: [ absent, present ] + type: str + +extends_documentation_fragment: +- hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a basic route + hetzner.hcloud.route: + network: my-network + destination: 10.100.1.0/24 + gateway: 10.0.1.1 + state: present + +- name: Ensure the route is absent + hetzner.hcloud.route: + network: my-network + destination: 10.100.1.0/24 + gateway: 10.0.1.1 + state: absent +""" + +RETURN = """ +hcloud_route: + description: One Route of a Network + returned: always + type: complex + contains: + network: + description: Name of the Network + type: str + returned: always + sample: my-network + destination: + description: Destination network or host of this route + type: str + returned: always + sample: 10.0.0.0/8 + gateway: + description: Gateway of the route + type: str + returned: always + sample: 10.0.0.1 +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.networks import BoundNetwork, NetworkRoute + + +class AnsibleHCloudRoute(AnsibleHCloud): + represent = "hcloud_route" + + hcloud_network: BoundNetwork | None = None + hcloud_route: NetworkRoute | None = None + + def _prepare_result(self): + return { + "network": to_native(self.hcloud_network.name), + "destination": to_native(self.hcloud_route.destination), + "gateway": self.hcloud_route.gateway, + } + + def _get_network(self): + try: + self.hcloud_network = self._client_get_by_name_or_id( + "networks", + self.module.params.get("network"), + ) + self.hcloud_route = None + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _get_route(self): + destination = self.module.params.get("destination") + gateway = self.module.params.get("gateway") + for route in self.hcloud_network.routes: + if route.destination == destination and route.gateway == gateway: + self.hcloud_route = route + + def _create_route(self): + route = NetworkRoute( + destination=self.module.params.get("destination"), gateway=self.module.params.get("gateway") + ) + + if not self.module.check_mode: + try: + self.hcloud_network.add_route(route=route).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + + self._mark_as_changed() + self._get_network() + self._get_route() + + def present_route(self): + self._get_network() + self._get_route() + if self.hcloud_route is None: + self._create_route() + + def delete_route(self): + self._get_network() + self._get_route() + if self.hcloud_route is not None and self.hcloud_network is not None: + if not self.module.check_mode: + try: + self.hcloud_network.delete_route(self.hcloud_route).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self.hcloud_route = None + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + network={"type": "str", "required": True}, + gateway={"type": "str", "required": True}, + destination={"type": "str", "required": True}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudRoute.define_module() + + hcloud = AnsibleHCloudRoute(module) + state = module.params["state"] + if state == "absent": + hcloud.delete_route() + elif state == "present": + hcloud.present_route() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/server.py b/ansible_collections/hetzner/hcloud/plugins/modules/server.py new file mode 100644 index 000000000..f5cadb807 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/server.py @@ -0,0 +1,952 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: server + +short_description: Create and manage cloud servers on the Hetzner Cloud. + + +description: + - Create, update and manage cloud servers on the Hetzner Cloud. + +author: + - Lukas Kaemmerling (@LKaemmerling) + +options: + id: + description: + - The ID of the Hetzner Cloud server to manage. + - Only required if no server I(name) is given + type: int + name: + description: + - The Name of the Hetzner Cloud server to manage. + - Only required if no server I(id) is given or a server does not exist. + type: str + server_type: + description: + - The Server Type of the Hetzner Cloud server to manage. + - Required if server does not exist. + type: str + ssh_keys: + description: + - List of SSH key names + - The key names correspond to the SSH keys configured for your + Hetzner Cloud account access. + type: list + elements: str + volumes: + description: + - List of Volumes IDs that should be attached to the server on server creation. + type: list + elements: str + firewalls: + description: + - List of Firewall IDs that should be attached to the server on server creation. + type: list + elements: str + image: + description: + - Image the server should be created from. + - Required if server does not exist. + type: str + location: + description: + - Location of Server. + - Required if no I(datacenter) is given and server does not exist. + type: str + datacenter: + description: + - Datacenter of Server. + - Required if no I(location) is given and server does not exist. + type: str + backups: + description: + - Enable or disable Backups for the given Server. + type: bool + upgrade_disk: + description: + - Resize the disk size, when resizing a server. + - If you want to downgrade the server later, this value should be False. + type: bool + default: false + enable_ipv4: + description: + - Enables the public ipv4 address + type: bool + default: true + enable_ipv6: + description: + - Enables the public ipv6 address + type: bool + default: true + ipv4: + description: + - ID of the ipv4 Primary IP to use. If omitted and enable_ipv4 is true, a new ipv4 Primary IP will automatically be created + type: str + ipv6: + description: + - ID of the ipv6 Primary IP to use. If omitted and enable_ipv6 is true, a new ipv6 Primary IP will automatically be created. + type: str + private_networks: + description: + - List of private networks the server is attached to (name or ID) + - If None, private networks are left as they are (e.g. if previously added by hcloud_server_network), + if it has any other value (including []), only those networks are attached to the server. + type: list + elements: str + force_upgrade: + description: + - Deprecated + - Force the upgrade of the server. + - Power off the server if it is running on upgrade. + type: bool + force: + description: + - Force the update of the server. + - May power off the server if update. + type: bool + default: false + allow_deprecated_image: + description: + - Allows the creation of servers with deprecated images. + type: bool + default: false + user_data: + description: + - User Data to be passed to the server on creation. + - Only used if server does not exist. + type: str + rescue_mode: + description: + - Add the Hetzner rescue system type you want the server to be booted into. + type: str + labels: + description: + - User-defined labels (key-value pairs). + type: dict + delete_protection: + description: + - Protect the Server for deletion. + - Needs to be the same as I(rebuild_protection). + type: bool + rebuild_protection: + description: + - Protect the Server for rebuild. + - Needs to be the same as I(delete_protection). + type: bool + placement_group: + description: + - Placement Group of the server. + type: str + state: + description: + - State of the server. + default: present + choices: [ absent, present, restarted, started, stopped, rebuild ] + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Create a basic server + hetzner.hcloud.server: + name: my-server + server_type: cx11 + image: ubuntu-22.04 + state: present + +- name: Create a basic server with ssh key + hetzner.hcloud.server: + name: my-server + server_type: cx11 + image: ubuntu-22.04 + location: fsn1 + ssh_keys: + - me@myorganisation + state: present + +- name: Resize an existing server + hetzner.hcloud.server: + name: my-server + server_type: cx21 + upgrade_disk: true + state: present + +- name: Ensure the server is absent (remove if needed) + hetzner.hcloud.server: + name: my-server + state: absent + +- name: Ensure the server is started + hetzner.hcloud.server: + name: my-server + state: started + +- name: Ensure the server is stopped + hetzner.hcloud.server: + name: my-server + state: stopped + +- name: Ensure the server is restarted + hetzner.hcloud.server: + name: my-server + state: restarted + +- name: Ensure the server is will be booted in rescue mode and therefore restarted + hetzner.hcloud.server: + name: my-server + rescue_mode: linux64 + state: restarted + +- name: Ensure the server is rebuild + hetzner.hcloud.server: + name: my-server + image: ubuntu-22.04 + state: rebuild + +- name: Add server to placement group + hetzner.hcloud.server: + name: my-server + placement_group: my-placement-group + force: true + state: present + +- name: Remove server from placement group + hetzner.hcloud.server: + name: my-server + placement_group: + state: present + +- name: Add server with private network only + hetzner.hcloud.server: + name: my-server + enable_ipv4: false + enable_ipv6: false + private_networks: + - my-network + - 4711 + state: present +""" + +RETURN = """ +hcloud_server: + description: The server instance + returned: Always + type: complex + contains: + id: + description: Numeric identifier of the server + returned: always + type: int + sample: 1937415 + name: + description: Name of the server + returned: always + type: str + sample: my-server + created: + description: Point in time when the Server was created (in ISO-8601 format) + returned: always + type: str + sample: "2023-11-06T13:36:56+00:00" + status: + description: Status of the server + returned: always + type: str + sample: running + server_type: + description: Name of the server type of the server + returned: always + type: str + sample: cx11 + ipv4_address: + description: Public IPv4 address of the server + returned: always + type: str + sample: 116.203.104.109 + ipv6: + description: IPv6 network of the server + returned: always + type: str + sample: 2a01:4f8:1c1c:c140::/64 + private_networks: + description: List of private networks the server is attached to (name or ID) + returned: always + type: list + elements: str + sample: ['my-network', 'another-network', '4711'] + private_networks_info: + description: List of private networks the server is attached to (dict with name and ip) + returned: always + type: list + elements: dict + sample: [{'name': 'my-network', 'ip': '192.168.1.1'}, {'name': 'another-network', 'ip': '10.185.50.40'}] + location: + description: Name of the location of the server + returned: always + type: str + sample: fsn1 + placement_group: + description: Placement Group of the server + type: str + returned: always + sample: 4711 + version_added: "1.5.0" + datacenter: + description: Name of the datacenter of the server + returned: always + type: str + sample: fsn1-dc14 + rescue_enabled: + description: True if rescue mode is enabled, Server will then boot into rescue system on next reboot + returned: always + type: bool + sample: false + backup_window: + description: Time window (UTC) in which the backup will run, or null if the backups are not enabled + returned: always + type: bool + sample: 22-02 + labels: + description: User-defined labels (key-value pairs) + returned: always + type: dict + delete_protection: + description: True if server is protected for deletion + type: bool + returned: always + sample: false + version_added: "0.1.0" + rebuild_protection: + description: True if server is protected for rebuild + type: bool + returned: always + sample: false + version_added: "0.1.0" +""" + +from datetime import datetime, timedelta, timezone + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.firewalls import FirewallResource +from ..module_utils.vendor.hcloud.servers import ( + BoundServer, + Server, + ServerCreatePublicNetwork, +) +from ..module_utils.vendor.hcloud.ssh_keys import SSHKey +from ..module_utils.vendor.hcloud.volumes import Volume + + +class AnsibleHCloudServer(AnsibleHCloud): + represent = "hcloud_server" + + hcloud_server: BoundServer | None = None + + def _prepare_result(self): + image = None if self.hcloud_server.image is None else to_native(self.hcloud_server.image.name) + placement_group = ( + None if self.hcloud_server.placement_group is None else to_native(self.hcloud_server.placement_group.name) + ) + ipv4_address = ( + None if self.hcloud_server.public_net.ipv4 is None else to_native(self.hcloud_server.public_net.ipv4.ip) + ) + ipv6 = None if self.hcloud_server.public_net.ipv6 is None else to_native(self.hcloud_server.public_net.ipv6.ip) + backup_window = ( + None if self.hcloud_server.backup_window is None else to_native(self.hcloud_server.backup_window) + ) + return { + "id": to_native(self.hcloud_server.id), + "name": to_native(self.hcloud_server.name), + "created": to_native(self.hcloud_server.created.isoformat()), + "ipv4_address": ipv4_address, + "ipv6": ipv6, + "private_networks": [to_native(net.network.name) for net in self.hcloud_server.private_net], + "private_networks_info": [ + {"name": to_native(net.network.name), "ip": net.ip} for net in self.hcloud_server.private_net + ], + "image": image, + "server_type": to_native(self.hcloud_server.server_type.name), + "datacenter": to_native(self.hcloud_server.datacenter.name), + "location": to_native(self.hcloud_server.datacenter.location.name), + "placement_group": placement_group, + "rescue_enabled": self.hcloud_server.rescue_enabled, + "backup_window": backup_window, + "labels": self.hcloud_server.labels, + "delete_protection": self.hcloud_server.protection["delete"], + "rebuild_protection": self.hcloud_server.protection["rebuild"], + "status": to_native(self.hcloud_server.status), + } + + def _get_server(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_server = self.client.servers.get_by_id(self.module.params.get("id")) + else: + self.hcloud_server = self.client.servers.get_by_name(self.module.params.get("name")) + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _create_server(self): + self.module.fail_on_missing_params(required_params=["name", "server_type", "image"]) + + server_type = self._get_server_type() + + params = { + "name": self.module.params.get("name"), + "server_type": server_type, + "user_data": self.module.params.get("user_data"), + "labels": self.module.params.get("labels"), + "image": self._get_image(server_type), + "placement_group": self._get_placement_group(), + "public_net": ServerCreatePublicNetwork( + enable_ipv4=self.module.params.get("enable_ipv4"), + enable_ipv6=self.module.params.get("enable_ipv6"), + ), + } + + if self.module.params.get("ipv4") is not None: + primary_ip = self.client.primary_ips.get_by_name(self.module.params.get("ipv4")) + if not primary_ip: + primary_ip = self.client.primary_ips.get_by_id(self.module.params.get("ipv4")) + params["public_net"].ipv4 = primary_ip + + if self.module.params.get("ipv6") is not None: + primary_ip = self.client.primary_ips.get_by_name(self.module.params.get("ipv6")) + if not primary_ip: + primary_ip = self.client.primary_ips.get_by_id(self.module.params.get("ipv6")) + params["public_net"].ipv6 = primary_ip + + if self.module.params.get("private_networks") is not None: + _networks = [] + for network_name_or_id in self.module.params.get("private_networks"): + _networks.append( + self.client.networks.get_by_name(network_name_or_id) + or self.client.networks.get_by_id(network_name_or_id) + ) + params["networks"] = _networks + + if self.module.params.get("ssh_keys") is not None: + params["ssh_keys"] = [SSHKey(name=ssh_key_name) for ssh_key_name in self.module.params.get("ssh_keys")] + + if self.module.params.get("volumes") is not None: + params["volumes"] = [Volume(id=volume_id) for volume_id in self.module.params.get("volumes")] + if self.module.params.get("firewalls") is not None: + params["firewalls"] = [] + for firewall_param in self.module.params.get("firewalls"): + firewall = self.client.firewalls.get_by_name(firewall_param) + if firewall is not None: + # When firewall name is not available look for id instead + params["firewalls"].append(firewall) + else: + params["firewalls"].append(self.client.firewalls.get_by_id(firewall_param)) + + if self.module.params.get("location") is None and self.module.params.get("datacenter") is None: + # When not given, the API will choose the location. + params["location"] = None + params["datacenter"] = None + elif self.module.params.get("location") is not None and self.module.params.get("datacenter") is None: + params["location"] = self.client.locations.get_by_name(self.module.params.get("location")) + elif self.module.params.get("location") is None and self.module.params.get("datacenter") is not None: + params["datacenter"] = self.client.datacenters.get_by_name(self.module.params.get("datacenter")) + + if self.module.params.get("state") == "stopped": + params["start_after_create"] = False + if not self.module.check_mode: + try: + resp = self.client.servers.create(**params) + self.result["root_password"] = resp.root_password + resp.action.wait_until_finished(max_retries=1000) + [action.wait_until_finished() for action in resp.next_actions] + + rescue_mode = self.module.params.get("rescue_mode") + if rescue_mode: + self._get_server() + self._set_rescue_mode(rescue_mode) + + backups = self.module.params.get("backups") + if backups: + self._get_server() + self.hcloud_server.enable_backup().wait_until_finished() + + delete_protection = self.module.params.get("delete_protection") + rebuild_protection = self.module.params.get("rebuild_protection") + if delete_protection is not None and rebuild_protection is not None: + self._get_server() + self.hcloud_server.change_protection( + delete=delete_protection, + rebuild=rebuild_protection, + ).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self._get_server() + + def _get_image(self, server_type): + image_resp = self.client.images.get_list( + name=self.module.params.get("image"), + architecture=server_type.architecture, + include_deprecated=True, + ) + images = getattr(image_resp, "images") + image = None + if images is not None and len(images) > 0: + # If image name is not available look for id instead + image = images[0] + else: + try: + image = self.client.images.get_by_id(self.module.params.get("image")) + except HCloudException as exception: + self.fail_json_hcloud(exception, msg=f"Image {self.module.params.get('image')} was not found") + if image.deprecated is not None: + available_until = image.deprecated + timedelta(days=90) + if self.module.params.get("allow_deprecated_image"): + self.module.warn( + f"You try to use a deprecated image. The image {image.name} will " + f"continue to be available until {available_until.strftime('%Y-%m-%d')}." + ) + else: + self.module.fail_json( + msg=( + f"You try to use a deprecated image. The image {image.name} will " + f"continue to be available until {available_until.strftime('%Y-%m-%d')}. " + "If you want to use this image use allow_deprecated_image=true." + ) + ) + return image + + def _get_server_type(self): + server_type = self.client.server_types.get_by_name(self.module.params.get("server_type")) + if server_type is None: + try: + server_type = self.client.server_types.get_by_id(self.module.params.get("server_type")) + except HCloudException as exception: + self.fail_json_hcloud( + exception, + msg=f"server_type {self.module.params.get('server_type')} was not found", + ) + + self._check_and_warn_deprecated_server(server_type) + + return server_type + + def _check_and_warn_deprecated_server(self, server_type): + if server_type.deprecation is None: + return + + if server_type.deprecation.unavailable_after < datetime.now(timezone.utc): + self.module.warn( + f"Attention: The server plan {server_type.name} is deprecated and can " + "no longer be ordered. Existing servers of that plan will continue to " + "work as before and no action is required on your part. " + "It is possible to migrate this server to another server plan by setting " + "the server_type parameter on the hetzner.hcloud.server module." + ) + else: + server_type_unavailable_date = server_type.deprecation.unavailable_after.strftime("%Y-%m-%d") + self.module.warn( + f"Attention: The server plan {server_type.name} is deprecated and will " + f"no longer be available for order as of {server_type_unavailable_date}. " + "Existing servers of that plan will continue to work as before and no " + "action is required on your part. " + "It is possible to migrate this server to another server plan by setting " + "the server_type parameter on the hetzner.hcloud.server module." + ) + + def _get_placement_group(self): + if self.module.params.get("placement_group") is None: + return None + + placement_group = self.client.placement_groups.get_by_name(self.module.params.get("placement_group")) + if placement_group is None: + try: + placement_group = self.client.placement_groups.get_by_id(self.module.params.get("placement_group")) + except HCloudException as exception: + self.fail_json_hcloud( + exception, + msg=f"placement_group {self.module.params.get('placement_group')} was not found", + ) + + return placement_group + + def _get_primary_ip(self, field): + if self.module.params.get(field) is None: + return None + + primary_ip = self.client.primary_ips.get_by_name(self.module.params.get(field)) + if primary_ip is None: + try: + primary_ip = self.client.primary_ips.get_by_id(self.module.params.get(field)) + except HCloudException as exception: + self.fail_json_hcloud(exception, msg=f"primary_ip {self.module.params.get(field)} was not found") + + return primary_ip + + def _update_server(self): + if "force_upgrade" in self.module.params and self.module.params.get("force_upgrade") is not None: + self.module.warn("force_upgrade is deprecated, use force instead") + + try: + previous_server_status = self.hcloud_server.status + + rescue_mode = self.module.params.get("rescue_mode") + if rescue_mode and self.hcloud_server.rescue_enabled is False: + if not self.module.check_mode: + self._set_rescue_mode(rescue_mode) + self._mark_as_changed() + elif not rescue_mode and self.hcloud_server.rescue_enabled is True: + if not self.module.check_mode: + self.hcloud_server.disable_rescue().wait_until_finished() + self._mark_as_changed() + + backups = self.module.params.get("backups") + if backups and self.hcloud_server.backup_window is None: + if not self.module.check_mode: + self.hcloud_server.enable_backup().wait_until_finished() + self._mark_as_changed() + elif backups is not None and not backups and self.hcloud_server.backup_window is not None: + if not self.module.check_mode: + self.hcloud_server.disable_backup().wait_until_finished() + self._mark_as_changed() + + labels = self.module.params.get("labels") + if labels is not None and labels != self.hcloud_server.labels: + if not self.module.check_mode: + self.hcloud_server.update(labels=labels) + self._mark_as_changed() + + wanted_firewalls = self.module.params.get("firewalls") + if wanted_firewalls is not None: + # Removing existing but not wanted firewalls + for current_firewall in self.hcloud_server.public_net.firewalls: + if current_firewall.firewall.name not in wanted_firewalls: + self._mark_as_changed() + if self.module.check_mode: + continue + + firewall_resource = FirewallResource(type="server", server=self.hcloud_server) + actions = self.client.firewalls.remove_from_resources( + current_firewall.firewall, + [firewall_resource], + ) + for action in actions: + action.wait_until_finished() + + # Adding wanted firewalls that doesn't exist yet + for firewall_name in wanted_firewalls: + found = False + for firewall in self.hcloud_server.public_net.firewalls: + if firewall.firewall.name == firewall_name: + found = True + break + + if not found: + self._mark_as_changed() + if not self.module.check_mode: + firewall = self.client.firewalls.get_by_name(firewall_name) + if firewall is None: + self.module.fail_json(msg=f"firewall {firewall_name} was not found") + firewall_resource = FirewallResource(type="server", server=self.hcloud_server) + actions = self.client.firewalls.apply_to_resources(firewall, [firewall_resource]) + for action in actions: + action.wait_until_finished() + + if "placement_group" in self.module.params: + if self.module.params["placement_group"] is None and self.hcloud_server.placement_group is not None: + if not self.module.check_mode: + self.hcloud_server.remove_from_placement_group().wait_until_finished() + self._mark_as_changed() + else: + placement_group = self._get_placement_group() + if placement_group is not None and ( + self.hcloud_server.placement_group is None + or self.hcloud_server.placement_group.id != placement_group.id + ): + self.stop_server_if_forced() + if not self.module.check_mode: + self.hcloud_server.add_to_placement_group(placement_group).wait_until_finished() + self._mark_as_changed() + + if "ipv4" in self.module.params: + if ( + self.module.params["ipv4"] is None + and self.hcloud_server.public_net.primary_ipv4 is not None + and not self.module.params.get("enable_ipv4") + ): + self.stop_server_if_forced() + if not self.module.check_mode: + self.hcloud_server.public_net.primary_ipv4.unassign().wait_until_finished() + self._mark_as_changed() + else: + primary_ip = self._get_primary_ip("ipv4") + if primary_ip is not None and ( + self.hcloud_server.public_net.primary_ipv4 is None + or self.hcloud_server.public_net.primary_ipv4.id != primary_ip.id + ): + self.stop_server_if_forced() + if not self.module.check_mode: + if self.hcloud_server.public_net.primary_ipv4: + self.hcloud_server.public_net.primary_ipv4.unassign().wait_until_finished() + primary_ip.assign(self.hcloud_server.id, "server").wait_until_finished() + self._mark_as_changed() + if "ipv6" in self.module.params: + if ( + (self.module.params["ipv6"] is None or self.module.params["ipv6"] == "") + and self.hcloud_server.public_net.primary_ipv6 is not None + and not self.module.params.get("enable_ipv6") + ): + self.stop_server_if_forced() + if not self.module.check_mode: + self.hcloud_server.public_net.primary_ipv6.unassign().wait_until_finished() + self._mark_as_changed() + else: + primary_ip = self._get_primary_ip("ipv6") + if primary_ip is not None and ( + self.hcloud_server.public_net.primary_ipv6 is None + or self.hcloud_server.public_net.primary_ipv6.id != primary_ip.id + ): + self.stop_server_if_forced() + if not self.module.check_mode: + if self.hcloud_server.public_net.primary_ipv6 is not None: + self.hcloud_server.public_net.primary_ipv6.unassign().wait_until_finished() + primary_ip.assign(self.hcloud_server.id, "server").wait_until_finished() + self._mark_as_changed() + if "private_networks" in self.module.params and self.module.params["private_networks"] is not None: + if not bool(self.module.params["private_networks"]): + # This handles None, "" and [] + networks_target = {} + else: + _networks = {} + for network_name_or_id in self.module.params.get("private_networks"): + _found_network = self.client.networks.get_by_name( + network_name_or_id + ) or self.client.networks.get_by_id(network_name_or_id) + _networks.update({_found_network.id: _found_network}) + networks_target = _networks + networks_is = dict() + for p_network in self.hcloud_server.private_net: + networks_is.update({p_network.network.id: p_network.network}) + for network_id in set(list(networks_is) + list(networks_target)): + if network_id in networks_is and network_id not in networks_target: + self.stop_server_if_forced() + if not self.module.check_mode: + self.hcloud_server.detach_from_network(networks_is[network_id]).wait_until_finished() + self._mark_as_changed() + elif network_id in networks_target and network_id not in networks_is: + self.stop_server_if_forced() + if not self.module.check_mode: + self.hcloud_server.attach_to_network(networks_target[network_id]).wait_until_finished() + self._mark_as_changed() + + server_type = self.module.params.get("server_type") + if server_type is not None: + if self.hcloud_server.server_type.name == server_type: + # Check if we should warn for using an deprecated server type + self._check_and_warn_deprecated_server(self.hcloud_server.server_type) + + else: + # Server type should be changed + self.stop_server_if_forced() + + timeout = 100 + if self.module.params.get("upgrade_disk"): + timeout = 1000 # When we upgrade the disk to the resize progress takes some more time. + if not self.module.check_mode: + self.hcloud_server.change_type( + server_type=self._get_server_type(), + upgrade_disk=self.module.params.get("upgrade_disk"), + ).wait_until_finished(timeout) + self._mark_as_changed() + + if not self.module.check_mode and ( + (self.module.params.get("state") == "present" and previous_server_status == Server.STATUS_RUNNING) + or self.module.params.get("state") == "started" + ): + self.start_server() + + delete_protection = self.module.params.get("delete_protection") + rebuild_protection = self.module.params.get("rebuild_protection") + if (delete_protection is not None and rebuild_protection is not None) and ( + delete_protection != self.hcloud_server.protection["delete"] + or rebuild_protection != self.hcloud_server.protection["rebuild"] + ): + if not self.module.check_mode: + self.hcloud_server.change_protection( + delete=delete_protection, + rebuild=rebuild_protection, + ).wait_until_finished() + self._mark_as_changed() + self._get_server() + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _set_rescue_mode(self, rescue_mode): + if self.module.params.get("ssh_keys"): + resp = self.hcloud_server.enable_rescue( + type=rescue_mode, + ssh_keys=[ + self.client.ssh_keys.get_by_name(ssh_key_name).id + for ssh_key_name in self.module.params.get("ssh_keys") + ], + ) + else: + resp = self.hcloud_server.enable_rescue(type=rescue_mode) + resp.action.wait_until_finished() + self.result["root_password"] = resp.root_password + + def start_server(self): + try: + if self.hcloud_server: + if self.hcloud_server.status != Server.STATUS_RUNNING: + if not self.module.check_mode: + self.client.servers.power_on(self.hcloud_server).wait_until_finished() + self._mark_as_changed() + self._get_server() + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def stop_server(self): + try: + if self.hcloud_server: + if self.hcloud_server.status != Server.STATUS_OFF: + if not self.module.check_mode: + self.client.servers.power_off(self.hcloud_server).wait_until_finished() + self._mark_as_changed() + self._get_server() + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def stop_server_if_forced(self): + previous_server_status = self.hcloud_server.status + if previous_server_status == Server.STATUS_RUNNING and not self.module.check_mode: + if ( + self.module.params.get("force_upgrade") + or self.module.params.get("force") + or self.module.params.get("state") == "stopped" + ): + self.stop_server() # Only stopped server can be upgraded + return previous_server_status + else: + self.module.warn( + f"You can not upgrade a running instance {self.hcloud_server.name}. " + "You need to stop the instance or use force=true." + ) + + return None + + def rebuild_server(self): + self.module.fail_on_missing_params(required_params=["image"]) + try: + if not self.module.check_mode: + image = self._get_image(self.hcloud_server.server_type) + # When we rebuild the server progress takes some more time. + resp = self.client.servers.rebuild(self.hcloud_server, image, return_response=True) + resp.action.wait_until_finished(1000) + self._mark_as_changed() + + self._get_server() + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def present_server(self): + self._get_server() + if self.hcloud_server is None: + self._create_server() + else: + self._update_server() + + def delete_server(self): + try: + self._get_server() + if self.hcloud_server is not None: + if not self.module.check_mode: + self.client.servers.delete(self.hcloud_server).wait_until_finished() + self._mark_as_changed() + self.hcloud_server = None + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + image={"type": "str"}, + server_type={"type": "str"}, + location={"type": "str"}, + datacenter={"type": "str"}, + user_data={"type": "str"}, + ssh_keys={"type": "list", "elements": "str", "no_log": False}, + volumes={"type": "list", "elements": "str"}, + firewalls={"type": "list", "elements": "str"}, + labels={"type": "dict"}, + backups={"type": "bool"}, + upgrade_disk={"type": "bool", "default": False}, + enable_ipv4={"type": "bool", "default": True}, + enable_ipv6={"type": "bool", "default": True}, + ipv4={"type": "str"}, + ipv6={"type": "str"}, + private_networks={"type": "list", "elements": "str", "default": None}, + force={"type": "bool", "default": False}, + force_upgrade={"type": "bool"}, + allow_deprecated_image={"type": "bool", "default": False}, + rescue_mode={"type": "str"}, + delete_protection={"type": "bool"}, + rebuild_protection={"type": "bool"}, + placement_group={"type": "str"}, + state={ + "choices": ["absent", "present", "restarted", "started", "stopped", "rebuild"], + "default": "present", + }, + **super().base_module_arguments(), + ), + required_one_of=[["id", "name"]], + mutually_exclusive=[["location", "datacenter"]], + required_together=[["delete_protection", "rebuild_protection"]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudServer.define_module() + + hcloud = AnsibleHCloudServer(module) + state = module.params.get("state") + if state == "absent": + hcloud.delete_server() + elif state == "present": + hcloud.present_server() + elif state == "started": + hcloud.present_server() + hcloud.start_server() + elif state == "stopped": + hcloud.present_server() + hcloud.stop_server() + elif state == "restarted": + hcloud.present_server() + hcloud.stop_server() + hcloud.start_server() + elif state == "rebuild": + hcloud.present_server() + hcloud.rebuild_server() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/server_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/server_info.py new file mode 100644 index 000000000..cee1634cb --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/server_info.py @@ -0,0 +1,238 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: server_info + +short_description: Gather infos about your Hetzner Cloud servers. + + +description: + - Gather infos about your Hetzner Cloud servers. + +author: + - Lukas Kaemmerling (@LKaemmerling) + +options: + id: + description: + - The ID of the server you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name of the server you want to get. + type: str + label_selector: + description: + - The label selector for the server you want to get. + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Gather hcloud server infos + hetzner.hcloud.server_info: + register: output + +- name: Print the gathered infos + debug: + var: output.hcloud_server_info +""" + +RETURN = """ +hcloud_server_info: + description: The server infos as list + returned: always + type: complex + contains: + id: + description: Numeric identifier of the server + returned: always + type: int + sample: 1937415 + name: + description: Name of the server + returned: always + type: str + sample: my-server + created: + description: Point in time when the Server was created (in ISO-8601 format) + returned: always + type: str + sample: "2023-11-06T13:36:56+00:00" + status: + description: Status of the server + returned: always + type: str + sample: running + server_type: + description: Name of the server type of the server + returned: always + type: str + sample: cx11 + ipv4_address: + description: Public IPv4 address of the server + returned: always + type: str + sample: 116.203.104.109 + ipv6: + description: IPv6 network of the server + returned: always + type: str + sample: 2a01:4f8:1c1c:c140::/64 + private_networks: + description: List of private networks the server is attached to (name) + returned: always + type: list + elements: str + sample: ['my-network', 'another-network'] + private_networks_info: + description: List of private networks the server is attached to (dict with name and ip) + returned: always + type: list + elements: dict + sample: [{'name': 'my-network', 'ip': '192.168.1.1'}, {'name': 'another-network', 'ip': '10.185.50.40'}] + location: + description: Name of the location of the server + returned: always + type: str + sample: fsn1 + placement_group: + description: Placement Group of the server + type: str + returned: always + sample: 4711 + version_added: "1.5.0" + datacenter: + description: Name of the datacenter of the server + returned: always + type: str + sample: fsn1-dc14 + rescue_enabled: + description: True if rescue mode is enabled, Server will then boot into rescue system on next reboot + returned: always + type: bool + sample: false + backup_window: + description: Time window (UTC) in which the backup will run, or null if the backups are not enabled + returned: always + type: bool + sample: 22-02 + labels: + description: User-defined labels (key-value pairs) + returned: always + type: dict + delete_protection: + description: True if server is protected for deletion + type: bool + returned: always + sample: false + version_added: "0.1.0" + rebuild_protection: + description: True if server is protected for rebuild + type: bool + returned: always + sample: false + version_added: "0.1.0" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.servers import BoundServer + + +class AnsibleHCloudServerInfo(AnsibleHCloud): + represent = "hcloud_server_info" + + hcloud_server_info: list[BoundServer] | None = None + + def _prepare_result(self): + tmp = [] + + for server in self.hcloud_server_info: + if server is not None: + image = None if server.image is None else to_native(server.image.name) + placement_group = None if server.placement_group is None else to_native(server.placement_group.name) + ipv4_address = None if server.public_net.ipv4 is None else to_native(server.public_net.ipv4.ip) + ipv6 = None if server.public_net.ipv6 is None else to_native(server.public_net.ipv6.ip) + backup_window = None if server.backup_window is None else to_native(server.backup_window) + tmp.append( + { + "id": to_native(server.id), + "name": to_native(server.name), + "created": to_native(server.created.isoformat()), + "ipv4_address": ipv4_address, + "ipv6": ipv6, + "private_networks": [to_native(net.network.name) for net in server.private_net], + "private_networks_info": [ + {"name": to_native(net.network.name), "ip": net.ip} for net in server.private_net + ], + "image": image, + "server_type": to_native(server.server_type.name), + "datacenter": to_native(server.datacenter.name), + "location": to_native(server.datacenter.location.name), + "placement_group": placement_group, + "rescue_enabled": server.rescue_enabled, + "backup_window": backup_window, + "labels": server.labels, + "status": to_native(server.status), + "delete_protection": server.protection["delete"], + "rebuild_protection": server.protection["rebuild"], + } + ) + return tmp + + def get_servers(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_server_info = [self.client.servers.get_by_id(self.module.params.get("id"))] + elif self.module.params.get("name") is not None: + self.hcloud_server_info = [self.client.servers.get_by_name(self.module.params.get("name"))] + elif self.module.params.get("label_selector") is not None: + self.hcloud_server_info = self.client.servers.get_all( + label_selector=self.module.params.get("label_selector") + ) + else: + self.hcloud_server_info = self.client.servers.get_all() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + label_selector={"type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudServerInfo.define_module() + hcloud = AnsibleHCloudServerInfo(module) + + hcloud.get_servers() + result = hcloud.get_result() + + ansible_info = {"hcloud_server_info": result["hcloud_server_info"]} + module.exit_json(**ansible_info) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/server_network.py b/ansible_collections/hetzner/hcloud/plugins/modules/server_network.py new file mode 100644 index 000000000..ca80a8a76 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/server_network.py @@ -0,0 +1,246 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: server_network + +short_description: Manage the relationship between Hetzner Cloud Networks and servers + + +description: + - Create and delete the relationship Hetzner Cloud Networks and servers + +author: + - Lukas Kaemmerling (@lkaemmerling) + +options: + network: + description: + - Name or ID of the Hetzner Cloud Networks. + type: str + required: true + server: + description: + - Name or ID of the Hetzner Cloud server. + type: str + required: true + ip: + description: + - The IP the server should have. + type: str + alias_ips: + description: + - Alias IPs the server has. + type: list + elements: str + state: + description: + - State of the server_network. + default: present + choices: [ absent, present ] + type: str + +extends_documentation_fragment: +- hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a basic server network + hetzner.hcloud.server_network: + network: my-network + server: my-server + state: present + +- name: Create a server network and specify the ip address + hetzner.hcloud.server_network: + network: my-network + server: my-server + ip: 10.0.0.1 + state: present + +- name: Create a server network and add alias ips + hetzner.hcloud.server_network: + network: my-network + server: my-server + ip: 10.0.0.1 + alias_ips: + - 10.1.0.1 + - 10.2.0.1 + state: present + +- name: Ensure the server network is absent (remove if needed) + hetzner.hcloud.server_network: + network: my-network + server: my-server + state: absent +""" + +RETURN = """ +hcloud_server_network: + description: The relationship between a server and a network + returned: always + type: complex + contains: + network: + description: Name of the Network + type: str + returned: always + sample: my-network + server: + description: Name of the server + type: str + returned: always + sample: my-server + ip: + description: IP of the server within the Network ip range + type: str + returned: always + sample: 10.0.0.8 + alias_ips: + description: Alias IPs of the server within the Network ip range + type: list + elements: str + returned: always + sample: [10.1.0.1, ...] +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import APIException, HCloudException +from ..module_utils.vendor.hcloud.networks import BoundNetwork +from ..module_utils.vendor.hcloud.servers import BoundServer, PrivateNet + + +class AnsibleHCloudServerNetwork(AnsibleHCloud): + represent = "hcloud_server_network" + + hcloud_network: BoundNetwork | None = None + hcloud_server: BoundServer | None = None + hcloud_server_network: PrivateNet | None = None + + def _prepare_result(self): + return { + "network": to_native(self.hcloud_network.name), + "server": to_native(self.hcloud_server.name), + "ip": to_native(self.hcloud_server_network.ip), + "alias_ips": self.hcloud_server_network.alias_ips, + } + + def _get_server_and_network(self): + try: + self.hcloud_network = self._client_get_by_name_or_id( + "networks", + self.module.params.get("network"), + ) + self.hcloud_server = self._client_get_by_name_or_id( + "servers", + self.module.params.get("server"), + ) + self.hcloud_server_network = None + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _get_server_network(self): + for private_net in self.hcloud_server.private_net: + if private_net.network.id == self.hcloud_network.id: + self.hcloud_server_network = private_net + + def _create_server_network(self): + params = { + "network": self.hcloud_network, + } + + if self.module.params.get("ip") is not None: + params["ip"] = self.module.params.get("ip") + if self.module.params.get("alias_ips") is not None: + params["alias_ips"] = self.module.params.get("alias_ips") + + if not self.module.check_mode: + try: + self.hcloud_server.attach_to_network(**params).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + + self._mark_as_changed() + self._get_server_and_network() + self._get_server_network() + + def _update_server_network(self): + params = { + "network": self.hcloud_network, + } + alias_ips = self.module.params.get("alias_ips") + if alias_ips is not None and sorted(self.hcloud_server_network.alias_ips) != sorted(alias_ips): + params["alias_ips"] = alias_ips + + if not self.module.check_mode: + try: + self.hcloud_server.change_alias_ips(**params).wait_until_finished() + except APIException as exception: + self.fail_json_hcloud(exception) + + self._mark_as_changed() + self._get_server_and_network() + self._get_server_network() + + def present_server_network(self): + self._get_server_and_network() + self._get_server_network() + if self.hcloud_server_network is None: + self._create_server_network() + else: + self._update_server_network() + + def delete_server_network(self): + self._get_server_and_network() + self._get_server_network() + if self.hcloud_server_network is not None and self.hcloud_server is not None: + if not self.module.check_mode: + try: + self.hcloud_server.detach_from_network(self.hcloud_server_network.network).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self.hcloud_server_network = None + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + network={"type": "str", "required": True}, + server={"type": "str", "required": True}, + ip={"type": "str"}, + alias_ips={"type": "list", "elements": "str"}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudServerNetwork.define_module() + + hcloud = AnsibleHCloudServerNetwork(module) + state = module.params["state"] + if state == "absent": + hcloud.delete_server_network() + elif state == "present": + hcloud.present_server_network() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/server_type_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/server_type_info.py new file mode 100644 index 000000000..61f1f5011 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/server_type_info.py @@ -0,0 +1,204 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: server_type_info + +short_description: Gather infos about the Hetzner Cloud server types. + + +description: + - Gather infos about your Hetzner Cloud server types. + +author: + - Lukas Kaemmerling (@LKaemmerling) + +options: + id: + description: + - The ID of the server type you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name of the server type you want to get. + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Gather hcloud server type infos + hetzner.hcloud.server_type_info: + register: output + +- name: Print the gathered infos + debug: + var: output.hcloud_server_type_info +""" + +RETURN = """ +hcloud_server_type_info: + description: The server type infos as list + returned: always + type: complex + contains: + id: + description: Numeric identifier of the server type + returned: always + type: int + sample: 1937415 + name: + description: Name of the server type + returned: always + type: str + sample: fsn1 + description: + description: Detail description of the server type + returned: always + type: str + sample: Falkenstein DC Park 1 + cores: + description: Number of cpu cores a server of this type will have + returned: always + type: int + sample: 1 + memory: + description: Memory a server of this type will have in GB + returned: always + type: int + sample: 1 + disk: + description: Disk size a server of this type will have in GB + returned: always + type: int + sample: 25 + storage_type: + description: Type of server boot drive + returned: always + type: str + sample: local + cpu_type: + description: Type of cpu + returned: always + type: str + sample: shared + architecture: + description: Architecture of cpu + returned: always + type: str + sample: x86 + included_traffic: + description: Free traffic per month in bytes + returned: always + type: int + sample: 21990232555520 + deprecation: + description: | + Describes if, when & how the resources was deprecated. + If this field is set to None the resource is not deprecated. If it has a value, it is considered deprecated. + returned: success + type: dict + contains: + announced: + description: Date of when the deprecation was announced. + returned: success + type: str + sample: "2021-11-09T09:00:00+00:00" + unavailable_after: + description: | + After the time in this field, the resource will not be available from the general listing + endpoint of the resource type, and it can not be used in new resources. For example, if this is + an image, you can not create new servers with this image after the mentioned date. + returned: success + type: str + sample: "2021-12-01T00:00:00+00:00" + +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.server_types import BoundServerType + + +class AnsibleHCloudServerTypeInfo(AnsibleHCloud): + represent = "hcloud_server_type_info" + + hcloud_server_type_info: list[BoundServerType] | None = None + + def _prepare_result(self): + tmp = [] + + for server_type in self.hcloud_server_type_info: + if server_type is not None: + tmp.append( + { + "id": to_native(server_type.id), + "name": to_native(server_type.name), + "description": to_native(server_type.description), + "cores": server_type.cores, + "memory": server_type.memory, + "disk": server_type.disk, + "storage_type": to_native(server_type.storage_type), + "cpu_type": to_native(server_type.cpu_type), + "architecture": to_native(server_type.architecture), + "included_traffic": server_type.included_traffic, + "deprecation": ( + { + "announced": server_type.deprecation.announced.isoformat(), + "unavailable_after": server_type.deprecation.unavailable_after.isoformat(), + } + if server_type.deprecation is not None + else None + ), + } + ) + return tmp + + def get_server_types(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_server_type_info = [self.client.server_types.get_by_id(self.module.params.get("id"))] + elif self.module.params.get("name") is not None: + self.hcloud_server_type_info = [self.client.server_types.get_by_name(self.module.params.get("name"))] + else: + self.hcloud_server_type_info = self.client.server_types.get_all() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudServerTypeInfo.define_module() + hcloud = AnsibleHCloudServerTypeInfo(module) + + hcloud.get_server_types() + result = hcloud.get_result() + + ansible_info = {"hcloud_server_type_info": result["hcloud_server_type_info"]} + module.exit_json(**ansible_info) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/ssh_key.py b/ansible_collections/hetzner/hcloud/plugins/modules/ssh_key.py new file mode 100644 index 000000000..349c52c68 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/ssh_key.py @@ -0,0 +1,234 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: ssh_key + +short_description: Create and manage ssh keys on the Hetzner Cloud. + + +description: + - Create, update and manage ssh keys on the Hetzner Cloud. + +author: + - Lukas Kaemmerling (@LKaemmerling) + +options: + id: + description: + - The ID of the Hetzner Cloud ssh_key to manage. + - Only required if no ssh_key I(name) is given + type: int + name: + description: + - The Name of the Hetzner Cloud ssh_key to manage. + - Only required if no ssh_key I(id) is given or a ssh_key does not exist. + type: str + fingerprint: + description: + - The Fingerprint of the Hetzner Cloud ssh_key to manage. + - Only required if no ssh_key I(id) or I(name) is given. + type: str + labels: + description: + - User-defined labels (key-value pairs) + type: dict + public_key: + description: + - The Public Key to add. + - Required if ssh_key does not exist. + type: str + state: + description: + - State of the ssh_key. + default: present + choices: [ absent, present ] + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Create a basic ssh_key + hetzner.hcloud.ssh_key: + name: my-ssh_key + public_key: ssh-rsa AAAjjk76kgf...Xt + state: present + +- name: Create a ssh_key with labels + hetzner.hcloud.ssh_key: + name: my-ssh_key + public_key: ssh-rsa AAAjjk76kgf...Xt + labels: + key: value + mylabel: 123 + state: present + +- name: Ensure the ssh_key is absent (remove if needed) + hetzner.hcloud.ssh_key: + name: my-ssh_key + state: absent +""" + +RETURN = """ +hcloud_ssh_key: + description: The ssh_key instance + returned: Always + type: complex + contains: + id: + description: ID of the ssh_key + type: int + returned: Always + sample: 12345 + name: + description: Name of the ssh_key + type: str + returned: Always + sample: my-ssh-key + fingerprint: + description: Fingerprint of the ssh_key + type: str + returned: Always + sample: b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f + public_key: + description: Public key of the ssh_key + type: str + returned: Always + sample: "ssh-rsa AAAjjk76kgf...Xt" + labels: + description: User-defined labels (key-value pairs) + type: dict + returned: Always + sample: + key: value + mylabel: 123 +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.ssh_keys import BoundSSHKey + + +class AnsibleHCloudSSHKey(AnsibleHCloud): + represent = "hcloud_ssh_key" + + hcloud_ssh_key: BoundSSHKey | None = None + + def _prepare_result(self): + return { + "id": to_native(self.hcloud_ssh_key.id), + "name": to_native(self.hcloud_ssh_key.name), + "fingerprint": to_native(self.hcloud_ssh_key.fingerprint), + "public_key": to_native(self.hcloud_ssh_key.public_key), + "labels": self.hcloud_ssh_key.labels, + } + + def _get_ssh_key(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_ssh_key = self.client.ssh_keys.get_by_id(self.module.params.get("id")) + elif self.module.params.get("fingerprint") is not None: + self.hcloud_ssh_key = self.client.ssh_keys.get_by_fingerprint(self.module.params.get("fingerprint")) + elif self.module.params.get("name") is not None: + self.hcloud_ssh_key = self.client.ssh_keys.get_by_name(self.module.params.get("name")) + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _create_ssh_key(self): + self.module.fail_on_missing_params(required_params=["name", "public_key"]) + params = { + "name": self.module.params.get("name"), + "public_key": self.module.params.get("public_key"), + "labels": self.module.params.get("labels"), + } + + if not self.module.check_mode: + try: + self.client.ssh_keys.create(**params) + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self._get_ssh_key() + + def _update_ssh_key(self): + name = self.module.params.get("name") + if name is not None and self.hcloud_ssh_key.name != name: + self.module.fail_on_missing_params(required_params=["id"]) + if not self.module.check_mode: + self.hcloud_ssh_key.update(name=name) + self._mark_as_changed() + + labels = self.module.params.get("labels") + if labels is not None and self.hcloud_ssh_key.labels != labels: + if not self.module.check_mode: + self.hcloud_ssh_key.update(labels=labels) + self._mark_as_changed() + + self._get_ssh_key() + + def present_ssh_key(self): + self._get_ssh_key() + if self.hcloud_ssh_key is None: + self._create_ssh_key() + else: + self._update_ssh_key() + + def delete_ssh_key(self): + self._get_ssh_key() + if self.hcloud_ssh_key is not None: + if not self.module.check_mode: + try: + self.client.ssh_keys.delete(self.hcloud_ssh_key) + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self.hcloud_ssh_key = None + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + public_key={"type": "str"}, + fingerprint={"type": "str"}, + labels={"type": "dict"}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + required_one_of=[["id", "name", "fingerprint"]], + required_if=[["state", "present", ["name"]]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudSSHKey.define_module() + + hcloud = AnsibleHCloudSSHKey(module) + state = module.params.get("state") + if state == "absent": + hcloud.delete_ssh_key() + elif state == "present": + hcloud.present_ssh_key() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/ssh_key_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/ssh_key_info.py new file mode 100644 index 000000000..7a4ab5928 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/ssh_key_info.py @@ -0,0 +1,156 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: ssh_key_info +short_description: Gather infos about your Hetzner Cloud ssh_keys. +description: + - Gather facts about your Hetzner Cloud ssh_keys. +author: + - Christopher Schmitt (@cschmitt-hcloud) +options: + id: + description: + - The ID of the ssh key you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name of the ssh key you want to get. + type: str + fingerprint: + description: + - The fingerprint of the ssh key you want to get. + type: str + label_selector: + description: + - The label selector for the ssh key you want to get. + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Gather hcloud sshkey infos + hetzner.hcloud.ssh_key_info: + register: output +- name: Print the gathered infos + debug: + var: output.hcloud_ssh_key_info +""" + +RETURN = """ +hcloud_ssh_key_info: + description: The ssh key instances + returned: Always + type: complex + contains: + id: + description: Numeric identifier of the ssh_key + returned: always + type: int + sample: 1937415 + name: + description: Name of the ssh_key + returned: always + type: str + sample: my-ssh-key + fingerprint: + description: Fingerprint of the ssh key + returned: always + type: str + sample: 0e:e0:bd:c7:2d:1f:69:49:94:44:91:f1:19:fd:35:f3 + public_key: + description: The actual public key + returned: always + type: str + sample: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGpl/tnk74nnQJxxLAtutUApUZMRJxryKh7VXkNbd4g9 john@example.com" + labels: + description: User-defined labels (key-value pairs) + returned: always + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.ssh_keys import BoundSSHKey + + +class AnsibleHCloudSSHKeyInfo(AnsibleHCloud): + represent = "hcloud_ssh_key_info" + + hcloud_ssh_key_info: list[BoundSSHKey] | None = None + + def _prepare_result(self): + ssh_keys = [] + + for ssh_key in self.hcloud_ssh_key_info: + if ssh_key: + ssh_keys.append( + { + "id": to_native(ssh_key.id), + "name": to_native(ssh_key.name), + "fingerprint": to_native(ssh_key.fingerprint), + "public_key": to_native(ssh_key.public_key), + "labels": ssh_key.labels, + } + ) + return ssh_keys + + def get_ssh_keys(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_ssh_key_info = [self.client.ssh_keys.get_by_id(self.module.params.get("id"))] + elif self.module.params.get("name") is not None: + self.hcloud_ssh_key_info = [self.client.ssh_keys.get_by_name(self.module.params.get("name"))] + elif self.module.params.get("fingerprint") is not None: + self.hcloud_ssh_key_info = [ + self.client.ssh_keys.get_by_fingerprint(self.module.params.get("fingerprint")) + ] + elif self.module.params.get("label_selector") is not None: + self.hcloud_ssh_key_info = self.client.ssh_keys.get_all( + label_selector=self.module.params.get("label_selector") + ) + else: + self.hcloud_ssh_key_info = self.client.ssh_keys.get_all() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + fingerprint={"type": "str"}, + label_selector={"type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudSSHKeyInfo.define_module() + hcloud = AnsibleHCloudSSHKeyInfo(module) + + hcloud.get_ssh_keys() + result = hcloud.get_result() + + ansible_info = {"hcloud_ssh_key_info": result["hcloud_ssh_key_info"]} + module.exit_json(**ansible_info) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/subnetwork.py b/ansible_collections/hetzner/hcloud/plugins/modules/subnetwork.py new file mode 100644 index 000000000..aea40bb13 --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/subnetwork.py @@ -0,0 +1,236 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: subnetwork + +short_description: Manage cloud subnetworks on the Hetzner Cloud. + + +description: + - Create, update and delete cloud subnetworks on the Hetzner Cloud. + +author: + - Lukas Kaemmerling (@lkaemmerling) + +options: + network: + description: + - The name or ID of the Hetzner Cloud Networks. + type: str + required: true + ip_range: + description: + - IP range of the subnetwork. + type: str + required: true + type: + description: + - Type of subnetwork. + type: str + choices: [ server, cloud, vswitch ] + required: true + network_zone: + description: + - Name of network zone. + type: str + required: true + vswitch_id: + description: + - ID of the vSwitch you want to couple with your Network. + - Required if type == vswitch + type: int + state: + description: + - State of the subnetwork. + default: present + choices: [ absent, present ] + type: str + +extends_documentation_fragment: +- hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a basic subnetwork + hetzner.hcloud.subnetwork: + network: my-network + ip_range: 10.0.0.0/16 + network_zone: eu-central + type: cloud + state: present + +- name: Create a basic subnetwork + hetzner.hcloud.subnetwork: + network: my-vswitch-network + ip_range: 10.0.0.0/24 + network_zone: eu-central + type: vswitch + vswitch_id: 123 + state: present + +- name: Ensure the subnetwork is absent (remove if needed) + hetzner.hcloud.subnetwork: + network: my-network + ip_range: 10.0.0.0/8 + network_zone: eu-central + type: cloud + state: absent +""" + +RETURN = """ +hcloud_subnetwork: + description: One Subnet of a Network + returned: always + type: complex + contains: + network: + description: Name of the Network + type: str + returned: always + sample: my-network + ip_range: + description: IP range of the Network + type: str + returned: always + sample: 10.0.0.0/8 + type: + description: Type of subnetwork + type: str + returned: always + sample: server + network_zone: + description: Name of network zone + type: str + returned: always + sample: eu-central + vswitch_id: + description: ID of the vswitch, null if not type vswitch + type: int + returned: always + sample: 123 + gateway: + description: Gateway of the subnetwork + type: str + returned: always + sample: 10.0.0.1 +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.networks import BoundNetwork, NetworkSubnet + + +class AnsibleHCloudSubnetwork(AnsibleHCloud): + represent = "hcloud_subnetwork" + + hcloud_network: BoundNetwork | None = None + hcloud_subnetwork: NetworkSubnet | None = None + + def _prepare_result(self): + return { + "network": to_native(self.hcloud_network.name), + "ip_range": to_native(self.hcloud_subnetwork.ip_range), + "type": to_native(self.hcloud_subnetwork.type), + "network_zone": to_native(self.hcloud_subnetwork.network_zone), + "gateway": self.hcloud_subnetwork.gateway, + "vswitch_id": self.hcloud_subnetwork.vswitch_id, + } + + def _get_network(self): + try: + self.hcloud_network = self._client_get_by_name_or_id( + "networks", + self.module.params.get("network"), + ) + self.hcloud_subnetwork = None + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _get_subnetwork(self): + subnet_ip_range = self.module.params.get("ip_range") + for subnetwork in self.hcloud_network.subnets: + if subnetwork.ip_range == subnet_ip_range: + self.hcloud_subnetwork = subnetwork + + def _create_subnetwork(self): + params = { + "ip_range": self.module.params.get("ip_range"), + "type": self.module.params.get("type"), + "network_zone": self.module.params.get("network_zone"), + } + if self.module.params.get("type") == NetworkSubnet.TYPE_VSWITCH: + self.module.fail_on_missing_params(required_params=["vswitch_id"]) + params["vswitch_id"] = self.module.params.get("vswitch_id") + + if not self.module.check_mode: + try: + self.hcloud_network.add_subnet(subnet=NetworkSubnet(**params)).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + + self._mark_as_changed() + self._get_network() + self._get_subnetwork() + + def present_subnetwork(self): + self._get_network() + self._get_subnetwork() + if self.hcloud_subnetwork is None: + self._create_subnetwork() + + def delete_subnetwork(self): + self._get_network() + self._get_subnetwork() + if self.hcloud_subnetwork is not None and self.hcloud_network is not None: + if not self.module.check_mode: + try: + self.hcloud_network.delete_subnet(self.hcloud_subnetwork).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self.hcloud_subnetwork = None + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + network={"type": "str", "required": True}, + network_zone={"type": "str", "required": True}, + type={"type": "str", "required": True, "choices": ["server", "cloud", "vswitch"]}, + ip_range={"type": "str", "required": True}, + vswitch_id={"type": "int"}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudSubnetwork.define_module() + + hcloud = AnsibleHCloudSubnetwork(module) + state = module.params["state"] + if state == "absent": + hcloud.delete_subnetwork() + elif state == "present": + hcloud.present_subnetwork() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/volume.py b/ansible_collections/hetzner/hcloud/plugins/modules/volume.py new file mode 100644 index 000000000..8442ed90b --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/volume.py @@ -0,0 +1,331 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: volume + +short_description: Create and manage block Volume on the Hetzner Cloud. + + +description: + - Create, update and attach/detach block Volume on the Hetzner Cloud. + +author: + - Christopher Schmitt (@cschmitt-hcloud) + +options: + id: + description: + - The ID of the Hetzner Cloud Block Volume to manage. + - Only required if no volume I(name) is given + type: int + name: + description: + - The Name of the Hetzner Cloud Block Volume to manage. + - Only required if no volume I(id) is given or a volume does not exist. + type: str + size: + description: + - The size of the Block Volume in GB. + - Required if volume does not yet exists. + type: int + automount: + description: + - Automatically mount the Volume. + type: bool + default: False + format: + description: + - Automatically Format the volume on creation + - Can only be used in case the Volume does not exist. + type: str + choices: [xfs, ext4] + location: + description: + - Location of the Hetzner Cloud Volume. + - Required if no I(server) is given and Volume does not exist. + type: str + server: + description: + - Server Name the Volume should be assigned to. + - Required if no I(location) is given and Volume does not exist. + type: str + delete_protection: + description: + - Protect the Volume for deletion. + type: bool + labels: + description: + - User-defined key-value pairs. + type: dict + state: + description: + - State of the Volume. + default: present + choices: [absent, present] + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Create a Volume + hetzner.hcloud.volume: + name: my-volume + location: fsn1 + size: 100 + state: present +- name: Create a Volume and format it with ext4 + hetzner.hcloud.volume: + name: my-volume + location: fsn + format: ext4 + size: 100 + state: present +- name: Mount a existing Volume and automount + hetzner.hcloud.volume: + name: my-volume + server: my-server + automount: true + state: present +- name: Mount a existing Volume and automount + hetzner.hcloud.volume: + name: my-volume + server: my-server + automount: true + state: present +- name: Ensure the Volume is absent (remove if needed) + hetzner.hcloud.volume: + name: my-volume + state: absent +""" + +RETURN = """ +hcloud_volume: + description: The block Volume + returned: Always + type: complex + contains: + id: + description: ID of the Volume + type: int + returned: Always + sample: 12345 + name: + description: Name of the Volume + type: str + returned: Always + sample: my-volume + size: + description: Size in GB of the Volume + type: int + returned: Always + sample: 1337 + linux_device: + description: Path to the device that contains the Volume. + returned: always + type: str + sample: /dev/disk/by-id/scsi-0HC_Volume_12345 + version_added: "0.1.0" + location: + description: Location name where the Volume is located at + type: str + returned: Always + sample: "fsn1" + labels: + description: User-defined labels (key-value pairs) + type: dict + returned: Always + sample: + key: value + mylabel: 123 + server: + description: Server name where the Volume is attached to + type: str + returned: Always + sample: "my-server" + delete_protection: + description: True if Volume is protected for deletion + type: bool + returned: always + sample: false + version_added: "0.1.0" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.volumes import BoundVolume + + +class AnsibleHCloudVolume(AnsibleHCloud): + represent = "hcloud_volume" + + hcloud_volume: BoundVolume | None = None + + def _prepare_result(self): + server_name = None + if self.hcloud_volume.server is not None: + server_name = to_native(self.hcloud_volume.server.name) + + return { + "id": to_native(self.hcloud_volume.id), + "name": to_native(self.hcloud_volume.name), + "size": self.hcloud_volume.size, + "location": to_native(self.hcloud_volume.location.name), + "labels": self.hcloud_volume.labels, + "server": server_name, + "linux_device": to_native(self.hcloud_volume.linux_device), + "delete_protection": self.hcloud_volume.protection["delete"], + } + + def _get_volume(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_volume = self.client.volumes.get_by_id(self.module.params.get("id")) + else: + self.hcloud_volume = self.client.volumes.get_by_name(self.module.params.get("name")) + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def _create_volume(self): + self.module.fail_on_missing_params(required_params=["name", "size"]) + params = { + "name": self.module.params.get("name"), + "size": self.module.params.get("size"), + "automount": self.module.params.get("automount"), + "format": self.module.params.get("format"), + "labels": self.module.params.get("labels"), + } + if self.module.params.get("server") is not None: + params["server"] = self.client.servers.get_by_name(self.module.params.get("server")) + elif self.module.params.get("location") is not None: + params["location"] = self.client.locations.get_by_name(self.module.params.get("location")) + else: + self.module.fail_json(msg="server or location is required") + + if not self.module.check_mode: + try: + resp = self.client.volumes.create(**params) + resp.action.wait_until_finished() + [action.wait_until_finished() for action in resp.next_actions] + delete_protection = self.module.params.get("delete_protection") + if delete_protection is not None: + self._get_volume() + self.hcloud_volume.change_protection(delete=delete_protection).wait_until_finished() + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() + self._get_volume() + + def _update_volume(self): + try: + size = self.module.params.get("size") + if size: + if self.hcloud_volume.size < size: + if not self.module.check_mode: + self.hcloud_volume.resize(size).wait_until_finished() + self._mark_as_changed() + elif self.hcloud_volume.size > size: + self.module.warn("Shrinking of volumes is not supported") + + server_name = self.module.params.get("server") + if server_name: + server = self.client.servers.get_by_name(server_name) + if self.hcloud_volume.server is None or self.hcloud_volume.server.name != server.name: + if not self.module.check_mode: + automount = self.module.params.get("automount", False) + self.hcloud_volume.attach(server, automount=automount).wait_until_finished() + self._mark_as_changed() + else: + if self.hcloud_volume.server is not None: + if not self.module.check_mode: + self.hcloud_volume.detach().wait_until_finished() + self._mark_as_changed() + + labels = self.module.params.get("labels") + if labels is not None and labels != self.hcloud_volume.labels: + if not self.module.check_mode: + self.hcloud_volume.update(labels=labels) + self._mark_as_changed() + + delete_protection = self.module.params.get("delete_protection") + if delete_protection is not None and delete_protection != self.hcloud_volume.protection["delete"]: + if not self.module.check_mode: + self.hcloud_volume.change_protection(delete=delete_protection).wait_until_finished() + self._mark_as_changed() + + self._get_volume() + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def present_volume(self): + self._get_volume() + if self.hcloud_volume is None: + self._create_volume() + else: + self._update_volume() + + def delete_volume(self): + try: + self._get_volume() + if self.hcloud_volume is not None: + if not self.module.check_mode: + if self.hcloud_volume.server is not None: + self.hcloud_volume.detach().wait_until_finished() + self.client.volumes.delete(self.hcloud_volume) + self._mark_as_changed() + self.hcloud_volume = None + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + size={"type": "int"}, + location={"type": "str"}, + server={"type": "str"}, + labels={"type": "dict"}, + automount={"type": "bool", "default": False}, + format={"type": "str", "choices": ["xfs", "ext4"]}, + delete_protection={"type": "bool"}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + required_one_of=[["id", "name"]], + mutually_exclusive=[["location", "server"]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudVolume.define_module() + + hcloud = AnsibleHCloudVolume(module) + state = module.params.get("state") + if state == "absent": + module.fail_on_missing_params(required_params=["name"]) + hcloud.delete_volume() + else: + hcloud.present_volume() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/volume_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/volume_info.py new file mode 100644 index 000000000..1e507690e --- /dev/null +++ b/ansible_collections/hetzner/hcloud/plugins/modules/volume_info.py @@ -0,0 +1,174 @@ +#!/usr/bin/python + +# 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 annotations + +DOCUMENTATION = """ +--- +module: volume_info + +short_description: Gather infos about your Hetzner Cloud Volumes. + +description: + - Gather infos about your Hetzner Cloud Volumes. + +author: + - Lukas Kaemmerling (@LKaemmerling) + +options: + id: + description: + - The ID of the Volume you want to get. + - The module will fail if the provided ID is invalid. + type: int + name: + description: + - The name of the Volume you want to get. + type: str + label_selector: + description: + - The label selector for the Volume you want to get. + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +""" + +EXAMPLES = """ +- name: Gather hcloud Volume infos + hetzner.hcloud.volume_info: + register: output +- name: Print the gathered infos + debug: + var: output.hcloud_volume_info +""" + +RETURN = """ +hcloud_volume_info: + description: The Volume infos as list + returned: always + type: complex + contains: + id: + description: Numeric identifier of the Volume + returned: always + type: int + sample: 1937415 + name: + description: Name of the Volume + returned: always + type: str + sample: my-volume + size: + description: Size of the Volume + returned: always + type: str + sample: 10 + linux_device: + description: Path to the device that contains the Volume. + returned: always + type: str + sample: /dev/disk/by-id/scsi-0HC_Volume_12345 + version_added: "0.1.0" + location: + description: Name of the location where the Volume resides in + returned: always + type: str + sample: fsn1 + server: + description: Name of the server where the Volume is attached to + returned: always + type: str + sample: my-server + delete_protection: + description: True if the Volume is protected for deletion + returned: always + type: bool + version_added: "0.1.0" + labels: + description: User-defined labels (key-value pairs) + returned: always + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.volumes import BoundVolume + + +class AnsibleHCloudVolumeInfo(AnsibleHCloud): + represent = "hcloud_volume_info" + + hcloud_volume_info: list[BoundVolume] | None = None + + def _prepare_result(self): + tmp = [] + + for volume in self.hcloud_volume_info: + if volume is not None: + server_name = None + if volume.server is not None: + server_name = to_native(volume.server.name) + tmp.append( + { + "id": to_native(volume.id), + "name": to_native(volume.name), + "size": volume.size, + "location": to_native(volume.location.name), + "labels": volume.labels, + "server": server_name, + "linux_device": to_native(volume.linux_device), + "delete_protection": volume.protection["delete"], + } + ) + + return tmp + + def get_volumes(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_volume_info = [self.client.volumes.get_by_id(self.module.params.get("id"))] + elif self.module.params.get("name") is not None: + self.hcloud_volume_info = [self.client.volumes.get_by_name(self.module.params.get("name"))] + elif self.module.params.get("label_selector") is not None: + self.hcloud_volume_info = self.client.volumes.get_all( + label_selector=self.module.params.get("label_selector") + ) + else: + self.hcloud_volume_info = self.client.volumes.get_all() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + label_selector={"type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHCloudVolumeInfo.define_module() + hcloud = AnsibleHCloudVolumeInfo(module) + + hcloud.get_volumes() + result = hcloud.get_result() + + ansible_info = {"hcloud_volume_info": result["hcloud_volume_info"]} + module.exit_json(**ansible_info) + + +if __name__ == "__main__": + main() |