From bd2161667463dd8075320400f1defdab4826c45f Mon Sep 17 00:00:00 2001 From: "Jonas L." Date: Tue, 9 Dec 2025 13:28:11 +0100 Subject: [PATCH] feat: support Storage Box Snapshot modules (#754) 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_snapshot` - `storage_box_snapshot_info` --------- Co-authored-by: Julian Tölle --- plugins/module_utils/storage_box.py | 23 ++ plugins/module_utils/storage_box_snapshot.py | 21 ++ plugins/modules/storage_box_snapshot.py | 275 ++++++++++++++++++ plugins/modules/storage_box_snapshot_info.py | 205 +++++++++++++ .../targets/storage_box_snapshot/aliases | 3 + .../defaults/main/common.yml | 35 +++ .../defaults/main/main.yml | 6 + .../storage_box_snapshot/meta/main.yml | 3 + .../storage_box_snapshot/tasks/cleanup.yml | 5 + .../storage_box_snapshot/tasks/main.yml | 31 ++ .../storage_box_snapshot/tasks/prepare.yml | 15 + .../storage_box_snapshot/tasks/test.yml | 119 ++++++++ .../targets/storage_box_snapshot_info/aliases | 3 + .../defaults/main/common.yml | 35 +++ .../defaults/main/main.yml | 6 + .../storage_box_snapshot_info/meta/main.yml | 3 + .../tasks/cleanup.yml | 5 + .../storage_box_snapshot_info/tasks/main.yml | 31 ++ .../tasks/prepare.yml | 28 ++ .../storage_box_snapshot_info/tasks/test.yml | 81 ++++++ 20 files changed, 933 insertions(+) create mode 100644 plugins/module_utils/storage_box_snapshot.py create mode 100644 plugins/modules/storage_box_snapshot.py create mode 100644 plugins/modules/storage_box_snapshot_info.py create mode 100644 tests/integration/targets/storage_box_snapshot/aliases create mode 100644 tests/integration/targets/storage_box_snapshot/defaults/main/common.yml create mode 100644 tests/integration/targets/storage_box_snapshot/defaults/main/main.yml create mode 100644 tests/integration/targets/storage_box_snapshot/meta/main.yml create mode 100644 tests/integration/targets/storage_box_snapshot/tasks/cleanup.yml create mode 100644 tests/integration/targets/storage_box_snapshot/tasks/main.yml create mode 100644 tests/integration/targets/storage_box_snapshot/tasks/prepare.yml create mode 100644 tests/integration/targets/storage_box_snapshot/tasks/test.yml create mode 100644 tests/integration/targets/storage_box_snapshot_info/aliases create mode 100644 tests/integration/targets/storage_box_snapshot_info/defaults/main/common.yml create mode 100644 tests/integration/targets/storage_box_snapshot_info/defaults/main/main.yml create mode 100644 tests/integration/targets/storage_box_snapshot_info/meta/main.yml create mode 100644 tests/integration/targets/storage_box_snapshot_info/tasks/cleanup.yml create mode 100644 tests/integration/targets/storage_box_snapshot_info/tasks/main.yml create mode 100644 tests/integration/targets/storage_box_snapshot_info/tasks/prepare.yml create mode 100644 tests/integration/targets/storage_box_snapshot_info/tasks/test.yml diff --git a/plugins/module_utils/storage_box.py b/plugins/module_utils/storage_box.py index aa0b521..5e307bc 100644 --- a/plugins/module_utils/storage_box.py +++ b/plugins/module_utils/storage_box.py @@ -1,10 +1,33 @@ from __future__ import annotations +from ..module_utils.client import client_resource_not_found from ..module_utils.vendor.hcloud.storage_boxes import ( BoundStorageBox, + StorageBoxesClient, ) +def get(client: StorageBoxesClient, param: str | int) -> BoundStorageBox: + """ + Get a Bound Storage Box either by ID or name. + + If the given parameter is an ID, return a partial Bound Storage Box to reduce the amount + of API requests. + """ + try: + return BoundStorageBox( + client, + data={"id": int(param)}, + complete=False, + ) + except ValueError: # param is not an id + result = client.get_by_name(param) + if result is None: + # pylint: disable=raise-missing-from + raise client_resource_not_found("storage box", param) + return result + + def prepare_result(o: BoundStorageBox): return { "id": o.id, diff --git a/plugins/module_utils/storage_box_snapshot.py b/plugins/module_utils/storage_box_snapshot.py new file mode 100644 index 0000000..4fe10c1 --- /dev/null +++ b/plugins/module_utils/storage_box_snapshot.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from ..module_utils.vendor.hcloud.storage_boxes import ( + BoundStorageBoxSnapshot, +) + + +def prepare_result(o: BoundStorageBoxSnapshot): + return { + "storage_box": o.storage_box.id, + "id": o.id, + "name": o.name, + "description": o.description, + "labels": o.labels, + "stats": { + "size": o.stats.size, + "size_filesystem": o.stats.size_filesystem, + }, + "is_automatic": o.is_automatic, + "created": o.created.isoformat(), + } diff --git a/plugins/modules/storage_box_snapshot.py b/plugins/modules/storage_box_snapshot.py new file mode 100644 index 0000000..14123ff --- /dev/null +++ b/plugins/modules/storage_box_snapshot.py @@ -0,0 +1,275 @@ +#!/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_snapshot + +short_description: Create and manage Storage Box Snapshots in Hetzner. + +description: + - Create, update and delete Storage Box Snapshots in Hetzner. + - See the L(Storage Box Snapshots API documentation,https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots) 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 Snapshot to manage. + - Required when updating or deleting and if no Storage Box Snapshot O(name) is given. + type: int + name: + description: + - Name of the Storage Box Snapshot to manage. + - Storage Box Snapshot names are defined by the API and cannot be changed. + - Required when updating or deleting and if no Storage Box Snapshot O(id) is given. + type: str + description: + description: + - Description of the Storage Box Snapshot. + type: str + labels: + description: + - User-defined labels (key-value pairs) for the Storage Box Snapshot. + type: dict + state: + description: + - State of the Storage Box Snapshot. + default: present + choices: [absent, present] + type: str + +extends_documentation_fragment: + - hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Create a Storage Box Snapshot + hetzner.hcloud.storage_box_snapshot: + storage_box: my-storage-box + description: before app migration + labels: + env: prod + state: present + +- name: Delete a Storage Box Snapshot by name + hetzner.hcloud.storage_box_snapshot: + storage_box: my-storage-box + name: 2025-12-03T13-47-47 + state: absent + +- name: Delete a Storage Box Snapshot by id + hetzner.hcloud.storage_box_snapshot: + storage_box: 497436 + id: 405920 + state: absent +""" + +RETURN = """ +hcloud_storage_box_snapshot: + description: Details about the Storage Box Snapshot. + returned: always + type: dict + contains: + storage_box: + description: ID of the Storage Box. + returned: always + type: int + sample: 497436 + id: + description: ID of the Storage Box Snapshot. + returned: always + type: int + sample: 405920 + name: + description: Name of the Storage Box Snapshot. + returned: always + type: str + sample: 2025-02-12T11-35-19 + description: + description: Description of the Storage Box Snapshot. + returned: always + type: str + sample: before app migration + labels: + description: User-defined labels (key-value pairs) of the Storage Box Snapshot. + returned: always + type: dict + sample: + env: prod + stats: + description: Statistics of the Storage Box Snapshot. + returned: always + type: dict + contains: + size: + description: Current storage requirements of the Snapshot in bytes. + returned: always + type: int + sample: 10485760 + size_filesystem: + description: Size of the compressed file system contained in the Snapshot in bytes. + returned: always + type: int + sample: 10485760 + is_automatic: + description: Whether the Storage Box Snapshot was created automatically. + returned: always + type: bool + sample: false + created: + description: Point in time when the Storage Box Snapshot was created (in RFC3339 format). + returned: always + type: str + sample: "2025-12-03T13:47:47Z" +""" + +from ..module_utils import storage_box, storage_box_snapshot +from ..module_utils.hcloud import AnsibleHCloud, AnsibleModule +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.storage_boxes import ( + BoundStorageBox, + BoundStorageBoxSnapshot, +) + + +class AnsibleStorageBoxSnapshot(AnsibleHCloud): + represent = "storage_box_snapshot" + + storage_box: BoundStorageBox | None = None + storage_box_snapshot: BoundStorageBoxSnapshot | None = None + + def _prepare_result(self): + if self.storage_box_snapshot is None: + return {} + return storage_box_snapshot.prepare_result(self.storage_box_snapshot) + + 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_snapshot = self.storage_box.get_snapshot_by_id(value) + elif (value := self.module.params.get("name")) is not None: + self.storage_box_snapshot = self.storage_box.get_snapshot_by_name(value) + + def _create(self): + params = {} + + 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 not self.module.check_mode: + resp = self.storage_box.create_snapshot(**params) + self.storage_box_snapshot = resp.snapshot + self.actions.append(resp.action) + + self._wait_actions() + self.storage_box_snapshot.reload() + + self._mark_as_changed() + + def _update(self): + self.fail_on_invalid_params( + required_one_of=[["id", "name"]], + ) + + params = {} + if (value := self.module.params.get("description")) is not None: + if value != self.storage_box_snapshot.description: + params["description"] = value + self._mark_as_changed() + + if (value := self.module.params.get("labels")) is not None: + if value != self.storage_box_snapshot.labels: + params["labels"] = value + self._mark_as_changed() + + # Update only if params holds changes + if params: + if not self.module.check_mode: + self.storage_box_snapshot = self.storage_box_snapshot.update(**params) + + def _delete(self): + if not self.module.check_mode: + resp = self.storage_box_snapshot.delete() + resp.action.wait_until_finished() + + self.storage_box_snapshot = None + self._mark_as_changed() + + def present(self): + try: + self._fetch() + if self.storage_box_snapshot 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_snapshot is None: + return + self._delete() + + 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"}, + description={"type": "str"}, + labels={"type": "dict"}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **super().base_module_arguments(), + ), + supports_check_mode=True, + ) + + +def main(): + module = AnsibleStorageBoxSnapshot.define_module() + o = AnsibleStorageBoxSnapshot(module) + + match module.params.get("state"): + case "absent": + o.absent() + case _: + o.present() + + result = o.get_result() + + # Legacy return value naming pattern + result["hcloud_storage_box_snapshot"] = result.pop("storage_box_snapshot") + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/storage_box_snapshot_info.py b/plugins/modules/storage_box_snapshot_info.py new file mode 100644 index 0000000..d52dc7c --- /dev/null +++ b/plugins/modules/storage_box_snapshot_info.py @@ -0,0 +1,205 @@ +#!/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_snapshot_info + +short_description: Gather infos about the Hetzner Storage Box Snapshots. + +description: + - Gather infos about available Hetzner Storage Box Snapshots. + - See the L(Storage Boxes API documentation,https://docs.hetzner.cloud/reference/hetzner#storage-boxes) 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 Snapshot to get. + type: int + name: + description: + - Name of the Storage Box Snapshot to get. + type: str + label_selector: + description: + - Label selector to filter the Storage Box Snapshots to get. + type: str + +extends_documentation_fragment: + - hetzner.hcloud.hcloud +""" + +EXAMPLES = """ +- name: Gather all Storage Box Snapshots + hetzner.hcloud.storage_box_snapshot_info: + storage_box: my-storage-box + register: output + +- name: Gather Storage Box Snapshots by label + hetzner.hcloud.storage_box_snapshot_info: + storage_box: my-storage-box + label_selector: key=value + register: output + +- name: Gather Storage Box Snapshot by id + hetzner.hcloud.storage_box_snapshot_info: + storage_box: 497436 + id: 405920 + register: output + +- name: Gather Storage Box Snapshot by name + hetzner.hcloud.storage_box_snapshot_info: + storage_box: my-storage-box + name: 2025-02-12T11-35-19 + register: output + +- name: Print the gathered infos + debug: + var: output.hcloud_storage_box_snapshot_info +""" + +RETURN = """ +hcloud_storage_box_snapshot_info: + description: List of Storage Box Snapshots. + returned: always + type: list + contains: + storage_box: + description: ID of the parent Storage Box. + returned: always + type: int + sample: 497436 + id: + description: ID of the Storage Box Snapshot. + returned: always + type: int + sample: 405920 + name: + description: Name of the Storage Box Snapshot. + returned: always + type: str + sample: 2025-02-12T11-35-19 + description: + description: Description of the Storage Box Snapshot. + returned: always + type: str + sample: before app migration + labels: + description: User-defined labels (key-value pairs) of the Storage Box Snapshot. + returned: always + type: dict + sample: + env: prod + stats: + description: Statistics of the Storage Box Snapshot. + returned: always + type: dict + contains: + size: + description: Current storage requirements of the Snapshot in bytes. + returned: always + type: int + sample: 10485760 + size_filesystem: + description: Size of the compressed file system contained in the Snapshot in bytes. + returned: always + type: int + sample: 10485760 + is_automatic: + description: Whether the Storage Box Snapshot was created automatically. + returned: always + type: bool + sample: false + created: + description: Point in time when the Storage Box Snapshot was created (in RFC3339 format). + returned: always + type: str + sample: "2025-12-03T13:47:47Z" +""" + +from ..module_utils import storage_box, storage_box_snapshot +from ..module_utils.hcloud import AnsibleHCloud, AnsibleModule +from ..module_utils.vendor.hcloud import HCloudException +from ..module_utils.vendor.hcloud.storage_boxes import ( + BoundStorageBox, + BoundStorageBoxSnapshot, +) + + +class AnsibleStorageBoxSnapshotInfo(AnsibleHCloud): + represent = "storage_box_snapshots" + + storage_box: BoundStorageBox | None = None + storage_box_snapshots: list[BoundStorageBoxSnapshot] | None = None + + def _prepare_result(self): + result = [] + + for o in self.storage_box_snapshots or []: + if o is not None: + result.append(storage_box_snapshot.prepare_result(o)) + return result + + def fetch(self): + try: + self.storage_box = storage_box.get( + self.client.storage_boxes, + self.module.params.get("storage_box"), + ) + + if (id_ := self.module.params.get("id")) is not None: + self.storage_box_snapshots = [self.storage_box.get_snapshot_by_id(id_)] + elif (name := self.module.params.get("name")) is not None: + self.storage_box_snapshots = [self.storage_box.get_snapshot_by_name(name)] + else: + params = {} + if (value := self.module.params.get("label_selector")) is not None: + params["label_selector"] = value + self.storage_box_snapshots = self.storage_box.get_snapshot_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 = AnsibleStorageBoxSnapshotInfo.define_module() + o = AnsibleStorageBoxSnapshotInfo(module) + + o.fetch() + result = o.get_result() + + # Legacy return value naming pattern + result["hcloud_storage_box_snapshot_info"] = result.pop("storage_box_snapshots") + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/storage_box_snapshot/aliases b/tests/integration/targets/storage_box_snapshot/aliases new file mode 100644 index 0000000..18b1111 --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot/aliases @@ -0,0 +1,3 @@ +cloud/hcloud +gather_facts/no +azp/group2 diff --git a/tests/integration/targets/storage_box_snapshot/defaults/main/common.yml b/tests/integration/targets/storage_box_snapshot/defaults/main/common.yml new file mode 100644 index 0000000..015c3b5 --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot/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_snapshot/defaults/main/main.yml b/tests/integration/targets/storage_box_snapshot/defaults/main/main.yml new file mode 100644 index 0000000..69c5659 --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot/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_snapshot/meta/main.yml b/tests/integration/targets/storage_box_snapshot/meta/main.yml new file mode 100644 index 0000000..3a96ecb --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - setup_ssh_keypair diff --git a/tests/integration/targets/storage_box_snapshot/tasks/cleanup.yml b/tests/integration/targets/storage_box_snapshot/tasks/cleanup.yml new file mode 100644 index 0000000..d0ab906 --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot/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_snapshot/tasks/main.yml b/tests/integration/targets/storage_box_snapshot/tasks/main.yml new file mode 100644 index 0000000..767fc46 --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot/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_snapshot/tasks/prepare.yml b/tests/integration/targets/storage_box_snapshot/tasks/prepare.yml new file mode 100644 index 0000000..5e009ac --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot/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_snapshot/tasks/test.yml b/tests/integration/targets/storage_box_snapshot/tasks/test.yml new file mode 100644 index 0000000..1bdac38 --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot/tasks/test.yml @@ -0,0 +1,119 @@ +--- +- name: Test missing required parameters # noqa: args[module] + hetzner.hcloud.storage_box_snapshot: + ignore_errors: true + register: result +- name: Verify missing required parameters + ansible.builtin.assert: + that: + - result is failed + - 'result.msg == "missing required arguments: storage_box"' + +- name: Test create with check mode + hetzner.hcloud.storage_box_snapshot: + storage_box: "{{ hcloud_storage_box_name }}" + description: migration-1 + labels: + key: value + 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_snapshot: + storage_box: "{{ hcloud_storage_box_name }}" + description: migration-1 + labels: + key: value + register: result +- name: Verify create + ansible.builtin.assert: + that: + - result is changed + - result.hcloud_storage_box_snapshot.storage_box == test_storage_box.hcloud_storage_box.id + - result.hcloud_storage_box_snapshot.id is not none + - result.hcloud_storage_box_snapshot.name is not none + - result.hcloud_storage_box_snapshot.description == "migration-1" + - result.hcloud_storage_box_snapshot.labels.key == "value" + - result.hcloud_storage_box_snapshot.is_automatic is false + - result.hcloud_storage_box_snapshot.stats.size >= 0 + - result.hcloud_storage_box_snapshot.stats.size_filesystem >= 0 + - result.hcloud_storage_box_snapshot.created is not none + +- name: Save Storage Box Snapshot + ansible.builtin.set_fact: + _storage_box_snapshot: "{{ result.hcloud_storage_box_snapshot }}" + +- name: Test create idempotency + hetzner.hcloud.storage_box_snapshot: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + id: "{{ _storage_box_snapshot.id }}" # Create is not idempotent, only with id or name + description: migration-1 + labels: + key: value + register: result +- name: Verify create idempotency + ansible.builtin.assert: + that: + - result is not changed + +- name: Test update + hetzner.hcloud.storage_box_snapshot: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + id: "{{ _storage_box_snapshot.id }}" + description: migration-changed # Update + labels: + key: changed # Update + register: result +- name: Verify update + ansible.builtin.assert: + that: + - result is changed + - result.hcloud_storage_box_snapshot.storage_box == test_storage_box.hcloud_storage_box.id + - result.hcloud_storage_box_snapshot.id is not none + - result.hcloud_storage_box_snapshot.name is not none + - result.hcloud_storage_box_snapshot.description == "migration-changed" + - result.hcloud_storage_box_snapshot.labels.key == "changed" + - result.hcloud_storage_box_snapshot.is_automatic is false + - result.hcloud_storage_box_snapshot.stats.size >= 0 + - result.hcloud_storage_box_snapshot.stats.size_filesystem >= 0 + - result.hcloud_storage_box_snapshot.created is not none + +- name: Test update idempotency + hetzner.hcloud.storage_box_snapshot: + storage_box: "{{ test_storage_box.hcloud_storage_box.name }}" + name: "{{ _storage_box_snapshot.name }}" + description: migration-changed # Update + labels: + key: changed # Update + register: result +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + +- name: Test delete + hetzner.hcloud.storage_box_snapshot: + storage_box: "{{ test_storage_box.hcloud_storage_box.name }}" + name: "{{ _storage_box_snapshot.name }}" + state: absent + register: result +- name: Verify delete + ansible.builtin.assert: + that: + - result is changed + - result.hcloud_storage_box_snapshot is none + +- name: Test delete idempotency + hetzner.hcloud.storage_box_snapshot: + storage_box: "{{ test_storage_box.hcloud_storage_box.name }}" + name: "{{ _storage_box_snapshot.name }}" + state: absent + register: result +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed diff --git a/tests/integration/targets/storage_box_snapshot_info/aliases b/tests/integration/targets/storage_box_snapshot_info/aliases new file mode 100644 index 0000000..18b1111 --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot_info/aliases @@ -0,0 +1,3 @@ +cloud/hcloud +gather_facts/no +azp/group2 diff --git a/tests/integration/targets/storage_box_snapshot_info/defaults/main/common.yml b/tests/integration/targets/storage_box_snapshot_info/defaults/main/common.yml new file mode 100644 index 0000000..015c3b5 --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot_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_snapshot_info/defaults/main/main.yml b/tests/integration/targets/storage_box_snapshot_info/defaults/main/main.yml new file mode 100644 index 0000000..69c5659 --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot_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_snapshot_info/meta/main.yml b/tests/integration/targets/storage_box_snapshot_info/meta/main.yml new file mode 100644 index 0000000..3a96ecb --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot_info/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - setup_ssh_keypair diff --git a/tests/integration/targets/storage_box_snapshot_info/tasks/cleanup.yml b/tests/integration/targets/storage_box_snapshot_info/tasks/cleanup.yml new file mode 100644 index 0000000..d0ab906 --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot_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_snapshot_info/tasks/main.yml b/tests/integration/targets/storage_box_snapshot_info/tasks/main.yml new file mode 100644 index 0000000..767fc46 --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot_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_snapshot_info/tasks/prepare.yml b/tests/integration/targets/storage_box_snapshot_info/tasks/prepare.yml new file mode 100644 index 0000000..e48e260 --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot_info/tasks/prepare.yml @@ -0,0 +1,28 @@ +--- +- 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_snapshot1 + hetzner.hcloud.storage_box_snapshot: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + description: snapshot1 + labels: + key: "{{ hcloud_ns }}" + register: test_storage_box_snapshot1 + +- name: Create test_storage_box_snapshot2 + hetzner.hcloud.storage_box_snapshot: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + register: test_storage_box_snapshot2 diff --git a/tests/integration/targets/storage_box_snapshot_info/tasks/test.yml b/tests/integration/targets/storage_box_snapshot_info/tasks/test.yml new file mode 100644 index 0000000..aa73890 --- /dev/null +++ b/tests/integration/targets/storage_box_snapshot_info/tasks/test.yml @@ -0,0 +1,81 @@ +# 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_snapshot_info + hetzner.hcloud.storage_box_snapshot_info: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + register: result +- name: Verify hcloud_storage_box_snapshot_info + ansible.builtin.assert: + that: + - result.hcloud_storage_box_snapshot_info | list | count == 2 + +- name: Gather hcloud_storage_box_snapshot_info in check mode + hetzner.hcloud.storage_box_snapshot_info: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + check_mode: true + register: result +- name: Verify hcloud_storage_box_snapshot_info in check mode + ansible.builtin.assert: + that: + - result.hcloud_storage_box_snapshot_info | list | count == 2 + +- name: Gather hcloud_storage_box_snapshot_info with label + hetzner.hcloud.storage_box_snapshot_info: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + label_selector: "key={{ hcloud_ns }}" + register: result +- name: Verify hcloud_storage_box_snapshot_info + ansible.builtin.assert: + that: + - result.hcloud_storage_box_snapshot_info | list | count == 1 + +- name: Gather hcloud_storage_box_snapshot_info with correct id + hetzner.hcloud.storage_box_snapshot_info: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + id: "{{ test_storage_box_snapshot1.hcloud_storage_box_snapshot.id }}" + register: result +- name: Verify hcloud_storage_box_snapshot_info with correct id + ansible.builtin.assert: + that: + - result.hcloud_storage_box_snapshot_info | list | count == 1 + - result.hcloud_storage_box_snapshot_info[0].storage_box == test_storage_box.hcloud_storage_box.id + - result.hcloud_storage_box_snapshot_info[0].id is not none + - result.hcloud_storage_box_snapshot_info[0].name is not none + - result.hcloud_storage_box_snapshot_info[0].description == "snapshot1" + - result.hcloud_storage_box_snapshot_info[0].labels.key == hcloud_ns + - result.hcloud_storage_box_snapshot_info[0].is_automatic is false + - result.hcloud_storage_box_snapshot_info[0].stats.size >= 0 + - result.hcloud_storage_box_snapshot_info[0].stats.size_filesystem >= 0 + - result.hcloud_storage_box_snapshot_info[0].created is not none + +- name: Gather hcloud_storage_box_snapshot_info with wrong id + hetzner.hcloud.storage_box_snapshot_info: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + id: "{{ test_storage_box_snapshot1.hcloud_storage_box_snapshot.id }}4321" + ignore_errors: true + register: result +- name: Verify hcloud_storage_box_snapshot_info with wrong id + ansible.builtin.assert: + that: + - result is failed + +- name: Gather hcloud_storage_box_snapshot_info with correct name + hetzner.hcloud.storage_box_snapshot_info: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + name: "{{ test_storage_box_snapshot1.hcloud_storage_box_snapshot.name }}" + register: result +- name: Verify hcloud_storage_box_snapshot_info with correct name + ansible.builtin.assert: + that: + - result.hcloud_storage_box_snapshot_info | list | count == 1 + +- name: Gather hcloud_storage_box_snapshot_info with wrong name + hetzner.hcloud.storage_box_snapshot_info: + storage_box: "{{ test_storage_box.hcloud_storage_box.id }}" + name: "{{ test_storage_box_snapshot1.hcloud_storage_box_snapshot.name }}-invalid" + register: result +- name: Verify hcloud_storage_box_snapshot_info with wrong name + ansible.builtin.assert: + that: + - result.hcloud_storage_box_snapshot_info | list | count == 0