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