diff --git a/.travis.yml b/.travis.yml
index 2440f1dc..883004a4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,19 @@
language: python
-python:
- - "3.3"
- - "3.4"
+env:
+ - TOX_ENV=py33
+ - TOX_ENV=py34
+
+before_install:
+ - sudo add-apt-repository ppa:gns3/ppa -y
+ - sudo apt-get update -q
install:
- - "pip install -r requirements.txt --use-mirrors"
- - "pip install tox"
+ - pip install tox
+ - sudo apt-get install vpcs dynamips
-script: "python setup.py test"
+script:
+ - tox -e $TOX_ENV
branches:
only:
diff --git a/README.rst b/README.rst
index 7dd682c4..8713028e 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,7 @@
GNS3-server
===========
-New GNS3 server repository (alpha stage).
+New GNS3 server repository (beta stage).
The GNS3 server manages emulators such as Dynamips, VirtualBox or Qemu/KVM.
Clients like the GNS3 GUI controls the server using a JSON-RPC API over Websockets.
@@ -34,4 +34,19 @@ Please use our all-in-one installer.
Mac OS X
--------
-Please use our DMG package.
+Please use our DMG package for a simple installation.
+
+
+If you want to test the current git version or contribute to the project.
+
+You can follow this instructions with virtualenwrapper: http://virtualenvwrapper.readthedocs.org/
+and homebrew: http://brew.sh/.
+
+.. code:: bash
+
+ brew install python3
+ mkvirtualenv gns3-server --python=/usr/local/bin/python3.4
+ python3 setup.py install
+ gns3server
+
+
diff --git a/gns3server/handlers/jsonrpc_websocket.py b/gns3server/handlers/jsonrpc_websocket.py
index a226be78..e14ae8c3 100644
--- a/gns3server/handlers/jsonrpc_websocket.py
+++ b/gns3server/handlers/jsonrpc_websocket.py
@@ -53,6 +53,9 @@ class JSONRPCWebSocket(GNS3WebSocketBaseHandler):
self._session_id = str(uuid.uuid4())
self.zmq_router = zmq_router
+ def check_origin(self, origin):
+ return True
+
@property
def session_id(self):
"""
diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py
index f38af25b..b451a69b 100644
--- a/gns3server/modules/__init__.py
+++ b/gns3server/modules/__init__.py
@@ -17,12 +17,13 @@
import sys
from .base import IModule
+from .deadman import DeadMan
from .dynamips import Dynamips
+from .qemu import Qemu
from .vpcs import VPCS
from .virtualbox import VirtualBox
-from .deadman import DeadMan
-MODULES = [Dynamips, VPCS, VirtualBox, DeadMan]
+MODULES = [DeadMan, Dynamips, VPCS, VirtualBox, Qemu]
if sys.platform.startswith("linux"):
# IOU runs only on Linux
diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py
index d26619a1..a35a71c5 100644
--- a/gns3server/modules/dynamips/__init__.py
+++ b/gns3server/modules/dynamips/__init__.py
@@ -111,7 +111,7 @@ class Dynamips(IModule):
dynamips_config = config.get_section_config(name.upper())
self._dynamips = dynamips_config.get("dynamips_path")
if not self._dynamips or not os.path.isfile(self._dynamips):
- paths = [os.getcwd()] + os.environ["PATH"].split(":")
+ paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for Dynamips in the current working directory and $PATH
for path in paths:
try:
@@ -137,7 +137,7 @@ class Dynamips(IModule):
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
- self._host = kwargs["host"]
+ self._host = dynamips_config.get("host", kwargs["host"])
if not sys.platform.startswith("win32"):
#FIXME: pickle issues Windows
diff --git a/gns3server/modules/dynamips/backends/vm.py b/gns3server/modules/dynamips/backends/vm.py
index 1da7e50c..6a471620 100644
--- a/gns3server/modules/dynamips/backends/vm.py
+++ b/gns3server/modules/dynamips/backends/vm.py
@@ -158,7 +158,8 @@ class VM(object):
router = PLATFORMS[platform](hypervisor, name, router_id)
router.ram = ram
router.image = image
- router.sparsemem = self._hypervisor_manager.sparse_memory_support
+ if platform not in ("c1700", "c2600"):
+ router.sparsemem = self._hypervisor_manager.sparse_memory_support
router.mmap = self._hypervisor_manager.mmap_support
if "console" in request:
router.console = request["console"]
diff --git a/gns3server/modules/dynamips/nodes/c1700.py b/gns3server/modules/dynamips/nodes/c1700.py
index 0d59f616..241601a2 100644
--- a/gns3server/modules/dynamips/nodes/c1700.py
+++ b/gns3server/modules/dynamips/nodes/c1700.py
@@ -51,6 +51,7 @@ class C1700(Router):
self._chassis = chassis
self._iomem = 15 # percentage
self._clock_divisor = 8
+ self._sparsemem = False
if chassis != "1720":
self.chassis = chassis
@@ -72,7 +73,8 @@ class C1700(Router):
"disk1": self._disk1,
"chassis": self._chassis,
"iomem": self._iomem,
- "clock_divisor": self._clock_divisor}
+ "clock_divisor": self._clock_divisor,
+ "sparsemem": self._sparsemem}
# update the router defaults with the platform specific defaults
router_defaults.update(platform_defaults)
diff --git a/gns3server/modules/dynamips/nodes/c2600.py b/gns3server/modules/dynamips/nodes/c2600.py
index 155fbf2f..56866235 100644
--- a/gns3server/modules/dynamips/nodes/c2600.py
+++ b/gns3server/modules/dynamips/nodes/c2600.py
@@ -66,6 +66,7 @@ class C2600(Router):
self._chassis = chassis
self._iomem = 15 # percentage
self._clock_divisor = 8
+ self._sparsemem = False
if chassis != "2610":
self.chassis = chassis
@@ -87,7 +88,8 @@ class C2600(Router):
"disk1": self._disk1,
"iomem": self._iomem,
"chassis": self._chassis,
- "clock_divisor": self._clock_divisor}
+ "clock_divisor": self._clock_divisor,
+ "sparsemem": self._sparsemem}
# update the router defaults with the platform specific defaults
router_defaults.update(platform_defaults)
diff --git a/gns3server/modules/dynamips/nodes/c7200.py b/gns3server/modules/dynamips/nodes/c7200.py
index e27a22d0..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__)
@@ -55,10 +54,6 @@ class C7200(Router):
if npe != "npe-400":
self.npe = npe
- if parse_version(hypervisor.version) <= parse_version('0.2.13'):
- # work around a bug when rebooting a router with NPE-400 in Dynamips <= 0.2.13
- self.npe = "npe-200"
-
# 4 sensors with a default temperature of 22C:
# sensor 1 = I/0 controller inlet
# sensor 2 = I/0 controller outlet
diff --git a/gns3server/modules/iou/__init__.py b/gns3server/modules/iou/__init__.py
index a3f0c47d..648223bf 100644
--- a/gns3server/modules/iou/__init__.py
+++ b/gns3server/modules/iou/__init__.py
@@ -68,7 +68,7 @@ class IOU(IModule):
iou_config = config.get_section_config(name.upper())
self._iouyap = iou_config.get("iouyap_path")
if not self._iouyap or not os.path.isfile(self._iouyap):
- paths = [os.getcwd()] + os.environ["PATH"].split(":")
+ paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for iouyap in the current working directory and $PATH
for path in paths:
try:
@@ -87,11 +87,11 @@ class IOU(IModule):
IModule.__init__(self, name, *args, **kwargs)
self._iou_instances = {}
self._console_start_port_range = iou_config.get("console_start_port_range", 4001)
- self._console_end_port_range = iou_config.get("console_end_port_range", 4512)
+ self._console_end_port_range = iou_config.get("console_end_port_range", 4500)
self._allocated_udp_ports = []
self._udp_start_port_range = iou_config.get("udp_start_port_range", 30001)
- self._udp_end_port_range = iou_config.get("udp_end_port_range", 40001)
- self._host = kwargs["host"]
+ self._udp_end_port_range = iou_config.get("udp_end_port_range", 35000)
+ self._host = iou_config.get("host", kwargs["host"])
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
diff --git a/gns3server/modules/iou/nios/nio.py b/gns3server/modules/iou/nios/nio.py
index 197d4817..059d56a3 100644
--- a/gns3server/modules/iou/nios/nio.py
+++ b/gns3server/modules/iou/nios/nio.py
@@ -22,7 +22,7 @@ Base interface for NIOs.
class NIO(object):
"""
- IOU NIO.
+ Network Input/Output.
"""
def __init__(self):
diff --git a/gns3server/modules/iou/nios/nio_generic_ethernet.py b/gns3server/modules/iou/nios/nio_generic_ethernet.py
index 45c89b4e..068e9fc3 100644
--- a/gns3server/modules/iou/nios/nio_generic_ethernet.py
+++ b/gns3server/modules/iou/nios/nio_generic_ethernet.py
@@ -24,7 +24,7 @@ from .nio import NIO
class NIO_GenericEthernet(NIO):
"""
- NIO generic Ethernet NIO.
+ Generic Ethernet NIO.
:param ethernet_device: Ethernet device name (e.g. eth0)
"""
diff --git a/gns3server/modules/iou/nios/nio_tap.py b/gns3server/modules/iou/nios/nio_tap.py
index 3164e933..95ec631d 100644
--- a/gns3server/modules/iou/nios/nio_tap.py
+++ b/gns3server/modules/iou/nios/nio_tap.py
@@ -24,7 +24,7 @@ from .nio import NIO
class NIO_TAP(NIO):
"""
- IOU TAP NIO.
+ TAP NIO.
:param tap_device: TAP device name (e.g. tap0)
"""
diff --git a/gns3server/modules/iou/nios/nio_udp.py b/gns3server/modules/iou/nios/nio_udp.py
index 41ffbc4f..2c850351 100644
--- a/gns3server/modules/iou/nios/nio_udp.py
+++ b/gns3server/modules/iou/nios/nio_udp.py
@@ -24,7 +24,7 @@ from .nio import NIO
class NIO_UDP(NIO):
"""
- IOU UDP NIO.
+ UDP NIO.
:param lport: local port number
:param rhost: remote address/host
diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py
new file mode 100644
index 00000000..754ffbbf
--- /dev/null
+++ b/gns3server/modules/qemu/__init__.py
@@ -0,0 +1,657 @@
+# -*- 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 .
+
+"""
+QEMU server module.
+"""
+
+import sys
+import os
+import socket
+import shutil
+import subprocess
+import re
+
+from gns3server.modules import IModule
+from gns3server.config import Config
+from .qemu_vm import QemuVM
+from .qemu_error import QemuError
+from .nios.nio_udp import NIO_UDP
+from ..attic import find_unused_port
+
+from .schemas import QEMU_CREATE_SCHEMA
+from .schemas import QEMU_DELETE_SCHEMA
+from .schemas import QEMU_UPDATE_SCHEMA
+from .schemas import QEMU_START_SCHEMA
+from .schemas import QEMU_STOP_SCHEMA
+from .schemas import QEMU_SUSPEND_SCHEMA
+from .schemas import QEMU_RELOAD_SCHEMA
+from .schemas import QEMU_ALLOCATE_UDP_PORT_SCHEMA
+from .schemas import QEMU_ADD_NIO_SCHEMA
+from .schemas import QEMU_DELETE_NIO_SCHEMA
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class Qemu(IModule):
+ """
+ QEMU module.
+
+ :param name: module name
+ :param args: arguments for the module
+ :param kwargs: named arguments for the module
+ """
+
+ def __init__(self, name, *args, **kwargs):
+
+ # a new process start when calling IModule
+ IModule.__init__(self, name, *args, **kwargs)
+ self._qemu_instances = {}
+
+ config = Config.instance()
+ qemu_config = config.get_section_config(name.upper())
+ self._console_start_port_range = qemu_config.get("console_start_port_range", 5001)
+ self._console_end_port_range = qemu_config.get("console_end_port_range", 5500)
+ self._allocated_udp_ports = []
+ self._udp_start_port_range = qemu_config.get("udp_start_port_range", 40001)
+ self._udp_end_port_range = qemu_config.get("udp_end_port_range", 45500)
+ self._host = qemu_config.get("host", kwargs["host"])
+ self._projects_dir = kwargs["projects_dir"]
+ self._tempdir = kwargs["temp_dir"]
+ self._working_dir = self._projects_dir
+
+ def stop(self, signum=None):
+ """
+ Properly stops the module.
+
+ :param signum: signal number (if called by the signal handler)
+ """
+
+ # delete all QEMU instances
+ for qemu_id in self._qemu_instances:
+ qemu_instance = self._qemu_instances[qemu_id]
+ qemu_instance.delete()
+
+ IModule.stop(self, signum) # this will stop the I/O loop
+
+ def get_qemu_instance(self, qemu_id):
+ """
+ Returns a QEMU VM instance.
+
+ :param qemu_id: QEMU VM identifier
+
+ :returns: QemuVM instance
+ """
+
+ if qemu_id not in self._qemu_instances:
+ log.debug("QEMU VM ID {} doesn't exist".format(qemu_id), exc_info=1)
+ self.send_custom_error("QEMU VM ID {} doesn't exist".format(qemu_id))
+ return None
+ return self._qemu_instances[qemu_id]
+
+ @IModule.route("qemu.reset")
+ def reset(self, request):
+ """
+ Resets the module.
+
+ :param request: JSON request
+ """
+
+ # delete all QEMU instances
+ for qemu_id in self._qemu_instances:
+ qemu_instance = self._qemu_instances[qemu_id]
+ qemu_instance.delete()
+
+ # resets the instance IDs
+ QemuVM.reset()
+
+ self._qemu_instances.clear()
+ self._allocated_udp_ports.clear()
+
+ log.info("QEMU module has been reset")
+
+ @IModule.route("qemu.settings")
+ def settings(self, request):
+ """
+ Set or update settings.
+
+ Optional request parameters:
+ - working_dir (path to a working directory)
+ - project_name
+ - console_start_port_range
+ - console_end_port_range
+ - udp_start_port_range
+ - udp_end_port_range
+
+ :param request: JSON request
+ """
+
+ if request is None:
+ self.send_param_error()
+ return
+
+ if "working_dir" in request:
+ new_working_dir = request["working_dir"]
+ log.info("this server is local with working directory path to {}".format(new_working_dir))
+ else:
+ new_working_dir = os.path.join(self._projects_dir, request["project_name"])
+ log.info("this server is remote with working directory path to {}".format(new_working_dir))
+ if self._projects_dir != self._working_dir != new_working_dir:
+ if not os.path.isdir(new_working_dir):
+ try:
+ shutil.move(self._working_dir, new_working_dir)
+ except OSError as e:
+ log.error("could not move working directory from {} to {}: {}".format(self._working_dir,
+ new_working_dir,
+ e))
+ return
+
+ # update the working directory if it has changed
+ if self._working_dir != new_working_dir:
+ self._working_dir = new_working_dir
+ for qemu_id in self._qemu_instances:
+ qemu_instance = self._qemu_instances[qemu_id]
+ qemu_instance.working_dir = os.path.join(self._working_dir, "qemu", "vm-{}".format(qemu_instance.id))
+
+ 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"]
+
+ if "udp_start_port_range" in request and "udp_end_port_range" in request:
+ self._udp_start_port_range = request["udp_start_port_range"]
+ self._udp_end_port_range = request["udp_end_port_range"]
+
+ log.debug("received request {}".format(request))
+
+ @IModule.route("qemu.create")
+ def qemu_create(self, request):
+ """
+ Creates a new QEMU VM instance.
+
+ Mandatory request parameters:
+ - name (QEMU VM name)
+ - qemu_path (path to the Qemu binary)
+
+ Optional request parameters:
+ - console (QEMU VM console port)
+
+ Response parameters:
+ - id (QEMU VM instance identifier)
+ - name (QEMU VM name)
+ - default settings
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, QEMU_CREATE_SCHEMA):
+ return
+
+ name = request["name"]
+ qemu_path = request["qemu_path"]
+ console = request.get("console")
+ qemu_id = request.get("qemu_id")
+
+ try:
+ qemu_instance = QemuVM(name,
+ qemu_path,
+ self._working_dir,
+ self._host,
+ qemu_id,
+ console,
+ self._console_start_port_range,
+ self._console_end_port_range)
+
+ except QemuError as e:
+ self.send_custom_error(str(e))
+ return
+
+ response = {"name": qemu_instance.name,
+ "id": qemu_instance.id}
+
+ defaults = qemu_instance.defaults()
+ response.update(defaults)
+ self._qemu_instances[qemu_instance.id] = qemu_instance
+ self.send_response(response)
+
+ @IModule.route("qemu.delete")
+ def qemu_delete(self, request):
+ """
+ Deletes a QEMU VM instance.
+
+ Mandatory request parameters:
+ - id (QEMU VM instance identifier)
+
+ Response parameter:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, QEMU_DELETE_SCHEMA):
+ return
+
+ # get the instance
+ qemu_instance = self.get_qemu_instance(request["id"])
+ if not qemu_instance:
+ return
+
+ try:
+ qemu_instance.clean_delete()
+ del self._qemu_instances[request["id"]]
+ except QemuError as e:
+ self.send_custom_error(str(e))
+ return
+
+ self.send_response(True)
+
+ @IModule.route("qemu.update")
+ def qemu_update(self, request):
+ """
+ Updates a QEMU VM instance
+
+ Mandatory request parameters:
+ - id (QEMU VM instance identifier)
+
+ Optional request parameters:
+ - any setting to update
+
+ Response parameters:
+ - updated settings
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, QEMU_UPDATE_SCHEMA):
+ return
+
+ # get the instance
+ qemu_instance = self.get_qemu_instance(request["id"])
+ if not qemu_instance:
+ return
+
+ # update the QEMU VM settings
+ response = {}
+ for name, value in request.items():
+ if hasattr(qemu_instance, name) and getattr(qemu_instance, name) != value:
+ try:
+ setattr(qemu_instance, name, value)
+ response[name] = value
+ except QemuError as e:
+ self.send_custom_error(str(e))
+ return
+
+ self.send_response(response)
+
+ @IModule.route("qemu.start")
+ def qemu_start(self, request):
+ """
+ Starts a QEMU VM instance.
+
+ Mandatory request parameters:
+ - id (QEMU VM instance identifier)
+
+ Response parameters:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, QEMU_START_SCHEMA):
+ return
+
+ # get the instance
+ qemu_instance = self.get_qemu_instance(request["id"])
+ if not qemu_instance:
+ return
+
+ try:
+ qemu_instance.start()
+ except QemuError as e:
+ self.send_custom_error(str(e))
+ return
+ self.send_response(True)
+
+ @IModule.route("qemu.stop")
+ def qemu_stop(self, request):
+ """
+ Stops a QEMU VM instance.
+
+ Mandatory request parameters:
+ - id (QEMU VM instance identifier)
+
+ Response parameters:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, QEMU_STOP_SCHEMA):
+ return
+
+ # get the instance
+ qemu_instance = self.get_qemu_instance(request["id"])
+ if not qemu_instance:
+ return
+
+ try:
+ qemu_instance.stop()
+ except QemuError as e:
+ self.send_custom_error(str(e))
+ return
+ self.send_response(True)
+
+ @IModule.route("qemu.reload")
+ def qemu_reload(self, request):
+ """
+ Reloads a QEMU VM instance.
+
+ Mandatory request parameters:
+ - id (QEMU VM identifier)
+
+ Response parameters:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, QEMU_RELOAD_SCHEMA):
+ return
+
+ # get the instance
+ qemu_instance = self.get_qemu_instance(request["id"])
+ if not qemu_instance:
+ return
+
+ try:
+ qemu_instance.reload()
+ except QemuError as e:
+ self.send_custom_error(str(e))
+ return
+ self.send_response(True)
+
+ @IModule.route("qemu.stop")
+ def qemu_stop(self, request):
+ """
+ Stops a QEMU VM instance.
+
+ Mandatory request parameters:
+ - id (QEMU VM instance identifier)
+
+ Response parameters:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, QEMU_STOP_SCHEMA):
+ return
+
+ # get the instance
+ qemu_instance = self.get_qemu_instance(request["id"])
+ if not qemu_instance:
+ return
+
+ try:
+ qemu_instance.stop()
+ except QemuError as e:
+ self.send_custom_error(str(e))
+ return
+ self.send_response(True)
+
+ @IModule.route("qemu.suspend")
+ def qemu_suspend(self, request):
+ """
+ Suspends a QEMU VM instance.
+
+ Mandatory request parameters:
+ - id (QEMU VM instance identifier)
+
+ Response parameters:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, QEMU_SUSPEND_SCHEMA):
+ return
+
+ # get the instance
+ qemu_instance = self.get_qemu_instance(request["id"])
+ if not qemu_instance:
+ return
+
+ try:
+ qemu_instance.suspend()
+ except QemuError as e:
+ self.send_custom_error(str(e))
+ return
+ self.send_response(True)
+
+ @IModule.route("qemu.allocate_udp_port")
+ def allocate_udp_port(self, request):
+ """
+ Allocates a UDP port in order to create an UDP NIO.
+
+ Mandatory request parameters:
+ - id (QEMU VM identifier)
+ - port_id (unique port identifier)
+
+ Response parameters:
+ - port_id (unique port identifier)
+ - lport (allocated local port)
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, QEMU_ALLOCATE_UDP_PORT_SCHEMA):
+ return
+
+ # get the instance
+ qemu_instance = self.get_qemu_instance(request["id"])
+ if not qemu_instance:
+ return
+
+ try:
+ port = find_unused_port(self._udp_start_port_range,
+ self._udp_end_port_range,
+ host=self._host,
+ socket_type="UDP",
+ ignore_ports=self._allocated_udp_ports)
+ except Exception as e:
+ self.send_custom_error(str(e))
+ return
+
+ self._allocated_udp_ports.append(port)
+ log.info("{} [id={}] has allocated UDP port {} with host {}".format(qemu_instance.name,
+ qemu_instance.id,
+ port,
+ self._host))
+
+ response = {"lport": port,
+ "port_id": request["port_id"]}
+ self.send_response(response)
+
+ @IModule.route("qemu.add_nio")
+ def add_nio(self, request):
+ """
+ Adds an NIO (Network Input/Output) for a QEMU VM instance.
+
+ Mandatory request parameters:
+ - id (QEMU VM instance identifier)
+ - port (port number)
+ - port_id (unique port identifier)
+ - nio (one of the following)
+ - type "nio_udp"
+ - lport (local port)
+ - rhost (remote host)
+ - rport (remote port)
+
+ Response parameters:
+ - port_id (unique port identifier)
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, QEMU_ADD_NIO_SCHEMA):
+ return
+
+ # get the instance
+ qemu_instance = self.get_qemu_instance(request["id"])
+ if not qemu_instance:
+ return
+
+ port = request["port"]
+ try:
+ nio = None
+ if request["nio"]["type"] == "nio_udp":
+ lport = request["nio"]["lport"]
+ rhost = request["nio"]["rhost"]
+ rport = request["nio"]["rport"]
+ try:
+ #TODO: handle IPv6
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
+ sock.connect((rhost, rport))
+ except OSError as e:
+ raise QemuError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
+ nio = NIO_UDP(lport, rhost, rport)
+ if not nio:
+ raise QemuError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
+ except QemuError as e:
+ self.send_custom_error(str(e))
+ return
+
+ try:
+ qemu_instance.port_add_nio_binding(port, nio)
+ except QemuError as e:
+ self.send_custom_error(str(e))
+ return
+
+ self.send_response({"port_id": request["port_id"]})
+
+ @IModule.route("qemu.delete_nio")
+ def delete_nio(self, request):
+ """
+ Deletes an NIO (Network Input/Output).
+
+ Mandatory request parameters:
+ - id (QEMU VM instance identifier)
+ - port (port identifier)
+
+ Response parameters:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, QEMU_DELETE_NIO_SCHEMA):
+ return
+
+ # get the instance
+ qemu_instance = self.get_qemu_instance(request["id"])
+ if not qemu_instance:
+ return
+
+ port = request["port"]
+ try:
+ nio = qemu_instance.port_remove_nio_binding(port)
+ if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports:
+ self._allocated_udp_ports.remove(nio.lport)
+ except QemuError as e:
+ self.send_custom_error(str(e))
+ return
+
+ self.send_response(True)
+
+ def _get_qemu_version(self, qemu_path):
+ """
+ Gets the Qemu version.
+
+ :param qemu_path: path to Qemu
+ """
+
+ if sys.platform.startswith("win"):
+ return ""
+ try:
+ output = subprocess.check_output([qemu_path, "--version"])
+ match = re.search("QEMU emulator version ([0-9a-z\-\.]+)", output.decode("utf-8"))
+ if match:
+ version = match.group(1)
+ return version
+ else:
+ raise QemuError("Could not determine the Qemu version for {}".format(qemu_path))
+ except (OSError, subprocess.CalledProcessError) as e:
+ raise QemuError("Error while looking for the Qemu version: {}".format(e))
+
+ @IModule.route("qemu.qemu_list")
+ def qemu_list(self, request):
+ """
+ Gets QEMU binaries list.
+
+ Response parameters:
+ - Server address/host
+ - List of Qemu binaries
+ """
+
+ qemus = []
+ paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
+ # look for Qemu binaries in the current working directory and $PATH
+ if sys.platform.startswith("win"):
+ # add specific Windows paths
+ paths.append(os.path.join(os.getcwd(), "qemu"))
+ if "PROGRAMFILES(X86)" in os.environ and os.path.exists(os.environ["PROGRAMFILES(X86)"]):
+ paths.append(os.path.join(os.environ["PROGRAMFILES(X86)"], "qemu"))
+ if "PROGRAMFILES" in os.environ and os.path.exists(os.environ["PROGRAMFILES"]):
+ paths.append(os.path.join(os.environ["PROGRAMFILES"], "qemu"))
+ for path in paths:
+ try:
+ for f in os.listdir(path):
+ if f.startswith("qemu-system") and os.access(os.path.join(path, f), os.X_OK):
+ qemu_path = os.path.join(path, f)
+ version = self._get_qemu_version(qemu_path)
+ qemus.append({"path": qemu_path, "version": version})
+ except OSError:
+ continue
+
+ response = {"server": self._host,
+ "qemus": qemus}
+ self.send_response(response)
+
+ @IModule.route("qemu.echo")
+ def echo(self, request):
+ """
+ Echo end point for testing purposes.
+
+ :param request: JSON request
+ """
+
+ if request is None:
+ self.send_param_error()
+ else:
+ log.debug("received request {}".format(request))
+ self.send_response(request)
diff --git a/gns3server/modules/qemu/adapters/__init__.py b/gns3server/modules/qemu/adapters/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gns3server/modules/qemu/adapters/adapter.py b/gns3server/modules/qemu/adapters/adapter.py
new file mode 100644
index 00000000..cf439427
--- /dev/null
+++ b/gns3server/modules/qemu/adapters/adapter.py
@@ -0,0 +1,104 @@
+# -*- 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 .
+
+
+class Adapter(object):
+ """
+ Base class for adapters.
+
+ :param interfaces: number of interfaces supported by this adapter.
+ """
+
+ def __init__(self, interfaces=1):
+
+ self._interfaces = interfaces
+
+ self._ports = {}
+ for port_id in range(0, interfaces):
+ self._ports[port_id] = None
+
+ def removable(self):
+ """
+ Returns True if the adapter can be removed from a slot
+ and False if not.
+
+ :returns: boolean
+ """
+
+ return True
+
+ def port_exists(self, port_id):
+ """
+ Checks if a port exists on this adapter.
+
+ :returns: True is the port exists,
+ False otherwise.
+ """
+
+ if port_id in self._ports:
+ return True
+ return False
+
+ def add_nio(self, port_id, nio):
+ """
+ Adds a NIO to a port on this adapter.
+
+ :param port_id: port ID (integer)
+ :param nio: NIO instance
+ """
+
+ self._ports[port_id] = nio
+
+ def remove_nio(self, port_id):
+ """
+ Removes a NIO from a port on this adapter.
+
+ :param port_id: port ID (integer)
+ """
+
+ self._ports[port_id] = None
+
+ def get_nio(self, port_id):
+ """
+ Returns the NIO assigned to a port.
+
+ :params port_id: port ID (integer)
+
+ :returns: NIO instance
+ """
+
+ return self._ports[port_id]
+
+ @property
+ def ports(self):
+ """
+ Returns port to NIO mapping
+
+ :returns: dictionary port -> NIO
+ """
+
+ return self._ports
+
+ @property
+ def interfaces(self):
+ """
+ Returns the number of interfaces supported by this adapter.
+
+ :returns: number of interfaces
+ """
+
+ return self._interfaces
diff --git a/gns3server/modules/qemu/adapters/ethernet_adapter.py b/gns3server/modules/qemu/adapters/ethernet_adapter.py
new file mode 100644
index 00000000..27426ec2
--- /dev/null
+++ b/gns3server/modules/qemu/adapters/ethernet_adapter.py
@@ -0,0 +1,31 @@
+# -*- 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 .
+
+from .adapter import Adapter
+
+
+class EthernetAdapter(Adapter):
+ """
+ QEMU Ethernet adapter.
+ """
+
+ def __init__(self):
+ Adapter.__init__(self, interfaces=1)
+
+ def __str__(self):
+
+ return "QEMU Ethernet adapter"
diff --git a/gns3server/modules/qemu/nios/__init__.py b/gns3server/modules/qemu/nios/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gns3server/modules/qemu/nios/nio.py b/gns3server/modules/qemu/nios/nio.py
new file mode 100644
index 00000000..eee5f1d5
--- /dev/null
+++ b/gns3server/modules/qemu/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):
+ """
+ Network Input/Output.
+ """
+
+ 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/qemu/nios/nio_udp.py b/gns3server/modules/qemu/nios/nio_udp.py
new file mode 100644
index 00000000..2c850351
--- /dev/null
+++ b/gns3server/modules/qemu/nios/nio_udp.py
@@ -0,0 +1,75 @@
+# -*- 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 .
+
+"""
+Interface for UDP NIOs.
+"""
+
+from .nio import NIO
+
+
+class NIO_UDP(NIO):
+ """
+ UDP NIO.
+
+ :param lport: local port number
+ :param rhost: remote address/host
+ :param rport: remote port number
+ """
+
+ _instance_count = 0
+
+ def __init__(self, lport, rhost, rport):
+
+ NIO.__init__(self)
+ self._lport = lport
+ self._rhost = rhost
+ self._rport = rport
+
+ @property
+ def lport(self):
+ """
+ Returns the local port
+
+ :returns: local port number
+ """
+
+ return self._lport
+
+ @property
+ def rhost(self):
+ """
+ Returns the remote host
+
+ :returns: remote address/host
+ """
+
+ return self._rhost
+
+ @property
+ def rport(self):
+ """
+ Returns the remote port
+
+ :returns: remote port number
+ """
+
+ return self._rport
+
+ def __str__(self):
+
+ return "NIO UDP"
diff --git a/gns3server/modules/qemu/qemu_error.py b/gns3server/modules/qemu/qemu_error.py
new file mode 100644
index 00000000..55135a34
--- /dev/null
+++ b/gns3server/modules/qemu/qemu_error.py
@@ -0,0 +1,39 @@
+# -*- 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 .
+
+"""
+Custom exceptions for QEMU module.
+"""
+
+
+class QemuError(Exception):
+
+ def __init__(self, message, original_exception=None):
+
+ Exception.__init__(self, message)
+ if isinstance(message, Exception):
+ message = str(message)
+ self._message = message
+ self._original_exception = original_exception
+
+ def __repr__(self):
+
+ return self._message
+
+ def __str__(self):
+
+ return self._message
diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py
new file mode 100644
index 00000000..ce341780
--- /dev/null
+++ b/gns3server/modules/qemu/qemu_vm.py
@@ -0,0 +1,785 @@
+# -*- 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 .
+
+"""
+QEMU VM instance.
+"""
+
+import os
+import shutil
+import random
+import subprocess
+import shlex
+
+from .qemu_error import QemuError
+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 QemuVM(object):
+ """
+ QEMU VM implementation.
+
+ :param name: name of this QEMU VM
+ :param qemu_path: path to the QEMU binary
+ :param working_dir: path to a working directory
+ :param host: host/address to bind for console and UDP connections
+ :param qemu_id: QEMU VM instance ID
+ :param console: TCP console port
+ :param console_start_port_range: TCP console port range start
+ :param console_end_port_range: TCP console port range end
+ """
+
+ _instances = []
+ _allocated_console_ports = []
+
+ def __init__(self,
+ name,
+ qemu_path,
+ working_dir,
+ host="127.0.0.1",
+ qemu_id=None,
+ console=None,
+ console_start_port_range=5001,
+ console_end_port_range=5500):
+
+ if not qemu_id:
+ self._id = 0
+ for identifier in range(1, 1024):
+ if identifier not in self._instances:
+ self._id = identifier
+ self._instances.append(self._id)
+ break
+
+ if self._id == 0:
+ raise QemuError("Maximum number of QEMU VM instances reached")
+ else:
+ if qemu_id in self._instances:
+ raise QemuError("QEMU identifier {} is already used by another QEMU VM instance".format(qemu_id))
+ self._id = qemu_id
+ self._instances.append(self._id)
+
+ self._name = name
+ self._working_dir = None
+ self._host = host
+ self._command = []
+ self._started = False
+ self._process = None
+ self._stdout_file = ""
+ self._console_start_port_range = console_start_port_range
+ self._console_end_port_range = console_end_port_range
+
+ # QEMU settings
+ self._qemu_path = qemu_path
+ self._hda_disk_image = ""
+ self._hdb_disk_image = ""
+ self._options = ""
+ self._ram = 256
+ self._console = console
+ self._ethernet_adapters = []
+ self._adapter_type = "e1000"
+ self._initrd = ""
+ self._kernel_image = ""
+ self._kernel_command_line = ""
+
+ working_dir_path = os.path.join(working_dir, "qemu", "vm-{}".format(self._id))
+
+ if qemu_id and not os.path.isdir(working_dir_path):
+ raise QemuError("Working directory {} doesn't exist".format(working_dir_path))
+
+ # create the device own working directory
+ self.working_dir = working_dir_path
+
+ if not self._console:
+ # allocate a console port
+ try:
+ self._console = find_unused_port(self._console_start_port_range,
+ self._console_end_port_range,
+ self._host,
+ ignore_ports=self._allocated_console_ports)
+ except Exception as e:
+ raise QemuError(e)
+
+ if self._console in self._allocated_console_ports:
+ raise QemuError("Console port {} is already used by another QEMU VM".format(console))
+ self._allocated_console_ports.append(self._console)
+
+ self.adapters = 1 # creates 1 adapter by default
+ log.info("QEMU VM {name} [id={id}] has been created".format(name=self._name,
+ id=self._id))
+
+ def defaults(self):
+ """
+ Returns all the default attribute values for this QEMU VM.
+
+ :returns: default values (dictionary)
+ """
+
+ qemu_defaults = {"name": self._name,
+ "qemu_path": self._qemu_path,
+ "ram": self._ram,
+ "hda_disk_image": self._hda_disk_image,
+ "hdb_disk_image": self._hdb_disk_image,
+ "options": self._options,
+ "adapters": self.adapters,
+ "adapter_type": self._adapter_type,
+ "console": self._console,
+ "initrd": self._initrd,
+ "kernel_image": self._kernel_image,
+ "kernel_command_line": self._kernel_command_line}
+
+ return qemu_defaults
+
+ @property
+ def id(self):
+ """
+ Returns the unique ID for this QEMU VM.
+
+ :returns: id (integer)
+ """
+
+ return self._id
+
+ @classmethod
+ def reset(cls):
+ """
+ Resets allocated instance list.
+ """
+
+ cls._instances.clear()
+ cls._allocated_console_ports.clear()
+
+ @property
+ def name(self):
+ """
+ Returns the name of this QEMU VM.
+
+ :returns: name
+ """
+
+ return self._name
+
+ @name.setter
+ def name(self, new_name):
+ """
+ Sets the name of this QEMU VM.
+
+ :param new_name: name
+ """
+
+ log.info("QEMU VM {name} [id={id}]: renamed to {new_name}".format(name=self._name,
+ id=self._id,
+ new_name=new_name))
+
+ self._name = new_name
+
+ @property
+ def working_dir(self):
+ """
+ Returns current working directory
+
+ :returns: path to the working directory
+ """
+
+ return self._working_dir
+
+ @working_dir.setter
+ def working_dir(self, working_dir):
+ """
+ Sets the working directory this QEMU VM.
+
+ :param working_dir: path to the working directory
+ """
+
+ try:
+ os.makedirs(working_dir)
+ except FileExistsError:
+ pass
+ except OSError as e:
+ raise QemuError("Could not create working directory {}: {}".format(working_dir, e))
+
+ self._working_dir = working_dir
+ log.info("QEMU VM {name} [id={id}]: working directory changed to {wd}".format(name=self._name,
+ id=self._id,
+ wd=self._working_dir))
+
+ @property
+ def console(self):
+ """
+ Returns the TCP console port.
+
+ :returns: console port (integer)
+ """
+
+ return self._console
+
+ @console.setter
+ def console(self, console):
+ """
+ Sets the TCP console port.
+
+ :param console: console port (integer)
+ """
+
+ if console in self._allocated_console_ports:
+ raise QemuError("Console port {} is already used by another QEMU VM".format(console))
+
+ self._allocated_console_ports.remove(self._console)
+ self._console = console
+ self._allocated_console_ports.append(self._console)
+
+ log.info("QEMU VM {name} [id={id}]: console port set to {port}".format(name=self._name,
+ id=self._id,
+ port=console))
+
+ def delete(self):
+ """
+ Deletes this QEMU VM.
+ """
+
+ self.stop()
+ if self._id in self._instances:
+ self._instances.remove(self._id)
+
+ if self.console and self.console in self._allocated_console_ports:
+ self._allocated_console_ports.remove(self.console)
+
+ log.info("QEMU VM {name} [id={id}] has been deleted".format(name=self._name,
+ id=self._id))
+
+ def clean_delete(self):
+ """
+ Deletes this QEMU VM & all files.
+ """
+
+ self.stop()
+ if self._id in self._instances:
+ self._instances.remove(self._id)
+
+ if self.console:
+ self._allocated_console_ports.remove(self.console)
+
+ try:
+ shutil.rmtree(self._working_dir)
+ except OSError as e:
+ log.error("could not delete QEMU VM {name} [id={id}]: {error}".format(name=self._name,
+ id=self._id,
+ error=e))
+ return
+
+ log.info("QEMU VM {name} [id={id}] has been deleted (including associated files)".format(name=self._name,
+ id=self._id))
+
+ @property
+ def qemu_path(self):
+ """
+ Returns the QEMU binary path for this QEMU VM.
+
+ :returns: QEMU path
+ """
+
+ return self._qemu_path
+
+ @qemu_path.setter
+ def qemu_path(self, qemu_path):
+ """
+ Sets the QEMU binary path this QEMU VM.
+
+ :param qemu_path: QEMU path
+ """
+
+ log.info("QEMU VM {name} [id={id}] has set the QEMU path to {qemu_path}".format(name=self._name,
+ id=self._id,
+ qemu_path=qemu_path))
+ self._qemu_path = qemu_path
+
+ @property
+ def hda_disk_image(self):
+ """
+ Returns the hda disk image path for this QEMU VM.
+
+ :returns: QEMU hda disk image path
+ """
+
+ return self._hda_disk_image
+
+ @hda_disk_image.setter
+ def hda_disk_image(self, hda_disk_image):
+ """
+ Sets the hda disk image for this QEMU VM.
+
+ :param hda_disk_image: QEMU hda disk image path
+ """
+
+ log.info("QEMU VM {name} [id={id}] has set the QEMU hda disk image path to {disk_image}".format(name=self._name,
+ id=self._id,
+ disk_image=hda_disk_image))
+ self._hda_disk_image = hda_disk_image
+
+ @property
+ def hdb_disk_image(self):
+ """
+ Returns the hdb disk image path for this QEMU VM.
+
+ :returns: QEMU hdb disk image path
+ """
+
+ return self._hdb_disk_image
+
+ @hdb_disk_image.setter
+ def hdb_disk_image(self, hdb_disk_image):
+ """
+ Sets the hdb disk image for this QEMU VM.
+
+ :param hdb_disk_image: QEMU hdb disk image path
+ """
+
+ log.info("QEMU VM {name} [id={id}] has set the QEMU hdb disk image path to {disk_image}".format(name=self._name,
+ id=self._id,
+ disk_image=hdb_disk_image))
+ self._hdb_disk_image = hdb_disk_image
+
+
+ @property
+ def adapters(self):
+ """
+ Returns the number of Ethernet adapters for this QEMU 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 QEMU VM instance.
+
+ :param adapters: number of adapters
+ """
+
+ self._ethernet_adapters.clear()
+ for adapter_id in range(0, adapters):
+ self._ethernet_adapters.append(EthernetAdapter())
+
+ log.info("QEMU VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name,
+ id=self._id,
+ adapters=adapters))
+
+ @property
+ def adapter_type(self):
+ """
+ Returns the adapter type for this QEMU 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 QEMU VM instance.
+
+ :param adapter_type: adapter type (string)
+ """
+
+ self._adapter_type = adapter_type
+
+ log.info("QEMU VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name,
+ id=self._id,
+ adapter_type=adapter_type))
+
+ @property
+ def ram(self):
+ """
+ Returns the RAM amount for this QEMU VM.
+
+ :returns: RAM amount in MB
+ """
+
+ return self._ram
+
+ @ram.setter
+ def ram(self, ram):
+ """
+ Sets the amount of RAM for this QEMU VM.
+
+ :param ram: RAM amount in MB
+ """
+
+ log.info("QEMU VM {name} [id={id}] has set the RAM to {ram}".format(name=self._name,
+ id=self._id,
+ ram=ram))
+ self._ram = ram
+
+ @property
+ def options(self):
+ """
+ Returns the options for this QEMU VM.
+
+ :returns: QEMU options
+ """
+
+ return self._options
+
+ @options.setter
+ def options(self, options):
+ """
+ Sets the options for this QEMU VM.
+
+ :param options: QEMU options
+ """
+
+ log.info("QEMU VM {name} [id={id}] has set the QEMU options to {options}".format(name=self._name,
+ id=self._id,
+ options=options))
+ self._options = options
+
+ @property
+ def initrd(self):
+ """
+ Returns the initrd path for this QEMU VM.
+
+ :returns: QEMU initrd path
+ """
+
+ return self._initrd
+
+ @initrd.setter
+ def initrd(self, initrd):
+ """
+ Sets the initrd path for this QEMU VM.
+
+ :param initrd: QEMU initrd path
+ """
+
+ log.info("QEMU VM {name} [id={id}] has set the QEMU initrd path to {initrd}".format(name=self._name,
+ id=self._id,
+ initrd=initrd))
+ self._initrd = initrd
+
+ @property
+ def kernel_image(self):
+ """
+ Returns the kernel image path for this QEMU VM.
+
+ :returns: QEMU kernel image path
+ """
+
+ return self._kernel_image
+
+ @kernel_image.setter
+ def kernel_image(self, kernel_image):
+ """
+ Sets the kernel image path for this QEMU VM.
+
+ :param kernel_image: QEMU kernel image path
+ """
+
+ log.info("QEMU VM {name} [id={id}] has set the QEMU kernel image path to {kernel_image}".format(name=self._name,
+ id=self._id,
+ kernel_image=kernel_image))
+ self._kernel_image = kernel_image
+
+ @property
+ def kernel_command_line(self):
+ """
+ Returns the kernel command line for this QEMU VM.
+
+ :returns: QEMU kernel command line
+ """
+
+ return self._kernel_command_line
+
+ @kernel_command_line.setter
+ def kernel_command_line(self, kernel_command_line):
+ """
+ Sets the kernel command line for this QEMU VM.
+
+ :param kernel_command_line: QEMU kernel command line
+ """
+
+ log.info("QEMU VM {name} [id={id}] has set the QEMU kernel command line to {kernel_command_line}".format(name=self._name,
+ id=self._id,
+ kernel_command_line=kernel_command_line))
+ self._kernel_command_line = kernel_command_line
+
+ def start(self):
+ """
+ Starts this QEMU VM.
+ """
+
+ if not self.is_running():
+
+ if not os.path.isfile(self._qemu_path) or not os.path.exists(self._qemu_path):
+ raise QemuError("QEMU binary '{}' is not accessible".format(self._qemu_path))
+
+ self._command = self._build_command()
+ try:
+ log.info("starting QEMU: {}".format(self._command))
+ self._stdout_file = os.path.join(self._working_dir, "qemu.log")
+ log.info("logging to {}".format(self._stdout_file))
+ with open(self._stdout_file, "w") as fd:
+ self._process = subprocess.Popen(self._command,
+ stdout=fd,
+ stderr=subprocess.STDOUT,
+ cwd=self._working_dir)
+ log.info("QEMU VM instance {} started PID={}".format(self._id, self._process.pid))
+ self._started = True
+ except OSError as e:
+ stdout = self.read_stdout()
+ log.error("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout))
+ raise QemuError("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout))
+
+ def stop(self):
+ """
+ Stops this QEMU VM.
+ """
+
+ # stop the QEMU process
+ if self.is_running():
+ log.info("stopping QEMU VM instance {} PID={}".format(self._id, self._process.pid))
+ try:
+ self._process.terminate()
+ self._process.wait(1)
+ except subprocess.TimeoutExpired:
+ self._process.kill()
+ if self._process.poll() is None:
+ log.warn("QEMU VM instance {} PID={} is still running".format(self._id,
+ self._process.pid))
+ self._process = None
+ self._started = False
+
+ def suspend(self):
+ """
+ Suspends this QEMU VM.
+ """
+
+ pass
+
+ def reload(self):
+ """
+ Reloads this QEMU VM.
+ """
+
+ pass
+
+ def resume(self):
+ """
+ Resumes this QEMU VM.
+ """
+
+ pass
+
+ 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 QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name,
+ adapter_id=adapter_id))
+
+ adapter.add_nio(0, nio)
+ log.info("QEMU 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 QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name,
+ adapter_id=adapter_id))
+
+ nio = adapter.get_nio(0)
+ adapter.remove_nio(0)
+ log.info("QEMU 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
+
+ @property
+ def started(self):
+ """
+ Returns either this QEMU VM has been started or not.
+
+ :returns: boolean
+ """
+
+ return self._started
+
+ def read_stdout(self):
+ """
+ Reads the standard output of the QEMU process.
+ Only use when the process has been stopped or has crashed.
+ """
+
+ output = ""
+ if self._stdout_file:
+ try:
+ with open(self._stdout_file, errors="replace") as file:
+ output = file.read()
+ except OSError as e:
+ log.warn("could not read {}: {}".format(self._stdout_file, e))
+ return output
+
+ def is_running(self):
+ """
+ Checks if the QEMU process is running
+
+ :returns: True or False
+ """
+
+ if self._process and self._process.poll() is None:
+ return True
+ return False
+
+ def command(self):
+ """
+ Returns the QEMU command line.
+
+ :returns: QEMU command line (string)
+ """
+
+ return " ".join(self._build_command())
+
+ def _serial_options(self):
+
+ if self._console:
+ return ["-serial", "telnet:{}:{},server,nowait".format(self._host, self._console)]
+ else:
+ return []
+
+ def _disk_options(self):
+
+ options = []
+ qemu_img_path = ""
+ qemu_path_dir = os.path.dirname(self._qemu_path)
+ try:
+ for f in os.listdir(qemu_path_dir):
+ if f.startswith("qemu-img"):
+ qemu_img_path = os.path.join(qemu_path_dir, f)
+ except OSError as e:
+ raise QemuError("Error while looking for qemu-img in {}: {}".format(qemu_path_dir, e))
+
+ if not qemu_img_path:
+ raise QemuError("Could not find qemu-img in {}".format(qemu_path_dir))
+
+ try:
+ if self._hda_disk_image:
+ hda_disk = os.path.join(self._working_dir, "hda_disk.qcow2")
+ if not os.path.exists(hda_disk):
+ retcode = subprocess.call([qemu_img_path, "create", "-o",
+ "backing_file={}".format(self._hda_disk_image),
+ "-f", "qcow2", hda_disk])
+ log.info("{} returned with {}".format(qemu_img_path, retcode))
+ else:
+ # create a "FLASH" with 256MB if no disk image has been specified
+ hda_disk = os.path.join(self._working_dir, "flash.qcow2")
+ if not os.path.exists(hda_disk):
+ retcode = subprocess.call([qemu_img_path, "create", "-f", "qcow2", hda_disk, "256M"])
+ log.info("{} returned with {}".format(qemu_img_path, retcode))
+
+ except OSError as e:
+ raise QemuError("Could not create disk image {}".format(e))
+
+ options.extend(["-hda", hda_disk])
+ if self._hdb_disk_image:
+ hdb_disk = os.path.join(self._working_dir, "hdb_disk.qcow2")
+ if not os.path.exists(hdb_disk):
+ try:
+ retcode = subprocess.call([qemu_img_path, "create", "-o",
+ "backing_file={}".format(self._hdb_disk_image),
+ "-f", "qcow2", hdb_disk])
+ log.info("{} returned with {}".format(qemu_img_path, retcode))
+ except OSError as e:
+ raise QemuError("Could not create disk image {}".format(e))
+ options.extend(["-hdb", hdb_disk])
+
+ return options
+
+ def _linux_boot_options(self):
+
+ options = []
+ if self._initrd:
+ options.extend(["-initrd", self._initrd])
+ if self._kernel_image:
+ options.extend(["-kernel", self._kernel_image])
+ if self._kernel_command_line:
+ options.extend(["-append", self._kernel_command_line])
+
+ return options
+
+ def _network_options(self):
+
+ network_options = []
+ adapter_id = 0
+ for adapter in self._ethernet_adapters:
+ #TODO: let users specify a base mac address
+ mac = "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_id)
+ network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_id)])
+ nio = adapter.get_nio(0)
+ if nio and isinstance(nio, NIO_UDP):
+ network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id,
+ nio.rhost,
+ nio.rport,
+ self._host,
+ nio.lport)])
+ else:
+ network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_id)])
+ adapter_id += 1
+
+ return network_options
+
+ def _build_command(self):
+ """
+ Command to start the QEMU process.
+ (to be passed to subprocess.Popen())
+ """
+
+ command = [self._qemu_path]
+ command.extend(["-name", self._name])
+ command.extend(["-m", str(self._ram)])
+ command.extend(self._disk_options())
+ command.extend(self._linux_boot_options())
+ command.extend(self._serial_options())
+ additional_options = self._options.strip()
+ if additional_options:
+ command.extend(shlex.split(additional_options))
+ command.extend(self._network_options())
+ return command
diff --git a/gns3server/modules/qemu/schemas.py b/gns3server/modules/qemu/schemas.py
new file mode 100644
index 00000000..5b00e98a
--- /dev/null
+++ b/gns3server/modules/qemu/schemas.py
@@ -0,0 +1,393 @@
+# -*- 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 .
+
+
+QEMU_CREATE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to create a new QEMU VM instance",
+ "type": "object",
+ "properties": {
+ "name": {
+ "description": "QEMU VM instance name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "qemu_path": {
+ "description": "Path to QEMU",
+ "type": "string",
+ "minLength": 1,
+ },
+ "qemu_id": {
+ "description": "QEMU VM instance ID",
+ "type": "integer"
+ },
+ "console": {
+ "description": "console TCP port",
+ "minimum": 1,
+ "maximum": 65535,
+ "type": "integer"
+ },
+ },
+ "additionalProperties": False,
+ "required": ["name", "qemu_path"],
+}
+
+QEMU_DELETE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to delete a QEMU VM instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "QEMU VM instance ID",
+ "type": "integer"
+ },
+ },
+ "additionalProperties": False,
+ "required": ["id"]
+}
+
+QEMU_UPDATE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to update a QEMU VM instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "QEMU VM instance ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "QEMU VM instance name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "qemu_path": {
+ "description": "path to QEMU",
+ "type": "string",
+ "minLength": 1,
+ },
+ "hda_disk_image": {
+ "description": "QEMU hda disk image path",
+ "type": "string",
+ "minLength": 1,
+ },
+ "hdb_disk_image": {
+ "description": "QEMU hdb disk image path",
+ "type": "string",
+ "minLength": 1,
+ },
+ "ram": {
+ "description": "amount of RAM in MB",
+ "type": "integer"
+ },
+ "adapters": {
+ "description": "number of adapters",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 8,
+ },
+ "adapter_type": {
+ "description": "QEMU adapter type",
+ "type": "string",
+ "minLength": 1,
+ },
+ "console": {
+ "description": "console TCP port",
+ "minimum": 1,
+ "maximum": 65535,
+ "type": "integer"
+ },
+ "initrd": {
+ "description": "QEMU initrd path",
+ "type": "string",
+ "minLength": 1,
+ },
+ "kernel_image": {
+ "description": "QEMU kernel image path",
+ "type": "string",
+ "minLength": 1,
+ },
+ "kernel_command_line": {
+ "description": "QEMU kernel command line",
+ "type": "string",
+ "minLength": 1,
+ },
+ "options": {
+ "description": "additional QEMU options",
+ "type": "string",
+ },
+ },
+ "additionalProperties": False,
+ "required": ["id"]
+}
+
+QEMU_START_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to start a QEMU VM instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "QEMU VM instance ID",
+ "type": "integer"
+ },
+ },
+ "additionalProperties": False,
+ "required": ["id"]
+}
+
+QEMU_STOP_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to stop a QEMU VM instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "QEMU VM instance ID",
+ "type": "integer"
+ },
+ },
+ "additionalProperties": False,
+ "required": ["id"]
+}
+
+QEMU_SUSPEND_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to suspend a QEMU VM instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "QEMU VM instance ID",
+ "type": "integer"
+ },
+ },
+ "additionalProperties": False,
+ "required": ["id"]
+}
+
+QEMU_RELOAD_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to reload a QEMU VM instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "QEMU VM instance ID",
+ "type": "integer"
+ },
+ },
+ "additionalProperties": False,
+ "required": ["id"]
+}
+
+QEMU_ALLOCATE_UDP_PORT_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to allocate an UDP port for a QEMU VM instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "QEMU VM instance ID",
+ "type": "integer"
+ },
+ "port_id": {
+ "description": "Unique port identifier for the QEMU VM instance",
+ "type": "integer"
+ },
+ },
+ "additionalProperties": False,
+ "required": ["id", "port_id"]
+}
+
+QEMU_ADD_NIO_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to add a NIO for a QEMU 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": "QEMU VM instance ID",
+ "type": "integer"
+ },
+ "port_id": {
+ "description": "Unique port identifier for the QEMU VM instance",
+ "type": "integer"
+ },
+ "port": {
+ "description": "Port number",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 8
+ },
+ "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"]
+}
+
+
+QEMU_DELETE_NIO_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to delete a NIO for a QEMU VM instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "QEMU VM instance ID",
+ "type": "integer"
+ },
+ "port": {
+ "description": "Port number",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 8
+ },
+ },
+ "additionalProperties": False,
+ "required": ["id", "port"]
+}
diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py
index d7e0f1f9..0d2a97fc 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
@@ -60,25 +61,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
+ # get the vboxwrapper location (only non-Windows platforms)
+ if not sys.platform.startswith("win"):
+ 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(os.pathsep)
+ # look for vboxwrapper 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")
+ 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)
@@ -91,7 +93,7 @@ class VirtualBox(IModule):
self._allocated_udp_ports = []
self._udp_start_port_range = vbox_config.get("udp_start_port_range", 35001)
self._udp_end_port_range = vbox_config.get("udp_end_port_range", 35500)
- self._host = kwargs["host"]
+ self._host = vbox_config.get("host", kwargs["host"])
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
@@ -105,18 +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 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,
@@ -131,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):
"""
@@ -154,9 +172,9 @@ class VirtualBox(IModule):
"""
Returns a VirtualBox VM instance.
- :param vbox_id: VirtualBox device identifier
+ :param vbox_id: VirtualBox VM identifier
- :returns: VBoxDevice instance
+ :returns: VirtualBoxVM instance
"""
if vbox_id not in self._vbox_instances:
@@ -253,6 +271,7 @@ class VirtualBox(IModule):
Mandatory request parameters:
- name (VirtualBox VM name)
+ - vmname (VirtualBox VM name in VirtualBox)
Optional request parameters:
- console (VirtualBox VM console port)
@@ -399,7 +418,10 @@ class VirtualBox(IModule):
try:
vbox_instance.start()
except VirtualBoxError as e:
- self.send_custom_error(str(e))
+ if self._vboxwrapper:
+ self.send_custom_error("{}: {}".format(e, self._vboxwrapper.read_stderr()))
+ else:
+ self.send_custom_error(str(e))
return
self.send_response(True)
@@ -632,7 +654,7 @@ class VirtualBox(IModule):
Deletes an NIO (Network Input/Output).
Mandatory request parameters:
- - id (VPCS instance identifier)
+ - id (VirtualBox instance identifier)
- port (port identifier)
Response parameters:
@@ -667,7 +689,7 @@ class VirtualBox(IModule):
Starts a packet capture.
Mandatory request parameters:
- - id (vm identifier)
+ - id (VirtualBox VM identifier)
- port (port number)
- port_id (port identifier)
- capture_file_name
@@ -708,7 +730,7 @@ class VirtualBox(IModule):
Stops a packet capture.
Mandatory request parameters:
- - id (vm identifier)
+ - id (VirtualBox VM identifier)
- port (port number)
- port_id (port identifier)
@@ -748,7 +770,11 @@ class VirtualBox(IModule):
"""
if not self._vboxwrapper and not self._vboxmanager:
- self._start_vbox_service()
+ try:
+ self._start_vbox_service()
+ except VirtualBoxError as e:
+ self.send_custom_error(str(e))
+ return
if self._vboxwrapper:
vms = self._vboxwrapper.get_vm_list()
diff --git a/gns3server/modules/virtualbox/nios/nio.py b/gns3server/modules/virtualbox/nios/nio.py
index c85569bd..eee5f1d5 100644
--- a/gns3server/modules/virtualbox/nios/nio.py
+++ b/gns3server/modules/virtualbox/nios/nio.py
@@ -22,7 +22,7 @@ Base interface for NIOs.
class NIO(object):
"""
- IOU NIO.
+ Network Input/Output.
"""
def __init__(self):
diff --git a/gns3server/modules/virtualbox/nios/nio_udp.py b/gns3server/modules/virtualbox/nios/nio_udp.py
index 41ffbc4f..2c850351 100644
--- a/gns3server/modules/virtualbox/nios/nio_udp.py
+++ b/gns3server/modules/virtualbox/nios/nio_udp.py
@@ -24,7 +24,7 @@ from .nio import NIO
class NIO_UDP(NIO):
"""
- IOU UDP NIO.
+ UDP NIO.
:param lport: local port number
:param rhost: remote address/host
diff --git a/gns3server/modules/virtualbox/schemas.py b/gns3server/modules/virtualbox/schemas.py
index b86839a0..bc72cb9a 100644
--- a/gns3server/modules/virtualbox/schemas.py
+++ b/gns3server/modules/virtualbox/schemas.py
@@ -82,9 +82,15 @@ VBOX_UPDATE_SCHEMA = {
"adapters": {
"description": "number of adapters",
"type": "integer",
- "minimum": 0,
+ "minimum": 1,
"maximum": 36, # maximum given by the ICH9 chipset in VirtualBox
},
+ "adapter_start_index": {
+ "description": "adapter index from which to start using adapters",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 35, # maximum given by the ICH9 chipset in VirtualBox
+ },
"adapter_type": {
"description": "VirtualBox adapter type",
"type": "string",
@@ -96,6 +102,10 @@ VBOX_UPDATE_SCHEMA = {
"maximum": 65535,
"type": "integer"
},
+ "enable_console": {
+ "description": "enable the console",
+ "type": "boolean"
+ },
"headless": {
"description": "headless mode",
"type": "boolean"
diff --git a/gns3server/modules/virtualbox/vboxwrapper_client.py b/gns3server/modules/virtualbox/vboxwrapper_client.py
index 43a1743d..911f1d50 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.1"):
+ 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..9ed37953
--- /dev/null
+++ b/gns3server/modules/virtualbox/virtualbox_controller.py
@@ -0,0 +1,557 @@
+# -*- 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._enable_console = True
+ 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 enable_console(self):
+
+ return self._enable_console
+
+ @enable_console.setter
+ def enable_console(self, enable_console):
+
+ self._enable_console = enable_console
+
+ @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 len(self._adapters) > self._maximum_adapters:
+ raise VirtualBoxError("Number of adapters above the maximum supported of {}".format(self._maximum_adapters))
+
+ if self._machine.state == self._vboxmanager.constants.MachineState_Paused:
+ self.resume()
+ return
+
+ self._get_session()
+ self._set_network_options()
+ if self._enable_console:
+ 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
+
+ if self._enable_console:
+ # 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)):
+ if self._adapters[adapter_id] is None:
+ continue
+ self._disable_adapter(adapter_id, disable=True)
+ if self._enable_console:
+ serial_port = self._session.machine.getSerialPort(0)
+ serial_port.enabled = False
+ 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)
+ if self._adapters[adapter_id] is None:
+ # force enable to avoid any discrepancy in the interface numbering inside the VM
+ # e.g. Ethernet2 in GNS3 becoming eth0 inside the VM when using a start index of 2.
+ adapter.enabled = True
+ continue
+
+ 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._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
diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py
index 10319044..dee20427 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,21 +87,13 @@ 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 = []
self._headless = False
+ self._enable_console = True
self._vmname = vmname
+ self._adapter_start_index = 0
self._adapter_type = "Automatic"
working_dir_path = os.path.join(working_dir, "vbox", "vm-{}".format(self._id))
@@ -140,19 +122,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))
@@ -165,9 +139,11 @@ class VirtualBoxVM(object):
vbox_defaults = {"name": self._name,
"vmname": self._vmname,
- "adapters": len(self._ethernet_adapters),
+ "adapters": self.adapters,
+ "adapter_start_index": self._adapter_start_index,
"adapter_type": "Automatic",
"console": self._console,
+ "enable_console": self._enable_console,
"headless": self._headless}
return vbox_defaults
@@ -274,6 +250,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,13 +322,49 @@ 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
+ @property
+ def enable_console(self):
+ """
+ Returns either the console is enabled or not
+
+ :returns: boolean
+ """
+
+ return self._enable_console
+
+ @enable_console.setter
+ def enable_console(self, enable_console):
+ """
+ Sets either the console is enabled or not
+
+ :param enable_console: boolean
+ """
+
+ if enable_console:
+ if self._vboxwrapper:
+ self._vboxwrapper.send('vbox setattr "{}" enable_console True'.format(self._name))
+ else:
+ self._vboxcontroller.enable_console = True
+ log.info("VirtualBox VM {name} [id={id}] has enabled the console".format(name=self._name, id=self._id))
+ else:
+ if self._vboxwrapper:
+ self._vboxwrapper.send('vbox setattr "{}" enable_console False'.format(self._name))
+ else:
+ self._vboxcontroller.enable_console = False
+ log.info("VirtualBox VM {name} [id={id}] has disabled the console".format(name=self._name, id=self._id))
+ self._enable_console = enable_console
+
@property
def vmname(self):
"""
@@ -372,13 +386,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
@@ -402,15 +410,47 @@ class VirtualBoxVM(object):
"""
self._ethernet_adapters.clear()
- for _ in range(0, adapters):
+ for adapter_id in range(0, self._adapter_start_index + adapters):
+ if adapter_id < self._adapter_start_index:
+ self._ethernet_adapters.append(None)
+ continue
self._ethernet_adapters.append(EthernetAdapter())
if self._vboxwrapper:
- self._vboxwrapper.send('vbox setattr "{}" nics {}'.format(self._name, len(self._ethernet_adapters)))
+ self._vboxwrapper.send('vbox setattr "{}" nics {}'.format(self._name, 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,
- adapters=len(self._ethernet_adapters)))
+ adapters=adapters))
+
+ @property
+ def adapter_start_index(self):
+ """
+ Returns the adapter start index for this VirtualBox VM instance.
+
+ :returns: index
+ """
+
+ return self._adapter_start_index
+
+ @adapter_start_index.setter
+ def adapter_start_index(self, adapter_start_index):
+ """
+ Sets the adapter start index for this VirtualBox VM instance.
+
+ :param adapter_start_index: index
+ """
+
+ if self._vboxwrapper:
+ self._vboxwrapper.send('vbox setattr "{}" nic_start_index {}'.format(self._name, adapter_start_index))
+
+ self._adapter_start_index = adapter_start_index
+ self.adapters = self.adapters # this forces to recreate the adapter list with the correct index
+ log.info("VirtualBox VM {name} [id={id}]: adapter start index changed to {index}".format(name=self._name,
+ id=self._id,
+ index=adapter_start_index))
@property
def adapter_type(self):
@@ -434,6 +474,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 +487,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 +497,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 +513,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 +523,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 +533,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 +551,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 +583,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 +652,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/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py
index 61de9308..b88324a4 100644
--- a/gns3server/modules/vpcs/__init__.py
+++ b/gns3server/modules/vpcs/__init__.py
@@ -63,7 +63,7 @@ class VPCS(IModule):
vpcs_config = config.get_section_config(name.upper())
self._vpcs = vpcs_config.get("vpcs_path")
if not self._vpcs or not os.path.isfile(self._vpcs):
- paths = [os.getcwd()] + os.environ["PATH"].split(":")
+ paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for VPCS in the current working directory and $PATH
for path in paths:
try:
@@ -81,12 +81,12 @@ class VPCS(IModule):
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
self._vpcs_instances = {}
- self._console_start_port_range = vpcs_config.get("console_start_port_range", 4512)
+ self._console_start_port_range = vpcs_config.get("console_start_port_range", 4501)
self._console_end_port_range = vpcs_config.get("console_end_port_range", 5000)
self._allocated_udp_ports = []
- self._udp_start_port_range = vpcs_config.get("udp_start_port_range", 40001)
- self._udp_end_port_range = vpcs_config.get("udp_end_port_range", 40512)
- self._host = kwargs["host"]
+ self._udp_start_port_range = vpcs_config.get("udp_start_port_range", 20501)
+ self._udp_end_port_range = vpcs_config.get("udp_end_port_range", 21000)
+ self._host = vpcs_config.get("host", kwargs["host"])
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
diff --git a/gns3server/modules/vpcs/nios/nio_tap.py b/gns3server/modules/vpcs/nios/nio_tap.py
index ee550e7b..4c3ed6b2 100644
--- a/gns3server/modules/vpcs/nios/nio_tap.py
+++ b/gns3server/modules/vpcs/nios/nio_tap.py
@@ -22,7 +22,7 @@ Interface for TAP NIOs (UNIX based OSes only).
class NIO_TAP(object):
"""
- IOU TAP NIO.
+ TAP NIO.
:param tap_device: TAP device name (e.g. tap0)
"""
diff --git a/gns3server/modules/vpcs/nios/nio_udp.py b/gns3server/modules/vpcs/nios/nio_udp.py
index 3142d70e..0527f675 100644
--- a/gns3server/modules/vpcs/nios/nio_udp.py
+++ b/gns3server/modules/vpcs/nios/nio_udp.py
@@ -22,7 +22,7 @@ Interface for UDP NIOs.
class NIO_UDP(object):
"""
- IOU UDP NIO.
+ UDP NIO.
:param lport: local port number
:param rhost: remote address/host
diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py
index 33d5d8e0..d5ad8c09 100644
--- a/gns3server/modules/vpcs/vpcs_device.py
+++ b/gns3server/modules/vpcs/vpcs_device.py
@@ -338,11 +338,10 @@ class VPCSDevice(object):
"""
try:
- output = subprocess.check_output([self._path, "-v"], stderr=subprocess.STDOUT, cwd=self._working_dir)
+ output = subprocess.check_output([self._path, "-v"], cwd=self._working_dir)
match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8"))
if match:
version = match.group(1)
- print(version)
if parse_version(version) < parse_version("0.5b1"):
raise VPCSError("VPCS executable version must be >= 0.5b1")
else:
diff --git a/gns3server/version.py b/gns3server/version.py
index c206840a..c7f8b7ef 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.0beta1"
+__version__ = "1.0beta4.dev1"
__version_info__ = (1, 0, 0, -99)
diff --git a/setup.py b/setup.py
index 5da49293..1255b7ad 100644
--- a/setup.py
+++ b/setup.py
@@ -20,7 +20,7 @@ from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
-class Tox(TestCommand):
+class PyTest(TestCommand):
def finalize_options(self):
TestCommand.finalize_options(self)
@@ -29,8 +29,8 @@ class Tox(TestCommand):
def run_tests(self):
#import here, cause outside the eggs aren't loaded
- import tox
- errcode = tox.cmdline(self.test_args)
+ import pytest
+ errcode = pytest.main(self.test_args)
sys.exit(errcode)
setup(
@@ -38,8 +38,8 @@ setup(
version=__import__("gns3server").__version__,
url="http://github.com/GNS3/gns3-server",
license="GNU General Public License v3 (GPLv3)",
- tests_require=["tox"],
- cmdclass={"test": Tox},
+ tests_require=["pytest"],
+ cmdclass={"test": PyTest},
author="Jeremy Grossmann",
author_email="package-maintainer@gns3.net",
description="GNS3 server to asynchronously manage emulators",
@@ -60,7 +60,7 @@ setup(
include_package_data=True,
platforms="any",
classifiers=[
- "Development Status :: 3 - Alpha",
+ "Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Information Technology",
"Topic :: System :: Networking",
diff --git a/tests/dynamips/conftest.py b/tests/dynamips/conftest.py
index fce9f54b..ff70cd58 100644
--- a/tests/dynamips/conftest.py
+++ b/tests/dynamips/conftest.py
@@ -6,10 +6,9 @@ import os
@pytest.fixture(scope="module")
def hypervisor(request):
- cwd = os.path.dirname(os.path.abspath(__file__))
- dynamips_path = os.path.join(cwd, "dynamips.stable")
+ dynamips_path = '/usr/bin/dynamips'
print("\nStarting Dynamips Hypervisor: {}".format(dynamips_path))
- manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1", 9000)
+ manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1")
hypervisor = manager.start_new_hypervisor()
def stop():
diff --git a/tests/dynamips/test_hypervisor.py b/tests/dynamips/test_hypervisor.py
index ed0ee2ab..e6c096b8 100644
--- a/tests/dynamips/test_hypervisor.py
+++ b/tests/dynamips/test_hypervisor.py
@@ -1,6 +1,5 @@
from gns3server.modules.dynamips import Hypervisor
import time
-import os
def test_is_started(hypervisor):
@@ -10,7 +9,7 @@ def test_is_started(hypervisor):
def test_port(hypervisor):
- assert hypervisor.port == 9000
+ assert hypervisor.port == 7200
def test_host(hypervisor):
@@ -25,8 +24,7 @@ def test_working_dir(hypervisor):
def test_path(hypervisor):
- cwd = os.path.dirname(os.path.abspath(__file__))
- dynamips_path = os.path.join(cwd, "dynamips.stable")
+ dynamips_path = '/usr/bin/dynamips'
assert hypervisor.path == dynamips_path
@@ -34,11 +32,10 @@ def test_stdout():
# try to launch Dynamips on the same port
# this will fail so that we can read its stdout/stderr
- cwd = os.path.dirname(os.path.abspath(__file__))
- dynamips_path = os.path.join(cwd, "dynamips.stable")
- hypervisor = Hypervisor(dynamips_path, "/tmp", "172.0.0.1", 7200)
+ dynamips_path = '/usr/bin/dynamips'
+ hypervisor = Hypervisor(dynamips_path, "/tmp", "127.0.0.1", 7200)
hypervisor.start()
# give some time for Dynamips to start
- time.sleep(0.01)
+ time.sleep(0.1)
output = hypervisor.read_stdout()
assert output
diff --git a/tests/dynamips/test_hypervisor_manager.py b/tests/dynamips/test_hypervisor_manager.py
index f670641c..adaa79a2 100644
--- a/tests/dynamips/test_hypervisor_manager.py
+++ b/tests/dynamips/test_hypervisor_manager.py
@@ -7,10 +7,9 @@ import os
@pytest.fixture(scope="module")
def hypervisor_manager(request):
- cwd = os.path.dirname(os.path.abspath(__file__))
- dynamips_path = os.path.join(cwd, "dynamips.stable")
+ dynamips_path = '/usr/bin/dynamips'
print("\nStarting Dynamips Hypervisor: {}".format(dynamips_path))
- manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1", 9000)
+ manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1")
#manager.start_new_hypervisor()
diff --git a/tests/dynamips/test_router.py b/tests/dynamips/test_router.py
index 1affa539..ebf9835c 100644
--- a/tests/dynamips/test_router.py
+++ b/tests/dynamips/test_router.py
@@ -9,7 +9,7 @@ import base64
@pytest.fixture
def router(request, hypervisor):
- router = Router(hypervisor, "router", "c3725")
+ router = Router(hypervisor, "router", platform="c3725")
request.addfinalizer(router.delete)
return router
@@ -127,9 +127,9 @@ def test_idlepc(router):
def test_idlemax(router):
- assert router.idlemax == 1500 # default value
- router.idlemax = 500
- assert router.idlemax == 500
+ assert router.idlemax == 500 # default value
+ router.idlemax = 1500
+ assert router.idlemax == 1500
def test_idlesleep(router):
@@ -172,7 +172,7 @@ def test_confreg(router):
def test_console(router):
- assert router.console == router.hypervisor.baseconsole + router.id
+ assert router.console == 2001
new_console_port = router.console + 100
router.console = new_console_port
assert router.console == new_console_port
@@ -180,7 +180,7 @@ def test_console(router):
def test_aux(router):
- assert router.aux == router.hypervisor.baseaux + router.id
+ assert router.aux == 2501
new_aux_port = router.aux + 100
router.aux = new_aux_port
assert router.aux == new_aux_port
diff --git a/tests/iou/test_iou_device.py b/tests/iou/test_iou_device.py
index 0749c97f..58581de9 100644
--- a/tests/iou/test_iou_device.py
+++ b/tests/iou/test_iou_device.py
@@ -3,17 +3,28 @@ import os
import pytest
+def no_iou():
+ cwd = os.path.dirname(os.path.abspath(__file__))
+ iou_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin")
+
+ if os.path.isfile(iou_path):
+ return False
+ else:
+ return True
+
+
@pytest.fixture(scope="session")
def iou(request):
cwd = os.path.dirname(os.path.abspath(__file__))
iou_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin")
- iou_device = IOUDevice(iou_path, "/tmp")
+ iou_device = IOUDevice("IOU1", iou_path, "/tmp")
iou_device.start()
request.addfinalizer(iou_device.delete)
return iou_device
+@pytest.mark.skipif(no_iou(), reason="IOU Image not available")
def test_iou_is_started(iou):
print(iou.command())
@@ -21,6 +32,7 @@ def test_iou_is_started(iou):
assert iou.is_running()
+@pytest.mark.skipif(no_iou(), reason="IOU Image not available")
def test_iou_restart(iou):
iou.stop()
diff --git a/tests/vpcs/test_vpcs_device.py b/tests/vpcs/test_vpcs_device.py
index 13609506..781166b4 100644
--- a/tests/vpcs/test_vpcs_device.py
+++ b/tests/vpcs/test_vpcs_device.py
@@ -6,9 +6,13 @@ import pytest
@pytest.fixture(scope="session")
def vpcs(request):
- cwd = os.path.dirname(os.path.abspath(__file__))
- vpcs_path = os.path.join(cwd, "vpcs")
- vpcs_device = VPCSDevice(vpcs_path, "/tmp")
+ if os.path.isfile("/usr/bin/vpcs"):
+ vpcs_path = "/usr/bin/vpcs"
+ else:
+ cwd = os.path.dirname(os.path.abspath(__file__))
+ vpcs_path = os.path.join(cwd, "vpcs")
+ vpcs_device = VPCSDevice("VPCS1", vpcs_path, "/tmp")
+ vpcs_device.port_add_nio_binding(0, 'nio_tap:tap0')
vpcs_device.start()
request.addfinalizer(vpcs_device.delete)
return vpcs_device
diff --git a/tox.ini b/tox.ini
index b10796da..200e7ce4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,6 +2,6 @@
envlist = py33, py34
[testenv]
-commands = py.test [] -s tests
+commands = python setup.py test
deps = -rdev-requirements.txt