diff --git a/CHANGELOG b/CHANGELOG index 3b6baac5..120f8721 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,77 @@ # Change Log +## 2.0.0rc4 20/04/2017 + +* Fix a race condition when handling error at project opening +* Fix an issue with editing network on windows +* Fix windows tests +* Catch timeout error on docker +* typing is already included in Py >= 3.5 (#979) +* Fix import of some old topologies +* Fix AttributeError: 'NoneType' object has no attribute 'returncode' +* Fix ghost vmware vms +* Fix required field in schema not use +* Catch error and log them when we can't write the config +* Fix bridge 'bridge0' already exist when we have trouble with a container +* Catch an error at startup when the remote GNS3 VM is not a real GNS3 VM +* Fixes Qemu sata option. Ref #875. +* Catch GNS3 VM loading error at startup + +## 1.5.4 13/04/2017 + +* Fix VPCS tests for recent version +* Freeze server dependencies to the same version used for 1.5.3 +* Fix 1.5: Error message, when stopping IOU router #769 +* Drop color logging for remote install, seem to fail in some conditions +* Cleanup the remote install script +* Support for Xenial in remote install + +## 2.0.0rc3 31/03/2017 + +* Support IOU image without .bin at the end +* Allow to change some properties of an already connected ethernet switch +* Ensure we start only one ubridge +* Catch some broken hostname for compute node +* Fix limit of 20 docker containers +* Fix race conditions in creation of Frame Relay Switch +* Fix conversion of project from 1.X with custom symbol for cloud +* Dissallow parallel pull of docker images +* Add a scripts for running current dev version on GNS3 VM +* Fix a crash with missing size in the svg files +* Fix an utf8 error in auth code +* Improve vmrun timeout message +* Support utf-8 characters in user and password for auth +* Handle password configuration change on remote servers +* Fix Bug when delete fake-running VMBox +* Fix Can't connect to compute local on some computers +* Add a modification uuid to settings returned by the server +* Check python version in setup.py only for install +* Fix Session is closed when listing docker images +* Cleanup docker source code +* Use aiohttp session for docker queries +* Escape special characters from SVG text +* Fix some port short name display issues +* Catch server disconnected errors from computes +* Generate a node uuid if the uuid is missing in the .gns3 +* Ensure to dump project before exporting it +* Fix return code check for SIGSEGV of IOU images +* Prevent vmname change for VirtualBox linked clone +* Upgrade to aiohttp 1.3.5 to solve issue with big file +* Handle some invalid svg +* Try to fix some 1.3 topology with corrupted data +* Fix ComputeError: Can't connect to Main server +* Catch error when the server as trouble to access to itself +* Catch a timeout error in docker +* Lock yarl version because 0.10 is not compatible with aiohttp 1.3 +* Raise error if image are not avaible on main server during export +* Fix a race condition when killing ubridge +* If your settings from 1.X are broken with skip them at import +* Catch a permission error on symbols +* Catch unicode error when you try to duplicate a project with invalid characters +* Catch error when you try to put an invalid server url +* Fix an error when handling ubridge errors +* Fix crash when handling an error in project creation + ## 2.0.0rc2 10/03/2017 * Drop color logging for remote install, seem to fail in some conditions diff --git a/README.rst b/README.rst index e3ebfde5..8d6fd28b 100644 --- a/README.rst +++ b/README.rst @@ -216,3 +216,8 @@ If you want test coverage: .. code:: bash py.test --cov-report term-missing --cov=gns3server + +Security issues +---------------- +Please contact us using contact informations available here: +http://docs.gns3.com/1ON9JBXSeR7Nt2-Qum2o3ZX0GU86BZwlmNSUgvmqNWGY/index.html diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..d35cafe6 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,21 @@ +version: '{build}-{branch}' + +image: Visual Studio 2015 + +platform: x64 + +environment: + PYTHON: "C:\\Python36-x64" + DISTUTILS_USE_SDK: "1" + API_TOKEN: + secure: VEKn4bYH3QO0ixtQW5ni4Enmn8cS1NlZV246ludBDgQ= + +install: + - cinst nmap + - "%PYTHON%\\python.exe -m pip install -r dev-requirements.txt" + - "%PYTHON%\\python.exe -m pip install -r win-requirements.txt" + +build: off + +test_script: + - "%PYTHON%\\python.exe -m pytest -v" diff --git a/dev-requirements.txt b/dev-requirements.txt index 4cfd2926..7e3a31c6 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,6 +1,6 @@ -rrequirements.txt -sphinx==1.5.3 +sphinx==1.5.5 pytest==3.0.7 pep8==1.7.0 pytest-catchlog==1.2.2 diff --git a/gns3server/compute/docker/__init__.py b/gns3server/compute/docker/__init__.py index 835e7cd1..53b4a7af 100644 --- a/gns3server/compute/docker/__init__.py +++ b/gns3server/compute/docker/__init__.py @@ -20,10 +20,10 @@ Docker server module. """ import sys +import json import asyncio import logging import aiohttp -import json from gns3server.utils import parse_version from gns3server.utils.asyncio import locked_coroutine from gns3server.compute.base_manager import BaseManager @@ -33,7 +33,7 @@ from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Err log = logging.getLogger(__name__) -DOCKER_MINIMUM_API_VERSION = "1.21" +DOCKER_MINIMUM_API_VERSION = "1.25" class Docker(BaseManager): @@ -113,7 +113,7 @@ class Docker(BaseManager): :returns: HTTP response """ data = json.dumps(data) - url = "http://docker/" + path + url = "http://docker/v" + DOCKER_MINIMUM_API_VERSION + "/" + path if timeout is None: timeout = 60 * 60 * 24 * 31 # One month timeout @@ -134,6 +134,8 @@ class Docker(BaseManager): ) except (aiohttp.ClientResponseError, aiohttp.ClientOSError) as e: raise DockerError("Docker has returned an error: {}".format(str(e))) + except (asyncio.TimeoutError): + raise DockerError("Docker timeout " + method + " " + path) if response.status >= 300: body = yield from response.read() try: @@ -187,7 +189,10 @@ class Docker(BaseManager): # The pull api will stream status via an HTTP JSON stream content = "" while True: - chunk = yield from response.content.read(1024) + try: + chunk = yield from response.content.read(1024) + except aiohttp.errors.ServerDisconnectedError: + break if not chunk: break content += chunk.decode("utf-8") diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 0ac110ac..c8ebd66f 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -361,6 +361,7 @@ class DockerVM(BaseNode): try: yield from self._add_ubridge_connection(nio, adapter_number) except UbridgeNamespaceError: + log.error("Container {} failed to start", self.name) yield from self.stop() # The container can crash soon after the start, this means we can not move the interface to the container namespace @@ -517,6 +518,8 @@ class DockerVM(BaseNode): state = yield from self._get_container_state() if state == "running": return True + if self.status == "started": # The container crashed we need to clean + yield from self.stop() return False @asyncio.coroutine diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 31688319..f0266632 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -1379,7 +1379,7 @@ class QemuVM(BaseNode): # special case, sata controller doesn't exist in Qemu options.extend(["-device", 'ahci,id=ahci{},bus=pci.{}'.format(disk_index, disk_index)]) options.extend(["-drive", 'file={},if=none,id=drive-sata-disk{},index={},media=disk'.format(disk, disk_index, disk_index)]) - options.extend(["-device", 'ide-drive,drive=drive-sata-disk{},bus=ahci{}.0'.format(disk_index, disk_index)]) + options.extend(["-device", 'ide-drive,drive=drive-sata-disk{},bus=ahci{}.0,id=drive-sata-disk{}'.format(disk_index, disk_index, disk_index)]) else: options.extend(["-drive", 'file={},if={},index={},media=disk'.format(disk, interface, disk_index)]) diff --git a/gns3server/compute/vmware/__init__.py b/gns3server/compute/vmware/__init__.py index db0591d5..4a4082c5 100644 --- a/gns3server/compute/vmware/__init__.py +++ b/gns3server/compute/vmware/__init__.py @@ -588,8 +588,9 @@ class VMware(BaseManager): for vm_settings in vm_entries.values(): if "displayname" in vm_settings and "config" in vm_settings: - log.debug('Found VM named "{}" with VMX file "{}"'.format(vm_settings["displayname"], vm_settings["config"])) - vmware_vms.append({"vmname": vm_settings["displayname"], "vmx_path": vm_settings["config"]}) + if os.path.exists(vm_settings["config"]): + log.debug('Found VM named "{}" with VMX file "{}"'.format(vm_settings["displayname"], vm_settings["config"])) + vmware_vms.append({"vmname": vm_settings["displayname"], "vmx_path": vm_settings["config"]}) return vmware_vms def _get_vms_from_directory(self, directory): diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 048b7489..22268452 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -35,6 +35,7 @@ from ..version import __version__ from .topology import load_topology from .gns3vm import GNS3VM from ..utils.get_resource import get_resource +from .gns3vm.gns3_vm_error import GNS3VMError import logging log = logging.getLogger(__name__) @@ -159,10 +160,13 @@ class Controller: for c in computes: try: yield from self.add_compute(**c) - except aiohttp.web_exceptions.HTTPConflict: + except (aiohttp.web_exceptions.HTTPConflict): pass # Skip not available servers at loading yield from self.load_projects() - yield from self.gns3vm.auto_start_vm() + try: + yield from self.gns3vm.auto_start_vm() + except GNS3VMError as e: + log.warn(str(e)) yield from self._project_auto_open() def _update_config(self): @@ -215,9 +219,12 @@ class Controller: "password": c.password, "compute_id": c.id }) - os.makedirs(os.path.dirname(self._config_file), exist_ok=True) - with open(self._config_file, 'w+') as f: - json.dump(data, f, indent=4) + try: + os.makedirs(os.path.dirname(self._config_file), exist_ok=True) + with open(self._config_file, 'w+') as f: + json.dump(data, f, indent=4) + except OSError as e: + log.error("Can't write the configuration {}: {}".format(self._config_file, str(e))) @asyncio.coroutine def _load_controller_settings(self): diff --git a/gns3server/controller/gns3vm/__init__.py b/gns3server/controller/gns3vm/__init__.py index 73a759fb..bace3022 100644 --- a/gns3server/controller/gns3vm/__init__.py +++ b/gns3server/controller/gns3vm/__init__.py @@ -18,6 +18,7 @@ import sys import copy import asyncio +import aiohttp from ...utils.asyncio import locked_coroutine from .vmware_gns3_vm import VMwareGNS3VM @@ -242,10 +243,13 @@ class GNS3VM: yield from self.start() except GNS3VMError as e: # User will receive the error later when they will try to use the node - yield from self._controller.add_compute(compute_id="vm", - name="GNS3 VM ({})".format(self.current_engine().vmname), - host=None, - force=True) + try: + yield from self._controller.add_compute(compute_id="vm", + name="GNS3 VM ({})".format(self.current_engine().vmname), + host=None, + force=True) + except aiohttp.web.HTTPConflict: + pass log.error("Can't start the GNS3 VM: {}", str(e)) @asyncio.coroutine diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index a182291a..fc5161e8 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -674,7 +674,7 @@ class Project: self.dump() # We catch all error to be able to rollback the .gns3 to the previous state except Exception as e: - for compute in self._project_created_on_compute: + for compute in list(self._project_created_on_compute): try: yield from compute.post("/projects/{}/close".format(self._id)) # We don't care if a compute is down at this step diff --git a/gns3server/controller/topology.py b/gns3server/controller/topology.py index 10e4de6a..32555eb3 100644 --- a/gns3server/controller/topology.py +++ b/gns3server/controller/topology.py @@ -291,8 +291,11 @@ def _convert_1_3_later(topo, topo_path): except KeyError: node["compute_id"] = "local" node["console_type"] = old_node["properties"].get("console_type", "telnet") - node["name"] = old_node["label"]["text"] - node["label"] = _convert_label(old_node["label"]) + if "label" in old_node: + node["name"] = old_node["label"]["text"] + node["label"] = _convert_label(old_node["label"]) + else: + node["name"] = old_node["properties"]["name"] node["node_id"] = old_node.get("vm_id", str(uuid.uuid4())) node["symbol"] = old_node.get("symbol", None) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index defbe8e1..6b486de1 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -54,7 +54,7 @@ class CrashReport: Report crash to a third party service """ - DSN = "sync+https://7c028290d17b4035916285b304d42311:ddf752e704c7423cacab93f8e34f713c@sentry.io/38482" + DSN = "sync+https://19cca90b55874be5862caf9b507fbd7b:1c0897efd092467a874e89b2e4803b29@sentry.io/38482" if hasattr(sys, "frozen"): cacert = get_resource("cacert.pem") if cacert is not None and os.path.isfile(cacert): diff --git a/gns3server/handlers/api/controller/node_handler.py b/gns3server/handlers/api/controller/node_handler.py index d9a715f0..50166e88 100644 --- a/gns3server/handlers/api/controller/node_handler.py +++ b/gns3server/handlers/api/controller/node_handler.py @@ -20,6 +20,7 @@ import aiohttp from gns3server.web.route import Route from gns3server.controller import Controller +from gns3server.utils import force_unix_path from gns3server.schemas.node import ( NODE_OBJECT_SCHEMA, @@ -337,7 +338,7 @@ class NodeHandler: project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"]) node = project.get_node(request.match_info["node_id"]) path = request.match_info["path"] - path = os.path.normpath(path) + path = force_unix_path(path) # Raise error if user try to escape if path[0] == ".": diff --git a/gns3server/handlers/api/vpcs_handler.py b/gns3server/handlers/api/vpcs_handler.py new file mode 100644 index 00000000..6af82ad7 --- /dev/null +++ b/gns3server/handlers/api/vpcs_handler.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 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 aiohttp.web import HTTPConflict +from ...web.route import Route +from ...schemas.nio import NIO_SCHEMA +from ...schemas.vpcs import VPCS_CREATE_SCHEMA +from ...schemas.vpcs import VPCS_UPDATE_SCHEMA +from ...schemas.vpcs import VPCS_OBJECT_SCHEMA +from ...modules.vpcs import VPCS + + +class VPCSHandler: + + """ + API entry points for VPCS. + """ + + @classmethod + @Route.post( + r"/projects/{project_id}/vpcs/vms", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 409: "Conflict" + }, + description="Create a new VPCS instance", + input=VPCS_CREATE_SCHEMA, + output=VPCS_OBJECT_SCHEMA) + def create(request, response): + + vpcs = VPCS.instance() + vm = yield from vpcs.create_vm(request.json["name"], + request.match_info["project_id"], + request.json.get("vm_id"), + console=request.json.get("console"), + startup_script=request.json.get("startup_script")) + response.set_status(201) + response.json(vm) + + @classmethod + @Route.get( + r"/projects/{project_id}/vpcs/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 200: "Success", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Get a VPCS instance", + output=VPCS_OBJECT_SCHEMA) + def show(request, response): + + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + response.json(vm) + + @classmethod + @Route.put( + r"/projects/{project_id}/vpcs/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 200: "Instance updated", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Conflict" + }, + description="Update a VPCS instance", + input=VPCS_UPDATE_SCHEMA, + output=VPCS_OBJECT_SCHEMA) + def update(request, response): + + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + vm.name = request.json.get("name", vm.name) + vm.console = request.json.get("console", vm.console) + vm.startup_script = request.json.get("startup_script", vm.startup_script) + response.json(vm) + + @classmethod + @Route.delete( + r"/projects/{project_id}/vpcs/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Delete a VPCS instance") + def delete(request, response): + + yield from VPCS.instance().delete_vm(request.match_info["vm_id"]) + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/vpcs/vms/{vm_id}/start", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance started", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Start a VPCS instance", + output=VPCS_OBJECT_SCHEMA) + def start(request, response): + + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.start() + response.json(vm) + + @classmethod + @Route.post( + r"/projects/{project_id}/vpcs/vms/{vm_id}/stop", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance stopped", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Stop a VPCS instance") + def stop(request, response): + + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.stop() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/vpcs/vms/{vm_id}/reload", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + }, + status_codes={ + 204: "Instance reloaded", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Reload a VPCS instance") + def reload(request, response): + + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.reload() + response.set_status(204) + + @Route.post( + r"/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Network adapter where the nio is located", + "port_number": "Port where the nio should be added" + }, + status_codes={ + 201: "NIO created", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Add a NIO to a VPCS instance", + input=NIO_SCHEMA, + output=NIO_SCHEMA) + def create_nio(request, response): + + vpcs_manager = VPCS.instance() + vm = vpcs_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"): + raise HTTPConflict(text="NIO of type {} is not supported".format(nio_type)) + nio = vpcs_manager.create_nio(vm.vpcs_path(), request.json) + vm.port_add_nio_binding(int(request.match_info["port_number"]), nio) + response.set_status(201) + response.json(nio) + + @classmethod + @Route.delete( + r"/projects/{project_id}/vpcs/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Network adapter where the nio is located", + "port_number": "Port from where the nio should be removed" + }, + status_codes={ + 204: "NIO deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Remove a NIO from a VPCS instance") + def delete_nio(request, response): + + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + vm.port_remove_nio_binding(int(request.match_info["port_number"])) + response.set_status(204) diff --git a/gns3server/run.py b/gns3server/run.py index ccfb1a9d..edcc6cb6 100644 --- a/gns3server/run.py +++ b/gns3server/run.py @@ -163,7 +163,7 @@ def pid_lock(path): pid = int(f.read()) try: os.kill(pid, 0) # If the proces is not running kill return an error - except OSError: + except (OSError, SystemError): pid = None except OSError as e: log.critical("Can't open pid file %s: %s", pid, str(e)) diff --git a/gns3server/schemas/atm_switch.py b/gns3server/schemas/atm_switch.py index e6a12f97..17cb109b 100644 --- a/gns3server/schemas/atm_switch.py +++ b/gns3server/schemas/atm_switch.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import copy ATM_SWITCH_CREATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", @@ -81,5 +82,5 @@ ATM_SWITCH_OBJECT_SCHEMA = { "required": ["name", "node_id", "project_id"] } -ATM_SWITCH_UPDATE_SCHEMA = ATM_SWITCH_OBJECT_SCHEMA +ATM_SWITCH_UPDATE_SCHEMA = copy.deepcopy(ATM_SWITCH_OBJECT_SCHEMA) del ATM_SWITCH_UPDATE_SCHEMA["required"] diff --git a/gns3server/schemas/cloud.py b/gns3server/schemas/cloud.py index a67d8d35..aaf58467 100644 --- a/gns3server/schemas/cloud.py +++ b/gns3server/schemas/cloud.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import copy from .port import PORT_OBJECT_SCHEMA HOST_INTERFACE_SCHEMA = { @@ -136,5 +137,5 @@ CLOUD_OBJECT_SCHEMA = { "required": ["name", "node_id", "project_id", "ports_mapping"] } -CLOUD_UPDATE_SCHEMA = CLOUD_OBJECT_SCHEMA +CLOUD_UPDATE_SCHEMA = copy.deepcopy(CLOUD_OBJECT_SCHEMA) del CLOUD_UPDATE_SCHEMA["required"] diff --git a/gns3server/schemas/compute.py b/gns3server/schemas/compute.py index 7d0d65ab..8e44de50 100644 --- a/gns3server/schemas/compute.py +++ b/gns3server/schemas/compute.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import copy from .capabilities import CAPABILITIES_SCHEMA COMPUTE_CREATE_SCHEMA = { @@ -52,10 +53,10 @@ COMPUTE_CREATE_SCHEMA = { } }, "additionalProperties": False, - "required": ["compute_id", "protocol", "host", "port"] + "required": ["protocol", "host", "port"] } -COMPUTE_UPDATE_SCHEMA = COMPUTE_CREATE_SCHEMA +COMPUTE_UPDATE_SCHEMA = copy.deepcopy(COMPUTE_CREATE_SCHEMA) del COMPUTE_UPDATE_SCHEMA["required"] COMPUTE_OBJECT_SCHEMA = { diff --git a/gns3server/schemas/ethernet_hub.py b/gns3server/schemas/ethernet_hub.py index e24b33e0..363ad470 100644 --- a/gns3server/schemas/ethernet_hub.py +++ b/gns3server/schemas/ethernet_hub.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import copy ETHERNET_HUB_CREATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", @@ -129,5 +130,5 @@ ETHERNET_HUB_OBJECT_SCHEMA = { "required": ["name", "node_id", "project_id", "ports_mapping"] } -ETHERNET_HUB_UPDATE_SCHEMA = ETHERNET_HUB_OBJECT_SCHEMA +ETHERNET_HUB_UPDATE_SCHEMA = copy.deepcopy(ETHERNET_HUB_OBJECT_SCHEMA) del ETHERNET_HUB_UPDATE_SCHEMA["required"] diff --git a/gns3server/schemas/ethernet_switch.py b/gns3server/schemas/ethernet_switch.py index 1a92a111..40dd2baa 100644 --- a/gns3server/schemas/ethernet_switch.py +++ b/gns3server/schemas/ethernet_switch.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import copy ETHERNET_SWITCH_CREATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", @@ -153,5 +154,5 @@ ETHERNET_SWITCH_OBJECT_SCHEMA = { "required": ["name", "node_id", "project_id"] } -ETHERNET_SWITCH_UPDATE_SCHEMA = ETHERNET_SWITCH_OBJECT_SCHEMA +ETHERNET_SWITCH_UPDATE_SCHEMA = copy.deepcopy(ETHERNET_SWITCH_OBJECT_SCHEMA) del ETHERNET_SWITCH_UPDATE_SCHEMA["required"] diff --git a/gns3server/schemas/frame_relay_switch.py b/gns3server/schemas/frame_relay_switch.py index 5d4967af..0f99508c 100644 --- a/gns3server/schemas/frame_relay_switch.py +++ b/gns3server/schemas/frame_relay_switch.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import copy FRAME_RELAY_SWITCH_CREATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", @@ -81,5 +82,5 @@ FRAME_RELAY_SWITCH_OBJECT_SCHEMA = { "required": ["name", "node_id", "project_id"] } -FRAME_RELAY_SWITCH_UPDATE_SCHEMA = FRAME_RELAY_SWITCH_OBJECT_SCHEMA +FRAME_RELAY_SWITCH_UPDATE_SCHEMA = copy.deepcopy(FRAME_RELAY_SWITCH_OBJECT_SCHEMA) del FRAME_RELAY_SWITCH_UPDATE_SCHEMA["required"] diff --git a/gns3server/schemas/node.py b/gns3server/schemas/node.py index a832c8cf..6e8f4e2a 100644 --- a/gns3server/schemas/node.py +++ b/gns3server/schemas/node.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import copy from .label import LABEL_OBJECT_SCHEMA NODE_TYPE_SCHEMA = { @@ -234,5 +235,5 @@ NODE_OBJECT_SCHEMA = { } NODE_CREATE_SCHEMA = NODE_OBJECT_SCHEMA -NODE_UPDATE_SCHEMA = NODE_OBJECT_SCHEMA +NODE_UPDATE_SCHEMA = copy.deepcopy(NODE_OBJECT_SCHEMA) del NODE_UPDATE_SCHEMA["required"] diff --git a/gns3server/templates/index.html b/gns3server/templates/index.html index 66c49e47..e013d37d 100644 --- a/gns3server/templates/index.html +++ b/gns3server/templates/index.html @@ -5,7 +5,7 @@ -

If you are looking for uploading the IOU. You can since 1.4 upload them directly from the client see: this documentation.

+

If you are looking for uploading the IOU. You can since 1.4 upload them directly from the client see: this documentation.

{% endblock %} diff --git a/gns3server/ubridge/hypervisor.py b/gns3server/ubridge/hypervisor.py index 1e6ae825..9deb7b83 100644 --- a/gns3server/ubridge/hypervisor.py +++ b/gns3server/ubridge/hypervisor.py @@ -199,7 +199,7 @@ class Hypervisor(UBridgeHypervisor): try: yield from wait_for_process_termination(self._process, timeout=3) except asyncio.TimeoutError: - if self._process.returncode is None: + if self._process and self._process.returncode is None: log.warn("uBridge process {} is still running... killing it".format(self._process.pid)) try: self._process.kill() diff --git a/gns3server/utils/images.py b/gns3server/utils/images.py index 0f7bc8f5..794d19e7 100644 --- a/gns3server/utils/images.py +++ b/gns3server/utils/images.py @@ -79,7 +79,7 @@ def list_images(type): images.append({ "filename": filename, - "path": path, + "path": force_unix_path(path), "md5sum": md5sum(os.path.join(root, filename)), "filesize": os.stat(os.path.join(root, filename)).st_size}) except OSError as e: diff --git a/gns3server/utils/picture.py b/gns3server/utils/picture.py index 50d17996..1ec072de 100644 --- a/gns3server/utils/picture.py +++ b/gns3server/utils/picture.py @@ -17,7 +17,7 @@ import io import struct -from xml.etree.ElementTree import ElementTree +from xml.etree.ElementTree import ElementTree, ParseError def get_size(data, default_width=0, default_height=0): @@ -95,7 +95,11 @@ def get_size(data, default_width=0, default_height=0): filetype = "svg" fhandle = io.BytesIO(data) tree = ElementTree() - tree.parse(fhandle) + try: + tree.parse(fhandle) + except ParseError: + raise ValueError("Invalid SVG file") + root = tree.getroot() try: diff --git a/requirements.txt b/requirements.txt index 21a43549..775d220d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ jsonschema>=2.4.0 aiohttp>=1.3.5,<=1.4.0 # pyup: ignore aiohttp-cors==0.5.1 # pyup: ignore yarl>=0.9.8,<0.10 # pyup: ignore -typing>=3.5.3.0 # Otherwise yarl fail with python 3.4 Jinja2>=2.7.3 raven>=5.23.0 psutil>=3.0.0 diff --git a/setup.py b/setup.py index 02663755..fd19894f 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,9 @@ class PyTest(TestCommand): dependencies = open("requirements.txt", "r").read().splitlines() +if sys.version_info <= (3, 4): + dependencies.append('typing>=3.5.3.0 # Otherwise yarl fail with python 3.4') + setup( name="gns3-server", version=__import__("gns3server").__version__, diff --git a/tests/compute/docker/test_docker.py b/tests/compute/docker/test_docker.py index 25779ca6..095a25de 100644 --- a/tests/compute/docker/test_docker.py +++ b/tests/compute/docker/test_docker.py @@ -47,7 +47,7 @@ def test_query_success(loop, vm): vm._session.request = AsyncioMagicMock(return_value=response) data = loop.run_until_complete(asyncio.async(vm.query("POST", "test", data={"a": True}, params={"b": 1}))) vm._session.request.assert_called_with('POST', - 'http://docker/test', + 'http://docker/v1.25/test', data='{"a": true}', headers={'content-type': 'application/json'}, params={'b': 1}, @@ -70,7 +70,7 @@ def test_query_error(loop, vm): with pytest.raises(DockerError): data = loop.run_until_complete(asyncio.async(vm.query("POST", "test", data={"a": True}, params={"b": 1}))) vm._session.request.assert_called_with('POST', - 'http://docker/test', + 'http://docker/v1.25/test', data='{"a": true}', headers={'content-type': 'application/json'}, params={'b': 1}, @@ -91,7 +91,7 @@ def test_query_error_json(loop, vm): with pytest.raises(DockerError): data = loop.run_until_complete(asyncio.async(vm.query("POST", "test", data={"a": True}, params={"b": 1}))) vm._session.request.assert_called_with('POST', - 'http://docker/test', + 'http://docker/v1.25/test', data='{"a": true}', headers={'content-type': 'application/json'}, params={'b': 1}, diff --git a/tests/compute/qemu/test_qemu_vm.py b/tests/compute/qemu/test_qemu_vm.py index 55040bc5..9c5c7953 100644 --- a/tests/compute/qemu/test_qemu_vm.py +++ b/tests/compute/qemu/test_qemu_vm.py @@ -154,6 +154,7 @@ def test_termination_callback(vm, async_run): assert event == vm +@pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows") def test_termination_callback_error(vm, tmpdir, async_run): with open(str(tmpdir / "qemu.log"), "w+") as f: diff --git a/tests/compute/test_manager.py b/tests/compute/test_manager.py index 72412ce9..ba7abaa1 100644 --- a/tests/compute/test_manager.py +++ b/tests/compute/test_manager.py @@ -219,18 +219,6 @@ def test_get_relative_image_path(qemu, tmpdir, config): assert qemu.get_relative_image_path(path5) == path5 -def test_get_relative_image_path_ova(qemu, tmpdir, config): - os.makedirs(str(tmpdir / "QEMU" / "test.ova")) - path = str(tmpdir / "QEMU" / "test.ova" / "test.bin") - open(path, 'w+').close() - - config.set_section_config("Server", { - "images_path": str(tmpdir) - }) - assert qemu.get_relative_image_path(path) == os.path.join("test.ova", "test.bin") - assert qemu.get_relative_image_path(os.path.join("test.ova", "test.bin")) == os.path.join("test.ova", "test.bin") - - def test_list_images(loop, qemu, tmpdir): fake_images = ["a.qcow2", "b.qcow2", ".blu.qcow2", "a.qcow2.md5sum"] @@ -262,7 +250,7 @@ def test_list_images_recursives(loop, qemu, tmpdir): assert loop.run_until_complete(qemu.list_images()) == [ {"filename": "a.qcow2", "path": "a.qcow2", "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1}, {"filename": "b.qcow2", "path": "b.qcow2", "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1}, - {"filename": "c.qcow2", "path": os.path.sep.join(["c", "c.qcow2"]), "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1} + {"filename": "c.qcow2", "path": force_unix_path(os.path.sep.join(["c", "c.qcow2"])), "md5sum": "c4ca4238a0b923820dcc509a6f75849b", "filesize": 1} ] diff --git a/tests/controller/test_import_project.py b/tests/controller/test_import_project.py index 7f8740c3..23d09457 100644 --- a/tests/controller/test_import_project.py +++ b/tests/controller/test_import_project.py @@ -181,6 +181,7 @@ def test_import_iou_linux_no_vm(linux_platform, async_run, tmpdir, controller): { "compute_id": "local", "node_type": "iou", + "name": "test", "properties": {} } ], @@ -224,6 +225,7 @@ def test_import_iou_linux_with_vm(linux_platform, async_run, tmpdir, controller) "compute_id": "local", "node_id": "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", "node_type": "iou", + "name": "test", "properties": {} } ], @@ -267,11 +269,13 @@ def test_import_iou_non_linux(windows_platform, async_run, tmpdir, controller): "compute_id": "local", "node_id": "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", "node_type": "iou", + "name": "test", "properties": {} }, { "compute_id": "local", "node_type": "vpcs", + "name": "test2", "properties": {} } ], @@ -319,12 +323,14 @@ def test_import_node_id(linux_platform, async_run, tmpdir, controller): "compute_id": "local", "node_id": "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", "node_type": "iou", + "name": "test", "properties": {} }, { "compute_id": "local", "node_id": "c3ae286c-c81f-40d9-a2d0-5874b2f2478d", "node_type": "iou", + "name": "test2", "properties": {} } ], @@ -409,6 +415,7 @@ def test_import_keep_compute_id(windows_platform, async_run, tmpdir, controller) "compute_id": "local", "node_id": "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", "node_type": "iou", + "name": "test", "properties": {} } ], diff --git a/tests/handlers/api/controller/test_symbol.py b/tests/handlers/api/controller/test_symbol.py index be6a4010..0d41f441 100644 --- a/tests/handlers/api/controller/test_symbol.py +++ b/tests/handlers/api/controller/test_symbol.py @@ -35,11 +35,7 @@ def test_symbols(http_controller): def test_get(http_controller): response = http_controller.get('/symbols/' + urllib.parse.quote(':/symbols/firewall.svg') + '/raw') assert response.status == 200 - # Different carriage return - if sys.platform.startswith("win"): - assert response.headers['CONTENT-LENGTH'] == '9568' - else: - assert response.headers['CONTENT-LENGTH'] == '9381' + assert response.headers['CONTENT-LENGTH'] == '9381' assert response.headers['CONTENT-TYPE'] == 'image/svg+xml' assert '' in response.html diff --git a/tests/test_utils.py b/tests/test_utils.py index af7d9815..0dbcc3b8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -24,6 +24,8 @@ def test_force_unix_path(): assert force_unix_path("a\\b") == "a/b" assert force_unix_path("a\\b\\..\\c") == "a/c" assert force_unix_path("C:\Temp") == "C:/Temp" + assert force_unix_path(force_unix_path("C:\Temp")) == "C:/Temp" + assert force_unix_path("a//b") == "a/b" def test_macaddress_to_int(): diff --git a/tests/utils/test_images.py b/tests/utils/test_images.py index 8b9b5c24..cce60775 100644 --- a/tests/utils/test_images.py +++ b/tests/utils/test_images.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import os +import sys from unittest.mock import patch @@ -105,9 +106,10 @@ def test_list_images(tmpdir): path = tmpdir / "images2" / "test_invalid.image" path.write(b'NOTANELF', ensure=True) - path3 = tmpdir / "images1" / "IOU" / "test3.bin" - path3.write(b'\x7fELF\x01\x02\x01', ensure=True) - path3 = force_unix_path(str(path3)) + if sys.platform.startswith("linux"): + path3 = tmpdir / "images1" / "IOU" / "test3.bin" + path3.write(b'\x7fELF\x01\x02\x01', ensure=True) + path3 = force_unix_path(str(path3)) path4 = tmpdir / "images1" / "QEMU" / "test4.qcow2" path4.write("1", ensure=True) @@ -137,14 +139,15 @@ def test_list_images(tmpdir): } ] - assert list_images("iou") == [ - { - 'filename': 'test3.bin', - 'filesize': 7, - 'md5sum': 'b0d5aa897d937aced5a6b1046e8f7e2e', - 'path': 'test3.bin' - } - ] + if sys.platform.startswith("linux"): + assert list_images("iou") == [ + { + 'filename': 'test3.bin', + 'filesize': 7, + 'md5sum': 'b0d5aa897d937aced5a6b1046e8f7e2e', + 'path': 'test3.bin' + } + ] assert list_images("qemu") == [ { diff --git a/win-requirements.txt b/win-requirements.txt new file mode 100644 index 00000000..5905e970 --- /dev/null +++ b/win-requirements.txt @@ -0,0 +1,3 @@ +-rrequirements.txt + +pypiwin32 # pyup: ignore