diff options
Diffstat (limited to 'ansible_collections/hetzner/hcloud/plugins')
116 files changed, 13265 insertions, 3867 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/hcloud_certificate.py b/ansible_collections/hetzner/hcloud/plugins/modules/certificate.py index 0f6dcf0f2..ea39be6ca 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_certificate.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/certificate.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_certificate +module: certificate short_description: Create and manage certificates on the Hetzner Cloud. @@ -39,17 +37,17 @@ options: 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. + - 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. + - Required if certificate does not exist and I(type=uploaded). type: str domain_names: description: - - Certificate key in PEM format. - - Required if certificate does not exist. + - 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 @@ -68,28 +66,37 @@ options: extends_documentation_fragment: - hetzner.hcloud.hcloud -''' +""" EXAMPLES = """ - name: Create a basic certificate - hcloud_certificate: + hetzner.hcloud.certificate: name: my-certificate - certificate: "ssh-rsa AAAjjk76kgf...Xt" - private_key: "ssh-rsa AAAjjk76kgf...Xt" + certificate: -----BEGIN CERTIFICATE-----... + private_key: -----BEGIN PRIVATE KEY-----... state: present - name: Create a certificate with labels - hcloud_certificate: + hetzner.hcloud.certificate: name: my-certificate - certificate: "ssh-rsa AAAjjk76kgf...Xt" - private_key: "ssh-rsa AAAjjk76kgf...Xt" + certificate: -----BEGIN CERTIFICATE-----... + private_key: -----BEGIN PRIVATE KEY-----... labels: - key: value - mylabel: 123 + 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) - hcloud_certificate: + hetzner.hcloud.certificate: name: my-certificate state: absent """ @@ -139,14 +146,17 @@ hcloud_certificate: """ 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.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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_certificate") - self.hcloud_certificate = None + +class AnsibleHCloudCertificate(AnsibleHCloud): + represent = "hcloud_certificate" + + hcloud_certificate: BoundCertificate | None = None def _prepare_result(self): return { @@ -158,54 +168,44 @@ class AnsibleHcloudCertificate(Hcloud): "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 + "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") - ) + 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") - ) + 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) def _create_certificate(self): - self.module.fail_on_missing_params( - required_params=["name"] - ) + self.module.fail_on_missing_params(required_params=["name"]) params = { "name": self.module.params.get("name"), - "labels": self.module.params.get("labels") + "labels": self.module.params.get("labels"), } - if self.module.params.get('type') == 'uploaded': - self.module.fail_on_missing_params( - required_params=["certificate", "private_key"] - ) + 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) else: - self.module.fail_on_missing_params( - required_params=["domain_names"] - ) + 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self._get_certificate() @@ -214,9 +214,7 @@ class AnsibleHcloudCertificate(Hcloud): 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"] - ) + 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() @@ -226,8 +224,8 @@ class AnsibleHcloudCertificate(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._get_certificate() def present_certificate(self): @@ -243,13 +241,13 @@ class AnsibleHcloudCertificate(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self.hcloud_certificate = None - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, @@ -266,18 +264,18 @@ class AnsibleHcloudCertificate(Hcloud): "choices": ["absent", "present"], "default": "present", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), - required_one_of=[['id', 'name']], - required_if=[['state', 'present', ['name']]], + required_one_of=[["id", "name"]], + required_if=[["state", "present", ["name"]]], supports_check_mode=True, ) def main(): - module = AnsibleHcloudCertificate.define_module() + module = AnsibleHCloudCertificate.define_module() - hcloud = AnsibleHcloudCertificate(module) + hcloud = AnsibleHCloudCertificate(module) state = module.params.get("state") if state == "absent": hcloud.delete_certificate() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_certificate_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/certificate_info.py index 855706f1f..e074046fd 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_certificate_info.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/certificate_info.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_certificate_info +module: certificate_info short_description: Gather infos about your Hetzner Cloud certificates. description: - Gather facts about your Hetzner Cloud certificates. @@ -20,6 +18,7 @@ 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: @@ -32,11 +31,11 @@ options: extends_documentation_fragment: - hetzner.hcloud.hcloud -''' +""" EXAMPLES = """ - name: Gather hcloud certificate infos - hcloud_certificate_info: + hetzner.hcloud.certificate_info: register: output - name: Print the gathered infos debug: @@ -86,75 +85,76 @@ hcloud_certificate_info: 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 +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" -class AnsibleHcloudCertificateInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_certificate_info") - self.hcloud_certificate_info = None + 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 - }) + 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") - )] + 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") - )] + 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")) + 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, name={"type": "str"}, label_selector={"type": "str"}, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), supports_check_mode=True, ) def main(): - module = AnsibleHcloudCertificateInfo.define_module() + module = AnsibleHCloudCertificateInfo.define_module() + hcloud = AnsibleHCloudCertificateInfo(module) - hcloud = AnsibleHcloudCertificateInfo(module) hcloud.get_certificates() result = hcloud.get_result() - ansible_info = { - 'hcloud_certificate_info': result['hcloud_certificate_info'] - } + ansible_info = {"hcloud_certificate_info": result["hcloud_certificate_info"]} module.exit_json(**ansible_info) 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/hcloud_firewall.py b/ansible_collections/hetzner/hcloud/plugins/modules/firewall.py index 34608977e..3c51b5c0a 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_firewall.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/firewall.py @@ -1,20 +1,16 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_firewall - +module: firewall short_description: Create and manage firewalls on the Hetzner Cloud. - description: - Create, update and manage firewalls on the Hetzner Cloud. @@ -24,181 +20,233 @@ author: options: id: description: - - The ID of the Hetzner Cloud firewall to manage. - - Only required if no firewall I(name) is given + - 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 I(id) is given, or a firewall does not exist. + - 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) + - User-defined labels (key-value pairs). type: dict rules: description: - - List of rules the firewall should contain. + - List of rules the firewall contain. type: list elements: dict suboptions: - direction: + description: description: - - The direction of the firewall rule. + - User defined description of this rule. type: str - choices: [ in, out ] - port: + direction: description: - - The port of the firewall rule. + - 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 ] + 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 + - 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: [ ] + default: [] destination_ips: description: - - List of CIDRs that are allowed within this rule + - 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: [ ] - description: - description: - - User defined description of this rule. - type: 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 ] + choices: [absent, present] type: str + extends_documentation_fragment: -- hetzner.hcloud.hcloud -''' + - hetzner.hcloud.hcloud +""" EXAMPLES = """ - name: Create a basic firewall - hcloud_firewall: + hetzner.hcloud.firewall: name: my-firewall state: present - name: Create a firewall with rules - hcloud_firewall: + hetzner.hcloud.firewall: name: my-firewall rules: - - direction: in - protocol: icmp - source_ips: - - 0.0.0.0/0 - - ::/0 - description: allow icmp in + - 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 - hcloud_firewall: + hetzner.hcloud.firewall: name: my-firewall labels: - key: value - mylabel: 123 + key: value + mylabel: 123 state: present - name: Ensure the firewall is absent (remove if needed) - hcloud_firewall: + hetzner.hcloud.firewall: name: my-firewall state: absent """ RETURN = """ hcloud_firewall: - description: The firewall instance - returned: Always - type: complex + description: The firewall instance. + returned: always + type: dict contains: id: - description: Numeric identifier of the firewall + description: Numeric identifier of the firewall. returned: always type: int sample: 1937415 name: - description: Name of the firewall + description: Name of the firewall. returned: always type: str - sample: my firewall + sample: my-firewall + labels: + description: User-defined labels (key-value pairs). + returned: always + type: dict rules: - description: List of Rules within this Firewall + description: List of rules the firewall contain. returned: always - type: complex + type: list + elements: dict contains: + description: + description: User defined description of this rule. + type: str + returned: always + sample: allow http from anywhere direction: - description: Direction of the Firewall Rule + description: The direction of the firewall rule. type: str returned: always sample: in protocol: - description: Protocol of the Firewall Rule + description: The protocol of the firewall rule. type: str returned: always - sample: icmp + sample: tcp port: - description: Port of the Firewall Rule, None/Null if protocol is icmp + description: The port or port range allowed by this rule. type: str - returned: always - sample: in + returned: if RV(hcloud_firewall.rules[].protocol=tcp) or RV(hcloud_firewall.rules[].protocol=udp) + sample: "80" source_ips: - description: Source IPs of the Firewall + 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: Source IPs of the Firewall + description: List of destination CIDRs that are allowed within this rule. 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) + sample: [] + applied_to: + description: List of Resources the Firewall is applied to. returned: always - type: dict + 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 """ -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 +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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_firewall") - self.hcloud_firewall = None +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 + "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): + def _prepare_result_rule(self, rule: FirewallRule): return { - "direction": rule.direction, + "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], @@ -206,27 +254,39 @@ class AnsibleHcloudFirewall(Hcloud): "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") - ) + 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") - ) + 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) def _create_firewall(self): - self.module.fail_on_missing_params( - required_params=["name"] - ) + self.module.fail_on_missing_params(required_params=["name"]) params = { "name": self.module.params.get("name"), - "labels": self.module.params.get("labels") + "labels": self.module.params.get("labels"), } rules = self.module.params.get("rules") if rules is not None: @@ -241,20 +301,20 @@ class AnsibleHcloudFirewall(Hcloud): ) 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) + 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"] - ) + 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() @@ -281,6 +341,7 @@ class AnsibleHcloudFirewall(Hcloud): ] self.hcloud_firewall.set_rules(new_rules) self._mark_as_changed() + self._get_firewall() def present_firewall(self): @@ -294,58 +355,76 @@ class AnsibleHcloudFirewall(Hcloud): 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 retry_count < 10: + while True: try: - self.client.firewalls.delete(self.hcloud_firewall) + self.hcloud_firewall.delete() break - except APIException as e: - if "is still in use" in e.message: - retry_count = retry_count + 1 + 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) - else: - self.module.fail_json(msg=e.message) - except Exception as e: - self.module.fail_json(msg=e.message) + continue + self.fail_json_hcloud(exception) + except HCloudException as exception: + self.fail_json_hcloud(exception) + self._mark_as_changed() self.hcloud_firewall = None - @staticmethod - def define_module(): + @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": []}, - description={"type": "str"}, ), required_together=[["direction", "protocol"]], + required_if=[ + ["protocol", "udp", ["port"]], + ["protocol", "tcp", ["port"]], + ], ), - labels={"type": "dict"}, + force={"type": "bool", "default": False}, state={ "choices": ["absent", "present"], "default": "present", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), - required_one_of=[['id', 'name']], - required_if=[['state', 'present', ['name']]], + required_one_of=[["id", "name"]], + required_if=[["state", "present", ["name"]]], supports_check_mode=True, ) def main(): - module = AnsibleHcloudFirewall.define_module() + module = AnsibleHCloudFirewall.define_module() - hcloud = AnsibleHcloudFirewall(module) + hcloud = AnsibleHCloudFirewall(module) state = module.params.get("state") if state == "absent": hcloud.delete_firewall() 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/hcloud_floating_ip.py b/ansible_collections/hetzner/hcloud/plugins/modules/floating_ip.py index 1ee61ea13..e037dd7a1 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_floating_ip.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/floating_ip.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_floating_ip +module: floating_ip short_description: Create and manage cloud Floating IPs on the Hetzner Cloud. @@ -71,40 +69,36 @@ options: 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: + hetzner.hcloud.floating_ip: name: my-floating-ip home_location: fsn1 type: ipv4 state: present - name: Create a basic IPv6 Floating IP - hcloud_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 - hcloud_floating_ip: + hetzner.hcloud.floating_ip: name: my-floating-ip server: 1234 state: present - name: Assign a Floating IP to another server - hcloud_floating_ip: + hetzner.hcloud.floating_ip: name: my-floating-ip server: 1234 - force: yes + force: true state: present - name: Floating IP should be absent - hcloud_floating_ip: + hetzner.hcloud.floating_ip: name: my-floating-ip state: absent """ @@ -166,14 +160,17 @@ hcloud_floating_ip: """ 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.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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_floating_ip") - self.hcloud_floating_ip = None +class AnsibleHCloudFloatingIP(AnsibleHCloud): + represent = "hcloud_floating_ip" + + hcloud_floating_ip: BoundFloatingIP | None = None def _prepare_result(self): server = None @@ -195,20 +192,14 @@ class AnsibleHcloudFloatingIP(Hcloud): 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") - ) + 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) + 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"] - ) + self.module.fail_on_missing_params(required_params=["type"]) try: params = { "description": self.module.params.get("description"), @@ -216,13 +207,9 @@ class AnsibleHcloudFloatingIP(Hcloud): "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") - ) + 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") - ) + 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") @@ -235,8 +222,8 @@ class AnsibleHcloudFloatingIP(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self._get_floating_ip() @@ -258,21 +245,18 @@ class AnsibleHcloudFloatingIP(Hcloud): 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.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 + "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.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: @@ -286,8 +270,8 @@ class AnsibleHcloudFloatingIP(Hcloud): self._mark_as_changed() self._get_floating_ip() - except Exception as e: - self.module.fail_json(msg=e.message) + except HCloudException as exception: + self.fail_json_hcloud(exception) def present_floating_ip(self): self._get_floating_ip() @@ -305,16 +289,17 @@ class AnsibleHcloudFloatingIP(Hcloud): 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 + "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 Exception as e: - self.module.fail_json(msg=e.message) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, @@ -330,18 +315,18 @@ class AnsibleHcloudFloatingIP(Hcloud): "choices": ["absent", "present"], "default": "present", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), - required_one_of=[['id', 'name']], - mutually_exclusive=[['home_location', 'server']], + required_one_of=[["id", "name"]], + mutually_exclusive=[["home_location", "server"]], supports_check_mode=True, ) def main(): - module = AnsibleHcloudFloatingIP.define_module() + module = AnsibleHCloudFloatingIP.define_module() - hcloud = AnsibleHcloudFloatingIP(module) + hcloud = AnsibleHCloudFloatingIP(module) state = module.params["state"] if state == "absent": hcloud.delete_floating_ip() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_floating_ip_facts.py b/ansible_collections/hetzner/hcloud/plugins/modules/floating_ip_info.py index 2ec359600..663d29622 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_floating_ip_facts.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/floating_ip_info.py @@ -1,23 +1,19 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_floating_ip_info +module: 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) @@ -26,7 +22,12 @@ 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. @@ -34,11 +35,11 @@ options: extends_documentation_fragment: - hetzner.hcloud.hcloud -''' +""" EXAMPLES = """ - name: Gather hcloud Floating ip infos - hcloud_floating_ip_info: + hetzner.hcloud.floating_ip_info: register: output - name: Print the gathered infos debug: @@ -99,14 +100,17 @@ hcloud_floating_ip_info: """ 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.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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_floating_ip_info") - self.hcloud_floating_ip_info = None +class AnsibleHCloudFloatingIPInfo(AnsibleHCloud): + represent = "hcloud_floating_ip_info" + + hcloud_floating_ip_info: list[BoundFloatingIP] | None = None def _prepare_result(self): tmp = [] @@ -116,69 +120,60 @@ class AnsibleHcloudFloatingIPInfo(Hcloud): 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"], - }) + 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") - )] + 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")) + 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, + name={"type": "str"}, label_selector={"type": "str"}, - **Hcloud.base_module_arguments() + **super().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) + module = AnsibleHCloudFloatingIPInfo.define_module() + 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) + + ansible_info = {"hcloud_floating_ip_info": result["hcloud_floating_ip_info"]} + module.exit_json(**ansible_info) if __name__ == "__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_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_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_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_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_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_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_image_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/image_info.py index 8acd8846a..b0d7fc482 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_image_info.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/image_info.py @@ -1,24 +1,20 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_image_info +module: 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) @@ -27,6 +23,7 @@ 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: @@ -50,11 +47,11 @@ options: extends_documentation_fragment: - hetzner.hcloud.hcloud -''' +""" EXAMPLES = """ - name: Gather hcloud image infos - hcloud_image_info: + hetzner.hcloud.image_info: register: output - name: Print the gathered infos @@ -87,7 +84,7 @@ hcloud_image_info: description: Name of the image returned: always type: str - sample: ubuntu-18.04 + sample: ubuntu-22.04 description: description: Detail description of the image returned: always @@ -115,48 +112,54 @@ hcloud_image_info: """ 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.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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_image_info") - self.hcloud_image_info = None +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, - }) + 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") - )] + 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") - )] + 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") - )] + 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") @@ -173,11 +176,11 @@ class AnsibleHcloudImageInfo(Hcloud): self.hcloud_image_info = self.client.images.get_all(**params) - except Exception as e: - self.module.fail_json(msg=e.message) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, @@ -185,34 +188,21 @@ class AnsibleHcloudImageInfo(Hcloud): label_selector={"type": "str"}, type={"choices": ["system", "snapshot", "backup"], "default": "system", "type": "str"}, architecture={"choices": ["x86", "arm"], "type": "str"}, - **Hcloud.base_module_arguments() + **super().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") + module = AnsibleHCloudImageInfo.define_module() + hcloud = AnsibleHCloudImageInfo(module) - 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) + ansible_info = {"hcloud_image_info": result["hcloud_image_info"]} + module.exit_json(**ansible_info) if __name__ == "__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/hcloud_load_balancer.py b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer.py index 9c6c2bbaf..1a0d8712a 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer.py @@ -1,20 +1,17 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_load_balancer +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. @@ -37,6 +34,12 @@ options: - 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. @@ -68,24 +71,21 @@ options: type: str extends_documentation_fragment: - hetzner.hcloud.hcloud - -requirements: - - hcloud-python >= 1.8.0 -''' +""" EXAMPLES = """ - name: Create a basic Load Balancer - hcloud_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) - hcloud_load_balancer: + hetzner.hcloud.load_balancer: name: my-Load Balancer state: absent - """ RETURN = """ @@ -114,6 +114,12 @@ hcloud_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 @@ -146,18 +152,27 @@ hcloud_load_balancer: """ 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.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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_load_balancer") - self.hcloud_load_balancer = None + +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) + 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), @@ -165,6 +180,7 @@ class AnsibleHcloudLoadBalancer(Hcloud): "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"], @@ -174,24 +190,18 @@ class AnsibleHcloudLoadBalancer(Hcloud): 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") - ) + 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) + 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"] - ) + 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") ), @@ -201,9 +211,7 @@ class AnsibleHcloudLoadBalancer(Hcloud): 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") - ) + 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") @@ -215,8 +223,8 @@ class AnsibleHcloudLoadBalancer(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self._get_load_balancer() @@ -236,7 +244,9 @@ class AnsibleHcloudLoadBalancer(Hcloud): 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 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() @@ -245,7 +255,10 @@ class AnsibleHcloudLoadBalancer(Hcloud): 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: + 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") @@ -255,9 +268,17 @@ class AnsibleHcloudLoadBalancer(Hcloud): ).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 Exception as e: - self.module.fail_json(msg=e.message) + except HCloudException as exception: + self.fail_json_hcloud(exception) def present_load_balancer(self): self._get_load_balancer() @@ -274,16 +295,17 @@ class AnsibleHcloudLoadBalancer(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @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"}, @@ -293,18 +315,18 @@ class AnsibleHcloudLoadBalancer(Hcloud): "choices": ["absent", "present"], "default": "present", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), - required_one_of=[['id', 'name']], + required_one_of=[["id", "name"]], mutually_exclusive=[["location", "network_zone"]], supports_check_mode=True, ) def main(): - module = AnsibleHcloudLoadBalancer.define_module() + module = AnsibleHCloudLoadBalancer.define_module() - hcloud = AnsibleHcloudLoadBalancer(module) + hcloud = AnsibleHCloudLoadBalancer(module) state = module.params.get("state") if state == "absent": hcloud.delete_load_balancer() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_info.py index 159dad258..19ead98c2 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_info.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_info.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_load_balancer_info +module: load_balancer_info short_description: Gather infos about your Hetzner Cloud Load Balancers. @@ -25,6 +23,7 @@ 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: @@ -37,11 +36,11 @@ options: extends_documentation_fragment: - hetzner.hcloud.hcloud -''' +""" EXAMPLES = """ - name: Gather hcloud load_balancer infos - hcloud_load_balancer_info: + hetzner.hcloud.load_balancer_info: register: output - name: Print the gathered infos @@ -137,10 +136,27 @@ hcloud_load_balancer_info: 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) + - 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 @@ -261,14 +277,17 @@ hcloud_load_balancer_info: """ 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.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" -class AnsibleHcloudLoadBalancerInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_load_balancer_info") - self.hcloud_load_balancer_info = None + hcloud_load_balancer_info: list[BoundLoadBalancer] | None = None def _prepare_result(self): tmp = [] @@ -278,22 +297,25 @@ class AnsibleHcloudLoadBalancerInfo(Hcloud): 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 - }) + 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 @@ -305,8 +327,7 @@ class AnsibleHcloudLoadBalancerInfo(Hcloud): "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], + "certificates": [to_native(certificate.name) for certificate in service.http.certificates], } health_check = { "protocol": to_native(service.health_check.protocol), @@ -320,8 +341,7 @@ class AnsibleHcloudLoadBalancerInfo(Hcloud): "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], + "certificates": [to_native(status_code) for status_code in service.health_check.http.status_codes], "tls": service.health_check.http.tls, } return { @@ -337,7 +357,7 @@ class AnsibleHcloudLoadBalancerInfo(Hcloud): def _prepare_target_result(target): result = { "type": to_native(target.type), - "use_private_ip": target.use_private_ip + "use_private_ip": target.use_private_ip, } if target.type == "server": result["server"] = to_native(target.server.name) @@ -345,18 +365,26 @@ class AnsibleHcloudLoadBalancerInfo(Hcloud): 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") - )] + 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") - )] + 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") @@ -365,32 +393,30 @@ class AnsibleHcloudLoadBalancerInfo(Hcloud): self.hcloud_load_balancer_info = self.client.load_balancers.get_all(**params) - except Exception as e: - self.module.fail_json(msg=e.message) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, name={"type": "str"}, label_selector={"type": "str"}, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), supports_check_mode=True, ) def main(): - module = AnsibleHcloudLoadBalancerInfo.define_module() + module = AnsibleHCloudLoadBalancerInfo.define_module() + hcloud = AnsibleHCloudLoadBalancerInfo(module) - hcloud = AnsibleHcloudLoadBalancerInfo(module) hcloud.get_load_balancers() result = hcloud.get_result() - ansible_info = { - 'hcloud_load_balancer_info': result['hcloud_load_balancer_info'] - } + ansible_info = {"hcloud_load_balancer_info": result["hcloud_load_balancer_info"]} module.exit_json(**ansible_info) diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_network.py b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_network.py index 63a7c5471..4560f8735 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_network.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_network.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_load_balancer_network +module: load_balancer_network short_description: Manage the relationship between Hetzner Cloud Networks and Load Balancers @@ -24,12 +22,12 @@ version_added: 0.1.0 options: network: description: - - The name of the Hetzner Cloud Networks. + - Name or ID of the Hetzner Cloud Networks. type: str required: true load_balancer: description: - - The name of the Hetzner Cloud Load Balancer. + - Name or ID of the Hetzner Cloud Load Balancer. type: str required: true ip: @@ -43,30 +41,26 @@ options: 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: + 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 - hcloud_load_balancer_network: + 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) - hcloud_load_balancer_network: + hetzner.hcloud.load_balancer_network: network: my-network load_balancer: my-LoadBalancer state: absent @@ -96,16 +90,20 @@ hcloud_load_balancer_network: """ 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.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(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 + +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 { @@ -116,31 +114,26 @@ class AnsibleHcloudLoadBalancerNetwork(Hcloud): 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 + 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"), ) - 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) 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 + 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 - } + params = {"network": self.hcloud_network} if self.module.params.get("ip") is not None: params["ip"] = self.module.params.get("ip") @@ -148,8 +141,8 @@ class AnsibleHcloudLoadBalancerNetwork(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self._get_load_balancer_and_network() @@ -168,15 +161,16 @@ class AnsibleHcloudLoadBalancerNetwork(Hcloud): if not self.module.check_mode: try: self.hcloud_load_balancer.detach_from_network( - self.hcloud_load_balancer_network.network).wait_until_finished() + self.hcloud_load_balancer_network.network + ).wait_until_finished() self._mark_as_changed() - except Exception as e: - self.module.fail_json(msg=e.message) + except HCloudException as exception: + self.fail_json_hcloud(exception) self.hcloud_load_balancer_network = None - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( network={"type": "str", "required": True}, @@ -186,16 +180,16 @@ class AnsibleHcloudLoadBalancerNetwork(Hcloud): "choices": ["absent", "present"], "default": "present", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), supports_check_mode=True, ) def main(): - module = AnsibleHcloudLoadBalancerNetwork.define_module() + module = AnsibleHCloudLoadBalancerNetwork.define_module() - hcloud = AnsibleHcloudLoadBalancerNetwork(module) + hcloud = AnsibleHCloudLoadBalancerNetwork(module) state = module.params["state"] if state == "absent": hcloud.delete_load_balancer_network() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_service.py b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_service.py index b5edcc6b5..1fc18deef 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_service.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_service.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_load_balancer_service +module: load_balancer_service short_description: Create and manage the services of cloud Load Balancers on the Hetzner Cloud. @@ -24,7 +22,7 @@ version_added: 0.1.0 options: load_balancer: description: - - The Name of the Hetzner Cloud Load Balancer the service belongs to + - Name or ID of the Hetzner Cloud Load Balancer the service belongs to type: str required: true listen_port: @@ -137,21 +135,18 @@ options: 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: + 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) - hcloud_load_balancer_service: + hetzner.hcloud.load_balancer_service: load_balancer: my-Load Balancer protocol: http listen_port: 80 @@ -284,22 +279,24 @@ hcloud_load_balancer_service: """ 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.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, +) -try: - from hcloud.load_balancers.domain import LoadBalancerService, LoadBalancerServiceHttp, \ - LoadBalancerHealthCheck, LoadBalancerHealtCheckHttp - from hcloud import APIException -except ImportError: - APIException = None +class AnsibleHCloudLoadBalancerService(AnsibleHCloud): + represent = "hcloud_load_balancer_service" -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 + hcloud_load_balancer: BoundLoadBalancer | None = None + hcloud_load_balancer_service: LoadBalancerService | None = None def _prepare_result(self): http = None @@ -309,8 +306,9 @@ class AnsibleHcloudLoadBalancerService(Hcloud): "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], + "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), @@ -324,8 +322,10 @@ class AnsibleHcloudLoadBalancerService(Hcloud): "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], + "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 { @@ -340,31 +340,23 @@ class AnsibleHcloudLoadBalancerService(Hcloud): 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 + self.hcloud_load_balancer = self._client_get_by_name_or_id( + "load_balancers", + self.module.params.get("load_balancer"), ) - 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) def _create_load_balancer_service(self): - - self.module.fail_on_missing_params( - required_params=["protocol"] - ) + 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"] - ) + 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") + "proxyprotocol": self.module.params.get("proxyprotocol"), } if self.module.params.get("destination_port"): @@ -375,14 +367,16 @@ class AnsibleHcloudLoadBalancerService(Hcloud): if self.module.params.get("health_check"): params["health_check"] = self.__get_service_health_checks( - health_check=self.module.params.get("health_check")) + 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) + 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() @@ -404,15 +398,11 @@ class AnsibleHcloudLoadBalancerService(Hcloud): hcloud_cert = None try: try: - hcloud_cert = self.client.certificates.get_by_name( - certificate - ) + 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) + 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 @@ -473,14 +463,16 @@ class AnsibleHcloudLoadBalancerService(Hcloud): 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")) + 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) + max_retries=1000 + ) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._get_load_balancer() if changed: @@ -505,16 +497,17 @@ class AnsibleHcloudLoadBalancerService(Hcloud): 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) + 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 e: - self.module.fail_json(msg=e.message) + except APIException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( load_balancer={"type": "str", "required": True}, @@ -528,26 +521,12 @@ class AnsibleHcloudLoadBalancerService(Hcloud): 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" - }, - - ) + 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", @@ -556,57 +535,36 @@ class AnsibleHcloudLoadBalancerService(Hcloud): "type": "str", "choices": ["http", "https", "tcp"], }, - port={ - "type": "int" - }, - interval={ - "type": "int" - }, - timeout={ - "type": "int" - }, - retries={ - "type": "int" - }, + 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 - }, - ) - } - ) - + 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() + **super().base_module_arguments(), ), supports_check_mode=True, ) def main(): - module = AnsibleHcloudLoadBalancerService.define_module() + module = AnsibleHCloudLoadBalancerService.define_module() - hcloud = AnsibleHcloudLoadBalancerService(module) + hcloud = AnsibleHCloudLoadBalancerService(module) state = module.params.get("state") if state == "absent": hcloud.delete_load_balancer_service() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_target.py b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_target.py index 760884466..36e7f608f 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_target.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_target.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_load_balancer_target +module: load_balancer_target short_description: Manage Hetzner Cloud Load Balancer targets @@ -30,12 +28,12 @@ options: required: true load_balancer: description: - - The name of the Hetzner Cloud Load Balancer. + - Name or ID of the Hetzner Cloud Load Balancer. type: str required: true server: description: - - The name of the Hetzner Cloud Server. + - Name or ID of the Hetzner Cloud Server. - Required if I(type) is server type: str label_selector: @@ -51,7 +49,7 @@ options: 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) + - Load Balancer needs to be attached to a network. See M(hetzner.hcloud.load_balancer_network) type: bool default: False state: @@ -61,38 +59,34 @@ options: 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: + 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.hcloud_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.hcloud_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.hcloud_load_balancer_target: + hetzner.hcloud.load_balancer_target: type: server load_balancer: my-LoadBalancer server: my-server @@ -133,36 +127,38 @@ hcloud_load_balancer_target: 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) + - 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._text import to_native -from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud +from ansible.module_utils.common.text.converters import to_native -try: - from hcloud.load_balancers.domain import LoadBalancerTarget, LoadBalancerTargetLabelSelector, LoadBalancerTargetIP -except ImportError: - LoadBalancerTarget = None - LoadBalancerTargetLabelSelector = None - LoadBalancerTargetIP = None +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(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 +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 + "use_private_ip": self.hcloud_load_balancer_target.use_private_ip, } if self.hcloud_load_balancer_target.type == "server": @@ -175,22 +171,20 @@ class AnsibleHcloudLoadBalancerTarget(Hcloud): 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 + self.hcloud_load_balancer = self._client_get_by_name_or_id( + "load_balancers", + self.module.params.get("load_balancer"), ) - 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_server = self._client_get_by_name_or_id( + "servers", + self.module.params.get("server"), + ) self.hcloud_load_balancer_target = None - except Exception as e: - self.module.fail_json(msg=e.message) + except HCloudException as exception: + self.fail_json_hcloud(exception) def _get_load_balancer_target(self): for target in self.hcloud_load_balancer.targets: @@ -205,40 +199,40 @@ class AnsibleHcloudLoadBalancerTarget(Hcloud): self.hcloud_load_balancer_target = target def _create_load_balancer_target(self): - params = { - "target": None - } + params = {"target": None} if self.module.params.get("type") == "server": - self.module.fail_on_missing_params( - required_params=["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"), ) - 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"] + 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"), ) - 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"] + 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, ) - 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": + except APIException as exception: + if exception.code == "locked" or exception.code == "conflict": self._create_load_balancer_target() else: - self.module.fail_json(msg=e.message) + self.fail_json_hcloud(exception) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self._get_load_balancer_and_target() @@ -257,35 +251,33 @@ class AnsibleHcloudLoadBalancerTarget(Hcloud): 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) + 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"] + 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"), ) - 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"] + 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, ) - 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self.hcloud_load_balancer_target = None - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( type={"type": "str", "required": True, "choices": ["server", "label_selector", "ip"]}, @@ -298,16 +290,16 @@ class AnsibleHcloudLoadBalancerTarget(Hcloud): "choices": ["absent", "present"], "default": "present", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), supports_check_mode=True, ) def main(): - module = AnsibleHcloudLoadBalancerTarget.define_module() + module = AnsibleHCloudLoadBalancerTarget.define_module() - hcloud = AnsibleHcloudLoadBalancerTarget(module) + hcloud = AnsibleHCloudLoadBalancerTarget(module) state = module.params["state"] if state == "absent": hcloud.delete_load_balancer_target() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_type_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_type_info.py index a481ea9c9..67feafd59 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_load_balancer_type_info.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/load_balancer_type_info.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_load_balancer_type_info +module: load_balancer_type_info short_description: Gather infos about the Hetzner Cloud Load Balancer types. @@ -25,6 +23,7 @@ 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: @@ -33,11 +32,11 @@ options: extends_documentation_fragment: - hetzner.hcloud.hcloud -''' +""" EXAMPLES = """ - name: Gather hcloud Load Balancer type infos - hcloud_load_balancer_type_info: + hetzner.hcloud.load_balancer_type_info: register: output - name: Print the gathered infos @@ -89,68 +88,72 @@ hcloud_load_balancer_type_info: """ 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.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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_load_balancer_type_info") - self.hcloud_load_balancer_type_info = None +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 - }) + 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") - )] + 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") - )] + 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, name={"type": "str"}, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), supports_check_mode=True, ) def main(): - module = AnsibleHcloudLoadBalancerTypeInfo.define_module() + module = AnsibleHCloudLoadBalancerTypeInfo.define_module() + hcloud = AnsibleHCloudLoadBalancerTypeInfo(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'] - } + + ansible_info = {"hcloud_load_balancer_type_info": result["hcloud_load_balancer_type_info"]} module.exit_json(**ansible_info) diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_location_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/location_info.py index 623c6ab68..ac495c6c8 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_location_info.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/location_info.py @@ -1,24 +1,20 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_location_info +module: 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) @@ -27,6 +23,7 @@ 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: @@ -35,11 +32,11 @@ options: extends_documentation_fragment: - hetzner.hcloud.hcloud -''' +""" EXAMPLES = """ - name: Gather hcloud location infos - hcloud_location_info: + hetzner.hcloud.location_info: register: output - name: Print the gathered infos @@ -81,78 +78,67 @@ hcloud_location_info: """ 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.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" -class AnsibleHcloudLocationInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_location_info") - self.hcloud_location_info = None + 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) - }) + 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") - )] + 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") - )] + 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, name={"type": "str"}, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), supports_check_mode=True, ) def main(): - module = AnsibleHcloudLocationInfo.define_module() + module = AnsibleHCloudLocationInfo.define_module() + hcloud = AnsibleHCloudLocationInfo(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) + + ansible_info = {"hcloud_location_info": result["hcloud_location_info"]} + module.exit_json(**ansible_info) if __name__ == "__main__": diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_network.py b/ansible_collections/hetzner/hcloud/plugins/modules/network.py index 9c005d29f..24e45a48d 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_network.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/network.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_network +module: network short_description: Create and manage cloud Networks on the Hetzner Cloud. @@ -38,6 +36,11 @@ options: - 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). @@ -53,23 +56,19 @@ options: 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: + hetzner.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: + hetzner.hcloud.network: name: my-network state: absent """ @@ -95,6 +94,11 @@ hcloud_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 @@ -111,20 +115,24 @@ hcloud_network: """ 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.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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_network") - self.hcloud_network = None + +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, } @@ -132,26 +140,24 @@ class AnsibleHcloudNetwork(Hcloud): 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") - ) + 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) + 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"] - ) + 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) @@ -160,8 +166,8 @@ class AnsibleHcloudNetwork(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self._get_network() @@ -179,13 +185,22 @@ class AnsibleHcloudNetwork(Hcloud): 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 Exception as e: - self.module.fail_json(msg=e.message) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._get_network() def present_network(self): @@ -202,34 +217,35 @@ class AnsibleHcloudNetwork(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self.hcloud_network = None - @staticmethod - def define_module(): + @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", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), - required_one_of=[['id', 'name']], + required_one_of=[["id", "name"]], supports_check_mode=True, ) def main(): - module = AnsibleHcloudNetwork.define_module() + module = AnsibleHCloudNetwork.define_module() - hcloud = AnsibleHcloudNetwork(module) + hcloud = AnsibleHCloudNetwork(module) state = module.params["state"] if state == "absent": hcloud.delete_network() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_network_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/network_info.py index 382e447aa..4008352b4 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_network_info.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/network_info.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_network_info +module: network_info short_description: Gather info about your Hetzner Cloud networks. @@ -25,6 +23,7 @@ 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: @@ -37,7 +36,7 @@ options: extends_documentation_fragment: - hetzner.hcloud.hcloud -''' +""" EXAMPLES = """ - name: Gather hcloud network info @@ -110,6 +109,11 @@ hcloud_network_info: 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 @@ -181,14 +185,17 @@ hcloud_network_info: """ 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.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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_network_info") - self.hcloud_network_info = None +class AnsibleHCloudNetworkInfo(AnsibleHCloud): + represent = "hcloud_network_info" + + hcloud_network_info: list[BoundNetwork] | None = None def _prepare_result(self): tmp = [] @@ -206,10 +213,7 @@ class AnsibleHcloudNetworkInfo(Hcloud): subnets.append(prepared_subnet) routes = [] for route in network.routes: - prepared_route = { - "destination": route.destination, - "gateway": route.gateway - } + prepared_route = {"destination": route.destination, "gateway": route.gateway} routes.append(prepared_route) servers = [] @@ -233,59 +237,58 @@ class AnsibleHcloudNetworkInfo(Hcloud): } 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"], - }) + 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") - )] + 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") - )] + 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")) + 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, name={"type": "str"}, label_selector={"type": "str"}, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), supports_check_mode=True, ) def main(): - module = AnsibleHcloudNetworkInfo.define_module() + module = AnsibleHCloudNetworkInfo.define_module() + hcloud = AnsibleHCloudNetworkInfo(module) - hcloud = AnsibleHcloudNetworkInfo(module) hcloud.get_networks() result = hcloud.get_result() - info = { - 'hcloud_network_info': result['hcloud_network_info'] - } + + info = {"hcloud_network_info": result["hcloud_network_info"]} module.exit_json(**info) diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_placement_group.py b/ansible_collections/hetzner/hcloud/plugins/modules/placement_group.py index 522bb679d..ba26fad22 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_placement_group.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/placement_group.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations DOCUMENTATION = """ --- -module: hcloud_placement_group +module: placement_group short_description: Create and manage placement groups on the Hetzner Cloud. @@ -47,31 +45,28 @@ options: 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: + hetzner.hcloud.placement_group: name: my-placement-group state: present type: spread - name: Create a placement group with labels - hcloud_placement_group: + hetzner.hcloud.placement_group: name: my-placement-group type: spread labels: - key: value - mylabel: 123 + key: value + mylabel: 123 state: present - name: Ensure the placement group is absent (remove if needed) - hcloud_placement_group: + hetzner.hcloud.placement_group: name: my-placement-group state: absent """ @@ -111,15 +106,18 @@ hcloud_placement_group: - 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 +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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_placement_group") - self.hcloud_placement_group = None + +class AnsibleHCloudPlacementGroup(AnsibleHCloud): + represent = "hcloud_placement_group" + + hcloud_placement_group: BoundPlacementGroup | None = None def _prepare_result(self): return { @@ -133,20 +131,14 @@ class AnsibleHcloudPlacementGroup(Hcloud): 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") - ) + 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) + 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"] - ) + self.module.fail_on_missing_params(required_params=["name"]) params = { "name": self.module.params.get("name"), "type": self.module.params.get("type"), @@ -155,17 +147,15 @@ class AnsibleHcloudPlacementGroup(Hcloud): 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) + 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"] - ) + 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() @@ -193,8 +183,8 @@ class AnsibleHcloudPlacementGroup(Hcloud): self._mark_as_changed() self.hcloud_placement_group = None - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, @@ -205,18 +195,18 @@ class AnsibleHcloudPlacementGroup(Hcloud): "choices": ["absent", "present"], "default": "present", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), - required_one_of=[['id', 'name']], - required_if=[['state', 'present', ['name']]], + required_one_of=[["id", "name"]], + required_if=[["state", "present", ["name"]]], supports_check_mode=True, ) def main(): - module = AnsibleHcloudPlacementGroup.define_module() + module = AnsibleHCloudPlacementGroup.define_module() - hcloud = AnsibleHcloudPlacementGroup(module) + hcloud = AnsibleHCloudPlacementGroup(module) state = module.params.get("state") if state == "absent": hcloud.delete_placement_group() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_primary_ip.py b/ansible_collections/hetzner/hcloud/plugins/modules/primary_ip.py index c192d5fec..607f6c7e1 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_primary_ip.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/primary_ip.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_primary_ip +module: primary_ip short_description: Create and manage cloud Primary IPs on the Hetzner Cloud. @@ -63,29 +61,25 @@ options: 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: + hetzner.hcloud.primary_ip: name: my-primary-ip datacenter: fsn1-dc14 type: ipv4 state: present - name: Create a basic IPv6 Primary IP - hcloud_primary_ip: + hetzner.hcloud.primary_ip: name: my-primary-ip datacenter: fsn1-dc14 type: ipv6 state: present - name: Primary IP should be absent - hcloud_primary_ip: + hetzner.hcloud.primary_ip: name: my-primary-ip state: absent """ @@ -136,14 +130,17 @@ hcloud_primary_ip: """ 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.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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_primary_ip") - self.hcloud_primary_ip = None + +class AnsibleHCloudPrimaryIP(AnsibleHCloud): + represent = "hcloud_primary_ip" + + hcloud_primary_ip: BoundPrimaryIP | None = None def _prepare_result(self): return { @@ -159,27 +156,19 @@ class AnsibleHcloudPrimaryIP(Hcloud): 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") - ) + 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) + 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"] - ) + 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") - ) + "datacenter": self.client.datacenters.get_by_name(self.module.params.get("datacenter")), } if self.module.params.get("labels") is not None: @@ -191,8 +180,8 @@ class AnsibleHcloudPrimaryIP(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self._get_primary_ip() @@ -211,8 +200,8 @@ class AnsibleHcloudPrimaryIP(Hcloud): self._mark_as_changed() self._get_primary_ip() - except Exception as e: - self.module.fail_json(msg=e.message) + except HCloudException as exception: + self.fail_json_hcloud(exception) def present_primary_ip(self): self._get_primary_ip() @@ -229,11 +218,11 @@ class AnsibleHcloudPrimaryIP(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, @@ -247,17 +236,17 @@ class AnsibleHcloudPrimaryIP(Hcloud): "choices": ["absent", "present"], "default": "present", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), - required_one_of=[['id', 'name']], + required_one_of=[["id", "name"]], supports_check_mode=True, ) def main(): - module = AnsibleHcloudPrimaryIP.define_module() + module = AnsibleHCloudPrimaryIP.define_module() - hcloud = AnsibleHcloudPrimaryIP(module) + hcloud = AnsibleHCloudPrimaryIP(module) state = module.params["state"] if state == "absent": hcloud.delete_primary_ip() 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/hcloud_rdns.py b/ansible_collections/hetzner/hcloud/plugins/modules/rdns.py index 9f79fbe70..b2decdec8 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_rdns.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/rdns.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_rdns +module: rdns short_description: Create and manage reverse DNS entries on the Hetzner Cloud. @@ -24,19 +22,19 @@ author: options: server: description: - - The name of the Hetzner Cloud server you want to add the reverse DNS entry to. + - Name or ID 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. + - Name or ID 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. + - Name or ID 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. + - Name or ID of the Hetzner Cloud Load Balancer you want to add the reverse DNS entry to. type: str ip_address: description: @@ -55,45 +53,41 @@ options: 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: + 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 - hcloud_rdns: + 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 - hcloud_rdns: + 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 - hcloud_rdns: + 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) - hcloud_rdns: + hetzner.hcloud.rdns: server: my-server ip_address: 123.123.123.123 dns_ptr: example.com @@ -138,17 +132,25 @@ hcloud_rdns: sample: example.com """ +import ipaddress +from typing import Any + 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 +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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_rdns") - self.hcloud_resource = None - self.hcloud_rdns = None + +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 = { @@ -172,35 +174,37 @@ class AnsibleHcloudReverseDNS(Hcloud): 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") + self.hcloud_resource = self._client_get_by_name_or_id( + "servers", + 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") + self.hcloud_resource = self._client_get_by_name_or_id( + "floating_ips", + 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") + self.hcloud_resource = self._client_get_by_name_or_id( + "primary_ips", + 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") + self.hcloud_resource = self._client_get_by_name_or_id( + "load_balancers", + 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) def _get_rdns(self): ip_address = self.module.params.get("ip_address") - if utils.validate_ip_address(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 = { @@ -234,7 +238,7 @@ class AnsibleHcloudReverseDNS(Hcloud): else: self.module.fail_json(msg="The selected Load Balancer does not have this IP address") - elif utils.validate_ip_v6_address(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: @@ -263,13 +267,9 @@ class AnsibleHcloudReverseDNS(Hcloud): "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"] - ) + 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"), @@ -278,8 +278,8 @@ class AnsibleHcloudReverseDNS(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self._get_resource() self._get_rdns() @@ -295,8 +295,8 @@ class AnsibleHcloudReverseDNS(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self._get_resource() self._get_rdns() @@ -315,14 +315,14 @@ class AnsibleHcloudReverseDNS(Hcloud): 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.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 - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( server={"type": "str"}, @@ -335,18 +335,18 @@ class AnsibleHcloudReverseDNS(Hcloud): "choices": ["absent", "present"], "default": "present", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), - required_one_of=[['server', 'floating_ip', 'load_balancer', 'primary_ip']], - mutually_exclusive=[["server", "floating_ip", 'load_balancer', 'primary_ip']], + 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() + module = AnsibleHCloudReverseDNS.define_module() - hcloud = AnsibleHcloudReverseDNS(module) + hcloud = AnsibleHCloudReverseDNS(module) state = module.params["state"] if state == "absent": hcloud.delete_rdns() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_route.py b/ansible_collections/hetzner/hcloud/plugins/modules/route.py index c75177953..3c96a7382 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_route.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/route.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_route +module: route short_description: Create and delete cloud routes on the Hetzner Cloud. @@ -24,7 +22,7 @@ author: options: network: description: - - The name of the Hetzner Cloud Network. + - Name or ID of the Hetzner Cloud Network. type: str required: true destination: @@ -44,24 +42,20 @@ options: 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: + 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 - hcloud_route: + hetzner.hcloud.route: network: my-network destination: 10.100.1.0/24 gateway: 10.0.1.1 @@ -92,20 +86,18 @@ hcloud_route: """ 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.module_utils.common.text.converters import to_native -try: - from hcloud.networks.domain import NetworkRoute -except ImportError: - NetworkRoute = None +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.networks import BoundNetwork, NetworkRoute -class AnsibleHcloudRoute(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_route") - self.hcloud_network = None - self.hcloud_route = None +class AnsibleHCloudRoute(AnsibleHCloud): + represent = "hcloud_route" + + hcloud_network: BoundNetwork | None = None + hcloud_route: NetworkRoute | None = None def _prepare_result(self): return { @@ -116,10 +108,13 @@ class AnsibleHcloudRoute(Hcloud): def _get_network(self): try: - self.hcloud_network = self.client.networks.get_by_name(self.module.params.get("network")) + self.hcloud_network = self._client_get_by_name_or_id( + "networks", + self.module.params.get("network"), + ) self.hcloud_route = None - except Exception as e: - self.module.fail_json(msg=e.message) + except HCloudException as exception: + self.fail_json_hcloud(exception) def _get_route(self): destination = self.module.params.get("destination") @@ -130,15 +125,14 @@ class AnsibleHcloudRoute(Hcloud): def _create_route(self): route = NetworkRoute( - destination=self.module.params.get("destination"), - gateway=self.module.params.get('gateway') + 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self._get_network() @@ -157,13 +151,13 @@ class AnsibleHcloudRoute(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self.hcloud_route = None - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( network={"type": "str", "required": True}, @@ -173,16 +167,16 @@ class AnsibleHcloudRoute(Hcloud): "choices": ["absent", "present"], "default": "present", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), supports_check_mode=True, ) def main(): - module = AnsibleHcloudRoute.define_module() + module = AnsibleHCloudRoute.define_module() - hcloud = AnsibleHcloudRoute(module) + hcloud = AnsibleHCloudRoute(module) state = module.params["state"] if state == "absent": hcloud.delete_route() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server.py b/ansible_collections/hetzner/hcloud/plugins/modules/server.py index 3a77da695..f5cadb807 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/server.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_server +module: server short_description: Create and manage cloud servers on the Hetzner Cloud. @@ -67,7 +65,7 @@ options: datacenter: description: - Datacenter of Server. - - Required of no I(location) is given and server does not exist. + - Required if no I(location) is given and server does not exist. type: str backups: description: @@ -78,17 +76,17 @@ options: - 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 + default: false enable_ipv4: description: - Enables the public ipv4 address type: bool - default: yes + default: true enable_ipv6: description: - Enables the public ipv6 address type: bool - default: yes + 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 @@ -110,18 +108,17 @@ options: - 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 + default: false allow_deprecated_image: description: - Allows the creation of servers with deprecated images. type: bool - default: no + default: false user_data: description: - User Data to be passed to the server on creation. @@ -158,80 +155,80 @@ options: extends_documentation_fragment: - hetzner.hcloud.hcloud -''' +""" EXAMPLES = """ - name: Create a basic server - hcloud_server: + hetzner.hcloud.server: name: my-server server_type: cx11 - image: ubuntu-18.04 + image: ubuntu-22.04 state: present - name: Create a basic server with ssh key - hcloud_server: + hetzner.hcloud.server: name: my-server server_type: cx11 - image: ubuntu-18.04 + image: ubuntu-22.04 location: fsn1 ssh_keys: - me@myorganisation state: present - name: Resize an existing server - hcloud_server: + hetzner.hcloud.server: name: my-server server_type: cx21 - upgrade_disk: yes + upgrade_disk: true state: present - name: Ensure the server is absent (remove if needed) - hcloud_server: + hetzner.hcloud.server: name: my-server state: absent - name: Ensure the server is started - hcloud_server: + hetzner.hcloud.server: name: my-server state: started - name: Ensure the server is stopped - hcloud_server: + hetzner.hcloud.server: name: my-server state: stopped - name: Ensure the server is restarted - hcloud_server: + hetzner.hcloud.server: name: my-server state: restarted - name: Ensure the server is will be booted in rescue mode and therefore restarted - hcloud_server: + hetzner.hcloud.server: name: my-server rescue_mode: linux64 state: restarted - name: Ensure the server is rebuild - hcloud_server: + hetzner.hcloud.server: name: my-server - image: ubuntu-18.04 + image: ubuntu-22.04 state: rebuild - name: Add server to placement group - hcloud_server: + hetzner.hcloud.server: name: my-server placement_group: my-placement-group - force: True + force: true state: present - name: Remove server from placement group - hcloud_server: + hetzner.hcloud.server: name: my-server - placement_group: null + placement_group: state: present - name: Add server with private network only - hcloud_server: + hetzner.hcloud.server: name: my-server enable_ipv4: false enable_ipv6: false @@ -257,6 +254,11 @@ hcloud_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 @@ -333,44 +335,50 @@ hcloud_server: version_added: "0.1.0" """ +from datetime import datetime, timedelta, timezone + 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 +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) + 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) + 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], + "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), @@ -387,20 +395,14 @@ class AnsibleHcloudServer(Hcloud): 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") - ) + 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) + 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"] - ) + self.module.fail_on_missing_params(required_params=["name", "server_type", "image"]) server_type = self._get_server_type() @@ -413,21 +415,21 @@ class AnsibleHcloudServer(Hcloud): "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") - ) + 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 + 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: - 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 + 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 = [] @@ -439,37 +441,28 @@ class AnsibleHcloudServer(Hcloud): 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") - ] + 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") - ] + 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: + 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(f) + params["firewalls"].append(firewall) else: - params["firewalls"].append(self.client.firewalls.get_by_id(fw)) + 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") - ) + 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") - ) + 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 @@ -494,16 +487,22 @@ class AnsibleHcloudServer(Hcloud): 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.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_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 @@ -511,46 +510,76 @@ class AnsibleHcloudServer(Hcloud): 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')) + 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( - "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')) + 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=("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'))) + 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") - ) + 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')) + 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") - ) + 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")) + 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 @@ -558,20 +587,17 @@ class AnsibleHcloudServer(Hcloud): if self.module.params.get(field) is None: return None - primary_ip = self.client.primary_ips.get_by_name( - self.module.params.get(field) - ) + 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)) + 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: + 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: @@ -609,30 +635,35 @@ class AnsibleHcloudServer(Hcloud): 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() + 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 fname in wanted_firewalls: + for firewall_name in wanted_firewalls: found = False - for f in self.hcloud_server.public_net.firewalls: - if f.firewall.name == fname: + 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: - 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() + 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: @@ -641,12 +672,9 @@ class AnsibleHcloudServer(Hcloud): 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 - ) + 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: @@ -655,9 +683,9 @@ class AnsibleHcloudServer(Hcloud): 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.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: @@ -665,12 +693,9 @@ class AnsibleHcloudServer(Hcloud): 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 - ) + 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: @@ -680,9 +705,9 @@ class AnsibleHcloudServer(Hcloud): 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.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: @@ -690,12 +715,9 @@ class AnsibleHcloudServer(Hcloud): 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 - ) + 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: @@ -710,11 +732,10 @@ class AnsibleHcloudServer(Hcloud): 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} - ) + _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: @@ -732,53 +753,56 @@ class AnsibleHcloudServer(Hcloud): 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 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) - 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" - ) + 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"]): + 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.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) + 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")]) + 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() @@ -792,8 +816,8 @@ class AnsibleHcloudServer(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) def stop_server(self): try: @@ -803,40 +827,40 @@ class AnsibleHcloudServer(Hcloud): 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) + 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.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 + 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"] - ) + 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. + # 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 Exception as e: - self.module.fail_json(msg=e.message) + except HCloudException as exception: + self.fail_json_hcloud(exception) def present_server(self): self._get_server() @@ -853,11 +877,11 @@ class AnsibleHcloudServer(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, @@ -879,7 +903,7 @@ class AnsibleHcloudServer(Hcloud): ipv6={"type": "str"}, private_networks={"type": "list", "elements": "str", "default": None}, force={"type": "bool", "default": False}, - force_upgrade={"type": "bool", "default": False}, + force_upgrade={"type": "bool"}, allow_deprecated_image={"type": "bool", "default": False}, rescue_mode={"type": "str"}, delete_protection={"type": "bool"}, @@ -889,9 +913,9 @@ class AnsibleHcloudServer(Hcloud): "choices": ["absent", "present", "restarted", "started", "stopped", "rebuild"], "default": "present", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), - required_one_of=[['id', 'name']], + required_one_of=[["id", "name"]], mutually_exclusive=[["location", "datacenter"]], required_together=[["delete_protection", "rebuild_protection"]], supports_check_mode=True, @@ -899,9 +923,9 @@ class AnsibleHcloudServer(Hcloud): def main(): - module = AnsibleHcloudServer.define_module() + module = AnsibleHCloudServer.define_module() - hcloud = AnsibleHcloudServer(module) + hcloud = AnsibleHCloudServer(module) state = module.params.get("state") if state == "absent": hcloud.delete_server() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/server_info.py index 102ceec0d..cee1634cb 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_info.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/server_info.py @@ -1,24 +1,20 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_server_info +module: 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) @@ -27,6 +23,7 @@ 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: @@ -39,11 +36,11 @@ options: extends_documentation_fragment: - hetzner.hcloud.hcloud -''' +""" EXAMPLES = """ - name: Gather hcloud server infos - hcloud_server_info: + hetzner.hcloud.server_info: register: output - name: Print the gathered infos @@ -67,6 +64,11 @@ hcloud_server_info: 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 @@ -144,14 +146,17 @@ hcloud_server_info: """ 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.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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_server_info") - self.hcloud_server_info = None + +class AnsibleHCloudServerInfo(AnsibleHCloud): + represent = "hcloud_server_info" + + hcloud_server_info: list[BoundServer] | None = None def _prepare_result(self): tmp = [] @@ -163,81 +168,70 @@ class AnsibleHcloudServerInfo(Hcloud): 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"], - }) + 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") - )] + 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") - )] + 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")) + 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, name={"type": "str"}, label_selector={"type": "str"}, - **Hcloud.base_module_arguments() + **super().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") + module = AnsibleHCloudServerInfo.define_module() + hcloud = AnsibleHCloudServerInfo(module) - 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) + ansible_info = {"hcloud_server_info": result["hcloud_server_info"]} + module.exit_json(**ansible_info) if __name__ == "__main__": diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_network.py b/ansible_collections/hetzner/hcloud/plugins/modules/server_network.py index 79f6838fd..ca80a8a76 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_server_network.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/server_network.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_server_network +module: server_network short_description: Manage the relationship between Hetzner Cloud Networks and servers @@ -24,12 +22,12 @@ author: options: network: description: - - The name of the Hetzner Cloud Networks. + - Name or ID of the Hetzner Cloud Networks. type: str required: true server: description: - - The name of the Hetzner Cloud server. + - Name or ID of the Hetzner Cloud server. type: str required: true ip: @@ -48,40 +46,36 @@ options: 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: + hetzner.hcloud.server_network: network: my-network server: my-server state: present - name: Create a server network and specify the ip address - hcloud_server_network: + 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 - hcloud_server_network: + 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 + - 10.1.0.1 + - 10.2.0.1 state: present - name: Ensure the server network is absent (remove if needed) - hcloud_server_network: + hetzner.hcloud.server_network: network: my-network server: my-server state: absent @@ -110,27 +104,27 @@ hcloud_server_network: sample: 10.0.0.8 alias_ips: description: Alias IPs of the server within the Network ip range - type: str + type: list + elements: 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 +from ansible.module_utils.common.text.converters import to_native -try: - from hcloud import APIException -except ImportError: - APIException = None +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(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 +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 { @@ -142,20 +136,26 @@ class AnsibleHcloudServerNetwork(Hcloud): 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_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 Exception as e: - self.module.fail_json(msg=e.message) + except HCloudException as exception: + self.fail_json_hcloud(exception) 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 + 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 + "network": self.hcloud_network, } if self.module.params.get("ip") is not None: @@ -166,8 +166,8 @@ class AnsibleHcloudServerNetwork(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self._get_server_and_network() @@ -175,7 +175,7 @@ class AnsibleHcloudServerNetwork(Hcloud): def _update_server_network(self): params = { - "network": self.hcloud_network + "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): @@ -184,8 +184,8 @@ class AnsibleHcloudServerNetwork(Hcloud): 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) + except APIException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self._get_server_and_network() @@ -206,13 +206,13 @@ class AnsibleHcloudServerNetwork(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self.hcloud_server_network = None - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( network={"type": "str", "required": True}, @@ -223,16 +223,16 @@ class AnsibleHcloudServerNetwork(Hcloud): "choices": ["absent", "present"], "default": "present", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), supports_check_mode=True, ) def main(): - module = AnsibleHcloudServerNetwork.define_module() + module = AnsibleHCloudServerNetwork.define_module() - hcloud = AnsibleHcloudServerNetwork(module) + hcloud = AnsibleHCloudServerNetwork(module) state = module.params["state"] if state == "absent": hcloud.delete_server_network() 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/hcloud_ssh_key.py b/ansible_collections/hetzner/hcloud/plugins/modules/ssh_key.py index 59a5197f5..349c52c68 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_ssh_key.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/ssh_key.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_ssh_key +module: ssh_key short_description: Create and manage ssh keys on the Hetzner Cloud. @@ -55,26 +53,26 @@ options: extends_documentation_fragment: - hetzner.hcloud.hcloud -''' +""" EXAMPLES = """ - name: Create a basic ssh_key - hcloud_ssh_key: + hetzner.hcloud.ssh_key: name: my-ssh_key - public_key: "ssh-rsa AAAjjk76kgf...Xt" + public_key: ssh-rsa AAAjjk76kgf...Xt state: present - name: Create a ssh_key with labels - hcloud_ssh_key: + hetzner.hcloud.ssh_key: name: my-ssh_key - public_key: "ssh-rsa AAAjjk76kgf...Xt" + public_key: ssh-rsa AAAjjk76kgf...Xt labels: - key: value - mylabel: 123 + key: value + mylabel: 123 state: present - name: Ensure the ssh_key is absent (remove if needed) - hcloud_ssh_key: + hetzner.hcloud.ssh_key: name: my-ssh_key state: absent """ @@ -115,14 +113,17 @@ hcloud_ssh_key: """ 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.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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_ssh_key") - self.hcloud_ssh_key = None + +class AnsibleHCloudSSHKey(AnsibleHCloud): + represent = "hcloud_ssh_key" + + hcloud_ssh_key: BoundSSHKey | None = None def _prepare_result(self): return { @@ -136,45 +137,35 @@ class AnsibleHcloudSSHKey(Hcloud): 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") - ) + 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") - ) + 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") - ) + 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) + 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"] - ) + 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") + "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) + 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"] - ) + 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() @@ -200,13 +191,13 @@ class AnsibleHcloudSSHKey(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self.hcloud_ssh_key = None - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, @@ -218,18 +209,18 @@ class AnsibleHcloudSSHKey(Hcloud): "choices": ["absent", "present"], "default": "present", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), - required_one_of=[['id', 'name', 'fingerprint']], - required_if=[['state', 'present', ['name']]], + required_one_of=[["id", "name", "fingerprint"]], + required_if=[["state", "present", ["name"]]], supports_check_mode=True, ) def main(): - module = AnsibleHcloudSSHKey.define_module() + module = AnsibleHCloudSSHKey.define_module() - hcloud = AnsibleHcloudSSHKey(module) + hcloud = AnsibleHCloudSSHKey(module) state = module.params.get("state") if state == "absent": hcloud.delete_ssh_key() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_ssh_key_facts.py b/ansible_collections/hetzner/hcloud/plugins/modules/ssh_key_info.py index aab98ed60..7a4ab5928 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_ssh_key_facts.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/ssh_key_info.py @@ -1,27 +1,24 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_ssh_key_info +module: 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. + - The module will fail if the provided ID is invalid. type: int name: description: @@ -38,11 +35,11 @@ options: extends_documentation_fragment: - hetzner.hcloud.hcloud -''' +""" EXAMPLES = """ - name: Gather hcloud sshkey infos - hcloud_ssh_key_info: + hetzner.hcloud.ssh_key_info: register: output - name: Print the gathered infos debug: @@ -80,89 +77,79 @@ hcloud_ssh_key_info: 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 +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(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_ssh_key_info") - self.hcloud_ssh_key_info = None +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 - }) + 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") - )] + 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") - )] + 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") - )] + 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")) + 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, name={"type": "str"}, fingerprint={"type": "str"}, label_selector={"type": "str"}, - **Hcloud.base_module_arguments() + **super().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") + module = AnsibleHCloudSSHKeyInfo.define_module() + hcloud = AnsibleHCloudSSHKeyInfo(module) - 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) + ansible_info = {"hcloud_ssh_key_info": result["hcloud_ssh_key_info"]} + module.exit_json(**ansible_info) if __name__ == "__main__": diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_subnetwork.py b/ansible_collections/hetzner/hcloud/plugins/modules/subnetwork.py index c2ba66d80..aea40bb13 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_subnetwork.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/subnetwork.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_subnetwork +module: subnetwork short_description: Manage cloud subnetworks on the Hetzner Cloud. @@ -24,7 +22,7 @@ author: options: network: description: - - The ID or Name of the Hetzner Cloud Networks. + - The name or ID of the Hetzner Cloud Networks. type: str required: true ip_range: @@ -55,17 +53,13 @@ options: 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: + hetzner.hcloud.subnetwork: network: my-network ip_range: 10.0.0.0/16 network_zone: eu-central @@ -73,7 +67,7 @@ EXAMPLES = """ state: present - name: Create a basic subnetwork - hcloud_subnetwork: + hetzner.hcloud.subnetwork: network: my-vswitch-network ip_range: 10.0.0.0/24 network_zone: eu-central @@ -82,7 +76,7 @@ EXAMPLES = """ state: present - name: Ensure the subnetwork is absent (remove if needed) - hcloud_subnetwork: + hetzner.hcloud.subnetwork: network: my-network ip_range: 10.0.0.0/8 network_zone: eu-central @@ -129,20 +123,18 @@ hcloud_subnetwork: """ 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.module_utils.common.text.converters import to_native -try: - from hcloud.networks.domain import NetworkSubnet -except ImportError: - NetworkSubnet = None +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.networks import BoundNetwork, NetworkSubnet -class AnsibleHcloudSubnetwork(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_subnetwork") - self.hcloud_network = None - self.hcloud_subnetwork = None +class AnsibleHCloudSubnetwork(AnsibleHCloud): + represent = "hcloud_subnetwork" + + hcloud_network: BoundNetwork | None = None + hcloud_subnetwork: NetworkSubnet | None = None def _prepare_result(self): return { @@ -156,10 +148,13 @@ class AnsibleHcloudSubnetwork(Hcloud): def _get_network(self): try: - self.hcloud_network = self.client.networks.get_by_name(self.module.params.get("network")) + self.hcloud_network = self._client_get_by_name_or_id( + "networks", + self.module.params.get("network"), + ) self.hcloud_subnetwork = None - except Exception as e: - self.module.fail_json(msg=e.message) + except HCloudException as exception: + self.fail_json_hcloud(exception) def _get_subnetwork(self): subnet_ip_range = self.module.params.get("ip_range") @@ -170,20 +165,18 @@ class AnsibleHcloudSubnetwork(Hcloud): 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') + "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 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self._get_network() @@ -202,38 +195,34 @@ class AnsibleHcloudSubnetwork(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self.hcloud_subnetwork = None - @staticmethod - def define_module(): + @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"] - }, + 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() + **super().base_module_arguments(), ), supports_check_mode=True, ) def main(): - module = AnsibleHcloudSubnetwork.define_module() + module = AnsibleHCloudSubnetwork.define_module() - hcloud = AnsibleHcloudSubnetwork(module) + hcloud = AnsibleHCloudSubnetwork(module) state = module.params["state"] if state == "absent": hcloud.delete_subnetwork() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_volume.py b/ansible_collections/hetzner/hcloud/plugins/modules/volume.py index 623a399b4..8442ed90b 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_volume.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/volume.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_volume +module: volume short_description: Create and manage block Volume on the Hetzner Cloud. @@ -75,36 +73,36 @@ options: extends_documentation_fragment: - hetzner.hcloud.hcloud -''' +""" EXAMPLES = """ - name: Create a Volume - hcloud_volume: + hetzner.hcloud.volume: name: my-volume location: fsn1 size: 100 state: present - name: Create a Volume and format it with ext4 - hcloud_volume: + hetzner.hcloud.volume: name: my-volume location: fsn format: ext4 size: 100 state: present - name: Mount a existing Volume and automount - hcloud_volume: + hetzner.hcloud.volume: name: my-volume server: my-server - automount: yes + automount: true state: present - name: Mount a existing Volume and automount - hcloud_volume: + hetzner.hcloud.volume: name: my-volume server: my-server - automount: yes + automount: true state: present - name: Ensure the Volume is absent (remove if needed) - hcloud_volume: + hetzner.hcloud.volume: name: my-volume state: absent """ @@ -162,14 +160,17 @@ hcloud_volume: """ 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.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" -class AnsibleHcloudVolume(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_volume") - self.hcloud_volume = None + hcloud_volume: BoundVolume | None = None def _prepare_result(self): server_name = None @@ -190,31 +191,25 @@ class AnsibleHcloudVolume(Hcloud): 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") - ) + 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) + 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"] - ) + 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") + "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")) + 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")) + params["location"] = self.client.locations.get_by_name(self.module.params.get("location")) else: self.module.fail_json(msg="server or location is required") @@ -227,8 +222,8 @@ class AnsibleHcloudVolume(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) self._mark_as_changed() self._get_volume() @@ -270,8 +265,8 @@ class AnsibleHcloudVolume(Hcloud): self._mark_as_changed() self._get_volume() - except Exception as e: - self.module.fail_json(msg=e.message) + except HCloudException as exception: + self.fail_json_hcloud(exception) def present_volume(self): self._get_volume() @@ -290,11 +285,11 @@ class AnsibleHcloudVolume(Hcloud): 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, @@ -304,31 +299,27 @@ class AnsibleHcloudVolume(Hcloud): server={"type": "str"}, labels={"type": "dict"}, automount={"type": "bool", "default": False}, - format={"type": "str", - "choices": ['xfs', 'ext4'], - }, + format={"type": "str", "choices": ["xfs", "ext4"]}, delete_protection={"type": "bool"}, state={ "choices": ["absent", "present"], "default": "present", }, - **Hcloud.base_module_arguments() + **super().base_module_arguments(), ), - required_one_of=[['id', 'name']], + required_one_of=[["id", "name"]], mutually_exclusive=[["location", "server"]], supports_check_mode=True, ) def main(): - module = AnsibleHcloudVolume.define_module() + module = AnsibleHCloudVolume.define_module() - hcloud = AnsibleHcloudVolume(module) + hcloud = AnsibleHCloudVolume(module) state = module.params.get("state") if state == "absent": - module.fail_on_missing_params( - required_params=["name"] - ) + module.fail_on_missing_params(required_params=["name"]) hcloud.delete_volume() else: hcloud.present_volume() diff --git a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_volume_info.py b/ansible_collections/hetzner/hcloud/plugins/modules/volume_info.py index 9520bfa14..1e507690e 100644 --- a/ansible_collections/hetzner/hcloud/plugins/modules/hcloud_volume_info.py +++ b/ansible_collections/hetzner/hcloud/plugins/modules/volume_info.py @@ -1,16 +1,14 @@ #!/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 +from __future__ import annotations -DOCUMENTATION = ''' +DOCUMENTATION = """ --- -module: hcloud_volume_info +module: volume_info short_description: Gather infos about your Hetzner Cloud Volumes. @@ -24,6 +22,7 @@ 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: @@ -36,11 +35,11 @@ options: extends_documentation_fragment: - hetzner.hcloud.hcloud -''' +""" EXAMPLES = """ - name: Gather hcloud Volume infos - hcloud_volume_info: + hetzner.hcloud.volume_info: register: output - name: Print the gathered infos debug: @@ -96,14 +95,17 @@ hcloud_volume_info: """ 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.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" -class AnsibleHcloudVolumeInfo(Hcloud): - def __init__(self, module): - Hcloud.__init__(self, module, "hcloud_volume_info") - self.hcloud_volume_info = None + hcloud_volume_info: list[BoundVolume] | None = None def _prepare_result(self): tmp = [] @@ -113,73 +115,59 @@ class AnsibleHcloudVolumeInfo(Hcloud): 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"], - }) + 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") - )] + 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") - )] + 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")) + 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) + except HCloudException as exception: + self.fail_json_hcloud(exception) - @staticmethod - def define_module(): + @classmethod + def define_module(cls): return AnsibleModule( argument_spec=dict( id={"type": "int"}, name={"type": "str"}, label_selector={"type": "str"}, - **Hcloud.base_module_arguments() + **super().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) + module = AnsibleHCloudVolumeInfo.define_module() + 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) + + ansible_info = {"hcloud_volume_info": result["hcloud_volume_info"]} + module.exit_json(**ansible_info) if __name__ == "__main__": |