diff --git a/examples/vm_snapshot_removal_based_on_timestamp.yml b/examples/vm_snapshot_removal_based_on_timestamp.yml new file mode 100644 index 000000000..2675f0e87 --- /dev/null +++ b/examples/vm_snapshot_removal_based_on_timestamp.yml @@ -0,0 +1,65 @@ +--- +- name: Example - delete all snapshots that are older than X days. + hosts: localhost + connection: local + gather_facts: false + vars: + # Format: 'YYYY-MM-DD hh:mm:ss' + # All snapshots older than this date will be deleted. + # use_date timezone should match the Scale cluster timezone + use_date: '1999-05-03 12:52:00' + + tasks: + # ------------------------------------------------------ + - name: List all snapshots + scale_computing.hypercore.vm_snapshot_info: + register: snapshot_results + + - name: Convert date to unix timestamp 'epoch' + ansible.builtin.set_fact: + epoch_timestamp: "{{ (use_date | to_datetime).strftime('%s') }}" + + - name: Show epoch_timestamp + ansible.builtin.debug: + var: epoch_timestamp + + - name: Create filtered_snapshots list + ansible.builtin.set_fact: + filtered_snapshots: [] + + - name: Loop through snapshots and add snapshots that are older than 'use_date' + ansible.builtin.set_fact: + filtered_snapshots: "{{ filtered_snapshots + [item] }}" + when: item.timestamp < epoch_timestamp | int + loop: "{{ snapshot_results.records }}" + no_log: true + + - name: Show only snapshots that are older than 'use_date' + ansible.builtin.debug: + var: filtered_snapshots + + # We could reuse "filtered_snapshots" here instead of "snapshot_results" and avoid the "when" statement. + # But leaving it as is for example purposes. + # Since this is the only mandatory task of the playbook, can be copy-pasted and reused as standalone task. + - name: Loop through list of snapshots and delete all older than the 'use_date' + scale_computing.hypercore.vm_snapshot: + vm_name: "{{ item.vm.name }}" + uuid: "{{ item.snapshot_uuid }}" + state: absent + when: item.timestamp < epoch_timestamp | int + loop: "{{ snapshot_results.records }}" + + - name: Create filtered_snapshots list - second time + ansible.builtin.set_fact: + filtered_snapshots: [] + + - name: Loop through snapshots and add snapshots that are older than 'use_date' - second time + ansible.builtin.set_fact: + filtered_snapshots: "{{ filtered_snapshots + [item] }}" + when: item.timestamp < epoch_timestamp | int + loop: "{{ snapshot_results.records }}" + no_log: true + + - name: Show only snapshots that are older than 'use_date' - second time + ansible.builtin.debug: + var: filtered_snapshots diff --git a/examples/vm_snapshot_special_cleanup.yml b/examples/vm_snapshot_special_cleanup.yml new file mode 100644 index 000000000..ce1637b3e --- /dev/null +++ b/examples/vm_snapshot_special_cleanup.yml @@ -0,0 +1,60 @@ +--- +- name: Example - delete all snapshots with label "TEST" and type "USER". + hosts: localhost + connection: local + gather_facts: false + vars: + # This variable is used to filter and delete snapshots. + # All snapshots with 'use_label' will be DELETED. + use_label: 'TEST' + + tasks: + # ------------------------------------------------------ + - name: List all snapshots + scale_computing.hypercore.vm_snapshot_info: + register: snapshot_results + + - name: Create filtered_snapshots list + ansible.builtin.set_fact: + filtered_snapshots: [] + + - name: Loop through snapshots and add snapshots with use_label and type 'USER' to filtered_snapshots + ansible.builtin.set_fact: + filtered_snapshots: "{{ filtered_snapshots + [item] }}" + when: item.label == use_label and item.type == 'USER' + loop: "{{ snapshot_results.records }}" + no_log: true + + - name: Show only snapshots with use_label and type "USER" + ansible.builtin.debug: + var: filtered_snapshots + + # We could reuse "filtered_snapshots" here instead of "snapshot_results" and avoid the "when" statement. + # But leaving it as is for example purposes. + # Since this is the only mandatory task of the playbook, can be copy-pasted and reused as standalone task. + - name: Loop through list of snapshots delete if label is use_label and type is 'USER' + scale_computing.hypercore.vm_snapshot: + vm_name: "{{ item.vm.name }}" + uuid: "{{ item.snapshot_uuid }}" + state: absent + when: item.label == use_label and item.type == 'USER' + loop: "{{ snapshot_results.records }}" + + - name: List all snapshots - second time + scale_computing.hypercore.vm_snapshot_info: + register: snapshot_results + + - name: Create filtered_snapshots list - second time + ansible.builtin.set_fact: + filtered_snapshots: [] + + - name: Loop through snapshots and add snapshots with use_label and type 'USER' to filtered_snapshots - second time + ansible.builtin.set_fact: + filtered_snapshots: "{{ filtered_snapshots + [item] }}" + when: item.label == use_label and item.type == 'USER' + loop: "{{ snapshot_results.records }}" + no_log: true + + - name: Show only snapshots with use_label and type 'USER' - second time + ansible.builtin.debug: + var: filtered_snapshots diff --git a/plugins/module_utils/vm_snapshot.py b/plugins/module_utils/vm_snapshot.py index 4af0b7fa9..9dd267d8e 100644 --- a/plugins/module_utils/vm_snapshot.py +++ b/plugins/module_utils/vm_snapshot.py @@ -226,8 +226,7 @@ def get_snapshot_by_uuid( cls, snapshot_uuid: str, rest_client: RestClient, must_exist: bool = False ) -> Optional[VMSnapshot]: hypercore_dict = rest_client.get_record( - endpoint="/rest/v1/VirDomainSnapshot", - query={"uuid": snapshot_uuid}, + endpoint=f"/rest/v1/VirDomainSnapshot/{snapshot_uuid}", must_exist=must_exist, ) vm_snapshot = cls.from_hypercore(hypercore_dict) diff --git a/plugins/modules/vm_snapshot.py b/plugins/modules/vm_snapshot.py index eba5269f5..b6181676e 100644 --- a/plugins/modules/vm_snapshot.py +++ b/plugins/modules/vm_snapshot.py @@ -28,7 +28,6 @@ description: source VM name. label: type: str - required: true description: - Snapshot label, used as identificator in combination with vm_name. - Must be unique for a specific VM. @@ -49,6 +48,12 @@ choices: [ present, absent] type: str required: True + uuid: + type: str + description: + - Snapshot uuid, used as identificator. + - Can be used instead of label. + - Must be unique. """ @@ -145,6 +150,29 @@ from ..module_utils.vm import VM +def get_snapshot( + module: AnsibleModule, rest_client: RestClient, vm_object: VM +) -> List[TypedVMSnapshotToAnsible]: + # Get snapshot by uuid first if parameter exists. + if module.params["uuid"]: + snapshot_list = VMSnapshot.get_snapshots_by_query( + dict(uuid=module.params["uuid"], domainUUID=vm_object.uuid), rest_client + ) + # Otherwise get by label + else: + snapshot_list = VMSnapshot.get_snapshots_by_query( + dict(label=module.params["label"], domainUUID=vm_object.uuid), rest_client + ) + + # Snapshot should be unique by this point. + if len(snapshot_list) > 1: + raise errors.ScaleComputingError( + f"Virtual machine - {module.params['vm_name']} - has more than one snapshot with label - {module.params['label']}, specify uuid instead." + ) + + return snapshot_list + + def ensure_present( module: AnsibleModule, rest_client: RestClient, @@ -203,15 +231,7 @@ def run( module: AnsibleModule, rest_client: RestClient ) -> Tuple[bool, Optional[TypedVMSnapshotToAnsible], TypedDiff]: vm_object: VM = VM.get_by_name(module.params, rest_client, must_exist=True) # type: ignore - snapshot_list = VMSnapshot.get_snapshots_by_query( - dict(label=module.params["label"], domainUUID=vm_object.uuid), rest_client - ) - - # VM should only have one snapshot with a specific label, we use vm_name and label as snapshot identificator. - if len(snapshot_list) > 1: - raise errors.ScaleComputingError( - f"Virtual machine - {module.params['vm_name']} - has more than one snapshot with label - {module.params['label']}." - ) + snapshot_list = get_snapshot(module, rest_client, vm_object) if module.params["state"] == State.present: return ensure_present(module, rest_client, vm_object, snapshot_list) @@ -237,7 +257,6 @@ def main() -> None: ), label=dict( type="str", - required=True, ), retain_for=dict( type="int", @@ -246,7 +265,15 @@ def main() -> None: type="bool", default=True, ), + uuid=dict( + type="str", + ), ), + mutually_exclusive=[("label", "uuid")], + required_if=[ + ("state", "absent", ("label", "uuid"), True), + ("state", "present", ["label"]), + ], ) try: diff --git a/tests/integration/targets/vm_snapshot/tasks/02_vm_snapshot.yml b/tests/integration/targets/vm_snapshot/tasks/02_1_vm_snapshot.yml similarity index 100% rename from tests/integration/targets/vm_snapshot/tasks/02_vm_snapshot.yml rename to tests/integration/targets/vm_snapshot/tasks/02_1_vm_snapshot.yml diff --git a/tests/integration/targets/vm_snapshot/tasks/02_2_vm_snapshot_uuid.yml b/tests/integration/targets/vm_snapshot/tasks/02_2_vm_snapshot_uuid.yml new file mode 100644 index 000000000..cbb7c13e1 --- /dev/null +++ b/tests/integration/targets/vm_snapshot/tasks/02_2_vm_snapshot_uuid.yml @@ -0,0 +1,329 @@ +# ----------------------------------Cleanup------------------------------------------------------------------------ +- name: Delete XLAB-snapshot-uuid-test + scale_computing.hypercore.vm: &delete-XLAB-snapshot-test + vm_name: "{{ item }}" + state: absent + loop: + - XLAB-snapshot-uuid-test + - XLAB-snapshot-uuid-test-2 + +# ----------------------------------Setup----------------------------------------------------------------------------- +- name: Create XLAB-snapshot-uuid-test + scale_computing.hypercore.api: + action: post + endpoint: /rest/v1/VirDomain + data: + dom: + name: XLAB-snapshot-uuid-test + tags: Xlab,CI,test,vm_snapshots + mem: 511705088 + numVCPU: 2 + blockDevs: + - type: VIRTIO_DISK + capacity: 8100100100 + name: jc1-disk-0 + netDevs: + - type: RTL8139 + vlan: 0 + connected: true + options: + attachGuestToolsISO: False + register: vm_created +- ansible.builtin.assert: + that: + - vm_created is succeeded + - vm_created is changed + +- name: Wait for the VM to be created + scale_computing.hypercore.task_wait: + task_tag: "{{ vm_created.record }}" + +- name: Create XLAB-snapshot-uuid-test-2 + scale_computing.hypercore.api: + action: post + endpoint: /rest/v1/VirDomain + data: + dom: + name: XLAB-snapshot-uuid-test-2 + tags: Xlab,CI,test,vm_snapshots + mem: 511705088 + numVCPU: 2 + blockDevs: + - type: VIRTIO_DISK + capacity: 8100100100 + name: jc1-disk-0 + netDevs: + - type: RTL8139 + vlan: 0 + connected: true + options: + attachGuestToolsISO: False + register: vm_created_2 +- ansible.builtin.assert: + that: + - vm_created_2 is succeeded + - vm_created_2 is changed + +- name: Wait for the VM-2 to be created + scale_computing.hypercore.task_wait: + task_tag: "{{ vm_created_2.record }}" + +# ----------------------------------Job------------------------------------------------------------------------------------- +# Test module input parameters with present (mutually exclusive uuid and label) +- name: Create snapshot of VM after create using label and uuid - must fail + scale_computing.hypercore.vm_snapshot: + state: present + vm_name: XLAB-snapshot-uuid-test + label: test-snapshot-integration + uuid: some-uuid + replication: true + register: snapshot_created + failed_when: snapshot_created is not failed +- ansible.builtin.assert: + that: + - snapshot_created is not changed + - snapshot_created.msg == "parameters are mutually exclusive: label|uuid" + +- name: Create snapshot of VM after create using uuid - must fail + scale_computing.hypercore.vm_snapshot: + state: present + vm_name: XLAB-snapshot-uuid-test + uuid: some-uuid + replication: true + register: snapshot_created + failed_when: snapshot_created is not failed +- ansible.builtin.assert: + that: + - snapshot_created is not changed + - snapshot_created.msg == "state is present but all of the following are missing: label" + +- name: Create snapshot of VM after create - this time succeed + scale_computing.hypercore.vm_snapshot: + state: present + vm_name: XLAB-snapshot-uuid-test + label: test-snapshot-integration + replication: true + register: snapshot_created +- ansible.builtin.assert: + that: + - snapshot_created is succeeded + - snapshot_created is changed + - snapshot_created.record.vm.name == "XLAB-snapshot-uuid-test" + - snapshot_created.record.label == "test-snapshot-integration" + - snapshot_created.record.replication is true + +- name: Create snapshot of VM after create - this time succeed - idempotence + scale_computing.hypercore.vm_snapshot: + state: present + vm_name: XLAB-snapshot-uuid-test + label: test-snapshot-integration + replication: true + register: snapshot_created +- ansible.builtin.assert: + that: + - snapshot_created is succeeded + - snapshot_created is not changed + - snapshot_created.record.vm.name == "XLAB-snapshot-uuid-test" + - snapshot_created.record.label == "test-snapshot-integration" + - snapshot_created.record.replication is true + +- name: Get snapshot info after create - assert it was created once + scale_computing.hypercore.vm_snapshot_info: + vm_name: XLAB-snapshot-uuid-test + label: test-snapshot-integration + register: snapshot_info +- ansible.builtin.assert: + that: + - snapshot_info is succeeded + - snapshot_info is not changed + - snapshot_info.records | length == 1 + - snapshot_created.record == snapshot_info.records.0 + +- name: Create snapshot of VM-2 after create + scale_computing.hypercore.vm_snapshot: + state: present + vm_name: XLAB-snapshot-uuid-test-2 + label: test-snapshot-integration + replication: true + register: snapshot_created +- ansible.builtin.assert: + that: + - snapshot_created is succeeded + - snapshot_created is changed + - snapshot_created.record.vm.name == "XLAB-snapshot-uuid-test-2" + - snapshot_created.record.label == "test-snapshot-integration" + - snapshot_created.record.replication is true + +- name: Create snapshot of VM-2 after create - idempotence + scale_computing.hypercore.vm_snapshot: + state: present + vm_name: XLAB-snapshot-uuid-test-2 + label: test-snapshot-integration + replication: true + register: snapshot_created +- ansible.builtin.assert: + that: + - snapshot_created is succeeded + - snapshot_created is not changed + - snapshot_created.record.vm.name == "XLAB-snapshot-uuid-test-2" + - snapshot_created.record.label == "test-snapshot-integration" + - snapshot_created.record.replication is true + +- name: Get snapshot info after create - assert it was created once + scale_computing.hypercore.vm_snapshot_info: + vm_name: XLAB-snapshot-uuid-test-2 + label: test-snapshot-integration + register: snapshot_info +- ansible.builtin.assert: + that: + - snapshot_info is succeeded + - snapshot_info is not changed + - snapshot_info.records | length == 1 + - snapshot_created.record == snapshot_info.records.0 + +# We can have multiple snapshots with same label. +# vm_snapshot module has constraints but API does not. +- name: Create another snapshot with label 'test-snapshot-integration' with API + scale_computing.hypercore.api: + action: post + endpoint: /rest/v1/VirDomainSnapshot + data: + domainUUID: "{{ vm_created.record.createdUUID }}" + label: test-snapshot-integration + type: USER + register: snapshot_created_api +- ansible.builtin.assert: + that: + - snapshot_created_api is succeeded + - snapshot_created_api is changed + +- name: Wait for the snapshot to crate + scale_computing.hypercore.task_wait: + task_tag: "{{ snapshot_created_api.record }}" + +- name: Get snapshot info after API create - assert there are two snapshot with same label + scale_computing.hypercore.vm_snapshot_info: + vm_name: XLAB-snapshot-uuid-test + label: test-snapshot-integration + register: snapshot_info +- ansible.builtin.assert: + that: + - snapshot_info is succeeded + - snapshot_info is not changed + - snapshot_info.records | length == 2 + +#------------------------------- Test vm_snapshot with absent when snapshots have the same label ------------------------------- +- name: Get snapshot info from VM-2 + scale_computing.hypercore.vm_snapshot_info: + vm_name: XLAB-snapshot-uuid-test-2 + label: test-snapshot-integration + register: snapshot_info +- ansible.builtin.assert: + that: + - snapshot_info is succeeded + - snapshot_info is not changed + - snapshot_info.records | length == 1 + +# Here we try to delete snapshot that doesn't belong to the specified VM +- name: Delete snapshot from VM-2 using VM-1 - Must be not changed + scale_computing.hypercore.vm_snapshot: + state: absent + vm_name: XLAB-snapshot-uuid-test + uuid: "{{ snapshot_info.records.0.snapshot_uuid }}" + register: snapshot_deleted + failed_when: snapshot_deleted is changed +- ansible.builtin.assert: + that: + - snapshot_deleted is succeeded + - snapshot_deleted is not changed + +- name: Assert that snapshot was NOT deleted + scale_computing.hypercore.vm_snapshot_info: + vm_name: XLAB-snapshot-uuid-test-2 + label: test-snapshot-integration + register: snapshot_info +- ansible.builtin.assert: + that: + - snapshot_info is succeeded + - snapshot_info is not changed + - snapshot_info.records | length == 1 + +# Here we pass both label and uuid, but they are mutually exclusive. +- name: Delete snapshot of VM when both label and uuid are passed - must fail + scale_computing.hypercore.vm_snapshot: + state: absent + vm_name: XLAB-snapshot-uuid-test + label: test-snapshot-integration + uuid: some-uuid + register: snapshot_deleted + failed_when: snapshot_deleted is not failed +- ansible.builtin.assert: + that: + - snapshot_deleted is succeeded + - snapshot_deleted is not changed + - snapshot_deleted.msg == "parameters are mutually exclusive: label|uuid" + +# Here we pass only label as parameter. +- name: Delete snapshot of VM when labels are the same, using label as parameter - must fail + scale_computing.hypercore.vm_snapshot: + state: absent + vm_name: XLAB-snapshot-uuid-test + label: test-snapshot-integration + register: snapshot_deleted + failed_when: snapshot_deleted is not failed +- ansible.builtin.assert: + that: + - snapshot_deleted is succeeded + - snapshot_deleted is not changed + - snapshot_deleted.msg == "Virtual machine - XLAB-snapshot-uuid-test - has more than one snapshot with label - test-snapshot-integration, specify uuid instead." + +- name: Delete snapshot of VM with uuid + scale_computing.hypercore.vm_snapshot: + state: absent + vm_name: XLAB-snapshot-uuid-test + uuid: "{{ snapshot_created_api.record.createdUUID }}" + register: snapshot_deleted +- ansible.builtin.assert: + that: + - snapshot_deleted is succeeded + - snapshot_deleted is changed + +- name: Get snapshot info after delete - assert delete happened + scale_computing.hypercore.vm_snapshot_info: + vm_name: XLAB-snapshot-uuid-test + label: test-snapshot-integration + register: snapshot_info +- ansible.builtin.assert: + that: + - snapshot_info is succeeded + - snapshot_info is not changed + - snapshot_info.records | length == 1 + - snapshot_info.records.0.snapshot_uuid != snapshot_created_api.record.createdUUID + +- name: Delete snapshot of VM with label + scale_computing.hypercore.vm_snapshot: + state: absent + vm_name: XLAB-snapshot-uuid-test + label: test-snapshot-integration + register: snapshot_deleted +- ansible.builtin.assert: + that: + - snapshot_deleted is succeeded + - snapshot_deleted is changed + +- name: Get snapshot info after second delete - assert delete happened + scale_computing.hypercore.vm_snapshot_info: + vm_name: XLAB-snapshot-uuid-test + label: test-snapshot-integration + register: snapshot_info +- ansible.builtin.assert: + that: + - snapshot_info is succeeded + - snapshot_info is not changed + - snapshot_info.records | length == 0 + +- name: Delete XLAB-snapshot-test + scale_computing.hypercore.vm: *delete-XLAB-snapshot-test + loop: + - XLAB-snapshot-uuid-test + - XLAB-snapshot-uuid-test-2 diff --git a/tests/integration/targets/vm_snapshot/tasks/main.yml b/tests/integration/targets/vm_snapshot/tasks/main.yml index eb97a46a8..5e45b646d 100644 --- a/tests/integration/targets/vm_snapshot/tasks/main.yml +++ b/tests/integration/targets/vm_snapshot/tasks/main.yml @@ -18,7 +18,9 @@ vars: test_vms_number: "{{ number_of_snapshot_testing_vms }}" - - include_tasks: 02_vm_snapshot.yml + - include_tasks: 02_1_vm_snapshot.yml + + - include_tasks: 02_2_vm_snapshot_uuid.yml - include_tasks: 03_vm_snapshot_attach_disk.yml vars: diff --git a/tests/unit/plugins/modules/test_vm_snapshot.py b/tests/unit/plugins/modules/test_vm_snapshot.py index 002e469c7..92146e131 100644 --- a/tests/unit/plugins/modules/test_vm_snapshot.py +++ b/tests/unit/plugins/modules/test_vm_snapshot.py @@ -156,7 +156,7 @@ class TestRun: ), ], ) - def test_run_virtual_disk( + def test_run_vm_snapshot( self, create_module, rest_client, @@ -176,6 +176,7 @@ def test_run_virtual_disk( retain_for=30, replication=True, state=state, + uuid=None, ) )