diff --git a/CHANGELOG b/CHANGELOG index de9e5d3d..91835c49 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,29 @@ # Change Log +## 1.3.3rc1 07/05/2015 + +* Return an error if an adapter slot doesn't exist on an IOS router. +* NIO NAT support for VirtualBox VMs. +* NIO NAT support for QEMU VMs (user mode back-end is used). +* Throw an error if user put an invalid port range in config file +* Turn off configuration parser interpolation +* Catch configuration file parsing errors +* Force closing the event loop to avoid warning with Python 3.4.3 +* Catch error when you can't mark a project as no longer temporary +* Catch BrokenPipeError for OSX frozen server +* Match how IOU initial-config is set for VPCS VM. +* Refactors how startup-config and private-config are handled for IOS routers. +* Catch the "WinError 0 The operation completed successfully" exception at a higher level. +* Fix temporary project not cleanup with save as +* If image is not found in VM directory look in images folder +* Ordered MAC addresses for QEMU based VMs. +* Merge remote-tracking branch 'origin/master' +* Force utf-8 configuraton files reading +* Do not list file starting with a . in upload handler +* Do not crash when closing a project if VirtualBox is not accessible +* Catch connection reset errors + + ## 1.3.2 28/04/2015 * Cleanup the VirtualBox Media Manager after closing a project. diff --git a/gns3server/config.py b/gns3server/config.py index 9bcc9038..e0086b5a 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -92,7 +92,7 @@ class Config(object): def clear(self): """Restart with a clean config""" - self._config = configparser.ConfigParser() + self._config = configparser.RawConfigParser() # Override config from command line even if we modify the config file and live reload it. self._override_config = {} @@ -135,7 +135,11 @@ class Config(object): Read the configuration files. """ - parsed_files = self._config.read(self._files) + try: + parsed_files = self._config.read(self._files, encoding="utf-8") + except configparser.Error as e: + log.error("Can't parse configuration file: %s", str(e)) + return if not parsed_files: log.warning("No configuration file could be found or read") else: diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index b82d4fa5..89800189 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -40,7 +40,7 @@ class CrashReport: Report crash to a third party service """ - DSN = "sync+https://22979234ab4749ceabce08e6da4c1476:1432c8c7a43d410b9b5bb33f8e55b2a6@app.getsentry.com/38482" + DSN = "sync+https://45147533567b4d529ca09c093758681f:12d8b456cdb34d23aba771325aa64ee6@app.getsentry.com/38482" if hasattr(sys, "frozen"): cacert = os.path.join(os.getcwd(), "cacert.pem") if os.path.isfile(cacert): diff --git a/gns3server/handlers/api/dynamips_vm_handler.py b/gns3server/handlers/api/dynamips_vm_handler.py index 13461897..75b197dc 100644 --- a/gns3server/handlers/api/dynamips_vm_handler.py +++ b/gns3server/handlers/api/dynamips_vm_handler.py @@ -27,6 +27,7 @@ from ...schemas.dynamips_vm import VM_OBJECT_SCHEMA from ...schemas.dynamips_vm import VM_CONFIGS_SCHEMA from ...schemas.dynamips_vm import VMS_LIST_SCHEMA from ...modules.dynamips import Dynamips +from ...modules.dynamips.dynamips_error import DynamipsError from ...modules.project_manager import ProjectManager DEFAULT_CHASSIS = { @@ -359,13 +360,39 @@ class DynamipsVMHandler: project_id=request.match_info["project_id"]) startup_config_base64, private_config_base64 = yield from vm.extract_config() + module_workdir = vm.project.module_working_directory(dynamips_manager.module_name.lower()) result = {} if startup_config_base64: - startup_config_content = base64.b64decode(startup_config_base64).decode(errors='replace') + startup_config_content = base64.b64decode(startup_config_base64).decode("utf-8", errors='replace') result["startup_config_content"] = startup_config_content + else: + # nvram doesn't contain anything if the router has not been started at least once + # in this case just use the startup-config file + startup_config_path = os.path.join(module_workdir, vm.startup_config) + if os.path.exists(startup_config_path): + try: + with open(startup_config_path, "rb") as f: + content = f.read().decode("utf-8", errors='replace') + if content: + result["startup_config_content"] = content + except OSError as e: + raise DynamipsError("Could not read the startup-config {}: {}".format(startup_config_path, e)) + if private_config_base64: - private_config_content = base64.b64decode(private_config_base64).decode(errors='replace') + private_config_content = base64.b64decode(private_config_base64).decode("utf-8", errors='replace') result["private_config_content"] = private_config_content + else: + # nvram doesn't contain anything if the router has not been started at least once + # in this case just use the private-config file + private_config_path = os.path.join(module_workdir, vm.private_config) + if os.path.exists(private_config_path): + try: + with open(private_config_path, "rb") as f: + content = f.read().decode("utf-8", errors='replace') + if content: + result["private_config_content"] = content + except OSError as e: + raise DynamipsError("Could not read the private-config {}: {}".format(private_config_path, e)) response.set_status(200) response.json(result) diff --git a/gns3server/handlers/api/project_handler.py b/gns3server/handlers/api/project_handler.py index e31d32bc..a67d9c57 100644 --- a/gns3server/handlers/api/project_handler.py +++ b/gns3server/handlers/api/project_handler.py @@ -81,13 +81,14 @@ class ProjectHandler: pm = ProjectManager.instance() project = pm.get_project(request.match_info["project_id"]) - project.temporary = request.json.get("temporary", project.temporary) project.name = request.json.get("name", project.name) project_path = request.json.get("path", project.path) if project_path != project.path: project.path = project_path for module in MODULES: yield from module.instance().project_moved(project) + # Very important we need to remove temporary flag after moving the project + project.temporary = request.json.get("temporary", project.temporary) response.json(project) @classmethod diff --git a/gns3server/handlers/api/qemu_handler.py b/gns3server/handlers/api/qemu_handler.py index 1b84fb4c..a703cdb1 100644 --- a/gns3server/handlers/api/qemu_handler.py +++ b/gns3server/handlers/api/qemu_handler.py @@ -248,7 +248,7 @@ class QEMUHandler: qemu_manager = Qemu.instance() vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) nio_type = request.json["type"] - if nio_type not in ("nio_udp", "nio_tap"): + if nio_type not in ("nio_udp", "nio_tap", "nio_nat"): raise HTTPConflict(text="NIO of type {} is not supported".format(nio_type)) nio = qemu_manager.create_nio(vm.qemu_path, request.json) yield from vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), nio) diff --git a/gns3server/handlers/api/virtualbox_handler.py b/gns3server/handlers/api/virtualbox_handler.py index f948fbe4..e8466a5e 100644 --- a/gns3server/handlers/api/virtualbox_handler.py +++ b/gns3server/handlers/api/virtualbox_handler.py @@ -294,7 +294,7 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) nio_type = request.json["type"] - if nio_type != "nio_udp": + if nio_type not in ("nio_udp", "nio_nat"): raise HTTPConflict(text="NIO of type {} is not supported".format(nio_type)) nio = vbox_manager.create_nio(vbox_manager.vboxmanage_path, request.json) yield from vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), nio) diff --git a/gns3server/main.py b/gns3server/main.py index 34670e10..4c964408 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -63,5 +63,6 @@ def main(): from gns3server.run import run run() + if __name__ == '__main__': main() diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 24a33066..b7f87f8b 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -34,6 +34,7 @@ from .project_manager import ProjectManager from .nios.nio_udp import NIOUDP from .nios.nio_tap import NIOTAP +from .nios.nio_nat import NIONAT from .nios.nio_generic_ethernet import NIOGenericEthernet @@ -370,6 +371,8 @@ class BaseManager: nio = NIOTAP(tap_device) elif nio_settings["type"] == "nio_generic_ethernet": nio = NIOGenericEthernet(nio_settings["ethernet_device"]) + elif nio_settings["type"] == "nio_nat": + nio = NIONAT() assert nio is not None return nio @@ -386,7 +389,16 @@ class BaseManager: img_directory = self.get_images_directory() if not os.path.isabs(path): s = os.path.split(path) - return os.path.normpath(os.path.join(img_directory, *s)) + path = os.path.normpath(os.path.join(img_directory, *s)) + + # Compatibility with old topologies we look in parent directory + # We look at first in new location + if not os.path.exists(path): + old_path = os.path.normpath(os.path.join(img_directory, '..', *s)) + if os.path.exists(old_path): + return old_path + + return path return path def get_relative_image_path(self, path): diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 3bbeaf5a..38aeac83 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -410,6 +410,8 @@ class Dynamips(BaseManager): nio = NIOVDE(node.hypervisor, control_file, local_file) elif nio_settings["type"] == "nio_null": nio = NIONull(node.hypervisor) + else: + raise aiohttp.web.HTTPConflict(text="NIO of type {} is not supported".format(nio_settings["type"])) yield from nio.create() return nio @@ -471,31 +473,42 @@ class Dynamips(BaseManager): if hasattr(vm, "set_{}".format(name)): setter = getattr(vm, "set_{}".format(name)) yield from setter(value) - elif name.startswith("slot") and value in ADAPTER_MATRIX: slot_id = int(name[-1]) adapter_name = value adapter = ADAPTER_MATRIX[adapter_name]() - if vm.slots[slot_id] and not isinstance(vm.slots[slot_id], type(adapter)): - yield from vm.slot_remove_binding(slot_id) - if not isinstance(vm.slots[slot_id], type(adapter)): - yield from vm.slot_add_binding(slot_id, adapter) + try: + if vm.slots[slot_id] and not isinstance(vm.slots[slot_id], type(adapter)): + yield from vm.slot_remove_binding(slot_id) + if not isinstance(vm.slots[slot_id], type(adapter)): + yield from vm.slot_add_binding(slot_id, adapter) + except IndexError: + raise DynamipsError("Slot {} doesn't exist on this router".format(slot_id)) elif name.startswith("slot") and value is None: slot_id = int(name[-1]) - if vm.slots[slot_id]: - yield from vm.slot_remove_binding(slot_id) + try: + if vm.slots[slot_id]: + yield from vm.slot_remove_binding(slot_id) + except IndexError: + raise DynamipsError("Slot {} doesn't exist on this router".format(slot_id)) elif name.startswith("wic") and value in WIC_MATRIX: wic_slot_id = int(name[-1]) wic_name = value wic = WIC_MATRIX[wic_name]() - if vm.slots[0].wics[wic_slot_id] and not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)): - yield from vm.uninstall_wic(wic_slot_id) - if not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)): - yield from vm.install_wic(wic_slot_id, wic) + try: + if vm.slots[0].wics[wic_slot_id] and not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)): + yield from vm.uninstall_wic(wic_slot_id) + if not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)): + yield from vm.install_wic(wic_slot_id, wic) + except IndexError: + raise DynamipsError("WIC slot {} doesn't exist on this router".format(wic_slot_id)) elif name.startswith("wic") and value is None: wic_slot_id = int(name[-1]) - if vm.slots[0].wics and vm.slots[0].wics[wic_slot_id]: - yield from vm.uninstall_wic(wic_slot_id) + try: + if vm.slots[0].wics and vm.slots[0].wics[wic_slot_id]: + yield from vm.uninstall_wic(wic_slot_id) + except IndexError: + raise DynamipsError("WIC slot {} doesn't exist on this router".format(wic_slot_id)) mmap_support = self.config.get_section_config("Dynamips").getboolean("mmap_support", True) if mmap_support is False: @@ -521,38 +534,32 @@ class Dynamips(BaseManager): default_startup_config_path = os.path.join(module_workdir, "configs", "i{}_startup-config.cfg".format(vm.dynamips_id)) default_private_config_path = os.path.join(module_workdir, "configs", "i{}_private-config.cfg".format(vm.dynamips_id)) - startup_config_content = settings.get("startup_config_content") - if startup_config_content: - startup_config_path = self._create_config(vm, startup_config_content, default_startup_config_path) + startup_config_path = settings.get("startup_config") + if startup_config_path: yield from vm.set_configs(startup_config_path) else: - startup_config_path = settings.get("startup_config") - if startup_config_path: - yield from vm.set_configs(startup_config_path) + startup_config_path = self._create_config(vm, default_startup_config_path, settings.get("startup_config_content")) + yield from vm.set_configs(startup_config_path) - private_config_content = settings.get("private_config_content") - if private_config_content: - private_config_path = self._create_config(vm, private_config_content, default_private_config_path) + private_config_path = settings.get("private_config") + if private_config_path: yield from vm.set_configs(vm.startup_config, private_config_path) else: - private_config_path = settings.get("private_config") - if private_config_path: - yield from vm.set_configs(vm.startup_config, private_config_path) + private_config_path = self._create_config(vm, default_private_config_path, settings.get("private_config_content")) + yield from vm.set_configs(vm.startup_config, private_config_path) - def _create_config(self, vm, content, path): + def _create_config(self, vm, path, content=None): """ Creates a config file. :param vm: VM instance - :param content: config content :param path: path to the destination config file + :param content: config content :returns: relative path to the created config file """ log.info("Creating config file {}".format(path)) - content = "!\n" + content.replace("\r", "") - content = content.replace('%h', vm.name) config_dir = os.path.dirname(path) try: os.makedirs(config_dir, exist_ok=True) @@ -561,7 +568,10 @@ class Dynamips(BaseManager): try: with open(path, "wb") as f: - f.write(content.encode("utf-8")) + if content: + content = "!\n" + content.replace("\r", "") + content = content.replace('%h', vm.name) + f.write(content.encode("utf-8")) except OSError as e: raise DynamipsError("Could not create config file {}: {}".format(path, e)) diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index e6ccc144..f4ef1de9 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -276,15 +276,16 @@ class DynamipsHypervisor: while True: try: try: - line = yield from self._reader.readline() + #line = yield from self._reader.readline() # this can lead to ValueError: Line is too long + chunk = yield from self._reader.read(1024) # match to Dynamips' buffer size except asyncio.CancelledError: # task has been canceled but continue to read # any remaining data sent by the hypervisor continue - if not line: + if not chunk: raise DynamipsError("No data returned from {host}:{port}, Dynamips process running: {run}" .format(host=self._host, port=self._port, run=self.is_running())) - buf += line.decode("utf-8") + buf += chunk.decode("utf-8") except OSError as e: raise DynamipsError("Lost communication with {host}:{port} :{error}, Dynamips process running: {run}" .format(host=self._host, port=self._port, error=e, run=self.is_running())) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 77045290..5816d175 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -1436,6 +1436,17 @@ class Router(BaseVM): private_config = private_config.replace("\\", '/') if self._startup_config != startup_config or self._private_config != private_config: + self._startup_config = startup_config + self._private_config = private_config + + module_workdir = self.project.module_working_directory(self.manager.module_name.lower()) + private_config_path = os.path.join(module_workdir, private_config) + try: + if not os.path.getsize(private_config_path): + # an empty private-config can prevent a router to boot. + private_config = '' + except OSError as e: + raise DynamipsError("Cannot access the private-config {}: {}".format(private_config_path, e)) yield from self._hypervisor.send('vm set_config "{name}" "{startup}" "{private}"'.format(name=self._name, startup=startup_config, @@ -1445,15 +1456,11 @@ class Router(BaseVM): id=self._id, startup=startup_config)) - self._startup_config = startup_config - if private_config: log.info('Router "{name}" [{id}]: has a new private-config set: "{private}"'.format(name=self._name, id=self._id, private=private_config)) - self._private_config = private_config - @asyncio.coroutine def extract_config(self): """ diff --git a/gns3server/modules/nios/nio_nat.py b/gns3server/modules/nios/nio_nat.py new file mode 100644 index 00000000..3293b8b8 --- /dev/null +++ b/gns3server/modules/nios/nio_nat.py @@ -0,0 +1,41 @@ +# -*- 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 NAT NIOs. +""" + +from .nio import NIO + + +class NIONAT(NIO): + + """ + NAT NIO. + """ + + def __init__(self): + + super().__init__() + + def __str__(self): + + return "NIO TAP" + + def __json__(self): + + return {"type": "nio_nat"} diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index b0f882d8..76248cd7 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -140,7 +140,7 @@ class PortManager: """ if end_port < start_port: - raise Exception("Invalid port range {}-{}".format(start_port, end_port)) + raise HTTPConflict(text="Invalid port range {}-{}".format(start_port, end_port)) if socket_type == "UDP": socket_type = socket.SOCK_DGRAM diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index edb1b6cf..c162e73e 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -139,9 +139,21 @@ class Project: if path != self._path and self.is_local() is False: raise aiohttp.web.HTTPForbidden(text="You are not allowed to modify the project directory location") + old_path = None + if hasattr(self, "_path"): + old_path = self._path + self._path = path self._update_temporary_file() + # The order of operation is important because we want to avoid losing + # data + if old_path: + try: + shutil.rmtree(old_path) + except OSError as e: + raise aiohttp.web.HTTPConflict(text="Can't remove temporary directory {}: {}".format(old_path, e)) + @property def name(self): @@ -228,7 +240,10 @@ class Project: raise aiohttp.web.HTTPInternalServerError(text="Could not create temporary project: {}".format(e)) else: if os.path.exists(os.path.join(self._path, ".gns3_temporary")): - os.remove(os.path.join(self._path, ".gns3_temporary")) + try: + os.remove(os.path.join(self._path, ".gns3_temporary")) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not mark project as no longer temporary: {}".format(e)) def module_working_directory(self, module_name): """ diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 85788a80..69015251 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -23,7 +23,6 @@ order to run a QEMU VM. import sys import os import shutil -import random import subprocess import shlex import asyncio @@ -33,6 +32,7 @@ from .qemu_error import QemuError from ..adapters.ethernet_adapter import EthernetAdapter from ..nios.nio_udp import NIOUDP from ..nios.nio_tap import NIOTAP +from ..nios.nio_nat import NIONAT from ..base_vm import BaseVM from ...schemas.qemu import QEMU_OBJECT_SCHEMA @@ -981,46 +981,47 @@ class QemuVM(BaseVM): return options - def _get_random_mac(self, adapter_number): - # TODO: let users specify a base mac address - return "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_number) - def _network_options(self): network_options = [] - adapter_number = 0 - for adapter in self._ethernet_adapters: - mac = self._get_random_mac(adapter_number) - if self._legacy_networking: - network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, self._adapter_type)]) - else: - network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_number)]) + network_options.extend(["-net", "none"]) # we do not want any user networking back-end if no adapter is connected. + for adapter_number, adapter in enumerate(self._ethernet_adapters): + # TODO: let users specify a base mac address + mac = "00:00:ab:%s:%s:%02x" % (self.id[-4:-2], self.id[-2:], adapter_number) nio = adapter.get_nio(0) - if nio: - if isinstance(nio, NIOUDP): - if self._legacy_networking: + if self._legacy_networking: + # legacy QEMU networking syntax (-net) + if nio: + network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, self._adapter_type)]) + if isinstance(nio, NIOUDP): network_options.extend(["-net", "udp,vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_number, adapter_number, nio.lport, nio.rport, nio.rhost)]) - else: + elif isinstance(nio, NIOTAP): + network_options.extend(["-net", "tap,name=gns3-{},ifname={}".format(adapter_number, nio.tap_device)]) + elif isinstance(nio, NIONAT): + network_options.extend(["-net", "user,vlan={},name=gns3-{}".format(adapter_number, adapter_number)]) + else: + network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, self._adapter_type)]) + + else: + # newer QEMU networking syntax + if nio: + network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_number)]) + if isinstance(nio, NIOUDP): network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_number, nio.rhost, nio.rport, self._host, nio.lport)]) - elif isinstance(nio, NIOTAP): - if self._legacy_networking: - network_options.extend(["-net", "tap,name=gns3-{},ifname={}".format(adapter_number, nio.tap_device)]) - else: + elif isinstance(nio, NIOTAP): network_options.extend(["-netdev", "tap,id=gns3-{},ifname={}".format(adapter_number, nio.tap_device)]) - else: - if self._legacy_networking: - network_options.extend(["-net", "user,vlan={},name=gns3-{}".format(adapter_number, adapter_number)]) + elif isinstance(nio, NIONAT): + network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_number)]) else: - network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_number)]) - adapter_number += 1 + network_options.extend(["-device", "{},mac={}".format(self._adapter_type, mac)]) return network_options diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index ed1580ab..320ed9c7 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -31,6 +31,7 @@ import asyncio from pkg_resources import parse_version from .virtualbox_error import VirtualBoxError from ..nios.nio_udp import NIOUDP +from ..nios.nio_nat import NIONAT from ..adapters.ethernet_adapter import EthernetAdapter from .telnet_server import TelnetServer # TODO: port TelnetServer to asyncio from ..base_vm import BaseVM @@ -659,12 +660,12 @@ class VirtualBoxVM(BaseVM): yield from self._modify_vm("--cableconnected{} off".format(adapter_number + 1)) nio = self._ethernet_adapters[adapter_number].get_nio(0) if nio: - if not self._use_any_adapter and attachment not in ("none", "null", "generic"): + if not isinstance(nio, NIONAT) and not self._use_any_adapter and attachment not in ("none", "null", "generic"): raise VirtualBoxError("Attachment ({}) already configured on adapter {}. " "Please set it to 'Not attached' to allow GNS3 to use it.".format(attachment, adapter_number + 1)) - yield from self._modify_vm("--nictrace{} off".format(adapter_number + 1)) + yield from self._modify_vm("--nictrace{} off".format(adapter_number + 1)) vbox_adapter_type = "82540EM" if self._adapter_type == "PCnet-PCI II (Am79C970A)": vbox_adapter_type = "Am79C970A" @@ -681,13 +682,17 @@ class VirtualBoxVM(BaseVM): args = [self._vmname, "--nictype{}".format(adapter_number + 1), vbox_adapter_type] yield from self.manager.execute("modifyvm", args) - log.debug("setting UDP params on adapter {}".format(adapter_number)) - yield from self._modify_vm("--nic{} generic".format(adapter_number + 1)) - yield from self._modify_vm("--nicgenericdrv{} UDPTunnel".format(adapter_number + 1)) - yield from self._modify_vm("--nicproperty{} sport={}".format(adapter_number + 1, nio.lport)) - yield from self._modify_vm("--nicproperty{} dest={}".format(adapter_number + 1, nio.rhost)) - yield from self._modify_vm("--nicproperty{} dport={}".format(adapter_number + 1, nio.rport)) - yield from self._modify_vm("--cableconnected{} on".format(adapter_number + 1)) + if isinstance(nio, NIOUDP): + log.debug("setting UDP params on adapter {}".format(adapter_number)) + yield from self._modify_vm("--nic{} generic".format(adapter_number + 1)) + yield from self._modify_vm("--nicgenericdrv{} UDPTunnel".format(adapter_number + 1)) + yield from self._modify_vm("--nicproperty{} sport={}".format(adapter_number + 1, nio.lport)) + yield from self._modify_vm("--nicproperty{} dest={}".format(adapter_number + 1, nio.rhost)) + yield from self._modify_vm("--nicproperty{} dport={}".format(adapter_number + 1, nio.rport)) + yield from self._modify_vm("--cableconnected{} on".format(adapter_number + 1)) + elif isinstance(nio, NIONAT): + yield from self._modify_vm("--nic{} nat".format(adapter_number + 1)) + yield from self._modify_vm("--cableconnected{} on".format(adapter_number + 1)) if nio.capturing: yield from self._modify_vm("--nictrace{} on".format(adapter_number + 1)) diff --git a/gns3server/modules/vpcs/vpcs_vm.py b/gns3server/modules/vpcs/vpcs_vm.py index cb7229d7..2f001ced 100644 --- a/gns3server/modules/vpcs/vpcs_vm.py +++ b/gns3server/modules/vpcs/vpcs_vm.py @@ -181,15 +181,15 @@ class VPCSVM(BaseVM): """ try: - script_file = os.path.join(self.working_dir, 'startup.vpc') - with open(script_file, "wb+") as f: + startup_script_path = os.path.join(self.working_dir, 'startup.vpc') + with open(startup_script_path, "w+", encoding='utf-8') as f: if startup_script is None: - f.write(b'') + f.write('') else: startup_script = startup_script.replace("%h", self._name) - f.write(startup_script.encode("utf-8")) + f.write(startup_script) except OSError as e: - raise VPCSError('Cannot write the startup script file "{}": {}'.format(self.script_file, e)) + raise VPCSError('Cannot write the startup script file "{}": {}'.format(startup_script_path, e)) @asyncio.coroutine def _check_vpcs_version(self): diff --git a/gns3server/run.py b/gns3server/run.py index 54953e29..52afb695 100644 --- a/gns3server/run.py +++ b/gns3server/run.py @@ -225,6 +225,10 @@ def run(): server = Server.instance(host, port) try: server.run() + except OSError as e: + # This is to ignore OSError: [WinError 0] The operation completed successfully exception on Windows. + if not sys.platform.startswith("win") and not e.winerror == 0: + raise except Exception as e: log.critical("Critical error while running the server: {}".format(e), exc_info=1) CrashReport.instance().capture_exception() diff --git a/gns3server/schemas/dynamips_vm.py b/gns3server/schemas/dynamips_vm.py index 78014a47..f505ac3c 100644 --- a/gns3server/schemas/dynamips_vm.py +++ b/gns3server/schemas/dynamips_vm.py @@ -745,7 +745,6 @@ VM_CONFIGS_SCHEMA = { }, }, "additionalProperties": False, - "required": ["startup_config_content", "private_config_content"] } VMS_LIST_SCHEMA = { diff --git a/gns3server/schemas/nio.py b/gns3server/schemas/nio.py index 52a97c66..ca701314 100644 --- a/gns3server/schemas/nio.py +++ b/gns3server/schemas/nio.py @@ -78,6 +78,16 @@ NIO_SCHEMA = { "required": ["type", "ethernet_device"], "additionalProperties": False }, + "NAT": { + "description": "NAT Network Input/Output", + "properties": { + "type": { + "enum": ["nio_nat"] + }, + }, + "required": ["type"], + "additionalProperties": False + }, "TAP": { "description": "TAP Network Input/Output", "properties": { @@ -148,6 +158,7 @@ NIO_SCHEMA = { {"$ref": "#/definitions/UDP"}, {"$ref": "#/definitions/Ethernet"}, {"$ref": "#/definitions/LinuxEthernet"}, + {"$ref": "#/definitions/NAT"}, {"$ref": "#/definitions/TAP"}, {"$ref": "#/definitions/UNIX"}, {"$ref": "#/definitions/VDE"}, diff --git a/gns3server/server.py b/gns3server/server.py index 0dd5d641..77c194a9 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -27,6 +27,7 @@ import aiohttp import functools import types import time +import atexit from .web.route import Route from .web.request_handler import RequestHandler @@ -173,6 +174,18 @@ class Server: return yield from embed(globals(), locals(), return_asyncio_coroutine=True, patch_stdout=True) + def _exit_handling(self): + def close_asyncio_loop(): + loop = None + try: + loop = asyncio.get_event_loop() + except AttributeError: + pass + if loop is not None: + loop.close() + + atexit.register(close_asyncio_loop) + def run(self): """ Starts the server. @@ -216,6 +229,8 @@ class Server: self._loop.run_until_complete(self._run_application(self._handler, ssl_context)) self._signal_handling() + self._exit_handling() + if server_config.getboolean("live"): log.info("Code live reload is enabled, watching for file changes") self._loop.call_later(1, self._reload_hook) @@ -225,11 +240,6 @@ class Server: try: self._loop.run_forever() - except OSError as e: - # This is to ignore OSError: [WinError 0] The operation completed successfully - # exception on Windows. - if not sys.platform.startswith("win") and not e.winerror == 0: - raise except TypeError as e: # This is to ignore an asyncio.windows_events exception # on Windows when the process gets the SIGBREAK signal diff --git a/gns3server/web/logger.py b/gns3server/web/logger.py index 90865f25..1fe40ffa 100644 --- a/gns3server/web/logger.py +++ b/gns3server/web/logger.py @@ -74,6 +74,9 @@ class ColouredStreamHandler(logging.StreamHandler): stream.write(msg) stream.write(self.terminator) self.flush() + # On OSX when frozen flush raise a BrokenPipeError + except BrokenPipeError: + pass except Exception: self.handleError(record) diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py index a8898b26..59dd6c26 100644 --- a/tests/modules/qemu/test_qemu_vm.py +++ b/tests/modules/qemu/test_qemu_vm.py @@ -260,24 +260,23 @@ def test_control_vm_expect_text(vm, loop, running_subprocess_mock): def test_build_command(vm, loop, fake_qemu_binary, port_manager): os.environ["DISPLAY"] = "0:0" - with patch("gns3server.modules.qemu.qemu_vm.QemuVM._get_random_mac", return_value="00:00:ab:7e:b5:00"): - with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: - cmd = loop.run_until_complete(asyncio.async(vm._build_command())) - assert cmd == [ - fake_qemu_binary, - "-name", - "test", - "-m", - "256", - "-hda", - os.path.join(vm.working_dir, "flash.qcow2"), - "-serial", - "telnet:127.0.0.1:{},server,nowait".format(vm.console), - "-device", - "e1000,mac=00:00:ab:7e:b5:00,netdev=gns3-0", - "-netdev", - "user,id=gns3-0" - ] + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: + cmd = loop.run_until_complete(asyncio.async(vm._build_command())) + assert cmd == [ + fake_qemu_binary, + "-name", + "test", + "-m", + "256", + "-hda", + os.path.join(vm.working_dir, "flash.qcow2"), + "-serial", + "telnet:127.0.0.1:{},server,nowait".format(vm.console), + "-device", + "e1000,mac=00:00:ab:0e:0f:00,netdev=gns3-0", + "-netdev", + "user,id=gns3-0" + ] @pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows") diff --git a/tests/modules/test_manager.py b/tests/modules/test_manager.py index 30fab2cd..2a0e94d2 100644 --- a/tests/modules/test_manager.py +++ b/tests/modules/test_manager.py @@ -96,10 +96,16 @@ def test_get_abs_image_path(qemu, tmpdir): with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}): assert qemu.get_abs_image_path(path1) == path1 + assert qemu.get_abs_image_path("test1.bin") == path1 assert qemu.get_abs_image_path(path2) == path2 assert qemu.get_abs_image_path("test2.bin") == path2 assert qemu.get_abs_image_path("../test1.bin") == path1 + # We look at first in new location + path2 = str(tmpdir / "QEMU" / "test1.bin") + open(path2, 'w+').close() + assert qemu.get_abs_image_path("test1.bin") == path2 + def test_get_relative_image_path(qemu, tmpdir): os.makedirs(str(tmpdir / "QEMU")) @@ -111,6 +117,7 @@ def test_get_relative_image_path(qemu, tmpdir): with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}): assert qemu.get_relative_image_path(path1) == path1 + assert qemu.get_relative_image_path("test1.bin") == path1 assert qemu.get_relative_image_path(path2) == "test2.bin" assert qemu.get_relative_image_path("test2.bin") == "test2.bin" assert qemu.get_relative_image_path("../test1.bin") == path1 diff --git a/tests/modules/test_port_manager.py b/tests/modules/test_port_manager.py index 05a2ac62..2f3b791e 100644 --- a/tests/modules/test_port_manager.py +++ b/tests/modules/test_port_manager.py @@ -45,3 +45,13 @@ def test_release_udp_port(): pm.reserve_udp_port(4242, project) pm.release_udp_port(4242, project) pm.reserve_udp_port(4242, project) + + +def test_find_unused_port(): + p = PortManager().find_unused_port(1000, 10000) + assert p is not None + + +def test_find_unused_port_invalid_range(): + with pytest.raises(aiohttp.web.HTTPConflict): + p = PortManager().find_unused_port(10000, 1000) diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 60ec1765..c4eaee4b 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -69,15 +69,14 @@ def test_changing_path_temporary_flag(tmpdir): with patch("gns3server.modules.project.Project.is_local", return_value=True): p = Project(temporary=True) assert os.path.exists(p.path) + original_path = p.path assert os.path.exists(os.path.join(p.path, ".gns3_temporary")) - p.temporary = False - assert not os.path.exists(os.path.join(p.path, ".gns3_temporary")) - - with open(str(tmpdir / ".gns3_temporary"), "w+") as f: - f.write("1") p.path = str(tmpdir) + p.temporary = False + assert not os.path.exists(os.path.join(p.path, ".gns3_temporary")) assert not os.path.exists(os.path.join(str(tmpdir), ".gns3_temporary")) + assert not os.path.exists(original_path) def test_temporary_path():