From 3d1ee4da3fe964aa3af1c43688065adbf2d2c379 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 30 Mar 2018 19:28:22 +0700 Subject: [PATCH] Support for suspend to disk / resume (Qemu). --- gns3server/compute/qemu/qemu_vm.py | 100 +++++++++++++++++- .../handlers/api/compute/qemu_handler.py | 4 +- gns3server/schemas/qemu.py | 13 +++ 3 files changed, 113 insertions(+), 4 deletions(-) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 0afb6402..1e21e068 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -31,6 +31,7 @@ import socket import gns3server import subprocess import time +import json from gns3server.utils import parse_version from gns3server.utils.asyncio import subprocess_check_output, cancellable_wait_run_in_executor @@ -116,6 +117,7 @@ class QemuVM(BaseNode): self._kernel_command_line = "" self._legacy_networking = False self._acpi_shutdown = False + self._save_vm_state = False self._cpu_throttling = 0 # means no CPU throttling self._process_priority = "low" @@ -594,6 +596,30 @@ class QemuVM(BaseNode): log.info('QEMU VM "{name}" [{id}] has disabled ACPI shutdown'.format(name=self._name, id=self._id)) self._acpi_shutdown = acpi_shutdown + @property + def save_vm_state(self): + """ + Returns either this QEMU VM state can be saved. + + :returns: boolean + """ + + return self._save_vm_state + + @save_vm_state.setter + def save_vm_state(self, save_vm_state): + """ + Sets either this QEMU VM state can be saved. + + :param save_vm_state: boolean + """ + + if save_vm_state: + log.info('QEMU VM "{name}" [{id}] has enabled the save VM state option'.format(name=self._name, id=self._id)) + else: + log.info('QEMU VM "{name}" [{id}] has disabled the save VM state option'.format(name=self._name, id=self._id)) + self._save_vm_state = save_vm_state + @property def cpu_throttling(self): """ @@ -1003,7 +1029,19 @@ class QemuVM(BaseNode): if self.is_running(): log.info('Stopping QEMU VM "{}" PID={}'.format(self._name, self._process.pid)) try: - if self.acpi_shutdown: + + if self.save_vm_state: + yield from self._control_vm("stop") + yield from self._control_vm("savevm GNS3_SAVED_STATE") + wait_for_savevm = 120 + while wait_for_savevm: + yield from asyncio.sleep(1) + status = yield from self._saved_state_option() + wait_for_savevm -= 1 + if status != []: + break + + if self.acpi_shutdown and not self.save_vm_state: yield from self._control_vm("system_powerdown") yield from gns3server.utils.asyncio.wait_for_process_termination(self._process, timeout=30) else: @@ -1021,6 +1059,8 @@ class QemuVM(BaseNode): log.warning('QEMU VM "{}" PID={} is still running'.format(self._name, self._process.pid)) self._process = None self._stop_cpulimit() + if not self.save_vm_state: + yield from self._clear_save_vm_stated() yield from super().stop() @asyncio.coroutine @@ -1480,7 +1520,7 @@ class QemuVM(BaseNode): def _get_qemu_img(self): """ Search the qemu-img binary in the same binary of the qemu binary - for avoiding version incompatibily. + for avoiding version incompatibility. :returns: qemu-img path or raise an error """ @@ -1814,6 +1854,57 @@ class QemuVM(BaseNode): return True return False + @asyncio.coroutine + def _clear_save_vm_stated(self, snapshot_name="GNS3_SAVED_STATE"): + + drives = ["a", "b", "c", "d"] + qemu_img_path = self._get_qemu_img() + for disk_index, drive in enumerate(drives): + disk_image = getattr(self, "_hd{}_disk_image".format(drive)) + if not disk_image: + continue + try: + if self.linked_clone: + disk = os.path.join(self.working_dir, "hd{}_disk.qcow2".format(drive)) + else: + disk = disk_image + command = [qemu_img_path, "snapshot", "-c", snapshot_name, disk] + retcode = yield from self._qemu_img_exec(command) + if retcode: + stdout = self.read_qemu_img_stdout() + log.warning("Could not delete saved VM state from disk {}: {}".format(disk, stdout)) + else: + log.info("Deleted saved VM state from disk {}".format(disk)) + except subprocess.SubprocessError as e: + raise QemuError("Error while looking for the Qemu VM saved state snapshot: {}".format(e)) + + @asyncio.coroutine + def _saved_state_option(self, snapshot_name="GNS3_SAVED_STATE"): + + drives = ["a", "b", "c", "d"] + qemu_img_path = self._get_qemu_img() + for disk_index, drive in enumerate(drives): + disk_image = getattr(self, "_hd{}_disk_image".format(drive)) + if not disk_image: + continue + try: + if self.linked_clone: + disk = os.path.join(self.working_dir, "hd{}_disk.qcow2".format(drive)) + else: + disk = disk_image + output = yield from subprocess_check_output(qemu_img_path, "info", "--output=json", disk) + json_data = json.loads(output) + if "snapshots" in json_data: + for snapshot in json_data["snapshots"]: + if snapshot["name"] == snapshot_name: + log.info('QEMU VM "{name}" [{id}] VM saved state detected (snapshot name: {snapshot})'.format(name=self._name, + id=self.id, + snapshot=snapshot_name)) + return ["-loadvm", snapshot_name] + except subprocess.SubprocessError as e: + raise QemuError("Error while looking for the Qemu VM saved state snapshot: {}".format(e)) + return [] + @asyncio.coroutine def _build_command(self): """ @@ -1860,6 +1951,11 @@ class QemuVM(BaseNode): command.extend(self._monitor_options()) command.extend((yield from self._network_options())) command.extend(self._graphic()) + if not self.save_vm_state: + yield from self._clear_save_vm_stated() + else: + command.extend((yield from self._saved_state_option())) + if additional_options: try: command.extend(shlex.split(additional_options)) diff --git a/gns3server/handlers/api/compute/qemu_handler.py b/gns3server/handlers/api/compute/qemu_handler.py index bc99621e..6e2c1e5c 100644 --- a/gns3server/handlers/api/compute/qemu_handler.py +++ b/gns3server/handlers/api/compute/qemu_handler.py @@ -544,8 +544,8 @@ class QEMUHandler: def download_image(request, response): filename = request.match_info["filename"] - iou_manager = Qemu.instance() - image_path = iou_manager.get_abs_image_path(filename) + qemu_manager = Qemu.instance() + image_path = qemu_manager.get_abs_image_path(filename) # Raise error if user try to escape if filename[0] == ".": diff --git a/gns3server/schemas/qemu.py b/gns3server/schemas/qemu.py index 95abaf8a..a2118254 100644 --- a/gns3server/schemas/qemu.py +++ b/gns3server/schemas/qemu.py @@ -188,6 +188,10 @@ QEMU_CREATE_SCHEMA = { "description": "ACPI shutdown support", "type": ["boolean", "null"], }, + "save_vm_state": { + "description": "Save VM state support", + "type": ["boolean", "null"], + }, "cpu_throttling": { "description": "Percentage of CPU allowed for QEMU", "minimum": 0, @@ -373,6 +377,10 @@ QEMU_UPDATE_SCHEMA = { "description": "ACPI shutdown support", "type": ["boolean", "null"], }, + "save_vm_state": { + "description": "Save VM state support", + "type": ["boolean", "null"], + }, "cpu_throttling": { "description": "Percentage of CPU allowed for QEMU", "minimum": 0, @@ -571,6 +579,10 @@ QEMU_OBJECT_SCHEMA = { "description": "ACPI shutdown support", "type": "boolean", }, + "save_vm_state": { + "description": "Save VM state support", + "type": ["boolean", "null"], + }, "cpu_throttling": { "description": "Percentage of CPU allowed for QEMU", "minimum": 0, @@ -633,6 +645,7 @@ QEMU_OBJECT_SCHEMA = { "kernel_command_line", "legacy_networking", "acpi_shutdown", + "save_vm_state", "cpu_throttling", "process_priority", "options",