From 6dd76f7bdef0c8d474a201e973037cb767be1e6a Mon Sep 17 00:00:00 2001 From: "Jonas L." Date: Wed, 10 Dec 2025 11:32:23 +0100 Subject: [PATCH] feat: support Storage Box Subaccount modules (#755) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ##### SUMMARY Add support for the Storage Box Snapshot resource with 2 new modules: - `storage_box_subaccount` - `storage_box_subaccount_info` --------- Co-authored-by: Julian Tölle --- .../module_utils/storage_box_subaccount.py | 44 ++ plugins/modules/storage_box_info.py | 8 +- plugins/modules/storage_box_subaccount.py | 473 ++++++++++++++++++ .../modules/storage_box_subaccount_info.py | 234 +++++++++ .../targets/storage_box_subaccount/aliases | 3 + .../defaults/main/common.yml | 35 ++ .../defaults/main/main.yml | 6 + .../storage_box_subaccount/meta/main.yml | 3 + .../storage_box_subaccount/tasks/cleanup.yml | 5 + .../storage_box_subaccount/tasks/main.yml | 31 ++ .../storage_box_subaccount/tasks/prepare.yml | 15 + .../storage_box_subaccount/tasks/test.yml | 190 +++++++ .../storage_box_subaccount_info/aliases | 3 + .../defaults/main/common.yml | 35 ++ .../defaults/main/main.yml | 6 + .../storage_box_subaccount_info/meta/main.yml | 3 + .../tasks/cleanup.yml | 5 + .../tasks/main.yml | 31 ++ .../tasks/prepare.yml | 37 ++ .../tasks/test.yml | 86 ++++ 20 files changed, 1249 insertions(+), 4 deletions(-) create mode 100644 plugins/module_utils/storage_box_subaccount.py create mode 100644 plugins/modules/storage_box_subaccount.py create mode 100644 plugins/modules/storage_box_subaccount_info.py create mode 100644 tests/integration/targets/storage_box_subaccount/aliases create mode 100644 tests/integration/targets/storage_box_subaccount/defaults/main/common.yml create mode 100644 tests/integration/targets/storage_box_subaccount/defaults/main/main.yml create mode 100644 tests/integration/targets/storage_box_subaccount/meta/main.yml create mode 100644 tests/integration/targets/storage_box_subaccount/tasks/cleanup.yml create mode 100644 tests/integration/targets/storage_box_subaccount/tasks/main.yml create mode 100644 tests/integration/targets/storage_box_subaccount/tasks/prepare.yml create mode 100644 tests/integration/targets/storage_box_subaccount/tasks/test.yml create mode 100644 tests/integration/targets/storage_box_subaccount_info/aliases create mode 100644 tests/integration/targets/storage_box_subaccount_info/defaults/main/common.yml create mode 100644 tests/integration/targets/storage_box_subaccount_info/defaults/main/main.yml create mode 100644 tests/integration/targets/storage_box_subaccount_info/meta/main.yml create mode 100644 tests/integration/targets/storage_box_subaccount_info/tasks/cleanup.yml create mode 100644 tests/integration/targets/storage_box_subaccount_info/tasks/main.yml create mode 100644 tests/integration/targets/storage_box_subaccount_info/tasks/prepare.yml create mode 100644 tests/integration/targets/storage_box_subaccount_info/tasks/test.yml diff --git a/plugins/module_utils/storage_box_subaccount.py b/plugins/module_utils/storage_box_subaccount.py new file mode 100644 index 0000000..b4175fd --- /dev/null +++ b/plugins/module_utils/storage_box_subaccount.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from ..module_utils.vendor.hcloud.storage_boxes import ( + BoundStorageBox, + BoundStorageBoxSubaccount, +) + +NAME_LABEL_KEY = "ansible-name" + + +def get_by_name(storage_box: BoundStorageBox, name: str): + if not name: + raise ValueError(f"invalid storage box subaccount name: '{name}'") + + result = storage_box.get_subaccount_list( + label_selector=f"{NAME_LABEL_KEY}={name}", + ) + if len(result.subaccounts) == 0: + return None + if len(result.subaccounts) == 1: + return result.subaccounts[0] + + raise ValueError(f"found multiple storage box subaccount with the same name: {name}") + + +def prepare_result(o: BoundStorageBoxSubaccount, name: str): + return { + "storage_box": o.storage_box.id, + "id": o.id, + "name": name, + "description": o.description, + "username": o.username, + "home_directory": o.home_directory, + "server": o.server, + "access_settings": { + "reachable_externally": o.access_settings.reachable_externally, + "samba_enabled": o.access_settings.samba_enabled, + "ssh_enabled": o.access_settings.ssh_enabled, + "webdav_enabled": o.access_settings.webdav_enabled, + "readonly": o.access_settings.readonly, + }, + "labels": o.labels, + "created": o.created.isoformat(), + } diff --git a/plugins/modules/storage_box_info.py b/plugins/modules/storage_box_info.py index 856bba7..3f331bf 100644 --- a/plugins/modules/storage_box_info.py +++ b/plugins/modules/storage_box_info.py @@ -40,14 +40,14 @@ extends_documentation_fragment: EXAMPLES = """ - name: Gather all Storage Boxes - hetzner.hcloud.ssh_key_info: + hetzner.hcloud.storage_box_info: register: output - name: Print the gathered infos debug: var: output.hcloud_storage_box_info - name: Gather Storage Boxes by label - hetzner.hcloud.ssh_key_info: + hetzner.hcloud.storage_box_info: label_selector: env=prod register: output - name: Print the gathered infos @@ -55,7 +55,7 @@ EXAMPLES = """ var: output.hcloud_storage_box_info - name: Gather a Storage Box by name - hetzner.hcloud.ssh_key_info: + hetzner.hcloud.storage_box_info: name: backups register: output - name: Print the gathered infos @@ -63,7 +63,7 @@ EXAMPLES = """ var: output.hcloud_storage_box_info[0] - name: Gather a Storage Box by id - hetzner.hcloud.ssh_key_info: + hetzner.hcloud.storage_box_info: name: 12345 register: output - name: Print the gathered infos diff --git a/plugins/modules/storage_box_subaccount.py b/plugins/modules/storage_box_subaccount.py new file mode 100644 index 0000000..40397f9 --- /dev/null +++ b/plugins/modules/storage_box_subaccount.py @@ -0,0 +1,473 @@ +#!/usr/bin/python + +# Copyright: (c) 2025, Hetzner Cloud GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +from __future__ import annotations + +DOCUMENTATION = """ +--- +module: storage_box_subaccount + +short_description: Create and manage Storage Box Subaccounts in Hetzner. + +description: + - Create, update and delete Storage Box Subaccounts in Hetzner. + - See the L(Storage Box Subaccounts API documentation,https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts) for more details. + +author: + - Jonas Lammler (@jooola) + +options: + storage_box: + description: + - ID or Name of the parent Storage Box. + - Using the ID is preferred, to reduce the amount of API requests. + type: str + required: true + id: + description: + - ID of the Storage Box Subaccount to manage. + - Required if no Storage Box Subaccount O(name) is given. + type: int + name: + description: + - Name of the Storage Box Subaccount to manage. + - Required if no Storage Box Subaccount O(id) is given. + - Required if the Storage Box Subaccount does not exist. + - Because the API resource does not have this property, the name is stored + in the Storage Box Subaccount labels. This ensures that the module is + idempotent, and removes the need to use different module arguments for + create and update. + type: str + password: + description: + - Password for the Storage Box Subaccount. + - Required if the Storage Box Subaccount does not exist or when O(state=reset_password). + type: str + home_directory: + description: + - Home directory of the Storage Box Subaccount. + - Required if the Storage Box Subaccount does not exist. + type: str + access_settings: + description: + - Access settings of the Storage Box Subaccount. + type: dict + suboptions: + reachable_externally: + description: + - Whether access from outside the Hetzner network is allowed. + type: bool + default: false + samba_enabled: + description: + - Whether the Samba subsystem is enabled. + type: bool + default: false + ssh_enabled: + description: + - Whether the SSH subsystem is enabled. + type: bool + default: false + webdav_enabled: + description: + - Whether the WebDAV subsystem is enabled. + type: bool + default: false + readonly: + description: + - Whether the Subaccount is read-only. + type: bool + default: false + description: + description: + - Description of the Storage Box Subaccount. + type: str + labels: + description: + - User-defined labels (key-value pairs) for the Storage Box Subaccount. + type: dict + state: + description: + - State of the Storage Box Subaccount. + - C(reset_password) is not idempotent. + default: present + choices: [absent, present, reset_password] + type: str + +extends_documentation_fragment: + - hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a Storage Box Subaccount + hetzner.hcloud.storage_box_subaccount: + storage_box: my-storage-box + name: subaccount1 + home_directory: backups/subaccount1 + password: secret + access_settings: + reachable_externally: false + ssh_enabled: true + samba_enabled: false + webdav_enabled: false + readonly: false + labels: + env: prod + state: present + +- name: Reset a Storage Box Subaccount password + hetzner.hcloud.storage_box_subaccount: + storage_box: my-storage-box + name: subaccount1 + password: secret + state: reset_password + +- name: Delete a Storage Box Subaccount by name + hetzner.hcloud.storage_box_subaccount: + storage_box: my-storage-box + name: subaccount1 + state: absent + +- name: Delete a Storage Box Subaccount by id + hetzner.hcloud.storage_box_subaccount: + storage_box: 497436 + id: 158045 + state: absent +""" + +RETURN = """ +hcloud_storage_box_subaccount: + description: Details about the Storage Box Subaccount. + returned: always + type: dict + contains: + storage_box: + description: ID of the parent Storage Box. + returned: always + type: int + sample: 514605 + id: + description: ID of the Storage Box Subaccount. + returned: always + type: int + sample: 158045 + name: + description: Name of the Storage Box Subaccount. + returned: always + type: str + sample: subaccount1 + description: + description: Description of the Storage Box Subaccount. + returned: always + type: str + sample: backups from subaccount1 + home_directory: + description: Home directory of the Storage Box Subaccount. + returned: always + type: str + sample: backups/subaccount1 + username: + description: Username of the Storage Box Subaccount. + returned: always + type: str + sample: u514605-sub1 + server: + description: FQDN of the Storage Box Subaccount. + returned: always + type: str + sample: u514605-sub1.your-storagebox.de + access_settings: + description: Access settings of the Storage Box Subaccount. + returned: always + type: dict + contains: + reachable_externally: + description: Whether access from outside the Hetzner network is allowed. + returned: always + type: bool + sample: false + samba_enabled: + description: Whether the Samba subsystem is enabled. + returned: always + type: bool + sample: false + ssh_enabled: + description: Whether the SSH subsystem is enabled. + returned: always + type: bool + sample: true + webdav_enabled: + description: Whether the WebDAV subsystem is enabled. + returned: always + type: bool + sample: false + readonly: + description: Whether the Subaccount is read-only. + returned: always + type: bool + sample: false + labels: + description: User-defined labels (key-value pairs) of the Storage Box Subaccount. + returned: always + type: dict + sample: + env: prod + created: + description: Point in time when the Storage Box Subaccount was created (in RFC3339 format). + returned: always + type: str + sample: "2025-12-03T13:47:47Z" +""" + +import string + +from ..module_utils import storage_box, storage_box_subaccount +from ..module_utils.client import client_resource_not_found +from ..module_utils.hcloud import AnsibleHCloud, AnsibleModule +from ..module_utils.storage_box_subaccount import NAME_LABEL_KEY +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.storage_boxes import ( + BoundStorageBox, + BoundStorageBoxSubaccount, + StorageBoxSubaccountAccessSettings, +) + + +class AnsibleStorageBoxSubaccount(AnsibleHCloud): + represent = "storage_box_subaccount" + + storage_box: BoundStorageBox | None = None + storage_box_subaccount: BoundStorageBoxSubaccount | None = None + storage_box_subaccount_name: str | None = None + + def _prepare_result(self): + if self.storage_box_subaccount is None: + return {} + return storage_box_subaccount.prepare_result(self.storage_box_subaccount, self.storage_box_subaccount_name) + + def _fetch(self): + self.storage_box = storage_box.get(self.client.storage_boxes, self.module.params.get("storage_box")) + + if (value := self.module.params.get("id")) is not None: + self.storage_box_subaccount = self.storage_box.get_subaccount_by_id(value) + elif (value := self.module.params.get("name")) is not None: + self.storage_box_subaccount = storage_box_subaccount.get_by_name(self.storage_box, value) + + # Workaround the missing name property + # Get the name of the resource from the labels + if self.storage_box_subaccount is not None: + self.storage_box_subaccount_name = self.storage_box_subaccount.labels.pop(NAME_LABEL_KEY) + + def _create(self): + self.fail_on_invalid_params( + required=["name", "home_directory", "password"], + ) + params = { + "home_directory": self.module.params.get("home_directory"), + "password": self.module.params.get("password"), + } + + if (value := self.module.params.get("description")) is not None: + params["description"] = value + + if (value := self.module.params.get("labels")) is not None: + params["labels"] = value + + if (value := self.module.params.get("access_settings")) is not None: + params["access_settings"] = StorageBoxSubaccountAccessSettings.from_dict(value) + + # Workaround the missing name property + # Save the name of the resource in the labels + if "labels" not in params: + params["labels"] = {} + params["labels"][NAME_LABEL_KEY] = self.module.params.get("name") + + if not self.module.check_mode: + resp = self.storage_box.create_subaccount(**params) + self.storage_box_subaccount = resp.subaccount + self.actions.append(resp.action) + + self._wait_actions() + self.storage_box_subaccount.reload() + self.storage_box_subaccount_name = self.storage_box_subaccount.labels.pop(NAME_LABEL_KEY) + + self._mark_as_changed() + + def _update(self): + need_reload = False + + if (value := self.module.params.get("home_directory")) is not None: + if self.storage_box_subaccount.home_directory != value: + if not self.module.check_mode: + action = self.storage_box_subaccount.change_home_directory(value) + self.actions.append(action) + need_reload = True + self._mark_as_changed() + + if (value := self.module.params.get("access_settings")) is not None: + access_settings = StorageBoxSubaccountAccessSettings.from_dict(value) + if self.storage_box_subaccount.access_settings.to_payload() != access_settings.to_payload(): + if not self.module.check_mode: + action = self.storage_box_subaccount.update_access_settings(access_settings) + self.actions.append(action) + need_reload = True + self._mark_as_changed() + + if not self.module.check_mode: + self._wait_actions() + + params = {} + if (value := self.module.params.get("description")) is not None: + if value != self.storage_box_subaccount.description: + params["description"] = value + self._mark_as_changed() + + if (value := self.module.params.get("labels")) is not None: + if value != self.storage_box_subaccount.labels: + params["labels"] = value + self._mark_as_changed() + + # Workaround the missing name property + # Preserve resource name in the labels, name update happens below + params["labels"][NAME_LABEL_KEY] = self.storage_box_subaccount_name + + # Workaround the missing name property + # Update resource name in the labels + if (value := self.module.params.get("name")) is not None: + if value != self.storage_box_subaccount_name: + self.fail_on_invalid_params(required=["id"]) + if "labels" not in params: + params["labels"] = self.storage_box_subaccount.labels + params["labels"][NAME_LABEL_KEY] = value + self._mark_as_changed() + + # Update only if params holds changes + if params or need_reload: + if not self.module.check_mode: + self.storage_box_subaccount = self.storage_box_subaccount.update(**params) + self.storage_box_subaccount_name = self.storage_box_subaccount.labels.pop(NAME_LABEL_KEY) + + def _delete(self): + if not self.module.check_mode: + resp = self.storage_box_subaccount.delete() + resp.action.wait_until_finished() + + self.storage_box_subaccount = None + self._mark_as_changed() + + def present(self): + try: + self._fetch() + if self.storage_box_subaccount is None: + self._create() + else: + self._update() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def absent(self): + try: + self._fetch() + if self.storage_box_subaccount is None: + return + self._delete() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + def reset_password(self): + self.fail_on_invalid_params( + required=["password"], + ) + try: + self._fetch() + if self.storage_box_subaccount is None: + raise client_resource_not_found( + "storage box", + self.module.params.get("id") or self.module.params.get("name"), + ) + if not self.module.check_mode: + action = self.storage_box_subaccount.reset_password(self.module.params.get("password")) + self.actions.append(action) + self._wait_actions() + + self._mark_as_changed() + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + storage_box={"type": "str", "required": True}, + id={"type": "int"}, + name={"type": "str"}, + home_directory={"type": "str"}, + password={"type": "str", "no_log": True}, + description={"type": "str"}, + labels={"type": "dict"}, + access_settings={ + "type": "dict", + "options": dict( + reachable_externally={"type": "bool", "default": False}, + samba_enabled={"type": "bool", "default": False}, + ssh_enabled={"type": "bool", "default": False}, + webdav_enabled={"type": "bool", "default": False}, + readonly={"type": "bool", "default": False}, + ), + }, + state={ + "choices": ["absent", "present", "reset_password"], + "default": "present", + }, + **super().base_module_arguments(), + ), + required_one_of=[["id", "name"]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleStorageBoxSubaccount.define_module() + o = AnsibleStorageBoxSubaccount(module) + + # Workaround the missing name property + # Validate name + if (value := module.params.get("name")) is not None: + if len(value) < 1: + module.fail_json(f"name '{value}' must be at least 1 character long") + + allowed_chars = string.ascii_letters + string.digits + "-" + has_letters = False + for c in value: + if c in string.ascii_letters: + has_letters = True + if c in allowed_chars: + continue + module.fail_json(f"name '{value}' must only have allowed characters: {allowed_chars}") + if not has_letters: + module.fail_json(f"name '{value}' must contain at least one letter") + + match module.params.get("state"): + case "reset_password": + o.reset_password() + case "absent": + o.absent() + case _: + o.present() + + result = o.get_result() + + # Legacy return value naming pattern + result["hcloud_storage_box_subaccount"] = result.pop(o.represent) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/storage_box_subaccount_info.py b/plugins/modules/storage_box_subaccount_info.py new file mode 100644 index 0000000..0954e3f --- /dev/null +++ b/plugins/modules/storage_box_subaccount_info.py @@ -0,0 +1,234 @@ +#!/usr/bin/python + +# Copyright: (c) 2025, Hetzner Cloud GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +from __future__ import annotations + +DOCUMENTATION = """ +--- +module: storage_box_subaccount_info + +short_description: Gather infos about Hetzner Storage Box Subaccounts. + +description: + - Gather infos about Hetzner Storage Box Subaccounts. + - See the L(Storage Box Subaccounts API documentation,https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts) for more details. + +author: + - Jonas Lammler (@jooola) + +options: + storage_box: + description: + - ID or Name of the parent Storage Box. + - Using the ID is preferred, to reduce the amount of API requests. + type: str + required: true + id: + description: + - ID of the Storage Box Subaccount to get. + type: int + name: + description: + - Name of the Storage Box Subaccount to get. + type: str + label_selector: + description: + - Label selector to filter the Storage Box Subaccounts to get. + type: str + +extends_documentation_fragment: + - hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Gather all Storage Box Subaccounts + hetzner.hcloud.storage_box_subaccount_info: + register: output + +- name: Gather Storage Box Subaccounts by label + hetzner.hcloud.storage_box_subaccount_info: + label_selector: env=prod + register: output + +- name: Gather a Storage Box Subaccount by name + hetzner.hcloud.storage_box_subaccount_info: + name: subaccount1 + register: output + +- name: Gather a Storage Box Subaccount by id + hetzner.hcloud.storage_box_subaccount_info: + name: 12345 + register: output + +- name: Print the gathered infos + debug: + var: output.hcloud_storage_box_subaccount_info +""" + +RETURN = """ +hcloud_storage_box_subaccount_info: + description: List of Storage Box Subaccounts. + returned: always + type: list + elements: dict + contains: + storage_box: + description: ID of the parent Storage Box. + returned: always + type: int + sample: 514605 + id: + description: ID of the Storage Box Subaccount. + returned: always + type: int + sample: 158045 + name: + description: Name of the Storage Box Subaccount. + returned: always + type: str + sample: subaccount1 + description: + description: Description of the Storage Box Subaccount. + returned: always + type: str + sample: backups from subaccount1 + home_directory: + description: Home directory of the Storage Box Subaccount. + returned: always + type: str + sample: backups/subaccount1 + username: + description: Username of the Storage Box Subaccount. + returned: always + type: str + sample: u514605-sub1 + server: + description: FQDN of the Storage Box Subaccount. + returned: always + type: str + sample: u514605-sub1.your-storagebox.de + access_settings: + description: Access settings of the Storage Box Subaccount. + returned: always + type: dict + contains: + reachable_externally: + description: Whether access from outside the Hetzner network is allowed. + returned: always + type: bool + sample: false + samba_enabled: + description: Whether the Samba subsystem is enabled. + returned: always + type: bool + sample: false + ssh_enabled: + description: Whether the SSH subsystem is enabled. + returned: always + type: bool + sample: true + webdav_enabled: + description: Whether the WebDAV subsystem is enabled. + returned: always + type: bool + sample: false + readonly: + description: Whether the Subaccount is read-only. + returned: always + type: bool + sample: false + labels: + description: User-defined labels (key-value pairs) of the Storage Box Subaccount. + returned: always + type: dict + sample: + env: prod + created: + description: Point in time when the Storage Box Subaccount was created (in RFC3339 format). + returned: always + type: str + sample: "2025-12-03T13:47:47Z" +""" + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils import storage_box, storage_box_subaccount +from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.storage_box_subaccount import NAME_LABEL_KEY +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.storage_boxes import ( + BoundStorageBox, + BoundStorageBoxSubaccount, +) + + +class AnsibleStorageBoxSubaccountInfo(AnsibleHCloud): + represent = "storage_box_subaccounts" + + storage_box: BoundStorageBox | None = None + storage_box_subaccounts: list[BoundStorageBoxSubaccount] | None = None + + def _prepare_result(self): + result = [] + + for o in self.storage_box_subaccounts or []: + if o is not None: + # Workaround the missing name property + # Get the name of the resource from the labels + name = o.labels.pop(NAME_LABEL_KEY) + + result.append(storage_box_subaccount.prepare_result(o, name)) + return result + + def fetch(self): + try: + self.storage_box = storage_box.get( + self.client.storage_boxes, + self.module.params.get("storage_box"), + ) + + if (value := self.module.params.get("id")) is not None: + self.storage_box_subaccounts = [self.storage_box.get_subaccount_by_id(value)] + elif (value := self.module.params.get("name")) is not None: + self.storage_box_subaccounts = [storage_box_subaccount.get_by_name(self.storage_box, value)] + else: + params = {} + if (value := self.module.params.get("label_selector")) is not None: + params["label_selector"] = value + self.storage_box_subaccounts = self.storage_box.get_subaccount_all(**params) + + except HCloudException as exception: + self.fail_json_hcloud(exception) + + @classmethod + def define_module(cls): + return AnsibleModule( + argument_spec=dict( + storage_box={"type": "str", "required": True}, + id={"type": "int"}, + name={"type": "str"}, + label_selector={"type": "str"}, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleStorageBoxSubaccountInfo.define_module() + o = AnsibleStorageBoxSubaccountInfo(module) + + o.fetch() + result = o.get_result() + + # Legacy return value naming pattern + result["hcloud_storage_box_subaccount_info"] = result.pop(o.represent) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/storage_box_subaccount/aliases b/tests/integration/targets/storage_box_subaccount/aliases new file mode 100644 index 0000000..18b1111 --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount/aliases @@ -0,0 +1,3 @@ +cloud/hcloud +gather_facts/no +azp/group2 diff --git a/tests/integration/targets/storage_box_subaccount/defaults/main/common.yml b/tests/integration/targets/storage_box_subaccount/defaults/main/common.yml new file mode 100644 index 0000000..015c3b5 --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount/defaults/main/common.yml @@ -0,0 +1,35 @@ +# +# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead. +# +--- +# Azure Pipelines will configure this value to something similar to +# "azp-84824-1-hetzner-2-13-test-2-13-hcloud-3-9-1-default-i" +hcloud_prefix: "tests" + +# Used to namespace resources created by concurrent test pipelines/targets +hcloud_run_ns: "{{ hcloud_prefix | md5 }}" +hcloud_role_ns: "{{ role_name | split('_') | map('batch', 2) | map('first') | flatten() | join() }}" +hcloud_ns: "ansible-{{ hcloud_run_ns }}-{{ hcloud_role_ns }}" + +# Used to easily update the server types and images across all our tests. +hcloud_server_type_name: cax11 +hcloud_server_type_id: 45 + +hcloud_server_type_upgrade_name: cax21 +hcloud_server_type_upgrade_id: 93 + +hcloud_image_name: debian-12 +hcloud_image_id: 114690389 # architecture=arm + +hcloud_location_name: hel1 +hcloud_location_id: 3 +hcloud_datacenter_name: hel1-dc2 +hcloud_datacenter_id: 3 + +hcloud_network_zone_name: eu-central + +hcloud_storage_box_type_name: bx11 +hcloud_storage_box_type_id: 1333 + +hcloud_storage_box_type_upgrade_name: bx21 +hcloud_storage_box_type_upgrade_id: 1334 diff --git a/tests/integration/targets/storage_box_subaccount/defaults/main/main.yml b/tests/integration/targets/storage_box_subaccount/defaults/main/main.yml new file mode 100644 index 0000000..69c5659 --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount/defaults/main/main.yml @@ -0,0 +1,6 @@ +# Copyright: (c) 2019, Hetzner Cloud GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +hcloud_storage_box_name: "{{ hcloud_ns }}" +hcloud_ssh_key_name: "{{ hcloud_ns }}" +hcloud_storage_box_password: 1-secret-PASSW0RD-=) diff --git a/tests/integration/targets/storage_box_subaccount/meta/main.yml b/tests/integration/targets/storage_box_subaccount/meta/main.yml new file mode 100644 index 0000000..3a96ecb --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - setup_ssh_keypair diff --git a/tests/integration/targets/storage_box_subaccount/tasks/cleanup.yml b/tests/integration/targets/storage_box_subaccount/tasks/cleanup.yml new file mode 100644 index 0000000..d0ab906 --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount/tasks/cleanup.yml @@ -0,0 +1,5 @@ +--- +- name: Cleanup test_storage_box + hetzner.hcloud.storage_box: + name: "{{ hcloud_storage_box_name }}" + state: absent diff --git a/tests/integration/targets/storage_box_subaccount/tasks/main.yml b/tests/integration/targets/storage_box_subaccount/tasks/main.yml new file mode 100644 index 0000000..767fc46 --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount/tasks/main.yml @@ -0,0 +1,31 @@ +# +# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead. +# +--- +- name: Check if cleanup.yml exists + ansible.builtin.stat: + path: "{{ role_path }}/tasks/cleanup.yml" + register: cleanup_file + +- name: Check if prepare.yml exists + ansible.builtin.stat: + path: "{{ role_path }}/tasks/prepare.yml" + register: prepare_file + +- name: Include cleanup tasks + ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml" + when: cleanup_file.stat.exists + +- name: Include prepare tasks + ansible.builtin.include_tasks: "{{ role_path }}/tasks/prepare.yml" + when: prepare_file.stat.exists + +- name: Run tests + block: + - name: Include test tasks + ansible.builtin.include_tasks: "{{ role_path }}/tasks/test.yml" + + always: + - name: Include cleanup tasks + ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml" + when: cleanup_file.stat.exists diff --git a/tests/integration/targets/storage_box_subaccount/tasks/prepare.yml b/tests/integration/targets/storage_box_subaccount/tasks/prepare.yml new file mode 100644 index 0000000..5e009ac --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount/tasks/prepare.yml @@ -0,0 +1,15 @@ +--- +- name: Create test_storage_box + hetzner.hcloud.storage_box: + name: "{{ hcloud_storage_box_name }}" + type: "{{ hcloud_storage_box_type_name }}" + location: "{{ hcloud_location_name }}" + password: "{{ hcloud_storage_box_password }}" + ssh_keys: + - "{{ test_ssh_keypair.public_key }}" + access_settings: + ssh_enabled: true + reachable_externally: false + labels: + key: "{{ hcloud_ns }}" + register: test_storage_box diff --git a/tests/integration/targets/storage_box_subaccount/tasks/test.yml b/tests/integration/targets/storage_box_subaccount/tasks/test.yml new file mode 100644 index 0000000..194dddc --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount/tasks/test.yml @@ -0,0 +1,190 @@ +--- +- name: Test missing required parameters # noqa: args[module] + hetzner.hcloud.storage_box_subaccount: + storage_box: "{{ hcloud_storage_box_name }}" + ignore_errors: true + register: result +- name: Verify missing required parameters + ansible.builtin.assert: + that: + - result is failed + - 'result.msg == "one of the following is required: id, name"' + +- name: Test create with check mode + hetzner.hcloud.storage_box_subaccount: + storage_box: "{{ hcloud_storage_box_name }}" + name: subaccount1 + description: backups from subaccount1 + home_directory: backups/subaccount1 + password: "{{ hcloud_storage_box_password }}" + labels: + key: value + access_settings: + ssh_enabled: true + readonly: false + check_mode: true + register: result +- name: Verify create with check mode + ansible.builtin.assert: + that: + - result is changed + +- name: Test create + hetzner.hcloud.storage_box_subaccount: + storage_box: "{{ hcloud_storage_box_name }}" + name: subaccount1 + description: backups from subaccount1 + home_directory: backups/subaccount1 + password: "{{ hcloud_storage_box_password }}" + labels: + key: value + access_settings: + ssh_enabled: true + readonly: false + register: result +- name: Verify create + ansible.builtin.assert: + that: + - result is changed + - result.hcloud_storage_box_subaccount.storage_box == test_storage_box.hcloud_storage_box.id + - result.hcloud_storage_box_subaccount.id is not none + - result.hcloud_storage_box_subaccount.name == "subaccount1" + - result.hcloud_storage_box_subaccount.description == "backups from subaccount1" + - result.hcloud_storage_box_subaccount.home_directory == "backups/subaccount1" + - result.hcloud_storage_box_subaccount.username is not none + - result.hcloud_storage_box_subaccount.server is not none + - result.hcloud_storage_box_subaccount.labels.key == "value" + - result.hcloud_storage_box_subaccount.access_settings.reachable_externally == false + - result.hcloud_storage_box_subaccount.access_settings.samba_enabled == false + - result.hcloud_storage_box_subaccount.access_settings.ssh_enabled == true + - result.hcloud_storage_box_subaccount.access_settings.webdav_enabled == false + - result.hcloud_storage_box_subaccount.access_settings.readonly == false + - result.hcloud_storage_box_subaccount.created is not none + +- name: Test create idempotency + hetzner.hcloud.storage_box_subaccount: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + name: subaccount1 + description: backups from subaccount1 + home_directory: backups/subaccount1 + password: "{{ hcloud_storage_box_password }}" + labels: + key: value + access_settings: + ssh_enabled: true + readonly: false + register: result +- name: Verify create idempotency + ansible.builtin.assert: + that: + - result is not changed + +- name: Test update + hetzner.hcloud.storage_box_subaccount: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + name: subaccount1 + description: backups from subaccount2 # Update + home_directory: backups/subaccount2 # Update + password: "{{ hcloud_storage_box_password }}" + labels: + key: changed # Update + access_settings: + ssh_enabled: true + readonly: true # Update + register: result +- name: Verify update + ansible.builtin.assert: + that: + - result is changed + - result.hcloud_storage_box_subaccount.storage_box == test_storage_box.hcloud_storage_box.id + - result.hcloud_storage_box_subaccount.id is not none + - result.hcloud_storage_box_subaccount.name == "subaccount1" + - result.hcloud_storage_box_subaccount.description == "backups from subaccount2" + - result.hcloud_storage_box_subaccount.home_directory == "backups/subaccount2" + - result.hcloud_storage_box_subaccount.username is not none + - result.hcloud_storage_box_subaccount.server is not none + - result.hcloud_storage_box_subaccount.labels.key == "changed" + - result.hcloud_storage_box_subaccount.access_settings.reachable_externally == false + - result.hcloud_storage_box_subaccount.access_settings.samba_enabled == false + - result.hcloud_storage_box_subaccount.access_settings.ssh_enabled == true + - result.hcloud_storage_box_subaccount.access_settings.webdav_enabled == false + - result.hcloud_storage_box_subaccount.access_settings.readonly == true + - result.hcloud_storage_box_subaccount.created is not none + +- name: Test update idempotency + hetzner.hcloud.storage_box_subaccount: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + name: subaccount1 + description: backups from subaccount2 # Update + home_directory: backups/subaccount2 # Update + password: "{{ hcloud_storage_box_password }}" + labels: + key: changed # Update + access_settings: + ssh_enabled: true + readonly: true # Update + register: result +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + +- name: Test update name + hetzner.hcloud.storage_box_subaccount: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + id: "{{ result.hcloud_storage_box_subaccount.id }}" + name: subaccount2 + register: result +- name: Verify update + ansible.builtin.assert: + that: + - result is changed + - result.hcloud_storage_box_subaccount.storage_box == test_storage_box.hcloud_storage_box.id + - result.hcloud_storage_box_subaccount.id is not none + - result.hcloud_storage_box_subaccount.name == "subaccount2" # Update + - result.hcloud_storage_box_subaccount.description == "backups from subaccount2" + - result.hcloud_storage_box_subaccount.home_directory == "backups/subaccount2" + - result.hcloud_storage_box_subaccount.username is not none + - result.hcloud_storage_box_subaccount.server is not none + - result.hcloud_storage_box_subaccount.labels.key == "changed" + - result.hcloud_storage_box_subaccount.access_settings.reachable_externally == false + - result.hcloud_storage_box_subaccount.access_settings.samba_enabled == false + - result.hcloud_storage_box_subaccount.access_settings.ssh_enabled == true + - result.hcloud_storage_box_subaccount.access_settings.webdav_enabled == false + - result.hcloud_storage_box_subaccount.access_settings.readonly == true + - result.hcloud_storage_box_subaccount.created is not none + +- name: Test reset password + hetzner.hcloud.storage_box_subaccount: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + name: subaccount2 + password: "{{ hcloud_storage_box_password }}" + state: reset_password + register: result +- name: Verify reset password + ansible.builtin.assert: + that: + - result is changed + +- name: Test delete + hetzner.hcloud.storage_box_subaccount: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + name: subaccount2 + state: absent + register: result +- name: Verify delete + ansible.builtin.assert: + that: + - result is changed + - result.hcloud_storage_box_subaccount is none + +- name: Test delete idempotency + hetzner.hcloud.storage_box_subaccount: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + name: subaccount2 + state: absent + register: result +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed diff --git a/tests/integration/targets/storage_box_subaccount_info/aliases b/tests/integration/targets/storage_box_subaccount_info/aliases new file mode 100644 index 0000000..18b1111 --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount_info/aliases @@ -0,0 +1,3 @@ +cloud/hcloud +gather_facts/no +azp/group2 diff --git a/tests/integration/targets/storage_box_subaccount_info/defaults/main/common.yml b/tests/integration/targets/storage_box_subaccount_info/defaults/main/common.yml new file mode 100644 index 0000000..015c3b5 --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount_info/defaults/main/common.yml @@ -0,0 +1,35 @@ +# +# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead. +# +--- +# Azure Pipelines will configure this value to something similar to +# "azp-84824-1-hetzner-2-13-test-2-13-hcloud-3-9-1-default-i" +hcloud_prefix: "tests" + +# Used to namespace resources created by concurrent test pipelines/targets +hcloud_run_ns: "{{ hcloud_prefix | md5 }}" +hcloud_role_ns: "{{ role_name | split('_') | map('batch', 2) | map('first') | flatten() | join() }}" +hcloud_ns: "ansible-{{ hcloud_run_ns }}-{{ hcloud_role_ns }}" + +# Used to easily update the server types and images across all our tests. +hcloud_server_type_name: cax11 +hcloud_server_type_id: 45 + +hcloud_server_type_upgrade_name: cax21 +hcloud_server_type_upgrade_id: 93 + +hcloud_image_name: debian-12 +hcloud_image_id: 114690389 # architecture=arm + +hcloud_location_name: hel1 +hcloud_location_id: 3 +hcloud_datacenter_name: hel1-dc2 +hcloud_datacenter_id: 3 + +hcloud_network_zone_name: eu-central + +hcloud_storage_box_type_name: bx11 +hcloud_storage_box_type_id: 1333 + +hcloud_storage_box_type_upgrade_name: bx21 +hcloud_storage_box_type_upgrade_id: 1334 diff --git a/tests/integration/targets/storage_box_subaccount_info/defaults/main/main.yml b/tests/integration/targets/storage_box_subaccount_info/defaults/main/main.yml new file mode 100644 index 0000000..69c5659 --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount_info/defaults/main/main.yml @@ -0,0 +1,6 @@ +# Copyright: (c) 2019, Hetzner Cloud GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +hcloud_storage_box_name: "{{ hcloud_ns }}" +hcloud_ssh_key_name: "{{ hcloud_ns }}" +hcloud_storage_box_password: 1-secret-PASSW0RD-=) diff --git a/tests/integration/targets/storage_box_subaccount_info/meta/main.yml b/tests/integration/targets/storage_box_subaccount_info/meta/main.yml new file mode 100644 index 0000000..3a96ecb --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount_info/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - setup_ssh_keypair diff --git a/tests/integration/targets/storage_box_subaccount_info/tasks/cleanup.yml b/tests/integration/targets/storage_box_subaccount_info/tasks/cleanup.yml new file mode 100644 index 0000000..d0ab906 --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount_info/tasks/cleanup.yml @@ -0,0 +1,5 @@ +--- +- name: Cleanup test_storage_box + hetzner.hcloud.storage_box: + name: "{{ hcloud_storage_box_name }}" + state: absent diff --git a/tests/integration/targets/storage_box_subaccount_info/tasks/main.yml b/tests/integration/targets/storage_box_subaccount_info/tasks/main.yml new file mode 100644 index 0000000..767fc46 --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount_info/tasks/main.yml @@ -0,0 +1,31 @@ +# +# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead. +# +--- +- name: Check if cleanup.yml exists + ansible.builtin.stat: + path: "{{ role_path }}/tasks/cleanup.yml" + register: cleanup_file + +- name: Check if prepare.yml exists + ansible.builtin.stat: + path: "{{ role_path }}/tasks/prepare.yml" + register: prepare_file + +- name: Include cleanup tasks + ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml" + when: cleanup_file.stat.exists + +- name: Include prepare tasks + ansible.builtin.include_tasks: "{{ role_path }}/tasks/prepare.yml" + when: prepare_file.stat.exists + +- name: Run tests + block: + - name: Include test tasks + ansible.builtin.include_tasks: "{{ role_path }}/tasks/test.yml" + + always: + - name: Include cleanup tasks + ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml" + when: cleanup_file.stat.exists diff --git a/tests/integration/targets/storage_box_subaccount_info/tasks/prepare.yml b/tests/integration/targets/storage_box_subaccount_info/tasks/prepare.yml new file mode 100644 index 0000000..5074e01 --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount_info/tasks/prepare.yml @@ -0,0 +1,37 @@ +--- +- name: Create test_storage_box + hetzner.hcloud.storage_box: + name: "{{ hcloud_storage_box_name }}" + type: "{{ hcloud_storage_box_type_name }}" + location: "{{ hcloud_location_name }}" + password: "{{ hcloud_storage_box_password }}" + ssh_keys: + - "{{ test_ssh_keypair.public_key }}" + access_settings: + ssh_enabled: true + reachable_externally: false + labels: + key: "{{ hcloud_ns }}" + register: test_storage_box + +- name: Create test_storage_box_subaccount1 + hetzner.hcloud.storage_box_subaccount: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + name: subaccount1 + description: backups from subaccount1 + home_directory: backups/subaccount1 + password: "{{ hcloud_storage_box_password }}" + access_settings: + ssh_enabled: true + readonly: false + labels: + key: "{{ hcloud_ns }}" + register: test_storage_box_subaccount1 + +- name: Create test_storage_box_subaccount2 + hetzner.hcloud.storage_box_subaccount: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + name: subaccount2 + home_directory: backups/subaccount2 + password: "{{ hcloud_storage_box_password }}" + register: test_storage_box_subaccount2 diff --git a/tests/integration/targets/storage_box_subaccount_info/tasks/test.yml b/tests/integration/targets/storage_box_subaccount_info/tasks/test.yml new file mode 100644 index 0000000..0178009 --- /dev/null +++ b/tests/integration/targets/storage_box_subaccount_info/tasks/test.yml @@ -0,0 +1,86 @@ +# Copyright: (c) 2025, Hetzner Cloud GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Gather hcloud_storage_box_subaccount_info + hetzner.hcloud.storage_box_subaccount_info: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + register: result +- name: Verify hcloud_storage_box_subaccount_info + ansible.builtin.assert: + that: + - result.hcloud_storage_box_subaccount_info | list | count == 2 + +- name: Gather hcloud_storage_box_subaccount_info in check mode + hetzner.hcloud.storage_box_subaccount_info: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + check_mode: true + register: result +- name: Verify hcloud_storage_box_subaccount_info in check mode + ansible.builtin.assert: + that: + - result.hcloud_storage_box_subaccount_info | list | count == 2 + +- name: Gather hcloud_storage_box_subaccount_info with label + hetzner.hcloud.storage_box_subaccount_info: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + label_selector: "key={{ hcloud_ns }}" + register: result +- name: Verify hcloud_storage_box_subaccount_info + ansible.builtin.assert: + that: + - result.hcloud_storage_box_subaccount_info | list | count == 1 + +- name: Gather hcloud_storage_box_subaccount_info with correct id + hetzner.hcloud.storage_box_subaccount_info: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + id: "{{ test_storage_box_subaccount1.hcloud_storage_box_subaccount.id }}" + register: result +- name: Verify hcloud_storage_box_subaccount_info with correct id + ansible.builtin.assert: + that: + - result.hcloud_storage_box_subaccount_info | list | count == 1 + - result.hcloud_storage_box_subaccount_info[0].storage_box == test_storage_box.hcloud_storage_box.id + - result.hcloud_storage_box_subaccount_info[0].id is not none + - result.hcloud_storage_box_subaccount_info[0].name == "subaccount1" + - result.hcloud_storage_box_subaccount_info[0].home_directory == "backups/subaccount1" + - result.hcloud_storage_box_subaccount_info[0].username is not none + - result.hcloud_storage_box_subaccount_info[0].server is not none + - result.hcloud_storage_box_subaccount_info[0].description == "backups from subaccount1" + - result.hcloud_storage_box_subaccount_info[0].labels.key == hcloud_ns + - result.hcloud_storage_box_subaccount_info[0].access_settings.reachable_externally == false + - result.hcloud_storage_box_subaccount_info[0].access_settings.samba_enabled == false + - result.hcloud_storage_box_subaccount_info[0].access_settings.ssh_enabled == true + - result.hcloud_storage_box_subaccount_info[0].access_settings.webdav_enabled == false + - result.hcloud_storage_box_subaccount_info[0].access_settings.readonly == false + - result.hcloud_storage_box_subaccount_info[0].created is not none + +- name: Gather hcloud_storage_box_subaccount_info with wrong id + hetzner.hcloud.storage_box_subaccount_info: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + id: "{{ test_storage_box_subaccount1.hcloud_storage_box_subaccount.id }}4321" + ignore_errors: true + register: result +- name: Verify hcloud_storage_box_subaccount_info with wrong id + ansible.builtin.assert: + that: + - result is failed + +- name: Gather hcloud_storage_box_subaccount_info with correct name + hetzner.hcloud.storage_box_subaccount_info: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + name: "{{ test_storage_box_subaccount1.hcloud_storage_box_subaccount.name }}" + register: result +- name: Verify hcloud_storage_box_subaccount_info with correct name + ansible.builtin.assert: + that: + - result.hcloud_storage_box_subaccount_info | list | count == 1 + +- name: Gather hcloud_storage_box_subaccount_info with wrong name + hetzner.hcloud.storage_box_subaccount_info: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + name: "{{ test_storage_box_subaccount1.hcloud_storage_box_subaccount.name }}-invalid" + register: result +- name: Verify hcloud_storage_box_subaccount_info with wrong name + ansible.builtin.assert: + that: + - result.hcloud_storage_box_subaccount_info | list | count == 0