From 14124622293444ca128f9bdf16113b463cb0e014 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 8 Sep 2016 15:32:35 +0200 Subject: [PATCH] Suspend the GNS3 VM Fix #656 --- gns3server/controller/__init__.py | 10 ++++-- gns3server/controller/gns3vm/__init__.py | 35 ++++++++++++------ gns3server/controller/gns3vm/base_gns3_vm.py | 29 +++++---------- .../controller/gns3vm/remote_gns3_vm.py | 7 ++++ .../controller/gns3vm/virtualbox_gns3_vm.py | 18 ++++++++-- .../controller/gns3vm/vmware_gns3_vm.py | 15 ++++++++ gns3server/schemas/gns3vm.py | 6 ++-- tests/controller/test_controller.py | 36 +++++++++++++++++-- 8 files changed, 114 insertions(+), 42 deletions(-) diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 3aaed2d1..a773da07 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -88,7 +88,7 @@ class Controller: # We don't care if a compute is down at this step except aiohttp.errors.ClientOSError: pass - yield from self.gns3vm.auto_stop_vm() + yield from self.gns3vm.exit_vm() self._computes = {} self._projects = {} @@ -204,10 +204,16 @@ class Controller: for compute in self._computes.values(): if compute.host == vm_settings.get("remote_vm_host") and compute.port == vm_settings.get("remote_vm_port"): vmname = compute.name + + if vm_settings.get("auto_stop", True): + when_exit = "stop" + else: + when_exit = "keep" + self.gns3vm.settings = { "engine": engine, "enable": vm_settings.get("auto_start", False), - "auto_stop": vm_settings.get("auto_stop", True), + "when_exit": when_exit, "headless": vm_settings.get("headless", False), "vmname": vmname } diff --git a/gns3server/controller/gns3vm/__init__.py b/gns3server/controller/gns3vm/__init__.py index 9eb9e7c6..135cca1e 100644 --- a/gns3server/controller/gns3vm/__init__.py +++ b/gns3server/controller/gns3vm/__init__.py @@ -40,7 +40,7 @@ class GNS3VM: self._engines = {} self._settings = { "vmname": None, - "auto_stop": True, + "when_exit": "stop", "headless": False, "enable": False, "engine": "vmware" @@ -54,7 +54,7 @@ class GNS3VM: vmware_informations = { "engine_id": "vmware", "description": "VMware is the recommended choice for best performances.", - "support_auto_stop": True, + "support_when_exit": True, "support_headless": True } if sys.platform.startswith("darwin"): @@ -66,7 +66,7 @@ class GNS3VM: "engine_id": "virtualbox", "name": "VirtualBox", "description": "VirtualBox doesn't support nested virtualization, this means running Qemu based VM could be very slow.", - "support_auto_stop": True, + "support_when_exit": True, "support_headless": True } @@ -74,7 +74,7 @@ class GNS3VM: "engine_id": "remote", "name": "Remote", "description": "Use a remote GNS3 server as the GNS3 VM.", - "support_auto_stop": False, + "support_when_exit": False, "support_headless": False } @@ -149,11 +149,11 @@ class GNS3VM: return self._settings.get("enable", False) @property - def auto_stop(self): + def when_exit(self): """ - The GNSVM should auto stop + What should be done when exit """ - return self._settings["auto_stop"] + return self._settings["when_exit"] @property def settings(self): @@ -224,10 +224,13 @@ class GNS3VM: force=True) @asyncio.coroutine - def auto_stop_vm(self): - if self.enable and self.auto_stop: + def exit_vm(self): + if self.enable: try: - yield from self._stop() + if self._settings["when_exit"] == "stop": + yield from self._stop() + elif self._settings["when_exit"] == "suspend": + yield from self._suspend() except GNS3VMError as e: log.warn(str(e)) @@ -250,6 +253,18 @@ class GNS3VM: password=self.password, force=True) + @locked_coroutine + def _suspend(self): + """ + Suspend the GNS3 VM + """ + engine = self._current_engine() + if "vm" in self._controller.computes: + yield from self._controller.delete_compute("vm") + if engine.running: + log.info("Suspend the GNS3 VM") + yield from engine.suspend() + @locked_coroutine def _stop(self): """ diff --git a/gns3server/controller/gns3vm/base_gns3_vm.py b/gns3server/controller/gns3vm/base_gns3_vm.py index 0f59e878..67542e70 100644 --- a/gns3server/controller/gns3vm/base_gns3_vm.py +++ b/gns3server/controller/gns3vm/base_gns3_vm.py @@ -28,7 +28,6 @@ class BaseGNS3VM: self._controller = controller self._vmname = None - self._auto_stop = False self._ip_address = None self._port = 3080 self._headless = False @@ -245,26 +244,6 @@ class BaseGNS3VM: self._ram = new_ram - @property - def auto_stop(self): - """ - Returns whether the VM should automatically be stopped when GNS3 quit - - :returns: boolean - """ - - return self._auto_start - - @auto_stop.setter - def auto_stop(self, new_auto_stop): - """ - Set whether the VM should automatically be stopped when GNS3 quit - - :param new_auto_stop: boolean - """ - - self._auto_stop = new_auto_stop - @property def engine(self): """ @@ -291,6 +270,14 @@ class BaseGNS3VM: raise NotImplementedError + @asyncio.coroutine + def suspend(self): + """ + Suspend the GNS3 VM. + """ + + raise NotImplementedError + @asyncio.coroutine def stop(self, force=False): """ diff --git a/gns3server/controller/gns3vm/remote_gns3_vm.py b/gns3server/controller/gns3vm/remote_gns3_vm.py index 085c018d..8b915fe4 100644 --- a/gns3server/controller/gns3vm/remote_gns3_vm.py +++ b/gns3server/controller/gns3vm/remote_gns3_vm.py @@ -63,6 +63,13 @@ class RemoteGNS3VM(BaseGNS3VM): return raise GNS3VMError("Can't start the GNS3 VM remote VM {} not found".format(self.vmname)) + @asyncio.coroutine + def suspend(self): + """ + Suspend do nothing for remote server + """ + self.running = False + @asyncio.coroutine def stop(self): """ diff --git a/gns3server/controller/gns3vm/virtualbox_gns3_vm.py b/gns3server/controller/gns3vm/virtualbox_gns3_vm.py index 544147f8..3b5e08cf 100644 --- a/gns3server/controller/gns3vm/virtualbox_gns3_vm.py +++ b/gns3server/controller/gns3vm/virtualbox_gns3_vm.py @@ -180,7 +180,9 @@ class VirtualBoxGNS3VM(BaseGNS3VM): if self._headless: args.extend(["--type", "headless"]) yield from self._execute("startvm", args) - + elif vm_state == "paused": + args = [self._vmname, "resume"] + yield from self._execute("controlvm", args) ip_address = "127.0.0.1" try: # get a random port on localhost @@ -221,7 +223,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM): try: resp = None resp = yield from session.get('http://127.0.0.1:{}/v2/compute/network/interfaces'.format(api_port)) - except OSError: + except (OSError, aiohttp.errors.ClientHttpProcessingError): pass if resp: @@ -242,6 +244,16 @@ class VirtualBoxGNS3VM(BaseGNS3VM): yield from asyncio.sleep(1) raise GNS3VMError("Could not get the GNS3 VM ip make sure the VM receive an IP from VirtualBox") + @asyncio.coroutine + def suspend(self): + """ + Suspend the GNS3 VM. + """ + + yield from self._execute("controlvm", [self._vmname, "savestate"], timeout=3) + log.info("GNS3 VM has been suspend") + self.running = False + @asyncio.coroutine def stop(self): """ @@ -249,7 +261,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM): """ yield from self._execute("controlvm", [self._vmname, "acpipowerbutton"], timeout=3) - log.info("GNS3 VM hsd been stopped") + log.info("GNS3 VM has been stopped") self.running = False @asyncio.coroutine diff --git a/gns3server/controller/gns3vm/vmware_gns3_vm.py b/gns3server/controller/gns3vm/vmware_gns3_vm.py index ad3e692b..18cf3753 100644 --- a/gns3server/controller/gns3vm/vmware_gns3_vm.py +++ b/gns3server/controller/gns3vm/vmware_gns3_vm.py @@ -126,6 +126,21 @@ class VMwareGNS3VM(BaseGNS3VM): log.info("GNS3 VM IP address set to {}".format(guest_ip_address)) self.running = True + @asyncio.coroutine + def suspend(self): + """ + Suspend the GNS3 VM. + """ + + if self._vmx_path is None: + raise GNS3VMError("No VMX path configured, can't suspend the VM") + try: + yield from self._execute("suspend", [self._vmx_path]) + except GNS3VMError as e: + log.warning("Error when suspending the VM: {}".format(str(e))) + log.info("GNS3 VM has been suspended") + self.running = False + @asyncio.coroutine def stop(self): """ diff --git a/gns3server/schemas/gns3vm.py b/gns3server/schemas/gns3vm.py index 4d08d8ec..d7f495dc 100644 --- a/gns3server/schemas/gns3vm.py +++ b/gns3server/schemas/gns3vm.py @@ -29,9 +29,9 @@ GNS3VM_SETTINGS_SCHEMA = { "type": "string", "description": "The name of the VM" }, - "auto_stop": { - "type": "boolean", - "description": "The VM auto stop with GNS3" + "when_exit": { + "description": "What to do with the VM when GNS3 exit", + "enum": ["stop", "suspend", "keep"] }, "headless": { "type": "boolean", diff --git a/tests/controller/test_controller.py b/tests/controller/test_controller.py index b828e1cd..fc29c693 100644 --- a/tests/controller/test_controller.py +++ b/tests/controller/test_controller.py @@ -149,7 +149,7 @@ def test_import_gns3vm_1_x(controller, controller_config_path, async_run): assert controller.gns3vm.settings["engine"] == "vmware" assert controller.gns3vm.settings["enable"] assert controller.gns3vm.settings["headless"] - assert controller.gns3vm.settings["auto_stop"] is False + assert controller.gns3vm.settings["when_exit"] == "keep" assert controller.gns3vm.settings["vmname"] == "GNS3 VM" @@ -408,12 +408,12 @@ def test_stop(controller, async_run): def test_stop_vm(controller, async_run): """ - Start the controller with a GNS3 VM running + Stop GNS3 VM if configured """ controller.gns3vm.settings = { "enable": True, "engine": "vmware", - "auto_stop": True + "when_exit": "stop" } controller.gns3vm._current_engine().running = True with asyncio_patch("gns3server.controller.gns3vm.vmware_gns3_vm.VMwareGNS3VM.stop") as mock: @@ -421,6 +421,36 @@ def test_stop_vm(controller, async_run): assert mock.called +def test_suspend_vm(controller, async_run): + """ + Suspend GNS3 VM if configured + """ + controller.gns3vm.settings = { + "enable": True, + "engine": "vmware", + "when_exit": "suspend" + } + controller.gns3vm._current_engine().running = True + with asyncio_patch("gns3server.controller.gns3vm.vmware_gns3_vm.VMwareGNS3VM.suspend") as mock: + async_run(controller.stop()) + assert mock.called + + +def test_keep_vm(controller, async_run): + """ + Keep GNS3 VM if configured + """ + controller.gns3vm.settings = { + "enable": True, + "engine": "vmware", + "when_exit": "keep" + } + controller.gns3vm._current_engine().running = True + with asyncio_patch("gns3server.controller.gns3vm.vmware_gns3_vm.VMwareGNS3VM.suspend") as mock: + async_run(controller.stop()) + assert not mock.called + + def test_get_free_project_name(controller, async_run): async_run(controller.add_project(project_id=str(uuid.uuid4()), name="Test"))