diff --git a/plugins/module_utils/hcloud.py b/plugins/module_utils/hcloud.py index 1174a9b..b229d1f 100644 --- a/plugins/module_utils/hcloud.py +++ b/plugins/module_utils/hcloud.py @@ -6,6 +6,7 @@ from __future__ import annotations import traceback +from copy import deepcopy from typing import Any, NoReturn from ansible.module_utils.basic import AnsibleModule as AnsibleModuleBase, env_fallback @@ -29,6 +30,23 @@ from .version import version # Provide typing definitions to the AnsibleModule class class AnsibleModule(AnsibleModuleBase): params: dict + params_raw: dict + + def _load_params(self): + """ + Copy the params before validation, to keep track whether a value was defined by the user. + + Validation will modify the params dict by adding missing keys. + """ + # https://github.com/ansible/ansible/blob/7b4d4ed672415f31689e7f25bc0b40c0697c0c88/lib/ansible/module_utils/basic.py#L1244-L1251 + super()._load_params() + self.params_raw = deepcopy(self.params) + + def param_is_defined(self, key: str): + """ + Check if a parameter was defined by the user. + """ + return key in self.params_raw class AnsibleHCloud: diff --git a/plugins/module_utils/storage_box.py b/plugins/module_utils/storage_box.py index 2f7268e..aa0b521 100644 --- a/plugins/module_utils/storage_box.py +++ b/plugins/module_utils/storage_box.py @@ -29,4 +29,15 @@ def prepare_result(o: BoundStorageBox): "size_data": o.stats.size_data, "size_snapshots": o.stats.size_snapshots, }, + "snapshot_plan": ( + None + if o.snapshot_plan is None + else { + "max_snapshots": o.snapshot_plan.max_snapshots, + "hour": o.snapshot_plan.hour, + "minute": o.snapshot_plan.minute, + "day_of_week": o.snapshot_plan.day_of_week, + "day_of_month": o.snapshot_plan.day_of_month, + } + ), } diff --git a/plugins/modules/storage_box.py b/plugins/modules/storage_box.py index ed477d4..ab84263 100644 --- a/plugins/modules/storage_box.py +++ b/plugins/modules/storage_box.py @@ -85,6 +85,36 @@ options: - Whether the ZFS snapshot folder is visible. type: bool default: false + snapshot_plan: + description: + - Snapshot plan of the Storage Box. + - Use null to disabled the snapshot plan. + type: dict + suboptions: + max_snapshots: + description: + - Maximum amount of Snapshots that will be created by this Snapshot Plan. + - Older Snapshots will be deleted. + type: int + hour: + description: + - Hour when the Snapshot Plan is executed (UTC). + type: int + minute: + description: + - Minute when the Snapshot Plan is executed (UTC). + type: int + day_of_week: + description: + - Day of the week when the Snapshot Plan is executed. + - Starts at 1 for Monday til 7 for Sunday. + - Null means every day. + type: int + day_of_month: + description: + - Day of the month when the Snapshot Plan is executed. + - Null means every day. + type: int delete_protection: description: - Protect the Storage Box from deletion. @@ -126,6 +156,24 @@ EXAMPLES = """ zfs_enabled: false state: present +- name: Create a Storage Box with snapshot plan + hetzner.hcloud.storage_box: + name: my-storage-box + storage_box_type: bx11 + location: fsn1 + password: my-secret + snapshot_plan: + max_snapshots: 10 + hour: 3 + minute: 30 + state: present + +- name: Disable a Storage Box snapshot plan + hetzner.hcloud.storage_box: + name: my-storage-box + snapshot_plan: null + state: present + - name: Delete a Storage Box hetzner.hcloud.storage_box: name: my-storage-box @@ -199,6 +247,36 @@ hcloud_storage_box: returned: always type: bool sample: false + snapshot_plan: + description: Snapshot plan of the Storage Box. + returned: when enabled + type: dict + contains: + max_snapshots: + description: Maximum amount of Snapshots that will be created by this Snapshot Plan. + returned: always + type: int + sample: 10 + hour: + description: Hour when the Snapshot Plan is executed (UTC). + returned: always + type: int + sample: 3 + minute: + description: Minute when the Snapshot Plan is executed (UTC). + returned: always + type: int + sample: 30 + day_of_week: + description: Day of the week when the Snapshot Plan is executed. Null means every day. + returned: always + type: int + sample: 1 + day_of_month: + description: Day of the month when the Snapshot Plan is executed. Null means every day. + returned: always + type: int + sample: 30 username: description: User name of the Storage Box. returned: always @@ -241,16 +319,15 @@ hcloud_storage_box: sample: 10485760 """ -from ansible.module_utils.basic import AnsibleModule - from ..module_utils import storage_box -from ..module_utils.hcloud import AnsibleHCloud +from ..module_utils.hcloud import AnsibleHCloud, AnsibleModule from ..module_utils.vendor.hcloud import HCloudException from ..module_utils.vendor.hcloud.locations import Location from ..module_utils.vendor.hcloud.storage_box_types import StorageBoxType from ..module_utils.vendor.hcloud.storage_boxes import ( BoundStorageBox, StorageBoxAccessSettings, + StorageBoxSnapshotPlan, ) @@ -294,9 +371,19 @@ class AnsibleStorageBox(AnsibleHCloud): self.storage_box = resp.storage_box + if not self.module.check_mode: + self._wait_actions() + if (value := self.module.params.get("delete_protection")) is not None: - action = self.storage_box.change_protection(delete=value) - self.actions.append(action) + if not self.module.check_mode: + action = self.storage_box.change_protection(delete=value) + self.actions.append(action) + + if self.module.param_is_defined("snapshot_plan"): + if (value := self.module.params.get("snapshot_plan")) is not None: + if not self.module.check_mode: + action = self.storage_box.enable_snapshot_plan(StorageBoxSnapshotPlan.from_dict(value)) + self.actions.append(action) if not self.module.check_mode: self._wait_actions() @@ -327,6 +414,24 @@ class AnsibleStorageBox(AnsibleHCloud): self.actions.append(action) self._mark_as_changed() + if self.module.param_is_defined("snapshot_plan"): + if (value := self.module.params.get("snapshot_plan")) is not None: + snapshot_plan = StorageBoxSnapshotPlan.from_dict(value) + if ( + self.storage_box.snapshot_plan is None + or self.storage_box.snapshot_plan.to_payload() != snapshot_plan.to_payload() + ): + if not self.module.check_mode: + action = self.storage_box.enable_snapshot_plan(snapshot_plan) + self.actions.append(action) + self._mark_as_changed() + else: + if self.storage_box.snapshot_plan is not None: + if not self.module.check_mode: + action = self.storage_box.disable_snapshot_plan() + self.actions.append(action) + self._mark_as_changed() + # self.storage_box.reset_password params = {} @@ -397,6 +502,16 @@ class AnsibleStorageBox(AnsibleHCloud): zfs_enabled={"type": "bool", "default": False}, ), }, + snapshot_plan={ + "type": "dict", + "options": dict( + max_snapshots={"type": "int", "required": True}, + hour={"type": "int", "required": True}, + minute={"type": "int", "required": True}, + day_of_week={"type": "int"}, + day_of_month={"type": "int"}, + ), + }, delete_protection={"type": "bool"}, state={ "choices": ["absent", "present"], diff --git a/tests/integration/targets/storage_box/tasks/test.yml b/tests/integration/targets/storage_box/tasks/test.yml index 7099c12..8067cc0 100644 --- a/tests/integration/targets/storage_box/tasks/test.yml +++ b/tests/integration/targets/storage_box/tasks/test.yml @@ -39,6 +39,10 @@ access_settings: ssh_enabled: true zfs_enabled: false + snapshot_plan: + max_snapshots: 10 + hour: 3 + minute: 30 labels: key: value register: result @@ -57,6 +61,9 @@ - result.hcloud_storage_box.access_settings.ssh_enabled == true - result.hcloud_storage_box.access_settings.webdav_enabled == false - result.hcloud_storage_box.access_settings.zfs_enabled == false + - result.hcloud_storage_box.snapshot_plan.max_snapshots == 10 + - result.hcloud_storage_box.snapshot_plan.hour == 3 + - result.hcloud_storage_box.snapshot_plan.minute == 30 - result.hcloud_storage_box.status == "active" - result.hcloud_storage_box.server is not none - result.hcloud_storage_box.system is not none @@ -76,6 +83,10 @@ access_settings: ssh_enabled: true zfs_enabled: false + snapshot_plan: + max_snapshots: 10 + hour: 3 + minute: 30 labels: key: value register: result @@ -95,6 +106,11 @@ - result is changed - result.hcloud_storage_box.name == hcloud_storage_box_name + "-changed" + # Ensure snapshot plan was not changed + - result.hcloud_storage_box.snapshot_plan.max_snapshots == 10 + - result.hcloud_storage_box.snapshot_plan.hour == 3 + - result.hcloud_storage_box.snapshot_plan.minute == 30 + - name: Test update hetzner.hcloud.storage_box: id: "{{ result.hcloud_storage_box.id }}" @@ -106,6 +122,10 @@ access_settings: # Update ssh_enabled: false reachable_externally: true + snapshot_plan: + max_snapshots: 10 + hour: 4 # Update + minute: 30 labels: key: changed # Update delete_protection: true # Update @@ -125,6 +145,9 @@ - result.hcloud_storage_box.access_settings.ssh_enabled == false - result.hcloud_storage_box.access_settings.webdav_enabled == false - result.hcloud_storage_box.access_settings.zfs_enabled == false + - result.hcloud_storage_box.snapshot_plan.max_snapshots == 10 + - result.hcloud_storage_box.snapshot_plan.hour == 4 + - result.hcloud_storage_box.snapshot_plan.minute == 30 - name: Test update idempotency hetzner.hcloud.storage_box: @@ -137,6 +160,10 @@ access_settings: # Update ssh_enabled: false reachable_externally: true + snapshot_plan: + max_snapshots: 10 + hour: 4 # Update + minute: 30 labels: key: changed # Update delete_protection: true # Update @@ -163,12 +190,23 @@ name: "{{ hcloud_storage_box_name }}" delete_protection: false register: result -- name: Verify update primary delete protection +- name: Verify update delete protection ansible.builtin.assert: that: - result is changed - result.hcloud_storage_box.delete_protection == false +- name: Test update snapshot plan + hetzner.hcloud.storage_box: + name: "{{ hcloud_storage_box_name }}" + snapshot_plan: null + register: result +- name: Verify update snapshot plan + ansible.builtin.assert: + that: + - result is changed + - result.hcloud_storage_box.snapshot_plan is none + - name: Test delete hetzner.hcloud.storage_box: name: "{{ hcloud_storage_box_name }}"