From 569a68a486728932003404870cc33aa56e0fe1f0 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 22 Aug 2014 17:36:12 -0600 Subject: [PATCH] VirtualBox support refactoring. --- gns3server/modules/dynamips/nodes/c7200.py | 1 - gns3server/modules/virtualbox/__init__.py | 30 +- .../modules/virtualbox/vboxwrapper_client.py | 43 +- .../virtualbox/virtualbox_controller.py | 529 ++++++++++++++++++ .../modules/virtualbox/virtualbox_vm.py | 460 +-------------- gns3server/version.py | 2 +- 6 files changed, 609 insertions(+), 456 deletions(-) create mode 100644 gns3server/modules/virtualbox/virtualbox_controller.py diff --git a/gns3server/modules/dynamips/nodes/c7200.py b/gns3server/modules/dynamips/nodes/c7200.py index df942c71..0dd7127b 100644 --- a/gns3server/modules/dynamips/nodes/c7200.py +++ b/gns3server/modules/dynamips/nodes/c7200.py @@ -24,7 +24,6 @@ from ..dynamips_error import DynamipsError from .router import Router from ..adapters.c7200_io_2fe import C7200_IO_2FE from ..adapters.c7200_io_ge_e import C7200_IO_GE_E -from pkg_resources import parse_version import logging log = logging.getLogger(__name__) diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index f441857a..0ff1bb28 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -24,6 +24,7 @@ import os import socket import shutil +from pkg_resources import parse_version from gns3server.modules import IModule from gns3server.config import Config from .virtualbox_vm import VirtualBoxVM @@ -106,25 +107,30 @@ class VirtualBox(IModule): """ if sys.platform.startswith("win"): + import pywintypes 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: + 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() + win32com.client.gencache.EnsureDispatch("VirtualBox.VirtualBox") - except: + except pywintypes.com_error: raise VirtualBoxError("VirtualBox is not installed.") try: from .vboxapi_py3 import VirtualBoxManager self._vboxmanager = VirtualBoxManager(None, None) + vbox_major_version, vbox_minor_version, _ = self._vboxmanager.vbox.version.split('.') + if parse_version("{}.{}".format(vbox_major_version, vbox_minor_version)) <= parse_version("4.1"): + raise VirtualBoxError("VirtualBox version must be >= 4.2") except Exception as e: + self._vboxmanager = None 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, @@ -139,7 +145,11 @@ class VirtualBox(IModule): self._vboxwrapper = VboxWrapperClient(self._vboxwrapper_path, self._tempdir, "127.0.0.1") #self._vboxwrapper.connect() - self._vboxwrapper.start() + try: + self._vboxwrapper.start() + except VirtualBoxError: + self._vboxwrapper = None + raise def stop(self, signum=None): """ diff --git a/gns3server/modules/virtualbox/vboxwrapper_client.py b/gns3server/modules/virtualbox/vboxwrapper_client.py index 43a1743d..bcaaed11 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 pkg_resources import parse_version from ..attic import wait_socket_is_ready from .virtualbox_error import VirtualBoxError @@ -53,7 +54,7 @@ class VboxWrapperClient(object): self._command = [] self._process = None self._working_dir = working_dir - self._stdout_file = "" + self._stderr_file = "" self._started = False self._host = host self._port = port @@ -139,19 +140,31 @@ class VboxWrapperClient(object): try: log.info("starting VirtualBox wrapper: {}".format(self._command)) with tempfile.NamedTemporaryFile(delete=False) as fd: - self._stdout_file = fd.name - log.info("VirtualBox wrapper process logging to {}".format(fd.name)) - self._process = subprocess.Popen(self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir) + with open(os.devnull, "w") as null: + self._stderr_file = fd.name + log.info("VirtualBox wrapper process logging to {}".format(fd.name)) + self._process = subprocess.Popen(self._command, + stdout=null, + stderr=fd, + cwd=self._working_dir) log.info("VirtualBox wrapper started PID={}".format(self._process.pid)) + + time.sleep(0.1) # give some time for vboxwrapper to start + if self._process.poll() is not None: + raise VirtualBoxError("Could not start VirtualBox wrapper: {}".format(self.read_stderr())) + self.wait_for_vboxwrapper(self._host, self._port) self.connect() self._started = True + + version = self.send('vboxwrapper version')[0] + if parse_version(version) < parse_version("0.9.2"): + self.stop() + raise VirtualBoxError("VirtualBox wrapper version must be >= 0.9.1") + except OSError as e: log.error("could not start VirtualBox wrapper: {}".format(e)) - raise VirtualBoxError("could not start VirtualBox wrapper: {}".format(e)) + raise VirtualBoxError("Could not start VirtualBox wrapper: {}".format(e)) def wait_for_vboxwrapper(self, host, port): """ @@ -198,26 +211,26 @@ class VboxWrapperClient(object): if self._process.poll() is None: log.warn("VirtualBox wrapper process {} is still running".format(self._process.pid)) - if self._stdout_file and os.access(self._stdout_file, os.W_OK): + if self._stderr_file and os.access(self._stderr_file, os.W_OK): try: - os.remove(self._stdout_file) + os.remove(self._stderr_file) except OSError as e: log.warning("could not delete temporary VirtualBox wrapper log file: {}".format(e)) self._started = False - def read_stdout(self): + def read_stderr(self): """ - Reads the standard output of the VirtualBox wrapper process. + Reads the standard error output of the VirtualBox wrapper process. Only use when the process has been stopped or has crashed. """ output = "" - if self._stdout_file and os.access(self._stdout_file, os.R_OK): + if self._stderr_file and os.access(self._stderr_file, os.R_OK): try: - with open(self._stdout_file, errors="replace") as file: + with open(self._stderr_file, errors="replace") as file: output = file.read() except OSError as e: - log.warn("could not read {}: {}".format(self._stdout_file, e)) + log.warn("could not read {}: {}".format(self._stderr_file, e)) return output def is_running(self): diff --git a/gns3server/modules/virtualbox/virtualbox_controller.py b/gns3server/modules/virtualbox/virtualbox_controller.py new file mode 100644 index 00000000..72cd8a96 --- /dev/null +++ b/gns3server/modules/virtualbox/virtualbox_controller.py @@ -0,0 +1,529 @@ +# -*- 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 . + +""" +Controls VirtualBox using the VBox API. +""" + +import sys +import os +import tempfile +import re +import time +import socket +import subprocess + +if sys.platform.startswith('win'): + import msvcrt + import win32file + +from .virtualbox_error import VirtualBoxError +from .pipe_proxy import PipeProxy + +import logging +log = logging.getLogger(__name__) + + +class VirtualBoxController(object): + + def __init__(self, vmname, vboxmanager, host): + + self._host = host + self._machine = None + self._session = None + self._vboxmanager = vboxmanager + self._maximum_adapters = 0 + self._serial_pipe_thread = None + self._serial_pipe = None + + self._vmname = vmname + self._console = 0 + self._adapters = [] + self._headless = False + self._adapter_type = "Automatic" + + 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) + + @property + def vmname(self): + + return self._vmname + + @vmname.setter + def vmname(self, new_vmname): + + self._vmname = new_vmname + try: + self._machine = self._vboxmanager.vbox.findMachine(new_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) + + @property + def console(self): + + return self._console + + @console.setter + def console(self, console): + + self._console = console + + @property + def headless(self): + + return self._headless + + @headless.setter + def headless(self, headless): + + self._headless = headless + + @property + def adapters(self): + + return self._adapters + + @adapters.setter + def adapters(self, adapters): + + self._adapters = adapters + + @property + def adapter_type(self): + + return self._adapter_type + + @adapter_type.setter + def adapter_type(self, adapter_type): + + self._adapter_type = adapter_type + + def start(self): + + 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 + + # starts the Telnet to pipe thread + pipe_name = self._get_pipe_name() + if sys.platform.startswith('win'): + try: + self._serial_pipe = open(pipe_name, "a+b") + except OSError as e: + raise VirtualBoxError("Could not open the pipe {}: {}".format(pipe_name, e)) + self._serial_pipe_thread = PipeProxy(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._host, self._console) + #self._serial_pipe_thread.setDaemon(True) + self._serial_pipe_thread.start() + else: + try: + self._serial_pipe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._serial_pipe.connect(pipe_name) + except OSError as e: + raise VirtualBoxError("Could not connect to the pipe {}: {}".format(pipe_name, e)) + self._serial_pipe_thread = PipeProxy(self._vmname, self._serial_pipe, self._host, self._console) + #self._serial_pipe_thread.setDaemon(True) + self._serial_pipe_thread.start() + + def stop(self): + + if self._serial_pipe_thread: + self._serial_pipe_thread.stop() + self._serial_pipe_thread.join(1) + if self._serial_pipe_thread.isAlive(): + log.warn("Serial pire thread is still alive!") + self._serial_pipe_thread = None + + if self._serial_pipe: + if sys.platform.startswith('win'): + win32file.CloseHandle(msvcrt.get_osfhandle(self._serial_pipe.fileno())) + else: + self._serial_pipe.close() + self._serial_pipe = None + + if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \ + self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline: + try: + if sys.platform.startswith('win') and "VBOX_INSTALL_PATH" in os.environ: + # work around VirtualBox bug #9239 + vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe") + command = '"{}" controlvm "{}" poweroff'.format(vboxmanage_path, self._vmname) + subprocess.call(command, timeout=3) + else: + progress = self._session.console.powerDown() + # wait for VM to actually go down + progress.waitForCompletion(3000) + log.info("VM is stopping with {}% completed".format(self.vmname, progress.percent)) + + self._lock_machine() + for adapter_id in range(0, len(self._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 suspend(self): + + try: + self._session.console.pause() + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + def reload(self): + + try: + self._session.console.reset() + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + def resume(self): + + try: + self._session.console.resume() + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + 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._adapters)): + try: + # VirtualBox starts counting from 0 + adapter = self._session.machine.getNetworkAdapter(adapter_id) + vbox_adapter_type = adapter.adapterType + + if self._adapter_type == "PCnet-PCI II (Am79C970A)": + vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C970A + if self._adapter_type == "PCNet-FAST III (Am79C973)": + vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C973 + if self._adapter_type == "Intel PRO/1000 MT Desktop (82540EM)": + vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82540EM + if self._adapter_type == "Intel PRO/1000 T Server (82543GC)": + vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82543GC + if self._adapter_type == "Intel PRO/1000 MT Server (82545EM)": + vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82545EM + if self._adapter_type == "Paravirtualized Network (virtio-net)": + vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_Virtio + if self._adapter_type == "Automatic": # "Auto-guess, based on first NIC" + vbox_adapter_type = first_adapter_type + + adapter.adapterType = vbox_adapter_type + + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + nio = self._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 _get_pipe_name(self): + + 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)) + return pipe_name + + def _set_console_options(self): + """ + # Example to manually set serial parameters using Python + + from vboxapi import VirtualBoxManager + mgr = VirtualBoxManager(None, None) + mach = mgr.vbox.findMachine("My VM") + session = mgr.mgr.getSessionObject(mgr.vbox) + mach.lockMachine(session, 1) + mach2=session.machine + serial_port = mach2.getSerialPort(0) + serial_port.enabled = True + serial_port.path = "/tmp/test_pipe" + serial_port.hostMode = 1 + serial_port.server = True + session.unlockMachine() + """ + + log.info("setting console options for {}".format(self._vmname)) + + self._lock_machine() + pipe_name = self._get_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 \ No newline at end of file diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 10319044..d05ea631 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -19,24 +19,14 @@ VirtualBox VM instance. """ -import sys import os import shutil -import tempfile -import re -import time -import socket -import subprocess -from .pipe_proxy import PipeProxy from .virtualbox_error import VirtualBoxError +from .virtualbox_controller import VirtualBoxController from .adapters.ethernet_adapter import EthernetAdapter from ..attic import find_unused_port -if sys.platform.startswith('win'): - import msvcrt - import win32file - import logging log = logging.getLogger(__name__) @@ -97,16 +87,6 @@ class VirtualBoxVM(object): self._console_start_port_range = console_start_port_range self._console_end_port_range = console_end_port_range - # Telnet to pipe mini-server - self._serial_pipe_thread = None - self._serial_pipe = None - - # VirtualBox API variables - self._machine = None - self._session = None - self._vboxmanager = vboxmanager - self._maximum_adapters = 0 - # VirtualBox settings self._console = console self._ethernet_adapters = [] @@ -140,19 +120,11 @@ class VirtualBoxVM(object): 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 + self._vboxcontroller = VirtualBoxController(self._vmname, vboxmanager, self._host) + self._vboxcontroller.console = self._console + self.adapters = 2 # creates 2 adapters by default log.info("VirtualBox VM {name} [id={id}] has been created".format(name=self._name, id=self._id)) @@ -274,6 +246,8 @@ class VirtualBoxVM(object): if self._vboxwrapper: self._vboxwrapper.send('vbox setattr "{}" console {}'.format(self._name, self._console)) + else: + self._vboxcontroller.console = console log.info("VirtualBox VM {name} [id={id}]: console port set to {port}".format(name=self._name, id=self._id, @@ -344,10 +318,14 @@ class VirtualBoxVM(object): if headless: if self._vboxwrapper: self._vboxwrapper.send('vbox setattr "{}" headless_mode True'.format(self._name)) + else: + self._vboxcontroller.headless = True 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)) + else: + self._vboxcontroller.headless = False log.info("VirtualBox VM {name} [id={id}] has disabled the headless mode".format(name=self._name, id=self._id)) self._headless = headless @@ -372,13 +350,7 @@ class VirtualBoxVM(object): 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) + self._vboxcontroller.vmname = vmname 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 @@ -407,6 +379,8 @@ class VirtualBoxVM(object): if self._vboxwrapper: self._vboxwrapper.send('vbox setattr "{}" nics {}'.format(self._name, len(self._ethernet_adapters))) + else: + self._vboxcontroller.adapters = self._ethernet_adapters log.info("VirtualBox VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, id=self._id, @@ -434,6 +408,8 @@ class VirtualBoxVM(object): if self._vboxwrapper: self._vboxwrapper.send('vbox setattr "{}" netcard "{}"'.format(self._name, adapter_type)) + else: + self._vboxcontroller.adapter_type = adapter_type log.info("VirtualBox VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name, id=self._id, @@ -445,54 +421,9 @@ class VirtualBoxVM(object): """ 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 - - # starts the Telnet to pipe thread - pipe_name = self._get_pipe_name() - if sys.platform.startswith('win'): - try: - self._serial_pipe = open(pipe_name, "a+b") - except OSError as e: - raise VirtualBoxError("Could not open the pipe {}: {}".format(pipe_name, e)) - self._serial_pipe_thread = PipeProxy(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._host, self._console) - #self._serial_pipe_thread.setDaemon(True) - self._serial_pipe_thread.start() - else: - try: - self._serial_pipe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self._serial_pipe.connect(pipe_name) - except OSError as e: - raise VirtualBoxError("Could not connect to the pipe {}: {}".format(pipe_name, e)) - self._serial_pipe_thread = PipeProxy(self._vmname, self._serial_pipe, self._host, self._console) - #self._serial_pipe_thread.setDaemon(True) - self._serial_pipe_thread.start() + self._vboxcontroller.start() def stop(self): """ @@ -500,48 +431,13 @@ class VirtualBoxVM(object): """ if self._vboxwrapper: - self._vboxwrapper.send('vbox stop "{}"'.format(self._name)) + try: + self._vboxwrapper.send('vbox stop "{}"'.format(self._name)) + except VirtualBoxError: + # probably lost the connection + return else: - - if self._serial_pipe_thread: - self._serial_pipe_thread.stop() - self._serial_pipe_thread.join(1) - if self._serial_pipe_thread.isAlive(): - log.warn("Serial pire thread is still alive!") - self._serial_pipe_thread = None - - if self._serial_pipe: - if sys.platform.startswith('win'): - win32file.CloseHandle(msvcrt.get_osfhandle(self._serial_pipe.fileno())) - else: - self._serial_pipe.close() - self._serial_pipe = None - - if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \ - self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline: - try: - if sys.platform.startswith('win') and "VBOX_INSTALL_PATH" in os.environ: - # work around VirtualBox bug #9239 - vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe") - command = '"{}" controlvm "{}" poweroff'.format(vboxmanage_path, self._vmname) - subprocess.call(command, timeout=3) - else: - progress = self._session.console.powerDown() - # wait for VM to actually go down - progress.waitForCompletion(3000) - log.info("VM is stopping with {}% completed".format(self.vmname, progress.percent)) - - 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 + self._vboxcontroller.stop() def suspend(self): """ @@ -551,10 +447,7 @@ class VirtualBoxVM(object): 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)) + self._vboxcontroller.suspend() def reload(self): """ @@ -564,11 +457,7 @@ class VirtualBoxVM(object): 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)) + self._vboxcontroller.reload() def resume(self): """ @@ -578,10 +467,7 @@ class VirtualBoxVM(object): 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)) + self._vboxcontroller.resume() def port_add_nio_binding(self, adapter_id, nio): """ @@ -599,12 +485,12 @@ class VirtualBoxVM(object): if self._vboxwrapper: self._vboxwrapper.send('vbox create_udp "{}" {} {} {} {}'.format(self._name, - adapter_id, - nio.lport, - nio.rhost, - nio.rport)) + adapter_id, + nio.lport, + nio.rhost, + nio.rport)) else: - self._create_udp(adapter_id, nio.lport, nio.rhost, nio.rport) + self._vboxcontroller.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, @@ -631,7 +517,7 @@ class VirtualBoxVM(object): self._vboxwrapper.send('vbox delete_udp "{}" {}'.format(self._name, adapter_id)) else: - self._delete_udp(adapter_id) + self._vboxcontroller.delete_udp(adapter_id) nio = adapter.get_nio(0) adapter.remove_nio(0) @@ -700,287 +586,3 @@ class VirtualBoxVM(object): 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 _get_pipe_name(self): - - 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)) - return pipe_name - - def _set_console_options(self): - - log.info("setting console options for {}".format(self.vmname)) - - self._lock_machine() - pipe_name = self._get_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/version.py b/gns3server/version.py index bb2875b1..573f2248 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.0beta2.dev1" +__version__ = "1.0beta2.dev2" __version_info__ = (1, 0, 0, -99)