From 33f905459a21a3c047a1d0b9e37a5e7d575bd8d0 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Mon, 5 Aug 2024 13:21:41 -0400 Subject: [PATCH] Implement VM rollback Closes #184 --- api-daemon/pvcapid/flaskapi.py | 48 ++++++++++++++++++++++++++ api-daemon/pvcapid/helper.py | 24 +++++++++++++ client-cli/pvc/cli/cli.py | 36 ++++++++++++++++++-- client-cli/pvc/lib/vm.py | 19 +++++++++++ daemon-common/vm.py | 62 +++++++++++++++++++++++++++++++++- 5 files changed, 186 insertions(+), 3 deletions(-) diff --git a/api-daemon/pvcapid/flaskapi.py b/api-daemon/pvcapid/flaskapi.py index ba7e77c1..75808967 100755 --- a/api-daemon/pvcapid/flaskapi.py +++ b/api-daemon/pvcapid/flaskapi.py @@ -3202,6 +3202,54 @@ class API_VM_Snapshot(Resource): api.add_resource(API_VM_Snapshot, "/vm//snapshot") +# /vm//snapshot/rollback +class API_VM_Snapshot_Rollback(Resource): + @RequestParser( + [ + { + "name": "snapshot_name", + "required": True, + "helptext": "A snapshot name must be specified", + }, + ] + ) + @Authenticator + def post(self, vm, reqargs): + """ + Roll back to 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 roll back to + 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.rollback_vm_snapshot(vm, snapshot_name) + + +api.add_resource(API_VM_Snapshot_Rollback, "/vm//snapshot/rollback") + + ########################################################## # Client API - Network ########################################################## diff --git a/api-daemon/pvcapid/helper.py b/api-daemon/pvcapid/helper.py index b7a4a6e6..3a14de08 100755 --- a/api-daemon/pvcapid/helper.py +++ b/api-daemon/pvcapid/helper.py @@ -813,6 +813,30 @@ def remove_vm_snapshot( return output, retcode +@ZKConnection(config) +def rollback_vm_snapshot( + zkhandler, + domain, + snapshot_name, +): + """ + Roll back to a snapshot of a VM. + """ + retflag, retdata = pvc_vm.rollback_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 919e0e6f..a389df2c 100644 --- a/client-cli/pvc/cli/cli.py +++ b/client-cli/pvc/cli/cli.py @@ -1787,7 +1787,7 @@ def cli_vm_snapshot(): @connection_req @click.argument("domain") @click.argument("snapshot_name", required=False, default=None) -def cli_vm_snapshot_remove(domain, snapshot_name): +def cli_vm_snapshot_create(domain, snapshot_name): """ Create a snapshot of the disks and XML configuration of virtual machine DOMAIN, with the optional name SNAPSHOT_NAME. DOMAIN may be a UUID or name. @@ -1819,7 +1819,7 @@ def cli_vm_snapshot_remove(domain, snapshot_name): @connection_req @click.argument("domain") @click.argument("snapshot_name") -def cli_vm_snapshot_create(domain, snapshot_name): +def cli_vm_snapshot_remove(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. @@ -1838,6 +1838,37 @@ def cli_vm_snapshot_create(domain, snapshot_name): finish(retcode, retmsg) +############################################################################### +# > pvc vm snapshot rollback +############################################################################### +@click.command( + name="rollback", short_help="Roll back to a snapshot of a virtual machine." +) +@connection_req +@click.argument("domain") +@click.argument("snapshot_name") +@confirm_opt( + "Roll back to snapshot {snapshot_name} of {domain} and lose all data and changes since this snapshot" +) +def cli_vm_snapshot_rollback(domain, snapshot_name): + """ + Roll back to 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"Rolling back to snapshot '{snapshot_name}' of VM '{domain}'... ", + newline=False, + ) + retcode, retmsg = pvc.lib.vm.vm_rollback_snapshot(CLI_CONFIG, domain, snapshot_name) + if retcode: + echo(CLI_CONFIG, "done.") + else: + echo(CLI_CONFIG, "failed.") + finish(retcode, retmsg) + + ############################################################################### # > pvc vm backup ############################################################################### @@ -6377,6 +6408,7 @@ 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_snapshot.add_command(cli_vm_snapshot_rollback) 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 8edfbac5..92a7a92f 100644 --- a/client-cli/pvc/lib/vm.py +++ b/client-cli/pvc/lib/vm.py @@ -538,6 +538,25 @@ def vm_remove_snapshot(config, vm, snapshot_name): return True, response.json().get("message", "") +def vm_rollback_snapshot(config, vm, snapshot_name): + """ + Roll back to a snapshot of a VM's disks and configuration + + API endpoint: POST /vm/{vm}/snapshot/rollback + API arguments: snapshot_name=snapshot_name + API schema: {"message":"{data}"} + """ + params = {"snapshot_name": snapshot_name} + response = call_api( + config, "post", "/vm/{vm}/snapshot/rollback".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 43527583..7478641a 100644 --- a/daemon-common/vm.py +++ b/daemon-common/vm.py @@ -1396,7 +1396,67 @@ def remove_vm_snapshot(zkhandler, domain, snapshot_name): def rollback_vm_snapshot(zkhandler, domain, snapshot_name): - pass + # 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) + + # Verify that the VM is in a stopped state; renaming is not supported otherwise + state = zkhandler.read(("domain.state", dom_uuid)) + if state not in ["stop", "disable"]: + return ( + False, + 'ERROR: VM "{}" is not in stopped state; VMs cannot be rolled back while running.'.format( + domain + ), + ) + + # Verify that the snapshot exists + 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}"!', + ) + + 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: + rbd, name = snap.split("@") + pool, volume = rbd.split("/") + ret, msg = ceph.rollback_snapshot(zkhandler, pool, volume, name) + if not ret: + return False, msg + + # Get the snapshot domain XML + vm_config = zkhandler.read( + ("domain.snapshots", dom_uuid, "domain_snapshot.xml", snapshot_name) + ) + + # Write the restored config to the main XML config + zkhandler.write( + [ + ( + ( + "domain.xml", + dom_uuid, + ), + vm_config, + ), + ] + ) + + tend = time.time() + ttot = round(tend - tstart, 2) + return ( + True, + f'Successfully rolled back to snapshot "{snapshot_name}" of VM "{domain}" in {ttot}s.', + ) #