diff --git a/gns3server/handlers/jsonrpc_websocket.py b/gns3server/handlers/jsonrpc_websocket.py index 677ebe2d..5b18496c 100644 --- a/gns3server/handlers/jsonrpc_websocket.py +++ b/gns3server/handlers/jsonrpc_websocket.py @@ -152,6 +152,7 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler): if method not in self.destinations: if request_id: + log.warn("JSON-RPC method not found: {}".format(method)) return self.write_message(JSONRPCMethodNotFound(request_id)()) else: # This is a notification, silently ignore this error... @@ -176,11 +177,7 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler): Invoked when the WebSocket is closed. """ - try: - log.info("Websocket client {} disconnected".format(self.session_id)) - except RuntimeError: - # to ignore logging exception: RuntimeError: reentrant call inside <_io.BufferedWriter name=''> - pass + log.info("Websocket client {} disconnected".format(self.session_id)) self.clients.remove(self) # Reset the modules if there are no clients anymore diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index ee944615..c8d7d7ec 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -19,8 +19,8 @@ VirtualBox server module. """ +import sys import os -import base64 import socket import shutil @@ -28,18 +28,28 @@ from gns3server.modules import IModule from gns3server.config import Config from .virtualbox_vm import VirtualBoxVM from .virtualbox_error import VirtualBoxError +from .vboxwrapper_client import VboxWrapperClient from .nios.nio_udp import NIO_UDP from ..attic import find_unused_port -#from .schemas import VBOX_CREATE_SCHEMA -#from .schemas import VBOX_DELETE_SCHEMA -#from .schemas import VBOX_UPDATE_SCHEMA -#from .schemas import VBOX_START_SCHEMA -#from .schemas import VBOX_STOP_SCHEMA -#from .schemas import VBOX_RELOAD_SCHEMA -#from .schemas import VBOX_ALLOCATE_UDP_PORT_SCHEMA -#from .schemas import VBOX_ADD_NIO_SCHEMA -#from .schemas import VBOX_DELETE_NIO_SCHEMA +if sys.platform.startswith("win"): + # automatically generate the Typelib wrapper + import win32com + win32com.client.gencache.is_readonly = False + win32com.client.gencache.GetGeneratePath() + +from .schemas import VBOX_CREATE_SCHEMA +from .schemas import VBOX_DELETE_SCHEMA +from .schemas import VBOX_UPDATE_SCHEMA +from .schemas import VBOX_START_SCHEMA +from .schemas import VBOX_STOP_SCHEMA +from .schemas import VBOX_SUSPEND_SCHEMA +from .schemas import VBOX_RELOAD_SCHEMA +from .schemas import VBOX_ALLOCATE_UDP_PORT_SCHEMA +from .schemas import VBOX_ADD_NIO_SCHEMA +from .schemas import VBOX_DELETE_NIO_SCHEMA +from .schemas import VBOX_START_CAPTURE_SCHEMA +from .schemas import VBOX_STOP_CAPTURE_SCHEMA import logging log = logging.getLogger(__name__) @@ -56,6 +66,26 @@ class VirtualBox(IModule): def __init__(self, name, *args, **kwargs): + # get the vboxwrapper location + config = Config.instance() + vbox_config = config.get_section_config(name.upper()) + self._vboxwrapper_path = vbox_config.get("vboxwrapper_path") + if not self._vboxwrapper_path or not os.path.isfile(self._vboxwrapper_path): + paths = [os.getcwd()] + os.environ["PATH"].split(":") + # look for iouyap in the current working directory and $PATH + for path in paths: + try: + if "vboxwrapper" in os.listdir(path) and os.access(os.path.join(path, "vboxwrapper"), os.X_OK): + self._vboxwrapper_path = os.path.join(path, "vboxwrapper") + break + except OSError: + continue + + if not self._vboxwrapper_path: + log.warning("vboxwrapper couldn't be found!") + elif not os.access(self._vboxwrapper_path, os.X_OK): + log.warning("vboxwrapper is not executable") + # a new process start when calling IModule IModule.__init__(self, name, *args, **kwargs) self._vbox_instances = {} @@ -71,6 +101,43 @@ class VirtualBox(IModule): self._projects_dir = kwargs["projects_dir"] self._tempdir = kwargs["temp_dir"] self._working_dir = self._projects_dir + self._vboxmanager = None + self._vboxwrapper = None + + def _start_vbox_service(self): + """ + Starts the VirtualBox backend. + vboxapi on Windows or vboxwrapper on other platforms. + """ + + if sys.platform.startswith("win"): + import win32com.client + if win32com.client.gencache.is_readonly is True: + # dynamically generate the cache + # http://www.py2exe.org/index.cgi/IncludingTypelibs + # http://www.py2exe.org/index.cgi/UsingEnsureDispatch + win32com.client.gencache.is_readonly = False + #win32com.client.gencache.Rebuild() + win32com.client.gencache.GetGeneratePath() + try: + from .vboxapi_py3 import VirtualBoxManager + self._vboxmanager = VirtualBoxManager(None, None) + except Exception as e: + raise VirtualBoxError("Could not initialize the VirtualBox Manager: {}".format(e)) + + log.info("VirtualBox Manager has successful started: version is {} r{}".format(self._vboxmanager.vbox.version, + self._vboxmanager.vbox.revision)) + else: + + if not self._vboxwrapper_path: + raise VirtualBoxError("No vboxwrapper path has been configured") + + if not os.path.isfile(self._vboxwrapper_path): + raise VirtualBoxError("vboxwrapper path doesn't exist {}".format(self._vboxwrapper_path)) + + self._vboxwrapper = VboxWrapperClient(self._vboxwrapper_path, self._tempdir, "127.0.0.1") + #self._vboxwrapper.connect() + self._vboxwrapper.start() def stop(self, signum=None): """ @@ -84,6 +151,9 @@ class VirtualBox(IModule): vbox_instance = self._vbox_instances[vbox_id] vbox_instance.delete() + if self._vboxwrapper and self._vboxwrapper.started: + self._vboxwrapper.stop() + IModule.stop(self, signum) # this will stop the I/O loop def get_vbox_instance(self, vbox_id): @@ -120,6 +190,9 @@ class VirtualBox(IModule): self._vbox_instances.clear() self._allocated_udp_ports.clear() + if self._vboxwrapper and self._vboxwrapper.connected(): + self._vboxwrapper.send("vboxwrapper reset") + log.info("VirtualBox module has been reset") @IModule.route("virtualbox.settings") @@ -129,6 +202,7 @@ class VirtualBox(IModule): Optional request parameters: - working_dir (path to a working directory) + - vboxwrapper_path (path to vboxwrapper) - project_name - console_start_port_range - console_end_port_range @@ -165,6 +239,9 @@ class VirtualBox(IModule): vbox_instance = self._vbox_instances[vbox_id] vbox_instance.working_dir = os.path.join(self._working_dir, "vbox", "vm-{}".format(vbox_instance.id)) + if "vboxwrapper_path" in request: + self._vboxwrapper_path = request["vboxwrapper_path"] + if "console_start_port_range" in request and "console_end_port_range" in request: self._console_start_port_range = request["console_start_port_range"] self._console_end_port_range = request["console_end_port_range"] @@ -195,16 +272,23 @@ class VirtualBox(IModule): """ # validate the request - #if not self.validate_request(request, VBOX_CREATE_SCHEMA): - # return + if not self.validate_request(request, VBOX_CREATE_SCHEMA): + return name = request["name"] + vmname = request["vmname"] console = request.get("console") vbox_id = request.get("vbox_id") try: - vbox_instance = VirtualBoxVM(name, + if not self._vboxwrapper and not self._vboxmanager: + self._start_vbox_service() + + vbox_instance = VirtualBoxVM(self._vboxwrapper, + self._vboxmanager, + name, + vmname, self._working_dir, self._host, vbox_id, @@ -239,8 +323,8 @@ class VirtualBox(IModule): """ # validate the request - #if not self.validate_request(request, VBOX_DELETE_SCHEMA): - # return + if not self.validate_request(request, VBOX_DELETE_SCHEMA): + return # get the instance vbox_instance = self.get_vbox_instance(request["id"]) @@ -274,8 +358,8 @@ class VirtualBox(IModule): """ # validate the request - #if not self.validate_request(request, VBOX_UPDATE_SCHEMA): - # return + if not self.validate_request(request, VBOX_UPDATE_SCHEMA): + return # get the instance vbox_instance = self.get_vbox_instance(request["id"]) @@ -310,8 +394,8 @@ class VirtualBox(IModule): """ # validate the request - #if not self.validate_request(request, VBOX_START_SCHEMA): - # return + if not self.validate_request(request, VBOX_START_SCHEMA): + return # get the instance vbox_instance = self.get_vbox_instance(request["id"]) @@ -340,8 +424,8 @@ class VirtualBox(IModule): """ # validate the request - #if not self.validate_request(request, VBOX_STOP_SCHEMA): - # return + if not self.validate_request(request, VBOX_STOP_SCHEMA): + return # get the instance vbox_instance = self.get_vbox_instance(request["id"]) @@ -370,18 +454,76 @@ class VirtualBox(IModule): """ # validate the request - #if not self.validate_request(request, VBOX_RELOAD_SCHEMA): - # return + if not self.validate_request(request, VBOX_RELOAD_SCHEMA): + return # get the instance - vbox_instance = self.get_vpcs_instance(request["id"]) + vbox_instance = self.get_vbox_instance(request["id"]) if not vbox_instance: return try: - if vbox_instance.is_running(): - vbox_instance.stop() - vbox_instance.start() + vbox_instance.reload() + except VirtualBoxError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("virtualbox.stop") + def vbox_stop(self, request): + """ + Stops a VirtualBox VM instance. + + Mandatory request parameters: + - id (VirtualBox VM instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VBOX_STOP_SCHEMA): + return + + # get the instance + vbox_instance = self.get_vbox_instance(request["id"]) + if not vbox_instance: + return + + try: + vbox_instance.stop() + except VirtualBoxError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("virtualbox.suspend") + def vbox_suspend(self, request): + """ + Suspends a VirtualBox VM instance. + + Mandatory request parameters: + - id (VirtualBox VM instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VBOX_SUSPEND_SCHEMA): + return + + # get the instance + vbox_instance = self.get_vbox_instance(request["id"]) + if not vbox_instance: + return + + try: + vbox_instance.suspend() except VirtualBoxError as e: self.send_custom_error(str(e)) return @@ -404,8 +546,8 @@ class VirtualBox(IModule): """ # validate the request - #if not self.validate_request(request, VBOX_ALLOCATE_UDP_PORT_SCHEMA): - # return + if not self.validate_request(request, VBOX_ALLOCATE_UDP_PORT_SCHEMA): + return # get the instance vbox_instance = self.get_vbox_instance(request["id"]) @@ -454,8 +596,8 @@ class VirtualBox(IModule): """ # validate the request - #if not self.validate_request(request, VBOX_ADD_NIO_SCHEMA): - # return + if not self.validate_request(request, VBOX_ADD_NIO_SCHEMA): + return # get the instance vbox_instance = self.get_vbox_instance(request["id"]) @@ -506,8 +648,8 @@ class VirtualBox(IModule): """ # validate the request - #if not self.validate_request(request, VBOX_DELETE_NIO_SCHEMA): - # return + if not self.validate_request(request, VBOX_DELETE_NIO_SCHEMA): + return # get the instance vbox_instance = self.get_vbox_instance(request["id"]) @@ -525,6 +667,110 @@ class VirtualBox(IModule): self.send_response(True) + @IModule.route("virtualbox.start_capture") + def vbox_start_capture(self, request): + """ + Starts a packet capture. + + Mandatory request parameters: + - id (vm identifier) + - port (port number) + - port_id (port identifier) + - capture_file_name + + Response parameters: + - port_id (port identifier) + - capture_file_path (path to the capture file) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VBOX_START_CAPTURE_SCHEMA): + return + + # get the instance + vbox_instance = self.get_vbox_instance(request["id"]) + if not vbox_instance: + return + + port = request["port"] + capture_file_name = request["capture_file_name"] + + try: + capture_file_path = os.path.join(self._working_dir, "captures", capture_file_name) + vbox_instance.start_capture(port, capture_file_path) + except VirtualBoxError as e: + self.send_custom_error(str(e)) + return + + response = {"port_id": request["port_id"], + "capture_file_path": capture_file_path} + self.send_response(response) + + @IModule.route("virtualbox.stop_capture") + def vbox_stop_capture(self, request): + """ + Stops a packet capture. + + Mandatory request parameters: + - id (vm identifier) + - port (port number) + - port_id (port identifier) + + Response parameters: + - port_id (port identifier) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VBOX_STOP_CAPTURE_SCHEMA): + return + + # get the instance + vbox_instance = self.get_vbox_instance(request["id"]) + if not vbox_instance: + return + + port = request["port"] + try: + vbox_instance.stop_capture(port) + except VirtualBoxError as e: + self.send_custom_error(str(e)) + return + + response = {"port_id": request["port_id"]} + self.send_response(response) + + @IModule.route("virtualbox.vm_list") + def vm_list(self, request): + """ + Gets VirtualBox VM list. + + Response parameters: + - Server address/host + - List of VM names + """ + + if not self._vboxwrapper and not self._vboxmanager: + self._start_vbox_service() + + if self._vboxwrapper: + vms = self._vboxwrapper.get_vm_list() + elif self._vboxmanager: + vms = [] + machines = self._vboxmanager.getArray(self._vboxmanager.vbox, "machines") + for machine in range(len(machines)): + vms.append(machines[machine].name) + else: + self.send_custom_error("Vboxmanager hasn't been initialized!") + return + + response = {"server": self._host, + "vms": vms} + self.send_response(response) + @IModule.route("virtualbox.echo") def echo(self, request): """ diff --git a/gns3server/modules/virtualbox/nios/nio.py b/gns3server/modules/virtualbox/nios/nio.py new file mode 100644 index 00000000..c85569bd --- /dev/null +++ b/gns3server/modules/virtualbox/nios/nio.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Base interface for NIOs. +""" + + +class NIO(object): + """ + IOU NIO. + """ + + def __init__(self): + + self._capturing = False + self._pcap_output_file = "" + + def startPacketCapture(self, pcap_output_file): + """ + + :param pcap_output_file: PCAP destination file for the capture + """ + + self._capturing = True + self._pcap_output_file = pcap_output_file + + def stopPacketCapture(self): + + self._capturing = False + self._pcap_output_file = "" + + @property + def capturing(self): + """ + Returns either a capture is configured on this NIO. + + :returns: boolean + """ + + return self._capturing + + @property + def pcap_output_file(self): + """ + Returns the path to the PCAP output file. + + :returns: path to the PCAP output file + """ + + return self._pcap_output_file diff --git a/gns3server/modules/virtualbox/nios/nio_udp.py b/gns3server/modules/virtualbox/nios/nio_udp.py index 3142d70e..41ffbc4f 100644 --- a/gns3server/modules/virtualbox/nios/nio_udp.py +++ b/gns3server/modules/virtualbox/nios/nio_udp.py @@ -19,8 +19,10 @@ Interface for UDP NIOs. """ +from .nio import NIO -class NIO_UDP(object): + +class NIO_UDP(NIO): """ IOU UDP NIO. @@ -33,6 +35,7 @@ class NIO_UDP(object): def __init__(self, lport, rhost, rport): + NIO.__init__(self) self._lport = lport self._rhost = rhost self._rport = rport diff --git a/gns3server/modules/virtualbox/schemas.py b/gns3server/modules/virtualbox/schemas.py new file mode 100644 index 00000000..b86839a0 --- /dev/null +++ b/gns3server/modules/virtualbox/schemas.py @@ -0,0 +1,418 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +VBOX_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new VirtualBox VM instance", + "type": "object", + "properties": { + "name": { + "description": "VirtualBox VM instance name", + "type": "string", + "minLength": 1, + }, + "vmname": { + "description": "VirtualBox VM name (in VirtualBox itself)", + "type": "string", + "minLength": 1, + }, + "vbox_id": { + "description": "VirtualBox VM instance ID", + "type": "integer" + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["name", "vmname"], +} + +VBOX_DELETE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete a VirtualBox VM instance", + "type": "object", + "properties": { + "id": { + "description": "VirtualBox VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +VBOX_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a VirtualBox VM instance", + "type": "object", + "properties": { + "id": { + "description": "VirtualBox VM instance ID", + "type": "integer" + }, + "name": { + "description": "VirtualBox VM instance name", + "type": "string", + "minLength": 1, + }, + "vmname": { + "description": "VirtualBox VM name (in VirtualBox itself)", + "type": "string", + "minLength": 1, + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 36, # maximum given by the ICH9 chipset in VirtualBox + }, + "adapter_type": { + "description": "VirtualBox adapter type", + "type": "string", + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "headless": { + "description": "headless mode", + "type": "boolean" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +VBOX_START_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a VirtualBox VM instance", + "type": "object", + "properties": { + "id": { + "description": "VirtualBox VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +VBOX_STOP_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to stop a VirtualBox VM instance", + "type": "object", + "properties": { + "id": { + "description": "VirtualBox VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +VBOX_SUSPEND_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to suspend a VirtualBox VM instance", + "type": "object", + "properties": { + "id": { + "description": "VirtualBox VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +VBOX_RELOAD_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to reload a VirtualBox VM instance", + "type": "object", + "properties": { + "id": { + "description": "VirtualBox VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +VBOX_ALLOCATE_UDP_PORT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to allocate an UDP port for a VirtualBox VM instance", + "type": "object", + "properties": { + "id": { + "description": "VirtualBox VM instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the VirtualBox VM instance", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id", "port_id"] +} + +VBOX_ADD_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to add a NIO for a VirtualBox VM instance", + "type": "object", + + "definitions": { + "UDP": { + "description": "UDP Network Input/Output", + "properties": { + "type": { + "enum": ["nio_udp"] + }, + "lport": { + "description": "Local port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "rhost": { + "description": "Remote host", + "type": "string", + "minLength": 1 + }, + "rport": { + "description": "Remote port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + } + }, + "required": ["type", "lport", "rhost", "rport"], + "additionalProperties": False + }, + "Ethernet": { + "description": "Generic Ethernet Network Input/Output", + "properties": { + "type": { + "enum": ["nio_generic_ethernet"] + }, + "ethernet_device": { + "description": "Ethernet device name e.g. eth0", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "ethernet_device"], + "additionalProperties": False + }, + "LinuxEthernet": { + "description": "Linux Ethernet Network Input/Output", + "properties": { + "type": { + "enum": ["nio_linux_ethernet"] + }, + "ethernet_device": { + "description": "Ethernet device name e.g. eth0", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "ethernet_device"], + "additionalProperties": False + }, + "TAP": { + "description": "TAP Network Input/Output", + "properties": { + "type": { + "enum": ["nio_tap"] + }, + "tap_device": { + "description": "TAP device name e.g. tap0", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "tap_device"], + "additionalProperties": False + }, + "UNIX": { + "description": "UNIX Network Input/Output", + "properties": { + "type": { + "enum": ["nio_unix"] + }, + "local_file": { + "description": "path to the UNIX socket file (local)", + "type": "string", + "minLength": 1 + }, + "remote_file": { + "description": "path to the UNIX socket file (remote)", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "local_file", "remote_file"], + "additionalProperties": False + }, + "VDE": { + "description": "VDE Network Input/Output", + "properties": { + "type": { + "enum": ["nio_vde"] + }, + "control_file": { + "description": "path to the VDE control file", + "type": "string", + "minLength": 1 + }, + "local_file": { + "description": "path to the VDE control file", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "control_file", "local_file"], + "additionalProperties": False + }, + "NULL": { + "description": "NULL Network Input/Output", + "properties": { + "type": { + "enum": ["nio_null"] + }, + }, + "required": ["type"], + "additionalProperties": False + }, + }, + + "properties": { + "id": { + "description": "VirtualBox VM instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the VirtualBox VM instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 0, + "maximum": 36 # maximum given by the ICH9 chipset in VirtualBox + }, + "nio": { + "type": "object", + "description": "Network Input/Output", + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + {"$ref": "#/definitions/Ethernet"}, + {"$ref": "#/definitions/LinuxEthernet"}, + {"$ref": "#/definitions/TAP"}, + {"$ref": "#/definitions/UNIX"}, + {"$ref": "#/definitions/VDE"}, + {"$ref": "#/definitions/NULL"}, + ] + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "port", "nio"] +} + + +VBOX_DELETE_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete a NIO for a VirtualBox VM instance", + "type": "object", + "properties": { + "id": { + "description": "VirtualBox VM instance ID", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 0, + "maximum": 36 # maximum given by the ICH9 chipset in VirtualBox + }, + }, + "additionalProperties": False, + "required": ["id", "port"] +} + +VBOX_START_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a packet capture on a VirtualBox VM instance port", + "type": "object", + "properties": { + "id": { + "description": "VirtualBox VM instance ID", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 0, + "maximum": 36 # maximum given by the ICH9 chipset in VirtualBox + }, + "port_id": { + "description": "Unique port identifier for the VirtualBox VM instance", + "type": "integer" + }, + "capture_file_name": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["id", "port", "port_id", "capture_file_name"] +} + +VBOX_STOP_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to stop a packet capture on a VirtualBox VM instance port", + "type": "object", + "properties": { + "id": { + "description": "VirtualBox VM instance ID", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 0, + "maximum": 36 # maximum given by the ICH9 chipset in VirtualBox + }, + "port_id": { + "description": "Unique port identifier for the VirtualBox VM instance", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id", "port", "port_id"] +} + diff --git a/gns3server/modules/virtualbox/vboxapi_py3 b/gns3server/modules/virtualbox/vboxapi_py3 new file mode 160000 index 00000000..ed33ed34 --- /dev/null +++ b/gns3server/modules/virtualbox/vboxapi_py3 @@ -0,0 +1 @@ +Subproject commit ed33ed344d5687302979972f518a4cee17517d28 diff --git a/gns3server/modules/virtualbox/vboxwrapper_client.py b/gns3server/modules/virtualbox/vboxwrapper_client.py index d1d5d0ed..43a1743d 100644 --- a/gns3server/modules/virtualbox/vboxwrapper_client.py +++ b/gns3server/modules/virtualbox/vboxwrapper_client.py @@ -26,6 +26,7 @@ import tempfile import socket import re +from ..attic import wait_socket_is_ready from .virtualbox_error import VirtualBoxError import logging @@ -51,6 +52,7 @@ class VboxWrapperClient(object): self._path = path self._command = [] self._process = None + self._working_dir = working_dir self._stdout_file = "" self._started = False self._host = host @@ -144,21 +146,47 @@ class VboxWrapperClient(object): stderr=subprocess.STDOUT, cwd=self._working_dir) log.info("VirtualBox wrapper started PID={}".format(self._process.pid)) + self.wait_for_vboxwrapper(self._host, self._port) + self.connect() self._started = True except OSError as e: log.error("could not start VirtualBox wrapper: {}".format(e)) raise VirtualBoxError("could not start VirtualBox wrapper: {}".format(e)) + def wait_for_vboxwrapper(self, host, port): + """ + Waits for vboxwrapper to be started (accepting a socket connection) + + :param host: host/address to connect to the vboxwrapper + :param port: port to connect to the vboxwrapper + """ + + begin = time.time() + # wait for the socket for a maximum of 10 seconds. + connection_success, last_exception = wait_socket_is_ready(host, port, wait=10.0) + + if not connection_success: + raise VirtualBoxError("Couldn't connect to vboxwrapper on {}:{} :{}".format(host, port, + last_exception)) + else: + log.info("vboxwrapper server ready after {:.4f} seconds".format(time.time() - begin)) + def stop(self): """ Stops the VirtualBox wrapper process. """ + if self.connected(): + try: + self.send("vboxwrapper stop") + except VirtualBoxError: + pass + if self._socket: + self._socket.shutdown(socket.SHUT_RDWR) + self._socket.close() + self._socket = None + if self.is_running(): - self.send("hypervisor stop") - self._socket.shutdown(socket.SHUT_RDWR) - self._socket.close() - self._socket = None log.info("stopping VirtualBox wrapper PID={}".format(self._process.pid)) try: # give some time for the VirtualBox wrapper to properly stop. @@ -210,10 +238,10 @@ class VboxWrapperClient(object): """ command = [self._path] - #if self._host != "0.0.0.0" and self._host != "::": - # command.extend(["-H", "{}:{}".format(self._host, self._port)]) - #else: - # command.extend(["-H", str(self._port)]) + if self._host != "0.0.0.0" and self._host != "::": + command.extend(["-l", self._host, "-p", str(self._port)]) + else: + command.extend(["-p", str(self._port)]) return command def connect(self): @@ -235,6 +263,17 @@ class VboxWrapperClient(object): except OSError as e: raise VirtualBoxError("Could not connect to the VirtualBox wrapper: {}".format(e)) + def connected(self): + """ + Returns either the client is connected to vboxwrapper or not. + + :return: boolean + """ + + if self._socket: + return True + return False + def reset(self): """ Resets the VirtualBox wrapper (used to get an empty configuration). @@ -276,6 +315,15 @@ class VboxWrapperClient(object): assert self._socket return self._socket + def get_vm_list(self): + """ + Returns the list of all VirtualBox VMs. + + :returns: list of VM names + """ + + return self.send('vbox vm_list') + def send(self, command): """ Sends commands to the VirtualBox wrapper. @@ -306,6 +354,7 @@ class VboxWrapperClient(object): log.debug("sending {}".format(command)) self.socket.sendall(command.encode('utf-8')) except OSError as e: + self._socket = None raise VirtualBoxError("Lost communication with {host}:{port} :{error}" .format(host=self._host, port=self._port, error=e)) @@ -317,14 +366,17 @@ class VboxWrapperClient(object): chunk = self.socket.recv(1024) buf += chunk.decode("utf-8") except OSError as e: + self._socket = None raise VirtualBoxError("Communication timed out with {host}:{port} :{error}" .format(host=self._host, port=self._port, error=e)) + # If the buffer doesn't end in '\n' then we can't be done try: if buf[-1] != '\n': continue except IndexError: + self._socket = None raise VirtualBoxError("Could not communicate with {host}:{port}" .format(host=self._host, port=self._port)) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index aedc5d8a..76a6532a 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -19,24 +19,28 @@ VirtualBox VM instance. """ +import sys import os import shutil +import tempfile +import re +import time -from pkg_resources import parse_version from .virtualbox_error import VirtualBoxError from .adapters.ethernet_adapter import EthernetAdapter -from .nios.nio_udp import NIO_UDP from ..attic import find_unused_port import logging log = logging.getLogger(__name__) - class VirtualBoxVM(object): """ VirtualBox VM implementation. + :param vboxwrapper client: VboxWrapperClient instance + :param vboxmanager: VirtualBox manager from the VirtualBox API :param name: name of this VirtualBox VM + :param vmname: name of this VirtualBox VM in VirtualBox itself :param working_dir: path to a working directory :param host: host/address to bind for console and UDP connections :param vbox_id: VirtalBox VM instance ID @@ -49,8 +53,10 @@ class VirtualBoxVM(object): _allocated_console_ports = [] def __init__(self, + vboxwrapper, + vboxmanager, name, - path, + vmname, working_dir, host="127.0.0.1", vbox_id=None, @@ -75,19 +81,28 @@ class VirtualBoxVM(object): self._instances.append(self._id) self._name = name - self._console = console self._working_dir = None self._host = host self._command = [] - self._vboxwrapper_process = None - self._vboxwrapper_stdout_file = "" + self._vboxwrapper = vboxwrapper + self._host = "127.0.0.1" self._started = False self._console_start_port_range = console_start_port_range self._console_end_port_range = console_end_port_range + # VirtualBox API variables + self._machine = None + self._session = None + self._vboxmanager = vboxmanager + self._maximum_adapters = 0 + # VirtualBox settings - self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface + self._console = console + self._ethernet_adapters = [] + self._headless = False + self._vmname = vmname + self._adapter_type = "Automatic" working_dir_path = os.path.join(working_dir, "vbox", "vm-{}".format(self._id)) @@ -111,6 +126,23 @@ class VirtualBoxVM(object): raise VirtualBoxError("Console port {} is already used by another VirtualBox VM".format(console)) self._allocated_console_ports.append(self._console) + if self._vboxwrapper: + self._vboxwrapper.send('vbox create vbox "{}"'.format(self._name)) + self._vboxwrapper.send('vbox setattr "{}" image "{}"'.format(self._name, vmname)) + self._vboxwrapper.send('vbox setattr "{}" console {}'.format(self._name, self._console)) + self._vboxwrapper.send('vbox setattr "{}" console_support True'.format(self._name)) + self._vboxwrapper.send('vbox setattr "{}" console_telnet_server True'.format(self._name)) + else: + try: + self._machine = self._vboxmanager.vbox.findMachine(self._vmname) + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + # The maximum support network cards depends on the Chipset (PIIX3 or ICH9) + self._maximum_adapters = self._vboxmanager.vbox.systemProperties.getMaxNetworkAdapters(self._machine.chipsetType) + + self.adapters = 2 + log.info("VirtualBox VM {name} [id={id}] has been created".format(name=self._name, id=self._id)) @@ -122,7 +154,11 @@ class VirtualBoxVM(object): """ vbox_defaults = {"name": self._name, - "console": self._console} + "vmname": self._vmname, + "adapters": len(self._ethernet_adapters), + "adapter_type": "Automatic", + "console": self._console, + "headless": self._headless} return vbox_defaults @@ -166,6 +202,9 @@ class VirtualBoxVM(object): log.info("VirtualBox VM {name} [id={id}]: renamed to {new_name}".format(name=self._name, id=self._id, new_name=new_name)) + + if self._vboxwrapper: + self._vboxwrapper.send('vbox rename "{}" "{}"'.format(self._name, new_name)) self._name = new_name @property @@ -222,6 +261,10 @@ class VirtualBoxVM(object): self._allocated_console_ports.remove(self._console) self._console = console self._allocated_console_ports.append(self._console) + + if self._vboxwrapper: + self._vboxwrapper.send('vbox setattr "{}" console {}'.format(self._name, self._console)) + log.info("VirtualBox VM {name} [id={id}]: console port set to {port}".format(name=self._name, id=self._id, port=console)) @@ -238,6 +281,9 @@ class VirtualBoxVM(object): if self.console and self.console in self._allocated_console_ports: self._allocated_console_ports.remove(self.console) + if self._vboxwrapper: + self._vboxwrapper.send('vbox delete "{}"'.format(self._name)) + log.info("VirtualBox VM {name} [id={id}] has been deleted".format(name=self._name, id=self._id)) @@ -253,6 +299,9 @@ class VirtualBoxVM(object): if self.console: self._allocated_console_ports.remove(self.console) + if self._vboxwrapper: + self._vboxwrapper.send('vbox delete "{}"'.format(self._name)) + try: shutil.rmtree(self._working_dir) except OSError as e: @@ -264,55 +313,618 @@ class VirtualBoxVM(object): log.info("VirtualBox VM {name} [id={id}] has been deleted (including associated files)".format(name=self._name, id=self._id)) + @property + def headless(self): + """ + Returns either the VM will start in headless mode + + :returns: boolean + """ + + return self._headless + + @headless.setter + def headless(self, headless): + """ + Sets either the VM will start in headless mode + + :param headless: boolean + """ + + if headless: + if self._vboxwrapper: + self._vboxwrapper.send('vbox setattr "{}" headless_mode True'.format(self._name)) + log.info("VirtualBox VM {name} [id={id}] has enabled the headless mode".format(name=self._name, id=self._id)) + else: + if self._vboxwrapper: + self._vboxwrapper.send('vbox setattr "{}" headless_mode False'.format(self._name)) + log.info("VirtualBox VM {name} [id={id}] has disabled the headless mode".format(name=self._name, id=self._id)) + self._headless = headless + + @property + def vmname(self): + """ + Returns the VM name associated with this VirtualBox VM. + + :returns: VirtualBox VM name + """ + + return self._vmname + + @vmname.setter + def vmname(self, vmname): + """ + Sets the VM name associated with this VirtualBox VM. + + :param vmname: VirtualBox VM name + """ + + if self._vboxwrapper: + self._vboxwrapper.send('vbox setattr "{}" image "{}"'.format(self._name, vmname)) + else: + try: + self._machine = self._vboxmanager.vbox.findMachine(vmname) + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + # The maximum support network cards depends on the Chipset (PIIX3 or ICH9) + self._maximum_adapters = self._vboxmanager.vbox.systemProperties.getMaxNetworkAdapters(self._machine.chipsetType) + + log.info("VirtualBox VM {name} [id={id}] has set the VM name to {vmname}".format(name=self._name, id=self._id, vmname=vmname)) + self._vmname = vmname + + @property + def adapters(self): + """ + Returns the number of Ethernet adapters for this VirtualBox VM instance. + + :returns: number of adapters + """ + + return len(self._ethernet_adapters) + + @adapters.setter + def adapters(self, adapters): + """ + Sets the number of Ethernet adapters for this VirtualBox VM instance. + + :param adapters: number of adapters + """ + + self._ethernet_adapters.clear() + for _ in range(0, adapters): + self._ethernet_adapters.append(EthernetAdapter()) + + if self._vboxwrapper: + self._vboxwrapper.send('vbox setattr "{}" nics {}'.format(self._name, len(self._ethernet_adapters))) + + log.info("VirtualBox VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, + id=self._id, + adapters=len(self._ethernet_adapters))) + + @property + def adapter_type(self): + """ + Returns the adapter type for this VirtualBox VM instance. + + :returns: adapter type (string) + """ + + return self._adapter_type + + @adapter_type.setter + def adapter_type(self, adapter_type): + """ + Sets the adapter type for this VirtualBox VM instance. + + :param adapter_type: adapter type (string) + """ + + self._adapter_type = adapter_type + + if self._vboxwrapper: + self._vboxwrapper.send('vbox setattr "{}" netcard "{}"'.format(self._name, adapter_type)) + + log.info("VirtualBox VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name, + id=self._id, + adapter_type=adapter_type)) + def start(self): """ Starts this VirtualBox VM. """ - pass + if self._vboxwrapper: + status = int(self._vboxwrapper.send('vbox status "{}"'.format(self._name))[0]) + if status == 6: # paused + self.resume() + return + self._vboxwrapper.send('vbox start "{}"'.format(self._name)) + else: + + if self._machine.state == self._vboxmanager.constants.MachineState_Paused: + self.resume() + return + + self._get_session() + self._set_network_options() + self._set_console_options() + + progress = self._launch_vm_process() + log.info("VM is starting with {}% completed".format(progress.percent)) + if progress.percent != 100: + # This will happen if you attempt to start VirtualBox with unloaded "vboxdrv" module. + # or have too little RAM or damaged vHDD, or connected to non-existent network. + # We must unlock machine, otherwise it locks the VirtualBox Manager GUI. (on Linux hosts) + self._unlock_machine() + raise VirtualBoxError("Unable to start the VM (failed at {}%)".format(progress.percent)) + + try: + self._machine.setGuestPropertyValue("NameInGNS3", self._name) + except Exception: + pass def stop(self): """ Stops this VirtualBox VM. """ - pass + if self._vboxwrapper: + self._vboxwrapper.send('vbox stop "{}"'.format(self._name)) + else: + try: + progress = self._session.console.powerDown() + # wait for VM to actually go down + progress.waitForCompletion(-1) + log.info("VM is stopping with {}% completed".format(self.vmname, progress.percent)) -# def port_add_nio_binding(self, port_id, nio): -# """ -# Adds a port NIO binding. -# -# :param port_id: port ID -# :param nio: NIO instance to add to the slot/port -# """ -# -# if not self._ethernet_adapter.port_exists(port_id): -# raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, -# port_id=port_id)) -# -# self._ethernet_adapter.add_nio(port_id, nio) -# log.info("VPCS {name} [id={id}]: {nio} added to port {port_id}".format(name=self._name, -# id=self._id, -# nio=nio, -# port_id=port_id)) + self._lock_machine() + for adapter_id in range(0, len(self._ethernet_adapters)): + self._disable_adapter(adapter_id, disable=True) + self._session.machine.saveSettings() + self._unlock_machine() + except Exception as e: + # Do not crash "vboxwrapper", if stopping VM fails. + # But return True anyway, so VM state in GNS3 can become "stopped" + # This can happen, if user manually kills VBox VM. + log.warn("could not stop VM for {}: {}".format(self._vmname, e)) + return -# def port_remove_nio_binding(self, port_id): -# """ -# Removes a port NIO binding. -# -# :param port_id: port ID -# -# :returns: NIO instance -# """ -# -# if not self._ethernet_adapter.port_exists(port_id): -# raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, -# port_id=port_id)) -# -# nio = self._ethernet_adapter.get_nio(port_id) -# self._ethernet_adapter.remove_nio(port_id) -# log.info("VPCS {name} [id={id}]: {nio} removed from port {port_id}".format(name=self._name, -# id=self._id, -# nio=nio, -# port_id=port_id)) -# return nio + def suspend(self): + """ + Suspends this VirtualBox VM. + """ + + if self._vboxwrapper: + self._vboxwrapper.send('vbox suspend "{}"'.format(self._name)) + else: + try: + self._session.console.pause() + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + def reload(self): + """ + Reloads this VirtualBox VM. + """ + + if self._vboxwrapper: + self._vboxwrapper.send('vbox reset "{}"'.format(self._name)) + else: + try: + progress = self._session.console.reset() + progress.waitForCompletion(-1) + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + def resume(self): + """ + Resumes this VirtualBox VM. + """ + + if self._vboxwrapper: + self._vboxwrapper.send('vbox resume "{}"'.format(self._name)) + else: + try: + self._session.console.resume() + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + def port_add_nio_binding(self, adapter_id, nio): + """ + Adds a port NIO binding. + + :param adapter_id: adapter ID + :param nio: NIO instance to add to the slot/port + """ + + try: + adapter = self._ethernet_adapters[adapter_id] + except IndexError: + raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM {name}".format(name=self._name, + adapter_id=adapter_id)) + + if self._vboxwrapper: + self._vboxwrapper.send('vbox create_udp "{}" {} {} {} {}'.format(self._name, + adapter_id, + nio.lport, + nio.rhost, + nio.rport)) + else: + self._create_udp(adapter_id, nio.lport, nio.rhost, nio.rport) + + adapter.add_nio(0, nio) + log.info("VirtualBox VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name, + id=self._id, + nio=nio, + adapter_id=adapter_id)) + + def port_remove_nio_binding(self, adapter_id): + """ + Removes a port NIO binding. + + :param adapter_id: adapter ID + + :returns: NIO instance + """ + + try: + adapter = self._ethernet_adapters[adapter_id] + except IndexError: + raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM {name}".format(name=self._name, + adapter_id=adapter_id)) + + if self._vboxwrapper: + self._vboxwrapper.send('vbox delete_udp "{}" {}'.format(self._name, + adapter_id)) + else: + self._delete_udp(adapter_id) + + nio = adapter.get_nio(0) + adapter.remove_nio(0) + log.info("VirtualBox VM {name} [id={id}]: {nio} removed from adapter {adapter_id}".format(name=self._name, + id=self._id, + nio=nio, + adapter_id=adapter_id)) + return nio + + def start_capture(self, adapter_id, output_file): + """ + Starts a packet capture. + + :param adapter_id: adapter ID + :param output_file: PCAP destination file for the capture + """ + + try: + adapter = self._ethernet_adapters[adapter_id] + except IndexError: + raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM {name}".format(name=self._name, + adapter_id=adapter_id)) + + nio = adapter.get_nio(0) + if nio.capturing: + raise VirtualBoxError("Packet capture is already activated on adapter {adapter_id}".format(adapter_id=adapter_id)) + + try: + os.makedirs(os.path.dirname(output_file)) + except FileExistsError: + pass + except OSError as e: + raise VirtualBoxError("Could not create captures directory {}".format(e)) + + nio.startPacketCapture(output_file) + + if self._vboxwrapper: + self._vboxwrapper.send('vbox create_capture "{}" {} "{}"'.format(self._name, + adapter_id, + output_file)) + + log.info("VirtualBox VM {name} [id={id}]: starting packet capture on adapter {adapter_id}".format(name=self._name, + id=self._id, + adapter_id=adapter_id)) + + def stop_capture(self, adapter_id): + """ + Stops a packet capture. + + :param adapter_id: adapter ID + """ + + try: + adapter = self._ethernet_adapters[adapter_id] + except IndexError: + raise VirtualBoxError("Adapter {adapter_id} doesn't exist on VirtualBox VM {name}".format(name=self._name, + adapter_id=adapter_id)) + + nio = adapter.get_nio(0) + nio.stopPacketCapture() + + if self._vboxwrapper: + self._vboxwrapper.send('vbox delete_capture "{}" {}'.format(self._name, + adapter_id)) + + log.info("VirtualBox VM {name} [id={id}]: stopping packet capture on adapter {adapter_id}".format(name=self._name, + id=self._id, + adapter_id=adapter_id)) + + def _get_session(self): + + log.debug("getting session for {}".format(self._vmname)) + try: + self._session = self._vboxmanager.mgr.getSessionObject(self._vboxmanager.vbox) + except Exception as e: + # fails on heavily loaded hosts... + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + def _set_network_options(self): + + log.debug("setting network options for {}".format(self._vmname)) + + self._lock_machine() + + first_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82540EM + try: + first_adapter = self._session.machine.getNetworkAdapter(0) + first_adapter_type = first_adapter.adapterType + except Exception as e: + pass + #raise VirtualBoxError("VirtualBox error: {}".format(e)) + + for adapter_id in range(0, len(self._ethernet_adapters)): + try: + # VirtualBox starts counting from 0 + adapter = self._session.machine.getNetworkAdapter(adapter_id) + adapter_type = adapter.adapterType + + if self._adapter_type == "PCnet-PCI II (Am79C970A)": + adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C970A + if self._adapter_type == "PCNet-FAST III (Am79C973)": + adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C973 + if self._adapter_type == "Intel PRO/1000 MT Desktop (82540EM)": + adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82540EM + if self._adapter_type == "Intel PRO/1000 T Server (82543GC)": + adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82543GC + if self._adapter_type == "Intel PRO/1000 MT Server (82545EM)": + adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82545EM + if self._adapter_type == "Paravirtualized Network (virtio-net)": + adapter_type = self._vboxmanager.constants.NetworkAdapterType_Virtio + if self._adapter_type == "Automatic": # "Auto-guess, based on first NIC" + adapter_type = first_adapter_type + + adapter.adapterType = adapter_type + + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + nio = self._ethernet_adapters[adapter_id].get_nio(0) + if nio: + log.debug("setting UDP params on adapter {}".format(adapter_id)) + try: + adapter.enabled = True + adapter.cableConnected = True + adapter.traceEnabled = False + # Temporary hack around VBox-UDP patch limitation: inability to use DNS + if nio.rhost == 'localhost': + rhost = '127.0.0.1' + else: + rhost = nio.rhost + adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Generic + adapter.genericDriver = "UDPTunnel" + adapter.setProperty("sport", str(nio.lport)) + adapter.setProperty("dest", rhost) + adapter.setProperty("dport", str(nio.rport)) + except Exception as e: + # usually due to COM Error: "The object is not ready" + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + if nio.capturing: + self._enable_capture(adapter, nio.pcap_output_file) + + else: + # shutting down unused adapters... + try: + adapter.enabled = True + adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null + adapter.cableConnected = False + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + #for adapter_id in range(len(self._ethernet_adapters), self._maximum_adapters): + # log.debug("disabling remaining adapter {}".format(adapter_id)) + # self._disable_adapter(adapter_id) + + try: + self._session.machine.saveSettings() + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + self._unlock_machine() + + def _disable_adapter(self, adapter_id, disable=True): + + log.debug("disabling network adapter for {}".format(self._vmname)) + # this command is retried several times, because it fails more often... + retries = 6 + last_exception = None + for retry in range(retries): + if retry == (retries - 1): + raise VirtualBoxError("Could not disable network adapter after 4 retries: {}".format(last_exception)) + try: + adapter = self._session.machine.getNetworkAdapter(adapter_id) + adapter.traceEnabled = False + adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null + if disable: + adapter.enabled = False + break + except Exception as e: + # usually due to COM Error: "The object is not ready" + log.warn("cannot disable network adapter for {}, retrying {}: {}".format(self._vmname, retry + 1, e)) + last_exception = e + time.sleep(1) + continue + + def _enable_capture(self, adapter, output_file): + + log.debug("enabling capture for {}".format(self._vmname)) + # this command is retried several times, because it fails more often... + retries = 4 + last_exception = None + for retry in range(retries): + if retry == (retries - 1): + raise VirtualBoxError("Could not enable packet capture after 4 retries: {}".format(last_exception)) + try: + adapter.traceEnabled = True + adapter.traceFile = output_file + break + except Exception as e: + log.warn("cannot enable packet capture for {}, retrying {}: {}".format(self._vmname, retry + 1, e)) + last_exception = e + time.sleep(0.75) + continue + + def _create_udp(self, adapter_id, sport, daddr, dport): + + if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \ + self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline: + # the machine is being executed + retries = 4 + last_exception = None + for retry in range(retries): + if retry == (retries - 1): + raise VirtualBoxError("Could not create an UDP tunnel after 4 retries :{}".format(last_exception)) + try: + adapter = self._session.machine.getNetworkAdapter(adapter_id) + adapter.cableConnected = True + adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null + self._session.machine.saveSettings() + adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Generic + adapter.genericDriver = "UDPTunnel" + adapter.setProperty("sport", str(sport)) + adapter.setProperty("dest", daddr) + adapter.setProperty("dport", str(dport)) + self._session.machine.saveSettings() + break + except Exception as e: + # usually due to COM Error: "The object is not ready" + log.warn("cannot create UDP tunnel for {}: {}".format(self._vmname, e)) + last_exception = e + time.sleep(0.75) + continue + + def _delete_udp(self, adapter_id): + + if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \ + self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline: + # the machine is being executed + retries = 4 + last_exception = None + for retry in range(retries): + if retry == (retries - 1): + raise VirtualBoxError("Could not delete an UDP tunnel after 4 retries :{}".format(last_exception)) + try: + adapter = self._session.machine.getNetworkAdapter(adapter_id) + adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null + adapter.cableConnected = False + self._session.machine.saveSettings() + break + except Exception as e: + # usually due to COM Error: "The object is not ready" + log.debug("cannot delete UDP tunnel for {}: {}".format(self._vmname, e)) + last_exception = e + time.sleep(0.75) + continue + + def _set_console_options(self): + + log.info("setting console options for {}".format(self.vmname)) + + self._lock_machine() + + # pick a pipe name + p = re.compile('\s+', re.UNICODE) + pipe_name = p.sub("_", self._vmname) + if sys.platform.startswith('win'): + pipe_name = r"\\.\pipe\VBOX\{}".format(pipe_name) + else: + pipe_name = os.path.join(tempfile.gettempdir(), "pipe_{}".format(pipe_name)) + + try: + serial_port = self._session.machine.getSerialPort(0) + serial_port.enabled = True + serial_port.path = pipe_name + serial_port.hostMode = 1 + serial_port.server = True + self._session.machine.saveSettings() + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + self._unlock_machine() + + def _launch_vm_process(self): + + log.debug("launching VM {}".format(self._vmname)) + # this command is retried several times, because it fails more often... + retries = 4 + last_exception = None + for retry in range(retries): + if retry == (retries - 1): + raise VirtualBoxError("Could not launch the VM after 4 retries: {}".format(last_exception)) + try: + if self._headless: + mode = "headless" + else: + mode = "gui" + log.info("starting {} in {} mode".format(self._vmname, mode)) + progress = self._machine.launchVMProcess(self._session, mode, "") + break + except Exception as e: + # This will usually happen if you try to start the same VM twice, + # but may happen on loaded hosts too... + log.warn("cannot launch VM {}, retrying {}: {}".format(self._vmname, retry + 1, e)) + last_exception = e + time.sleep(0.6) + continue + + try: + progress.waitForCompletion(-1) + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + return progress + + def _lock_machine(self): + + log.debug("locking machine for {}".format(self._vmname)) + # this command is retried several times, because it fails more often... + retries = 4 + last_exception = None + for retry in range(retries): + if retry == (retries - 1): + raise VirtualBoxError("Could not lock the machine after 4 retries: {}".format(last_exception)) + try: + self._machine.lockMachine(self._session, 1) + break + except Exception as e: + log.warn("cannot lock the machine for {}, retrying {}: {}".format(self._vmname, retry + 1, e)) + last_exception = e + time.sleep(1) + continue + + def _unlock_machine(self): + + log.debug("unlocking machine for {}".format(self._vmname)) + # this command is retried several times, because it fails more often... + retries = 4 + last_exception = None + for retry in range(retries): + if retry == (retries - 1): + raise VirtualBoxError("Could not unlock the machine after 4 retries: {}".format(last_exception)) + try: + self._session.unlockMachine() + break + except Exception as e: + log.warn("cannot unlock the machine for {}, retrying {}: {}".format(self._vmname, retry + 1, e)) + time.sleep(1) + last_exception = e + continue diff --git a/gns3server/server.py b/gns3server/server.py index ded769b6..d4869e53 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -173,8 +173,12 @@ class Server(object): tornado.autoreload.add_reload_hook(self._reload_callback) def signal_handler(signum=None, frame=None): - log.warning("Server got signal {}, exiting...".format(signum)) - self._cleanup(signum) + try: + log.warning("Server got signal {}, exiting...".format(signum)) + self._cleanup(signum) + except RuntimeError: + # to ignore logging exception: RuntimeError: reentrant call inside <_io.BufferedWriter name=''> + pass signals = [signal.SIGTERM, signal.SIGINT] if not sys.platform.startswith("win"):