From 0c240a5129c341a8f182189909dd751222660bf7 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Fri, 17 May 2024 10:55:42 -0400 Subject: [PATCH] Add VM snapshot removal --- api-daemon/pvcapid/flaskapi.py | 42 ++++++++++++++++++++++ api-daemon/pvcapid/helper.py | 24 +++++++++++++ client-cli/pvc/cli/cli.py | 31 ++++++++++++++-- client-cli/pvc/lib/vm.py | 19 ++++++++++ daemon-common/vm.py | 64 ++++++++++++++++++++++++++++++---- 5 files changed, 172 insertions(+), 8 deletions(-) diff --git a/api-daemon/pvcapid/flaskapi.py b/api-daemon/pvcapid/flaskapi.py index 29162f92..490b0a29 100755 --- a/api-daemon/pvcapid/flaskapi.py +++ b/api-daemon/pvcapid/flaskapi.py @@ -3130,6 +3130,48 @@ class API_VM_Snapshot(Resource): snapshot_name = reqargs.get("snapshot_name", None) return api_helper.create_vm_snapshot(vm, snapshot_name=snapshot_name) + @RequestParser( + [ + { + "name": "snapshot_name", + "required": True, + "helptext": "A snapshot name must be specified", + }, + ] + ) + @Authenticator + def delete(self, vm, reqargs): + """ + Remove a snapshot of a VM's disks and configuration + --- + tags: + - vm + parameters: + - in: query + name: snapshot_name + type: string + required: true + description: The name of the snapshot to remove + responses: + 200: + description: OK + schema: + type: object + id: Message + 400: + description: Execution error + schema: + type: object + id: Message + 404: + description: Not found + schema: + type: object + id: Message + """ + snapshot_name = reqargs.get("snapshot_name", None) + return api_helper.remove_vm_snapshot(vm, snapshot_name) + api.add_resource(API_VM_Snapshot, "/vm//snapshot") diff --git a/api-daemon/pvcapid/helper.py b/api-daemon/pvcapid/helper.py index 7d28f0f8..b7a4a6e6 100755 --- a/api-daemon/pvcapid/helper.py +++ b/api-daemon/pvcapid/helper.py @@ -789,6 +789,30 @@ def create_vm_snapshot( return output, retcode +@ZKConnection(config) +def remove_vm_snapshot( + zkhandler, + domain, + snapshot_name, +): + """ + Take a snapshot of a VM. + """ + retflag, retdata = pvc_vm.remove_vm_snapshot( + zkhandler, + domain, + snapshot_name, + ) + + if retflag: + retcode = 200 + else: + retcode = 400 + + output = {"message": retdata.replace('"', "'")} + return output, retcode + + @ZKConnection(config) def vm_attach_device(zkhandler, vm, device_spec_xml): """ diff --git a/client-cli/pvc/cli/cli.py b/client-cli/pvc/cli/cli.py index 67554333..919e0e6f 100644 --- a/client-cli/pvc/cli/cli.py +++ b/client-cli/pvc/cli/cli.py @@ -1787,10 +1787,10 @@ def cli_vm_snapshot(): @connection_req @click.argument("domain") @click.argument("snapshot_name", required=False, default=None) -def cli_vm_snapshot_create(domain, snapshot_name): +def cli_vm_snapshot_remove(domain, snapshot_name): """ Create a snapshot of the disks and XML configuration of virtual machine DOMAIN, with the - optional name SNAPSHOT_NAME. DOMAIN mayb e a UUID or name. + optional name SNAPSHOT_NAME. DOMAIN may be a UUID or name. WARNING: RBD snapshots are crash-consistent but not filesystem-aware. If a snapshot was taken of a running VM, restoring that snapshot will be equivalent to having forcibly restarted the @@ -1812,6 +1812,32 @@ def cli_vm_snapshot_create(domain, snapshot_name): finish(retcode, retmsg) +############################################################################### +# > pvc vm snapshot remove +############################################################################### +@click.command(name="remove", short_help="Remove a snapshot of a virtual machine.") +@connection_req +@click.argument("domain") +@click.argument("snapshot_name") +def cli_vm_snapshot_create(domain, snapshot_name): + """ + Remove the snapshot SNAPSHOT_NAME of the disks and XML configuration of virtual machine DOMAIN, + DOMAIN may be a UUID or name. + """ + + echo( + CLI_CONFIG, + f"Removing snapshot '{snapshot_name}' of VM '{domain}'... ", + newline=False, + ) + retcode, retmsg = pvc.lib.vm.vm_remove_snapshot(CLI_CONFIG, domain, snapshot_name) + if retcode: + echo(CLI_CONFIG, "done.") + else: + echo(CLI_CONFIG, "failed.") + finish(retcode, retmsg) + + ############################################################################### # > pvc vm backup ############################################################################### @@ -6350,6 +6376,7 @@ cli_vm.add_command(cli_vm_migrate) cli_vm.add_command(cli_vm_unmigrate) cli_vm.add_command(cli_vm_flush_locks) cli_vm_snapshot.add_command(cli_vm_snapshot_create) +cli_vm_snapshot.add_command(cli_vm_snapshot_remove) cli_vm.add_command(cli_vm_snapshot) cli_vm_backup.add_command(cli_vm_backup_create) cli_vm_backup.add_command(cli_vm_backup_restore) diff --git a/client-cli/pvc/lib/vm.py b/client-cli/pvc/lib/vm.py index 198e873c..ba259653 100644 --- a/client-cli/pvc/lib/vm.py +++ b/client-cli/pvc/lib/vm.py @@ -519,6 +519,25 @@ def vm_create_snapshot(config, vm, snapshot_name=None): return True, response.json().get("message", "") +def vm_remove_snapshot(config, vm, snapshot_name): + """ + Remove a snapshot of a VM's disks and configuration + + API endpoint: DELETE /vm/{vm}/snapshot + API arguments: snapshot_name=snapshot_name + API schema: {"message":"{data}"} + """ + params = {"snapshot_name": snapshot_name} + response = call_api( + config, "delete", "/vm/{vm}/snapshot".format(vm=vm), params=params + ) + + if response.status_code != 200: + return False, response.json().get("message", "") + else: + return True, response.json().get("message", "") + + def vm_vcpus_set(config, vm, vcpus, topology, restart): """ Set the vCPU count of the VM with topology diff --git a/daemon-common/vm.py b/daemon-common/vm.py index 98691403..0877d115 100644 --- a/daemon-common/vm.py +++ b/daemon-common/vm.py @@ -1249,7 +1249,7 @@ def get_list( # # VM Snapshot Tasks # -def create_vm_snapshot(zkhandler, domain, snapshot_name=None): +def create_vm_snapshot(zkhandler, domain, snapshot_name=None, is_backup=False): # Validate that VM exists in cluster dom_uuid = getDomainUUID(zkhandler, domain) if not dom_uuid: @@ -1310,7 +1310,7 @@ def create_vm_snapshot(zkhandler, domain, snapshot_name=None): "domain_snapshot.is_backup", snapshot_name, ), - False, + is_backup, ), ( ("domain.snapshots", dom_uuid, "domain_snapshot.xml", snapshot_name), @@ -1336,14 +1336,66 @@ def create_vm_snapshot(zkhandler, domain, snapshot_name=None): ) +def remove_vm_snapshot(zkhandler, domain, snapshot_name, remove_backup=False): + # Validate that VM exists in cluster + dom_uuid = getDomainUUID(zkhandler, domain) + if not dom_uuid: + return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) + + if not zkhandler.exists( + ("domain.snapshots", dom_uuid, "domain_snapshot.name", snapshot_name) + ): + return ( + False, + f'ERROR: Could not find snapshot "{snapshot_name}" of VM "{domain}"!', + ) + + if ( + zkhandler.read( + ("domain.snapshots", dom_uuid, "domain_snapshot.is_backup", snapshot_name) + ) + and not remove_backup + ): + # Disallow removing backups normally, but expose `remove_backup` flag for internal usage by refactored backup handlers + return ( + False, + f'ERROR: Snapshot "{snapshot_name}" of VM "{domain}" is a backup; please remove with "pvc backup"!', + ) + + tstart = time.time() + + _snapshots = zkhandler.read( + ("domain.snapshots", dom_uuid, "domain_snapshot.rbd_snapshots", snapshot_name) + ) + rbd_snapshots = _snapshots.split(",") + for snap in rbd_snapshots: + name, rbd = snap.split("@") + pool, volume = rbd.split("/") + ret, msg = ceph.remove_snapshot(zkhandler, pool, volume, name) + if not ret: + return False, msg + + ret = zkhandler.delete( + ("domain.snapshots", dom_uuid, "domain_snapshot.name", snapshot_name) + ) + if not ret: + return ( + False, + f'ERROR: Failed to delete snapshot "{snapshot_name}" of VM "{domain}" in Zookeeper.', + ) + + tend = time.time() + ttot = round(tend - tstart, 2) + return ( + True, + f'Successfully removed snapshot "{snapshot_name}" of VM "{domain}" in {ttot}s.', + ) + + def rollback_vm_snapshot(zkhandler, domain, snapshot_name): pass -def remove_vm_snapshot(zkhandler, domain, snapshot_name): - pass - - # # VM Backup Tasks #