From 3590985c073f8da3c3ea8f853be4f1657ff12c7f Mon Sep 17 00:00:00 2001 From: Dmitry Shmygov Date: Mon, 22 Dec 2014 17:44:16 +0300 Subject: [PATCH 1/3] Add QEMU monitor port to control running QEMU VMs --- gns3server/modules/qemu/__init__.py | 17 +++++- gns3server/modules/qemu/qemu_vm.py | 82 +++++++++++++++++++++++++++-- gns3server/modules/qemu/schemas.py | 12 +++++ 3 files changed, 105 insertions(+), 6 deletions(-) diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py index 4816dfc8..96e43e3f 100644 --- a/gns3server/modules/qemu/__init__.py +++ b/gns3server/modules/qemu/__init__.py @@ -67,11 +67,14 @@ class Qemu(IModule): qemu_config = config.get_section_config(name.upper()) self._console_start_port_range = qemu_config.get("console_start_port_range", 5001) self._console_end_port_range = qemu_config.get("console_end_port_range", 5500) + self._monitor_start_port_range = qemu_config.get("monitor_start_port_range", 5501) + self._monitor_end_port_range = qemu_config.get("monitor_end_port_range", 6000) self._allocated_udp_ports = [] self._udp_start_port_range = qemu_config.get("udp_start_port_range", 40001) self._udp_end_port_range = qemu_config.get("udp_end_port_range", 45500) self._host = qemu_config.get("host", kwargs["host"]) self._console_host = qemu_config.get("console_host", kwargs["console_host"]) + self._monitor_host = qemu_config.get("monitor_host", "0.0.0.0") self._projects_dir = kwargs["projects_dir"] self._tempdir = kwargs["temp_dir"] self._working_dir = self._projects_dir @@ -137,6 +140,8 @@ class Qemu(IModule): - project_name - console_start_port_range - console_end_port_range + - monitor_start_port_range + - monitor_end_port_range - udp_start_port_range - udp_end_port_range @@ -174,6 +179,10 @@ class Qemu(IModule): self._console_start_port_range = request["console_start_port_range"] self._console_end_port_range = request["console_end_port_range"] + if "monitor_start_port_range" in request and "monitor_end_port_range" in request: + self._monitor_start_port_range = request["monitor_start_port_range"] + self._monitor_end_port_range = request["monitor_end_port_range"] + if "udp_start_port_range" in request and "udp_end_port_range" in request: self._udp_start_port_range = request["udp_start_port_range"] self._udp_end_port_range = request["udp_end_port_range"] @@ -191,6 +200,7 @@ class Qemu(IModule): Optional request parameters: - console (QEMU VM console port) + - monitor (QEMU VM monitor port) Response parameters: - id (QEMU VM instance identifier) @@ -207,6 +217,7 @@ class Qemu(IModule): name = request["name"] qemu_path = request["qemu_path"] console = request.get("console") + monitor = request.get("monitor") qemu_id = request.get("qemu_id") try: @@ -218,7 +229,11 @@ class Qemu(IModule): console, self._console_host, self._console_start_port_range, - self._console_end_port_range) + self._console_end_port_range, + monitor, + self._monitor_host, + self._monitor_start_port_range, + self._monitor_end_port_range) except QemuError as e: self.send_custom_error(str(e)) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 5d715689..ea12d21c 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -52,10 +52,15 @@ class QemuVM(object): :param console_host: IP address to bind for console connections :param console_start_port_range: TCP console port range start :param console_end_port_range: TCP console port range end + :param monitor: TCP monitor port + :param monitor_host: IP address to bind for monitor connections + :param monitor_start_port_range: TCP monitor port range start + :param monitor_end_port_range: TCP monitor port range end """ _instances = [] _allocated_console_ports = [] + _allocated_monitor_ports = [] def __init__(self, name, @@ -66,7 +71,11 @@ class QemuVM(object): console=None, console_host="0.0.0.0", console_start_port_range=5001, - console_end_port_range=5500): + console_end_port_range=5500, + monitor=None, + monitor_host="0.0.0.0", + monitor_start_port_range=5501, + monitor_end_port_range=6000): if not qemu_id: self._id = 0 @@ -95,6 +104,9 @@ class QemuVM(object): self._console_host = console_host self._console_start_port_range = console_start_port_range self._console_end_port_range = console_end_port_range + self._monitor_host = monitor_host + self._monitor_start_port_range = monitor_start_port_range + self._monitor_end_port_range = monitor_end_port_range self._cloud_path = None # QEMU settings @@ -104,6 +116,7 @@ class QemuVM(object): self._options = "" self._ram = 256 self._console = console + self._monitor = monitor self._ethernet_adapters = [] self._adapter_type = "e1000" self._initrd = "" @@ -135,6 +148,20 @@ class QemuVM(object): raise QemuError("Console port {} is already used by another QEMU VM".format(console)) self._allocated_console_ports.append(self._console) + if not self._monitor: + # allocate a monitor port + try: + self._monitor = find_unused_port(self._monitor_start_port_range, + self._monitor_end_port_range, + self._monitor_host, + ignore_ports=self._allocated_monitor_ports) + except Exception as e: + raise QemuError(e) + + if self._monitor in self._allocated_monitor_ports: + raise QemuError("Monitor port {} is already used by another QEMU VM".format(monitor)) + self._allocated_monitor_ports.append(self._monitor) + self.adapters = 1 # creates 1 adapter by default log.info("QEMU VM {name} [id={id}] has been created".format(name=self._name, id=self._id)) @@ -155,6 +182,7 @@ class QemuVM(object): "adapters": self.adapters, "adapter_type": self._adapter_type, "console": self._console, + "monitor": self._monitor, "initrd": self._initrd, "kernel_image": self._kernel_image, "kernel_command_line": self._kernel_command_line, @@ -183,6 +211,7 @@ class QemuVM(object): cls._instances.clear() cls._allocated_console_ports.clear() + cls._allocated_monitor_ports.clear() @property def name(self): @@ -267,6 +296,35 @@ class QemuVM(object): id=self._id, port=console)) + @property + def monitor(self): + """ + Returns the TCP monitor port. + + :returns: monitor port (integer) + """ + + return self._monitor + + @monitor.setter + def monitor(self, monitor): + """ + Sets the TCP monitor port. + + :param monitor: monitor port (integer) + """ + + if monitor in self._allocated_monitor_ports: + raise QemuError("Monitor port {} is already used by another QEMU VM".format(monitor)) + + self._allocated_monitor_ports.remove(self._monitor) + self._monitor = monitor + self._allocated_monitor_ports.append(self._monitor) + + log.info("QEMU VM {name} [id={id}]: monitor port set to {port}".format(name=self._name, + id=self._id, + port=monitor)) + def delete(self): """ Deletes this QEMU VM. @@ -276,8 +334,11 @@ class QemuVM(object): if self._id in self._instances: self._instances.remove(self._id) - if self.console and self.console in self._allocated_console_ports: - self._allocated_console_ports.remove(self.console) + if self._console and self._console in self._allocated_console_ports: + self._allocated_console_ports.remove(self._console) + + if self._monitor and self._monitor in self._allocated_monitor_ports: + self._allocated_monitor_ports.remove(self._monitor) log.info("QEMU VM {name} [id={id}] has been deleted".format(name=self._name, id=self._id)) @@ -291,8 +352,11 @@ class QemuVM(object): if self._id in self._instances: self._instances.remove(self._id) - if self.console: - self._allocated_console_ports.remove(self.console) + if self._console: + self._allocated_console_ports.remove(self._console) + + if self._monitor: + self._allocated_monitor_ports.remove(self._monitor) try: shutil.rmtree(self._working_dir) @@ -941,6 +1005,13 @@ class QemuVM(object): else: return [] + def _monitor_options(self): + + if self._monitor: + return ["-monitor", "telnet:{}:{},server,nowait".format(self._monitor_host, self._monitor)] + else: + return [] + def _disk_options(self): options = [] @@ -1047,6 +1118,7 @@ class QemuVM(object): command.extend(self._disk_options()) command.extend(self._linux_boot_options()) command.extend(self._serial_options()) + command.extend(self._monitor_options()) additional_options = self._options.strip() if additional_options: command.extend(shlex.split(additional_options)) diff --git a/gns3server/modules/qemu/schemas.py b/gns3server/modules/qemu/schemas.py index 5807d2a7..b4a1b6c6 100644 --- a/gns3server/modules/qemu/schemas.py +++ b/gns3server/modules/qemu/schemas.py @@ -41,6 +41,12 @@ QEMU_CREATE_SCHEMA = { "maximum": 65535, "type": "integer" }, + "monitor": { + "description": "monitor TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, }, "additionalProperties": False, "required": ["name", "qemu_path"], @@ -108,6 +114,12 @@ QEMU_UPDATE_SCHEMA = { "maximum": 65535, "type": "integer" }, + "monitor": { + "description": "monitor TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, "initrd": { "description": "QEMU initrd path", "type": "string", From 84511d7b397301a0ce371751f75b7817a5ffca6d Mon Sep 17 00:00:00 2001 From: Dmitry Shmygov Date: Tue, 23 Dec 2014 02:59:02 +0300 Subject: [PATCH 2/3] QEMU VM suspend/resume and reload --- gns3server/modules/qemu/qemu_vm.py | 66 ++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index ea12d21c..77db25e3 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -26,6 +26,7 @@ import random import subprocess import shlex import ntpath +import telnetlib from gns3server.config import Config from gns3dms.cloud.rackspace_ctrl import get_provider @@ -782,7 +783,13 @@ class QemuVM(object): Starts this QEMU VM. """ - if not self.is_running(): + if self.is_running(): + + # resume the VM if it is paused + self.resume() + return + + else: if not os.path.isfile(self._qemu_path) or not os.path.exists(self._qemu_path): found = False @@ -889,26 +896,77 @@ class QemuVM(object): self._started = False self._stop_cpulimit() + def _control_vm(self, command, expected=None, timeout=30): + """ + Executes a command with QEMU monitor when this VM is running. + + :param command: QEMU monitor command (e.g. info status, stop etc.) + :param timeout: how long to wait for QEMU monitor + + :returns: result of the command (string) + """ + + result = None + if self.is_running() and self._monitor: + log.debug("Execute QEMU monitor command: {}".format(command)) + tn = telnetlib.Telnet(self._monitor_host, self._monitor, timeout=timeout) + try: + tn.write(command.encode('ascii') + b"\n") + except OSError as e: + log.warn("Could not write to QEMU monitor: {}".format(e)) + tn.close() + return result + if expected: + try: + ind, obj, dat = tn.expect(list=expected, timeout=timeout) + if ind >= 0: + result = expected[ind].decode('ascii') + except EOFError as e: + log.warn("Could not read from QEMU monitor: {}".format(e)) + tn.close() + return result + + def _get_vm_status(self): + """ + Returns this VM suspend status (running|paused) + + :returns: status (string) + """ + + result = self._control_vm("info status", [b"running", b"paused"]) + return result + def suspend(self): """ Suspends this QEMU VM. """ - pass + vm_status = self._get_vm_status() + if vm_status == "running": + self._control_vm("stop") + log.debug("QEMU VM has been suspended") + else: + log.info("QEMU VM is not running to be suspended, current status is {}".format(vm_status)) def reload(self): """ Reloads this QEMU VM. """ - pass + self._control_vm("system_reset") + log.debug("QEMU VM has been reset") def resume(self): """ Resumes this QEMU VM. """ - pass + vm_status = self._get_vm_status() + if vm_status == "paused": + self._control_vm("cont") + log.debug("QEMU VM has been resumed") + else: + log.info("QEMU VM is not paused to be resumed, current status is {}".format(vm_status)) def port_add_nio_binding(self, adapter_id, nio): """ From d9f44edcaf48972889491d1483f626bf83170986 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 23 Dec 2014 15:29:27 -0700 Subject: [PATCH 3/3] Fixes incompatibility for IOS startup-config and private-config paths created on Windows and loaded from a project on Linux/Mac OS X. --- gns3server/modules/dynamips/backends/vm.py | 31 +++++++++++----------- gns3server/modules/qemu/__init__.py | 2 +- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/gns3server/modules/dynamips/backends/vm.py b/gns3server/modules/dynamips/backends/vm.py index 187e61d1..710c1986 100644 --- a/gns3server/modules/dynamips/backends/vm.py +++ b/gns3server/modules/dynamips/backends/vm.py @@ -16,7 +16,6 @@ # along with this program. If not, see . import os -import base64 import ntpath import time from gns3server.modules import IModule @@ -412,36 +411,38 @@ class VM(object): response = {} try: - startup_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(router.id)) - private_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(router.id)) + default_startup_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(router.id)) + default_private_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(router.id)) # a new startup-config has been pushed if "startup_config_base64" in request: # update the request with the new local startup-config path - request["startup_config"] = self.create_config_from_base64(request["startup_config_base64"], router, startup_config_path) + request["startup_config"] = self.create_config_from_base64(request["startup_config_base64"], router, default_startup_config_path) # a new private-config has been pushed if "private_config_base64" in request: # update the request with the new local private-config path - request["private_config"] = self.create_config_from_base64(request["private_config_base64"], router, private_config_path) + request["private_config"] = self.create_config_from_base64(request["private_config_base64"], router, default_private_config_path) if "startup_config" in request: - if os.path.isfile(request["startup_config"]) and request["startup_config"] != startup_config_path: + startup_config_path = request["startup_config"].replace("\\", '/') + if os.path.isfile(startup_config_path) and startup_config_path != default_startup_config_path: # this is a local file set in the GUI - request["startup_config"] = self.create_config_from_file(request["startup_config"], router, startup_config_path) - router.set_config(request["startup_config"]) + startup_config_path = self.create_config_from_file(startup_config_path, router, default_startup_config_path) + router.set_config(startup_config_path) else: - router.set_config(request["startup_config"]) - response["startup_config"] = request["startup_config"] + router.set_config(startup_config_path) + response["startup_config"] = startup_config_path if "private_config" in request: - if os.path.isfile(request["private_config"]) and request["private_config"] != private_config_path: + private_config_path = request["private_config"].replace("\\", '/') + if os.path.isfile(private_config_path) and private_config_path != default_private_config_path: # this is a local file set in the GUI - request["private_config"] = self.create_config_from_file(request["private_config"], router, private_config_path) - router.set_config(router.startup_config, request["private_config"]) + private_config_path = self.create_config_from_file(private_config_path, router, default_private_config_path) + router.set_config(router.startup_config, private_config_path) else: - router.set_config(router.startup_config, request["private_config"]) - response["private_config"] = request["private_config"] + router.set_config(router.startup_config, private_config_path) + response["private_config"] = private_config_path except DynamipsError as e: self.send_custom_error(str(e)) diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py index 96e43e3f..beaac4ef 100644 --- a/gns3server/modules/qemu/__init__.py +++ b/gns3server/modules/qemu/__init__.py @@ -74,7 +74,7 @@ class Qemu(IModule): self._udp_end_port_range = qemu_config.get("udp_end_port_range", 45500) self._host = qemu_config.get("host", kwargs["host"]) self._console_host = qemu_config.get("console_host", kwargs["console_host"]) - self._monitor_host = qemu_config.get("monitor_host", "0.0.0.0") + self._monitor_host = qemu_config.get("monitor_host", "127.0.0.1") self._projects_dir = kwargs["projects_dir"] self._tempdir = kwargs["temp_dir"] self._working_dir = self._projects_dir