diff --git a/plugins/module_utils/storage_box.py b/plugins/module_utils/storage_box.py index 214b105..5e307bc 100644 --- a/plugins/module_utils/storage_box.py +++ b/plugins/module_utils/storage_box.py @@ -7,7 +7,13 @@ from ..module_utils.vendor.hcloud.storage_boxes import ( ) -def get(client: StorageBoxesClient, param: str | int): +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, diff --git a/plugins/modules/storage_box_snapshot.py b/plugins/modules/storage_box_snapshot.py index 261e1bb..cd3ae6b 100644 --- a/plugins/modules/storage_box_snapshot.py +++ b/plugins/modules/storage_box_snapshot.py @@ -160,10 +160,10 @@ class AnsibleStorageBoxSnapshot(AnsibleHCloud): def _fetch(self): self.storage_box = storage_box.get(self.client.storage_boxes, self.module.params.get("storage_box")) - if self.module.params.get("id") is not None: - self.storage_box_snapshot = self.storage_box.get_snapshot_by_id(self.module.params.get("id")) - else: - self.storage_box_snapshot = self.storage_box.get_snapshot_by_name(self.module.params.get("name")) + 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 = {} diff --git a/plugins/modules/storage_box_snapshot_info.py b/plugins/modules/storage_box_snapshot_info.py new file mode 100644 index 0000000..010c13a --- /dev/null +++ b/plugins/modules/storage_box_snapshot_info.py @@ -0,0 +1,204 @@ +#!/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. + type: int + 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 + name: 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_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..bf9410b --- /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 >= 1 + +- 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