From 46d405c8b35bd0da410ca41273be887f537f01ef Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 2 Feb 2017 11:52:55 +0100 Subject: [PATCH 01/63] Simplify conversion process from 1.3 to 2.0 This could avoid some corruption issue. --- gns3server/controller/topology.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/gns3server/controller/topology.py b/gns3server/controller/topology.py index 7842c536..b9400364 100644 --- a/gns3server/controller/topology.py +++ b/gns3server/controller/topology.py @@ -23,7 +23,6 @@ import glob import shutil import zipfile import aiohttp -import platform import jsonschema @@ -117,34 +116,33 @@ def load_topology(path): topo = json.load(f) except (OSError, UnicodeDecodeError, ValueError) as e: raise aiohttp.web.HTTPConflict(text="Could not load topology {}: {}".format(path, str(e))) - if "revision" not in topo or topo["revision"] < 5: + + if topo.get("revision", 0) > GNS3_FILE_FORMAT_REVISION: + raise aiohttp.web.HTTPConflict(text="This project is designed for a more recent version of GNS3 please update GNS3 to version {} or later".format(topo["version"])) + + changed = False + if "revision" not in topo or topo["revision"] < GNS3_FILE_FORMAT_REVISION: # If it's an old GNS3 file we need to convert it # first we backup the file shutil.copy(path, path + ".backup{}".format(topo.get("revision", 0))) + changed = True + + if "revision" not in topo or topo["revision"] < 5: topo = _convert_1_3_later(topo, path) - _check_topology_schema(topo) - with open(path, "w+", encoding="utf-8") as f: - json.dump(topo, f, indent=4, sort_keys=True) # Version before GNS3 2.0 alpha 4 if topo["revision"] < 6: - shutil.copy(path, path + ".backup{}".format(topo.get("revision", 0))) topo = _convert_2_0_0_alpha(topo, path) - _check_topology_schema(topo) - with open(path, "w+", encoding="utf-8") as f: - json.dump(topo, f, indent=4, sort_keys=True) # Version before GNS3 2.0 beta 3 if topo["revision"] < 7: - shutil.copy(path, path + ".backup{}".format(topo.get("revision", 0))) topo = _convert_2_0_0_beta_2(topo, path) - _check_topology_schema(topo) + + _check_topology_schema(topo) + + if changed: with open(path, "w+", encoding="utf-8") as f: json.dump(topo, f, indent=4, sort_keys=True) - - if topo["revision"] > GNS3_FILE_FORMAT_REVISION: - raise aiohttp.web.HTTPConflict(text="This project is designed for a more recent version of GNS3 please update GNS3 to version {} or later".format(topo["version"])) - _check_topology_schema(topo) return topo From 63b888a57e4f9b6fac4ec58b682fe8dccb9be20c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 2 Feb 2017 15:34:39 +0100 Subject: [PATCH 02/63] Drop unused code --- .../handlers/api/compute/iou_handler.py | 3 +-- gns3server/schemas/iou.py | 20 ------------------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/gns3server/handlers/api/compute/iou_handler.py b/gns3server/handlers/api/compute/iou_handler.py index 1663cbe6..e81ab11b 100644 --- a/gns3server/handlers/api/compute/iou_handler.py +++ b/gns3server/handlers/api/compute/iou_handler.py @@ -30,8 +30,7 @@ from gns3server.schemas.node import ( from gns3server.schemas.iou import ( IOU_CREATE_SCHEMA, IOU_START_SCHEMA, - IOU_OBJECT_SCHEMA, - IOU_CONFIGS_SCHEMA, + IOU_OBJECT_SCHEMA ) diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 5b14bc97..8e54be64 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -218,23 +218,3 @@ IOU_OBJECT_SCHEMA = { }, "additionalProperties": False } - - -IOU_CONFIGS_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to get the startup and private configuration file", - "type": "object", - "properties": { - "startup_config_content": { - "description": "Content of the startup configuration file", - "type": ["string", "null"], - "minLength": 1, - }, - "private_config_content": { - "description": "Content of the private configuration file", - "type": ["string", "null"], - "minLength": 1, - }, - }, - "additionalProperties": False, -} From e892e5dfabcd7f4c7eaed30c3d7506109259ea68 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 2 Feb 2017 19:13:47 +0100 Subject: [PATCH 03/63] Manage base configuration on server Fix #786 --- docs/file_format.rst | 1 + gns3server/compute/dynamips/__init__.py | 26 +-- gns3server/compute/dynamips/nodes/router.py | 197 ++++++------------ gns3server/compute/iou/iou_vm.py | 6 - gns3server/compute/vpcs/vpcs_vm.py | 4 +- .../configs/ios_base_startup-config.txt | 26 +++ .../ios_etherswitch_startup-config.txt | 181 ++++++++++++++++ .../configs/iou_l2_base_startup-config.txt | 132 ++++++++++++ .../configs/iou_l3_base_startup-config.txt | 108 ++++++++++ gns3server/configs/vpcs_base_config.txt | 1 + gns3server/controller/__init__.py | 28 ++- gns3server/controller/node.py | 43 ++-- gns3server/controller/topology.py | 34 ++- .../api/compute/dynamips_vm_handler.py | 2 - .../handlers/api/controller/node_handler.py | 12 +- gns3server/schemas/dynamips_vm.py | 40 ---- gns3server/schemas/iou.py | 32 --- gns3server/schemas/vpcs.py | 22 +- tests/controller/test_controller.py | 14 ++ tests/controller/test_node.py | 37 ++-- tests/controller/test_project.py | 8 +- tests/controller/test_project_open.py | 2 - tests/handlers/api/compute/test_iou.py | 8 - tests/handlers/api/compute/test_vpcs.py | 3 - .../1_3_dynamips/after/1_3_dynamips.gns3 | 1 - .../after/1_3_dynamips_missing_platform.gns3 | 1 - .../1_5_dynamips/after/1_5_dynamips.gns3 | 2 - .../1_5_internet/after/1_5_internet.gns3 | 1 - tests/topologies/1_5_iou/after/1_5_iou.gns3 | 1 - .../1_5_snapshot/after/1_5_snapshot.gns3 | 1 - tests/topologies/1_5_vpcs/after/1_5_vpcs.gns3 | 2 - .../after/dynamips_2_0_0_b2.gns3 | 10 +- 32 files changed, 652 insertions(+), 334 deletions(-) create mode 100644 gns3server/configs/ios_base_startup-config.txt create mode 100644 gns3server/configs/ios_etherswitch_startup-config.txt create mode 100644 gns3server/configs/iou_l2_base_startup-config.txt create mode 100644 gns3server/configs/iou_l3_base_startup-config.txt create mode 100644 gns3server/configs/vpcs_base_config.txt diff --git a/docs/file_format.rst b/docs/file_format.rst index d0a01ef4..d25b51ef 100644 --- a/docs/file_format.rst +++ b/docs/file_format.rst @@ -23,6 +23,7 @@ A minimal version: The revision is the version of file format: +* 8: GNS3 2.1 * 7: GNS3 2.0 * 6: GNS3 2.0 < beta 3 * 5: GNS3 2.0 < alpha 4 diff --git a/gns3server/compute/dynamips/__init__.py b/gns3server/compute/dynamips/__init__.py index 4ad0472f..6d6bafda 100644 --- a/gns3server/compute/dynamips/__init__.py +++ b/gns3server/compute/dynamips/__init__.py @@ -515,25 +515,12 @@ class Dynamips(BaseManager): default_startup_config_path = os.path.join(module_workdir, vm.id, "configs", "i{}_startup-config.cfg".format(vm.dynamips_id)) default_private_config_path = os.path.join(module_workdir, vm.id, "configs", "i{}_private-config.cfg".format(vm.dynamips_id)) - startup_config_path = settings.get("startup_config") startup_config_content = settings.get("startup_config_content") - if startup_config_path: - yield from vm.set_configs(startup_config_path) - elif startup_config_content: - startup_config_path = self._create_config(vm, default_startup_config_path, startup_config_content) - yield from vm.set_configs(startup_config_path) - elif os.path.isfile(default_startup_config_path) and os.path.getsize(default_startup_config_path) == 0: - # An empty startup-config may crash Dynamips - startup_config_path = self._create_config(vm, default_startup_config_path, "!\n") - yield from vm.set_configs(startup_config_path) - - private_config_path = settings.get("private_config") + if startup_config_content: + self._create_config(vm, default_startup_config_path, startup_config_content) private_config_content = settings.get("private_config_content") - if private_config_path: - yield from vm.set_configs(vm.startup_config, private_config_path) - elif private_config_content: - private_config_path = self._create_config(vm, default_private_config_path, private_config_content) - yield from vm.set_configs(vm.startup_config, private_config_path) + if private_config_content: + self._create_config(vm, default_private_config_path, private_config_content) def _create_config(self, vm, path, content=None): """ @@ -553,6 +540,11 @@ class Dynamips(BaseManager): except OSError as e: raise DynamipsError("Could not create Dynamips configs directory: {}".format(e)) + if content is None or len(content) == 0: + content = "!\n" + if os.path.exists(path): + return + try: with open(path, "wb") as f: if content: diff --git a/gns3server/compute/dynamips/nodes/router.py b/gns3server/compute/dynamips/nodes/router.py index e43b827f..33c01655 100644 --- a/gns3server/compute/dynamips/nodes/router.py +++ b/gns3server/compute/dynamips/nodes/router.py @@ -78,8 +78,6 @@ class Router(BaseNode): self._dynamips_id = dynamips_id self._platform = platform self._image = "" - self._startup_config = "" - self._private_config = "" self._ram = 128 # Megabytes self._nvram = 128 # Kilobytes self._mmap = True @@ -102,8 +100,6 @@ class Router(BaseNode): self._slots = [] self._ghost_flag = ghost_flag self._memory_watcher = None - self._startup_config_content = "" - self._private_config_content = "" if not ghost_flag: if not dynamips_id: @@ -152,8 +148,6 @@ class Router(BaseNode): "platform": self._platform, "image": self._image, "image_md5sum": md5sum(self._image), - "startup_config": self._startup_config, - "private_config": self._private_config, "ram": self._ram, "nvram": self._nvram, "mmap": self._mmap, @@ -171,9 +165,7 @@ class Router(BaseNode): "console_type": "telnet", "aux": self.aux, "mac_addr": self._mac_addr, - "system_id": self._system_id, - "startup_config_content": self._startup_config_content, - "private_config_content": self._private_config_content} + "system_id": self._system_id} # return the relative path if the IOS image is in the images_path directory router_info["image"] = self.manager.get_relative_image_path(self._image) @@ -289,6 +281,16 @@ class Router(BaseNode): if not self._ghost_flag: self.check_available_ram(self.ram) + startup_config_path = os.path.join("configs", "i{}_startup-config.cfg".format(self._dynamips_id)) + private_config_path = os.path.join("configs", "i{}_private-config.cfg".format(self._dynamips_id)) + + if not os.path.exists(private_config_path) or not os.path.getsize(private_config_path): + # an empty private-config can prevent a router to boot. + private_config_path = '' + yield from self._hypervisor.send('vm set_config "{name}" "{startup}" "{private}"'.format( + name=self._name, + startup=startup_config_path, + private=private_config_path)) yield from self._hypervisor.send('vm start "{name}"'.format(name=self._name)) self.status = "started" log.info('router "{name}" [{id}] has been started'.format(name=self._name, id=self._id)) @@ -1458,26 +1460,6 @@ class Router(BaseNode): return self._slots - @property - def startup_config(self): - """ - Returns the startup-config for this router. - - :returns: path to startup-config file - """ - - return self._startup_config - - @property - def private_config(self): - """ - Returns the private-config for this router. - - :returns: path to private-config file - """ - - return self._private_config - @asyncio.coroutine def set_name(self, new_name): """ @@ -1486,89 +1468,34 @@ class Router(BaseNode): :param new_name: new name string """ - if self._startup_config: - # change the hostname in the startup-config - startup_config_path = os.path.join(self._working_directory, "configs", "i{}_startup-config.cfg".format(self._dynamips_id)) - if os.path.isfile(startup_config_path): - try: - with open(startup_config_path, "r+", encoding="utf-8", errors="replace") as f: - old_config = f.read() - new_config = old_config.replace(self.name, new_name) - f.seek(0) - self._startup_config_content = new_config - f.write(new_config) - except OSError as e: - raise DynamipsError("Could not amend the configuration {}: {}".format(startup_config_path, e)) + # change the hostname in the startup-config + startup_config_path = os.path.join(self._working_directory, "configs", "i{}_startup-config.cfg".format(self._dynamips_id)) + if os.path.isfile(startup_config_path): + try: + with open(startup_config_path, "r+", encoding="utf-8", errors="replace") as f: + old_config = f.read() + new_config = old_config.replace(self.name, new_name) + f.seek(0) + f.write(new_config) + except OSError as e: + raise DynamipsError("Could not amend the configuration {}: {}".format(startup_config_path, e)) - if self._private_config: - # change the hostname in the private-config - private_config_path = os.path.join(self._working_directory, "configs", "i{}_private-config.cfg".format(self._dynamips_id)) - if os.path.isfile(private_config_path): - try: - with open(private_config_path, "r+", encoding="utf-8", errors="replace") as f: - old_config = f.read() - new_config = old_config.replace(self.name, new_name) - f.seek(0) - self._private_config_content = new_config - f.write(new_config) - except OSError as e: - raise DynamipsError("Could not amend the configuration {}: {}".format(private_config_path, e)) + # change the hostname in the private-config + private_config_path = os.path.join(self._working_directory, "configs", "i{}_private-config.cfg".format(self._dynamips_id)) + if os.path.isfile(private_config_path): + try: + with open(private_config_path, "r+", encoding="utf-8", errors="replace") as f: + old_config = f.read() + new_config = old_config.replace(self.name, new_name) + f.seek(0) + f.write(new_config) + except OSError as e: + raise DynamipsError("Could not amend the configuration {}: {}".format(private_config_path, e)) yield from self._hypervisor.send('vm rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name)) log.info('Router "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name, id=self._id, new_name=new_name)) self._name = new_name - @asyncio.coroutine - def set_configs(self, startup_config, private_config=''): - """ - Sets the config files that are pushed to startup-config and - private-config in NVRAM when the instance is started. - - :param startup_config: path to statup-config file - :param private_config: path to private-config file - (keep existing data when if an empty string) - """ - - startup_config = startup_config.replace("\\", '/') - 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 - - if private_config: - private_config_path = os.path.join(self._working_directory, private_config) - try: - if not os.path.getsize(private_config_path): - # an empty private-config can prevent a router to boot. - private_config = '' - self._private_config_content = "" - else: - with open(private_config_path) as f: - self._private_config_content = f.read() - except OSError as e: - raise DynamipsError("Cannot access the private-config {}: {}".format(private_config_path, e)) - - try: - startup_config_path = os.path.join(self._working_directory, startup_config) - with open(startup_config_path) as f: - self._startup_config_content = f.read() - except OSError as e: - raise DynamipsError("Cannot access the startup-config {}: {}".format(startup_config_path, e)) - - yield from self._hypervisor.send('vm set_config "{name}" "{startup}" "{private}"'.format(name=self._name, - startup=startup_config, - private=private_config)) - - log.info('Router "{name}" [{id}]: has a new startup-config set: "{startup}"'.format(name=self._name, - id=self._id, - startup=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)) - @asyncio.coroutine def extract_config(self): """ @@ -1594,41 +1521,35 @@ class Router(BaseNode): Saves the startup-config and private-config to files. """ - if self.startup_config or self.private_config: + try: + config_path = os.path.join(self._working_directory, "configs") + os.makedirs(config_path, exist_ok=True) + except OSError as e: + raise DynamipsError("Could could not create configuration directory {}: {}".format(config_path, e)) + startup_config_base64, private_config_base64 = yield from self.extract_config() + if startup_config_base64: + startup_config = os.path.join("configs", "i{}_startup-config.cfg".format(self._dynamips_id)) try: - config_path = os.path.join(self._working_directory, "configs") - os.makedirs(config_path, exist_ok=True) - except OSError as e: - raise DynamipsError("Could could not create configuration directory {}: {}".format(config_path, e)) + config = base64.b64decode(startup_config_base64).decode("utf-8", errors="replace") + config = "!\n" + config.replace("\r", "") + config_path = os.path.join(self._working_directory, startup_config) + with open(config_path, "wb") as f: + log.info("saving startup-config to {}".format(startup_config)) + f.write(config.encode("utf-8")) + except (binascii.Error, OSError) as e: + raise DynamipsError("Could not save the startup configuration {}: {}".format(config_path, e)) - startup_config_base64, private_config_base64 = yield from self.extract_config() - if startup_config_base64: - if not self.startup_config: - self._startup_config = os.path.join("configs", "i{}_startup-config.cfg".format(self._dynamips_id)) - try: - config = base64.b64decode(startup_config_base64).decode("utf-8", errors="replace") - config = "!\n" + config.replace("\r", "") - config_path = os.path.join(self._working_directory, self.startup_config) - with open(config_path, "wb") as f: - log.info("saving startup-config to {}".format(self.startup_config)) - self._startup_config_content = config - f.write(config.encode("utf-8")) - except (binascii.Error, OSError) as e: - raise DynamipsError("Could not save the startup configuration {}: {}".format(config_path, e)) - - if private_config_base64 and base64.b64decode(private_config_base64) != b'\nkerberos password \nend\n': - if not self.private_config: - self._private_config = os.path.join("configs", "i{}_private-config.cfg".format(self._dynamips_id)) - try: - config = base64.b64decode(private_config_base64).decode("utf-8", errors="replace") - config_path = os.path.join(self._working_directory, self.private_config) - with open(config_path, "wb") as f: - log.info("saving private-config to {}".format(self.private_config)) - self._private_config_content = config - f.write(config.encode("utf-8")) - except (binascii.Error, OSError) as e: - raise DynamipsError("Could not save the private configuration {}: {}".format(config_path, e)) + if private_config_base64 and base64.b64decode(private_config_base64) != b'\nkerberos password \nend\n': + private_config = os.path.join("configs", "i{}_private-config.cfg".format(self._dynamips_id)) + try: + config = base64.b64decode(private_config_base64).decode("utf-8", errors="replace") + config_path = os.path.join(self._working_directory, private_config) + with open(config_path, "wb") as f: + log.info("saving private-config to {}".format(private_config)) + f.write(config.encode("utf-8")) + except (binascii.Error, OSError) as e: + raise DynamipsError("Could not save the private configuration {}: {}".format(config_path, e)) def delete(self): """ diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py index fc5ce499..bed1b715 100644 --- a/gns3server/compute/iou/iou_vm.py +++ b/gns3server/compute/iou/iou_vm.py @@ -26,8 +26,6 @@ import re import asyncio import subprocess import shutil -import argparse -import threading import configparser import struct import hashlib @@ -207,10 +205,6 @@ class IOUVM(BaseNode): "ram": self._ram, "nvram": self._nvram, "l1_keepalives": self._l1_keepalives, - "startup_config": self.relative_startup_config_file, - "startup_config_content": self.startup_config_content, - "private_config_content": self.private_config_content, - "private_config": self.relative_private_config_file, "use_default_iou_values": self._use_default_iou_values, "command_line": self.command_line} diff --git a/gns3server/compute/vpcs/vpcs_vm.py b/gns3server/compute/vpcs/vpcs_vm.py index 85952be3..97439c7b 100644 --- a/gns3server/compute/vpcs/vpcs_vm.py +++ b/gns3server/compute/vpcs/vpcs_vm.py @@ -109,7 +109,7 @@ class VPCSVM(BaseNode): raise VPCSError("No path to a VPCS executable has been set") # This raise an error if ubridge is not available - ubridge_path = self.ubridge_path + self.ubridge_path if not os.path.isfile(path): raise VPCSError("VPCS program '{}' is not accessible".format(path)) @@ -128,8 +128,6 @@ class VPCSVM(BaseNode): "console": self._console, "console_type": "telnet", "project_id": self.project.id, - "startup_script": self.startup_script, - "startup_script_path": self.relative_startup_script, "command_line": self.command_line} @property diff --git a/gns3server/configs/ios_base_startup-config.txt b/gns3server/configs/ios_base_startup-config.txt new file mode 100644 index 00000000..8ba5c6a2 --- /dev/null +++ b/gns3server/configs/ios_base_startup-config.txt @@ -0,0 +1,26 @@ +! +service timestamps debug datetime msec +service timestamps log datetime msec +no service password-encryption +! +hostname %h +! +ip cef +no ip domain-lookup +no ip icmp rate-limit unreachable +ip tcp synwait 5 +no cdp log mismatch duplex +! +line con 0 + exec-timeout 0 0 + logging synchronous + privilege level 15 + no login +line aux 0 + exec-timeout 0 0 + logging synchronous + privilege level 15 + no login +! +! +end diff --git a/gns3server/configs/ios_etherswitch_startup-config.txt b/gns3server/configs/ios_etherswitch_startup-config.txt new file mode 100644 index 00000000..2367b347 --- /dev/null +++ b/gns3server/configs/ios_etherswitch_startup-config.txt @@ -0,0 +1,181 @@ +! +service timestamps debug datetime msec +service timestamps log datetime msec +no service password-encryption +no service dhcp +! +hostname %h +! +ip cef +no ip routing +no ip domain-lookup +no ip icmp rate-limit unreachable +ip tcp synwait 5 +no cdp log mismatch duplex +vtp file nvram:vlan.dat +! +! +interface FastEthernet0/0 + description *** Unused for Layer2 EtherSwitch *** + no ip address + shutdown +! +interface FastEthernet0/1 + description *** Unused for Layer2 EtherSwitch *** + no ip address + shutdown +! +interface FastEthernet1/0 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/1 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/2 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/3 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/4 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/5 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/6 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/7 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/8 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/9 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/10 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/11 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/12 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/13 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/14 + no shutdown + duplex full + speed 100 +! +interface FastEthernet1/15 + no shutdown + duplex full + speed 100 +! +interface Vlan1 + no ip address + shutdown +! +! +line con 0 + exec-timeout 0 0 + logging synchronous + privilege level 15 + no login +line aux 0 + exec-timeout 0 0 + logging synchronous + privilege level 15 + no login +! +! +banner exec $ + +*************************************************************** +This is a normal Router with a SW module inside (NM-16ESW) +It has been preconfigured with hard coded speed and duplex + +To create vlans use the command "vlan database" from exec mode +After creating all desired vlans use "exit" to apply the config + +To view existing vlans use the command "show vlan-switch brief" + +Warning: You are using an old IOS image for this router. +Please update the IOS to enable the "macro" command! +*************************************************************** + +$ +! +!Warning: If the IOS is old and doesn't support macro, it will stop the configuration loading from this point! +! +macro name add_vlan +end +vlan database +vlan $v +exit +@ +macro name del_vlan +end +vlan database +no vlan $v +exit +@ +! +! +banner exec $ + +*************************************************************** +This is a normal Router with a Switch module inside (NM-16ESW) +It has been pre-configured with hard-coded speed and duplex + +To create vlans use the command "vlan database" in exec mode +After creating all desired vlans use "exit" to apply the config + +To view existing vlans use the command "show vlan-switch brief" + +Alias(exec) : vl - "show vlan-switch brief" command +Alias(configure): va X - macro to add vlan X +Alias(configure): vd X - macro to delete vlan X +*************************************************************** + +$ +! +alias configure va macro global trace add_vlan $v +alias configure vd macro global trace del_vlan $v +alias exec vl show vlan-switch brief +! +! +end diff --git a/gns3server/configs/iou_l2_base_startup-config.txt b/gns3server/configs/iou_l2_base_startup-config.txt new file mode 100644 index 00000000..501355f6 --- /dev/null +++ b/gns3server/configs/iou_l2_base_startup-config.txt @@ -0,0 +1,132 @@ +! +service timestamps debug datetime msec +service timestamps log datetime msec +no service password-encryption +! +hostname %h +! +! +! +logging discriminator EXCESS severity drops 6 msg-body drops EXCESSCOLL +logging buffered 50000 +logging console discriminator EXCESS +! +no ip icmp rate-limit unreachable +! +ip cef +no ip domain-lookup +! +! +! +! +! +! +ip tcp synwait-time 5 +! +! +! +! +! +! +interface Ethernet0/0 + no ip address + no shutdown + duplex auto +! +interface Ethernet0/1 + no ip address + no shutdown + duplex auto +! +interface Ethernet0/2 + no ip address + no shutdown + duplex auto +! +interface Ethernet0/3 + no ip address + no shutdown + duplex auto +! +interface Ethernet1/0 + no ip address + no shutdown + duplex auto +! +interface Ethernet1/1 + no ip address + no shutdown + duplex auto +! +interface Ethernet1/2 + no ip address + no shutdown + duplex auto +! +interface Ethernet1/3 + no ip address + no shutdown + duplex auto +! +interface Ethernet2/0 + no ip address + no shutdown + duplex auto +! +interface Ethernet2/1 + no ip address + no shutdown + duplex auto +! +interface Ethernet2/2 + no ip address + no shutdown + duplex auto +! +interface Ethernet2/3 + no ip address + no shutdown + duplex auto +! +interface Ethernet3/0 + no ip address + no shutdown + duplex auto +! +interface Ethernet3/1 + no ip address + no shutdown + duplex auto +! +interface Ethernet3/2 + no ip address + no shutdown + duplex auto +! +interface Ethernet3/3 + no ip address + no shutdown + duplex auto +! +interface Vlan1 + no ip address + shutdown +! +! +! +! +! +! +! +! +! +line con 0 + exec-timeout 0 0 + privilege level 15 + logging synchronous +line aux 0 + exec-timeout 0 0 + privilege level 15 + logging synchronous +! +end diff --git a/gns3server/configs/iou_l3_base_startup-config.txt b/gns3server/configs/iou_l3_base_startup-config.txt new file mode 100644 index 00000000..81d574ff --- /dev/null +++ b/gns3server/configs/iou_l3_base_startup-config.txt @@ -0,0 +1,108 @@ +! +service timestamps debug datetime msec +service timestamps log datetime msec +no service password-encryption +! +hostname %h +! +! +! +no ip icmp rate-limit unreachable +! +! +! +! +ip cef +no ip domain-lookup +! +! +ip tcp synwait-time 5 +! +! +! +! +interface Ethernet0/0 + no ip address + shutdown +! +interface Ethernet0/1 + no ip address + shutdown +! +interface Ethernet0/2 + no ip address + shutdown +! +interface Ethernet0/3 + no ip address + shutdown +! +interface Ethernet1/0 + no ip address + shutdown +! +interface Ethernet1/1 + no ip address + shutdown +! +interface Ethernet1/2 + no ip address + shutdown +! +interface Ethernet1/3 + no ip address + shutdown +! +interface Serial2/0 + no ip address + shutdown + serial restart-delay 0 +! +interface Serial2/1 + no ip address + shutdown + serial restart-delay 0 +! +interface Serial2/2 + no ip address + shutdown + serial restart-delay 0 +! +interface Serial2/3 + no ip address + shutdown + serial restart-delay 0 +! +interface Serial3/0 + no ip address + shutdown + serial restart-delay 0 +! +interface Serial3/1 + no ip address + shutdown + serial restart-delay 0 +! +interface Serial3/2 + no ip address + shutdown + serial restart-delay 0 +! +interface Serial3/3 + no ip address + shutdown + serial restart-delay 0 +! +! +no cdp log mismatch duplex +! +line con 0 + exec-timeout 0 0 + privilege level 15 + logging synchronous +line aux 0 + exec-timeout 0 0 + privilege level 15 + logging synchronous +! +end diff --git a/gns3server/configs/vpcs_base_config.txt b/gns3server/configs/vpcs_base_config.txt new file mode 100644 index 00000000..9e5efd8f --- /dev/null +++ b/gns3server/configs/vpcs_base_config.txt @@ -0,0 +1 @@ +set pcname %h diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 295e6762..3e597d2c 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -16,9 +16,9 @@ # along with this program. If not, see . import os -import sys import json import socket +import shutil import asyncio import aiohttp @@ -30,7 +30,7 @@ from .symbols import Symbols from ..version import __version__ from .topology import load_topology from .gns3vm import GNS3VM - +from ..utils.get_resource import get_resource import logging log = logging.getLogger(__name__) @@ -54,6 +54,7 @@ class Controller: @asyncio.coroutine def start(self): log.info("Start controller") + self.load_base_files() yield from self.load() server_config = Config.instance().get_section_config("Server") host = server_config.get("host", "localhost") @@ -163,6 +164,20 @@ class Controller: except OSError as e: log.error(str(e)) + def load_base_files(self): + """ + At startup we copy base file to the user location to allow + them to customize it + """ + dst_path = self.configs_path() + src_path = get_resource('configs') + try: + for file in os.listdir(src_path): + if not os.path.exists(os.path.join(dst_path, file)): + shutil.copy(os.path.join(src_path, file), os.path.join(dst_path, file)) + except OSError: + pass + def images_path(self): """ Get the image storage directory @@ -172,6 +187,15 @@ class Controller: os.makedirs(images_path, exist_ok=True) return images_path + def configs_path(self): + """ + Get the configs storage directory + """ + server_config = Config.instance().get_section_config("Server") + images_path = os.path.expanduser(server_config.get("configs_path", "~/GNS3/projects")) + os.makedirs(images_path, exist_ok=True) + return images_path + @asyncio.coroutine def _import_gns3_gui_conf(self): """ diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index 9b330cf2..49337325 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -146,6 +146,15 @@ class Node: def properties(self, val): self._properties = val + def _base_config_file_content(self, path): + if not os.path.isabs(path): + path = os.path.join(self.project.controller.configs_path(), path) + try: + with open(path) as f: + return f.read() + except (PermissionError, OSError): + return None + @property def project(self): return self._project @@ -366,8 +375,12 @@ class Node: self._console_type = value elif key == "name": self.name = value - elif key in ["node_id", "project_id", "console_host"]: - pass + elif key in ["node_id", "project_id", "console_host", + "startup_config_content", + "private_config_content", + "startup_script"]: + if key in self._properties: + del self._properties[key] else: self._properties[key] = value self._list_ports() @@ -384,6 +397,17 @@ class Node: data = copy.copy(properties) else: data = copy.copy(self._properties) + # We replace the startup script name by the content of the file + mapping = { + "base_script_file": "startup_script", + "startup_config": "startup_config_content", + "private_config": "private_config_content", + } + for k, v in mapping.items(): + if k in list(self._properties.keys()): + data[v] = self._base_config_file_content(self._properties[k]) + del data[k] + del self._properties[k] # We send the file only one time data["name"] = self._name if self._console: # console is optional for builtin nodes @@ -585,17 +609,6 @@ class Node: return False return self.id == other.id and other.project.id == self.project.id - def _filter_properties(self): - """ - Some properties are private and should not be exposed - """ - PRIVATE_PROPERTIES = ("iourc_content", ) - prop = copy.copy(self._properties) - for k in list(prop.keys()): - if k in PRIVATE_PROPERTIES: - del prop[k] - return prop - def __json__(self, topology_dump=False): """ :param topology_dump: Filter to keep only properties require for saving on disk @@ -608,7 +621,7 @@ class Node: "name": self._name, "console": self._console, "console_type": self._console_type, - "properties": self._filter_properties(), + "properties": self._properties, "label": self._label, "x": self._x, "y": self._y, @@ -631,7 +644,7 @@ class Node: "console_host": str(self._compute.console_host), "console_type": self._console_type, "command_line": self._command_line, - "properties": self._filter_properties(), + "properties": self._properties, "status": self._status, "label": self._label, "x": self._x, diff --git a/gns3server/controller/topology.py b/gns3server/controller/topology.py index b9400364..a126593d 100644 --- a/gns3server/controller/topology.py +++ b/gns3server/controller/topology.py @@ -36,7 +36,7 @@ import logging log = logging.getLogger(__name__) -GNS3_FILE_FORMAT_REVISION = 7 +GNS3_FILE_FORMAT_REVISION = 8 def _check_topology_schema(topo): @@ -138,6 +138,10 @@ def load_topology(path): if topo["revision"] < 7: topo = _convert_2_0_0_beta_2(topo, path) + # Version before GNS3 2.1 + if topo["revision"] < 8: + topo = _convert_2_0_0(topo, path) + _check_topology_schema(topo) if changed: @@ -146,6 +150,34 @@ def load_topology(path): return topo +def _convert_2_0_0(topo, topo_path): + """ + Convert topologies from GNS3 2.0.0 to 2.1 + + Changes: + * Remove startup_script_path from VPCS and base config file for IOU and Dynamips + """ + topo["revision"] = 8 + + for node in topo.get("topology", {}).get("nodes", []): + if "properties" in node: + if node["node_type"] == "vpcs": + if "startup_script_path" in node["properties"]: + del node["properties"]["startup_script_path"] + if "startup_script" in node["properties"]: + del node["properties"]["startup_script"] + elif node["node_type"] == "dynamips" or node["node_type"] == "iou": + if "startup_config" in node["properties"]: + del node["properties"]["startup_config"] + if "private_config" in node["properties"]: + del node["properties"]["private_config"] + if "startup_config_content" in node["properties"]: + del node["properties"]["startup_config_content"] + if "private_config_content" in node["properties"]: + del node["properties"]["private_config_content"] + return topo + + def _convert_2_0_0_beta_2(topo, topo_path): """ Convert topologies from GNS3 2.0.0 beta 2 to beta 3. diff --git a/gns3server/handlers/api/compute/dynamips_vm_handler.py b/gns3server/handlers/api/compute/dynamips_vm_handler.py index c8b1f318..dd0352de 100644 --- a/gns3server/handlers/api/compute/dynamips_vm_handler.py +++ b/gns3server/handlers/api/compute/dynamips_vm_handler.py @@ -17,7 +17,6 @@ import os import sys -import base64 from gns3server.web.route import Route from gns3server.schemas.nio import NIO_SCHEMA @@ -78,7 +77,6 @@ class DynamipsVMHandler: aux=request.json.get("aux"), chassis=request.json.pop("chassis", default_chassis), node_type="dynamips") - yield from dynamips_manager.update_vm_settings(vm, request.json) response.set_status(201) response.json(vm) diff --git a/gns3server/handlers/api/controller/node_handler.py b/gns3server/handlers/api/controller/node_handler.py index 67857df7..d9a715f0 100644 --- a/gns3server/handlers/api/controller/node_handler.py +++ b/gns3server/handlers/api/controller/node_handler.py @@ -344,10 +344,7 @@ class NodeHandler: raise aiohttp.web.HTTPForbidden node_type = node.node_type - if node_type == "dynamips": - path = "/project-files/{}/{}".format(node_type, path) - else: - path = "/project-files/{}/{}/{}".format(node_type, node.id, path) + path = "/project-files/{}/{}/{}".format(node_type, node.id, path) res = yield from node.compute.http_query("GET", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), timeout=None, raw=True) response.set_status(200) @@ -384,12 +381,9 @@ class NodeHandler: raise aiohttp.web.HTTPForbidden node_type = node.node_type - if node_type == "dynamips": - path = "/project-files/{}/{}".format(node_type, path) - else: - path = "/project-files/{}/{}/{}".format(node_type, node.id, path) + path = "/project-files/{}/{}/{}".format(node_type, node.id, path) data = yield from request.content.read() - res = yield from node.compute.http_query("POST", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), data=data, timeout=None, raw=True) + yield from node.compute.http_query("POST", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), data=data, timeout=None, raw=True) response.set_status(201) diff --git a/gns3server/schemas/dynamips_vm.py b/gns3server/schemas/dynamips_vm.py index c5225c21..0a2bcc71 100644 --- a/gns3server/schemas/dynamips_vm.py +++ b/gns3server/schemas/dynamips_vm.py @@ -62,18 +62,10 @@ VM_CREATE_SCHEMA = { "type": ["string", "null"], "minLength": 1, }, - "startup_config": { - "description": "Path to the IOS startup configuration file", - "type": "string", - }, "startup_config_content": { "description": "Content of IOS startup configuration file", "type": "string", }, - "private_config": { - "description": "Path to the IOS private configuration file", - "type": "string", - }, "private_config_content": { "description": "Content of IOS private configuration file", "type": "string", @@ -296,22 +288,6 @@ VM_UPDATE_SCHEMA = { "description": "Dynamips ID", "type": "integer" }, - "startup_config": { - "description": "Path to the IOS startup configuration file.", - "type": "string", - }, - "private_config": { - "description": "Path to the IOS private configuration file.", - "type": "string", - }, - "startup_config_content": { - "description": "Content of IOS startup configuration file", - "type": "string", - }, - "private_config_content": { - "description": "Content of IOS private configuration file", - "type": "string", - }, "ram": { "description": "Amount of RAM in MB", "type": "integer" @@ -552,14 +528,6 @@ VM_OBJECT_SCHEMA = { "type": ["string", "null"], "minLength": 1, }, - "startup_config": { - "description": "Path to the IOS startup configuration file", - "type": "string", - }, - "private_config": { - "description": "Path to the IOS private configuration file", - "type": "string", - }, "ram": { "description": "Amount of RAM in MB", "type": "integer" @@ -706,14 +674,6 @@ VM_OBJECT_SCHEMA = { {"type": "null"} ] }, - "startup_config_content": { - "description": "Content of IOS startup configuration file", - "type": "string", - }, - "private_config_content": { - "description": "Content of IOS private configuration file", - "type": "string", - }, # C7200 properties "npe": { "description": "NPE model", diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 8e54be64..389dea6e 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -78,14 +78,6 @@ IOU_CREATE_SCHEMA = { "description": "Use default IOU values", "type": ["boolean", "null"] }, - "startup_config": { - "description": "Path to the startup-config of IOU", - "type": ["string", "null"] - }, - "private_config": { - "description": "Path to the private-config of IOU", - "type": ["string", "null"] - }, "startup_config_content": { "description": "Startup-config of IOU", "type": ["string", "null"] @@ -94,10 +86,6 @@ IOU_CREATE_SCHEMA = { "description": "Private-config of IOU", "type": ["string", "null"] }, - "iourc_content": { - "description": "Content of the iourc file. Ignored if Null", - "type": ["string", "null"] - } }, "additionalProperties": False, "required": ["name", "path"] @@ -187,30 +175,10 @@ IOU_OBJECT_SCHEMA = { "description": "Always up ethernet interface", "type": "boolean" }, - "startup_config": { - "description": "Path of the startup-config content relative to project directory", - "type": ["string", "null"] - }, - "private_config": { - "description": "Path of the private-config content relative to project directory", - "type": ["string", "null"] - }, "use_default_iou_values": { "description": "Use default IOU values", "type": ["boolean", "null"] }, - "startup_config_content": { - "description": "Startup-config of IOU", - "type": ["string", "null"] - }, - "private_config_content": { - "description": "Private-config of IOU", - "type": ["string", "null"] - }, - "iourc_content": { - "description": "Content of the iourc file. Ignored if Null", - "type": ["string", "null"] - }, "command_line": { "description": "Last command line used by GNS3 to start QEMU", "type": "string" diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index 283f1091..f36351a8 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -50,10 +50,6 @@ VPCS_CREATE_SCHEMA = { "description": "Content of the VPCS startup script", "type": ["string", "null"] }, - "startup_script_path": { - "description": "Path of the VPCS startup script relative to project directory (IGNORED)", - "type": ["string", "null"] - } }, "additionalProperties": False, "required": ["name"] @@ -79,14 +75,6 @@ VPCS_UPDATE_SCHEMA = { "description": "Console type", "enum": ["telnet"] }, - "startup_script": { - "description": "Content of the VPCS startup script", - "type": ["string", "null"] - }, - "startup_script_path": { - "description": "Path of the VPCS startup script relative to project directory (IGNORED)", - "type": ["string", "null"] - } }, "additionalProperties": False, } @@ -133,19 +121,11 @@ VPCS_OBJECT_SCHEMA = { "maxLength": 36, "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" }, - "startup_script": { - "description": "Content of the VPCS startup script", - "type": ["string", "null"] - }, - "startup_script_path": { - "description": "Path of the VPCS startup script relative to project directory", - "type": ["string", "null"] - }, "command_line": { "description": "Last command line used by GNS3 to start QEMU", "type": "string" } }, "additionalProperties": False, - "required": ["name", "node_id", "status", "console", "console_type", "project_id", "startup_script_path", "command_line"] + "required": ["name", "node_id", "status", "console", "console_type", "project_id", "command_line"] } diff --git a/tests/controller/test_controller.py b/tests/controller/test_controller.py index 3227c54f..34a98741 100644 --- a/tests/controller/test_controller.py +++ b/tests/controller/test_controller.py @@ -465,3 +465,17 @@ def test_get_free_project_name(controller, async_run): async_run(controller.add_project(project_id=str(uuid.uuid4()), name="Test-1")) assert controller.get_free_project_name("Test") == "Test-2" assert controller.get_free_project_name("Hello") == "Hello" + + +def test_load_base_files(controller, config, tmpdir): + config.set_section_config("Server", {"configs_path": str(tmpdir)}) + + with open(str(tmpdir / 'iou_l2_base_startup-config.txt'), 'w+') as f: + f.write('test') + + controller.load_base_files() + assert os.path.exists(str(tmpdir / 'iou_l3_base_startup-config.txt')) + + # Check is the file has not been overwrite + with open(str(tmpdir / 'iou_l2_base_startup-config.txt')) as f: + assert f.read() == 'test' diff --git a/tests/controller/test_node.py b/tests/controller/test_node.py index f3b6e55a..0d7e7456 100644 --- a/tests/controller/test_node.py +++ b/tests/controller/test_node.py @@ -88,19 +88,6 @@ def test_eq(compute, project, node, controller): assert node != Node(Project(str(uuid.uuid4()), controller=controller), compute, "demo3", node_id=node.id, node_type="qemu") -def test_properties_filter(project, compute): - """ - Some properties are private and should not be exposed - """ - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="vpcs", - console_type="vnc", - properties={"startup_script": "echo test", "iourc_content": "test"}) - assert node._properties == {"startup_script": "echo test", "iourc_content": "test"} - assert node._filter_properties() == {"startup_script": "echo test"} - - def test_json(node, compute): assert node.__json__() == { "compute_id": str(compute.id), @@ -207,6 +194,30 @@ def test_create_image_missing(node, compute, project, async_run): node._upload_missing_image.called is True +def test_create_base_script(node, config, compute, tmpdir, async_run): + config.set_section_config("Server", {"configs_path": str(tmpdir)}) + + with open(str(tmpdir / 'test.txt'), 'w+') as f: + f.write('hostname test') + + node._properties = {"base_script_file": "test.txt"} + node._console = 2048 + + response = MagicMock() + response.json = {"console": 2048} + compute.post = AsyncioMagicMock(return_value=response) + + assert async_run(node.create()) is True + data = { + "console": 2048, + "console_type": "vnc", + "node_id": node.id, + "startup_script": "hostname test", + "name": "demo" + } + compute.post.assert_called_with("/projects/{}/vpcs/nodes".format(node.project.id), data=data, timeout=120) + + def test_symbol(node, symbols_dir): """ Change symbol should change the node size diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index 1fec7320..bb3c1228 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -137,7 +137,7 @@ def test_add_node_local(async_run, controller): response.json = {"console": 2048} compute.post = AsyncioMagicMock(return_value=response) - node = async_run(project.add_node(compute, "test", None, node_type="vpcs", properties={"startup_config": "test.cfg"})) + node = async_run(project.add_node(compute, "test", None, node_type="vpcs", properties={"startup_script": "test.cfg"})) assert node.id in project._nodes compute.post.assert_any_call('/projects', data={ @@ -147,7 +147,7 @@ def test_add_node_local(async_run, controller): }) compute.post.assert_any_call('/projects/{}/vpcs/nodes'.format(project.id), data={'node_id': node.id, - 'startup_config': 'test.cfg', + 'startup_script': 'test.cfg', 'name': 'test'}, timeout=120) assert compute in project._project_created_on_compute @@ -167,7 +167,7 @@ def test_add_node_non_local(async_run, controller): response.json = {"console": 2048} compute.post = AsyncioMagicMock(return_value=response) - node = async_run(project.add_node(compute, "test", None, node_type="vpcs", properties={"startup_config": "test.cfg"})) + node = async_run(project.add_node(compute, "test", None, node_type="vpcs", properties={"startup_script": "test.cfg"})) compute.post.assert_any_call('/projects', data={ "name": project._name, @@ -175,7 +175,7 @@ def test_add_node_non_local(async_run, controller): }) compute.post.assert_any_call('/projects/{}/vpcs/nodes'.format(project.id), data={'node_id': node.id, - 'startup_config': 'test.cfg', + 'startup_script': 'test.cfg', 'name': 'test'}, timeout=120) assert compute in project._project_created_on_compute diff --git a/tests/controller/test_project_open.py b/tests/controller/test_project_open.py index 252692dd..ac0473d5 100644 --- a/tests/controller/test_project_open.py +++ b/tests/controller/test_project_open.py @@ -106,7 +106,6 @@ def demo_topology(): "node_type": "vpcs", "properties": { "startup_script": "", - "startup_script_path": "startup.vpc" }, "symbol": ":/symbols/computer.svg", "width": 65, @@ -131,7 +130,6 @@ def demo_topology(): "node_type": "vpcs", "properties": { "startup_script": "", - "startup_script_path": "startup.vpc" }, "symbol": ":/symbols/computer.svg", "width": 65, diff --git a/tests/handlers/api/compute/test_iou.py b/tests/handlers/api/compute/test_iou.py index 8d63cbdf..f31f367a 100644 --- a/tests/handlers/api/compute/test_iou.py +++ b/tests/handlers/api/compute/test_iou.py @@ -80,7 +80,6 @@ def test_iou_create_with_params(http_compute, project, base_params): params["l1_keepalives"] = True params["startup_config_content"] = "hostname test" params["use_default_iou_values"] = True - params["iourc_content"] = "test" response = http_compute.post("/projects/{project_id}/iou/nodes".format(project_id=project.id), params, example=True) assert response.status == 201 @@ -94,7 +93,6 @@ def test_iou_create_with_params(http_compute, project, base_params): assert response.json["l1_keepalives"] is True assert response.json["use_default_iou_values"] is True - assert "startup-config.cfg" in response.json["startup_config"] with open(startup_config_file(project, response.json)) as f: assert f.read() == "hostname test" @@ -115,7 +113,6 @@ def test_iou_create_startup_config_already_exist(http_compute, project, base_par assert response.status == 201 assert response.route == "/projects/{project_id}/iou/nodes" - assert "startup-config.cfg" in response.json["startup_config"] with open(startup_config_file(project, response.json)) as f: assert f.read() == "echo hello" @@ -183,9 +180,7 @@ def test_iou_update(http_compute, vm, tmpdir, free_console_port, project): "ethernet_adapters": 4, "serial_adapters": 0, "l1_keepalives": True, - "startup_config_content": "hostname test", "use_default_iou_values": True, - "iourc_content": "test" } response = http_compute.put("/projects/{project_id}/iou/nodes/{node_id}".format(project_id=vm["project_id"], node_id=vm["node_id"]), params, example=True) assert response.status == 200 @@ -197,9 +192,6 @@ def test_iou_update(http_compute, vm, tmpdir, free_console_port, project): assert response.json["nvram"] == 2048 assert response.json["l1_keepalives"] is True assert response.json["use_default_iou_values"] is True - assert "startup-config.cfg" in response.json["startup_config"] - with open(startup_config_file(project, response.json)) as f: - assert f.read() == "hostname test" def test_iou_nio_create_udp(http_compute, vm): diff --git a/tests/handlers/api/compute/test_vpcs.py b/tests/handlers/api/compute/test_vpcs.py index 6a0ea0b7..85456e5e 100644 --- a/tests/handlers/api/compute/test_vpcs.py +++ b/tests/handlers/api/compute/test_vpcs.py @@ -43,7 +43,6 @@ def test_vpcs_get(http_compute, project, vm): assert response.route == "/projects/{project_id}/vpcs/nodes/{node_id}" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id - assert response.json["startup_script_path"] is None assert response.json["status"] == "stopped" @@ -53,8 +52,6 @@ def test_vpcs_create_startup_script(http_compute, project): assert response.route == "/projects/{project_id}/vpcs/nodes" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id - assert response.json["startup_script"] == os.linesep.join(["ip 192.168.1.2", "echo TEST"]) - assert response.json["startup_script_path"] == "startup.vpc" def test_vpcs_create_port(http_compute, project, free_console_port): diff --git a/tests/topologies/1_3_dynamips/after/1_3_dynamips.gns3 b/tests/topologies/1_3_dynamips/after/1_3_dynamips.gns3 index eb264324..30326e4e 100644 --- a/tests/topologies/1_3_dynamips/after/1_3_dynamips.gns3 +++ b/tests/topologies/1_3_dynamips/after/1_3_dynamips.gns3 @@ -63,7 +63,6 @@ ], "slot0": "C7200-IO-FE", "sparsemem": true, - "startup_config": "configs/i1_startup-config.cfg", "system_id": "FTX0945W0MY" }, "x": -112, diff --git a/tests/topologies/1_3_dynamips_missing_platform/after/1_3_dynamips_missing_platform.gns3 b/tests/topologies/1_3_dynamips_missing_platform/after/1_3_dynamips_missing_platform.gns3 index 40db2eb6..948e0745 100644 --- a/tests/topologies/1_3_dynamips_missing_platform/after/1_3_dynamips_missing_platform.gns3 +++ b/tests/topologies/1_3_dynamips_missing_platform/after/1_3_dynamips_missing_platform.gns3 @@ -27,7 +27,6 @@ "slot0": "Leopard-2FE", "idlepc": "0x6057efc8", "chassis": "3660", - "startup_config": "configs/i1_startup-config.cfg", "image": "c3660-a3jk9s-mz.124-25c.bin", "mac_addr": "cc01.20b8.0000", "aux": 2103, diff --git a/tests/topologies/1_5_dynamips/after/1_5_dynamips.gns3 b/tests/topologies/1_5_dynamips/after/1_5_dynamips.gns3 index 81532226..ea1e08f0 100644 --- a/tests/topologies/1_5_dynamips/after/1_5_dynamips.gns3 +++ b/tests/topologies/1_5_dynamips/after/1_5_dynamips.gns3 @@ -53,7 +53,6 @@ "ram": 256, "slot0": "GT96100-FE", "sparsemem": true, - "startup_config": "configs/i1_startup-config.cfg", "system_id": "FTX0945W0MY" }, "symbol": ":/symbols/router.svg", @@ -100,7 +99,6 @@ "slot0": "Leopard-2FE", "slot1": "NM-16ESW", "sparsemem": true, - "startup_config": "configs/i2_startup-config.cfg", "system_id": "FTX0945W0MY" }, "symbol": ":/symbols/multilayer_switch.svg", diff --git a/tests/topologies/1_5_internet/after/1_5_internet.gns3 b/tests/topologies/1_5_internet/after/1_5_internet.gns3 index 7214d7c7..76109038 100644 --- a/tests/topologies/1_5_internet/after/1_5_internet.gns3 +++ b/tests/topologies/1_5_internet/after/1_5_internet.gns3 @@ -76,7 +76,6 @@ "port_segment_size": 0, "first_port_name": null, "properties": { - "startup_script_path": "startup.vpc" }, "symbol": ":/symbols/vpcs_guest.svg", "x": -29, diff --git a/tests/topologies/1_5_iou/after/1_5_iou.gns3 b/tests/topologies/1_5_iou/after/1_5_iou.gns3 index f45baa1c..ba57da26 100644 --- a/tests/topologies/1_5_iou/after/1_5_iou.gns3 +++ b/tests/topologies/1_5_iou/after/1_5_iou.gns3 @@ -41,7 +41,6 @@ "path": "i86bi-linux-l3-adventerprisek9-15.4.1T.bin", "ram": 256, "serial_adapters": 2, - "startup_config": "startup-config.cfg", "use_default_iou_values": true }, "symbol": ":/symbols/router.svg", diff --git a/tests/topologies/1_5_snapshot/after/1_5_snapshot.gns3 b/tests/topologies/1_5_snapshot/after/1_5_snapshot.gns3 index 08f8c367..e1d2afaf 100644 --- a/tests/topologies/1_5_snapshot/after/1_5_snapshot.gns3 +++ b/tests/topologies/1_5_snapshot/after/1_5_snapshot.gns3 @@ -18,7 +18,6 @@ "port_segment_size": 0, "first_port_name": null, "properties" : { - "startup_script_path" : "startup.vpc" }, "label" : { "y" : -25, diff --git a/tests/topologies/1_5_vpcs/after/1_5_vpcs.gns3 b/tests/topologies/1_5_vpcs/after/1_5_vpcs.gns3 index 59e98ebb..64c5083d 100644 --- a/tests/topologies/1_5_vpcs/after/1_5_vpcs.gns3 +++ b/tests/topologies/1_5_vpcs/after/1_5_vpcs.gns3 @@ -50,7 +50,6 @@ "port_segment_size": 0, "first_port_name": null, "properties": { - "startup_script_path": "startup.vpc" }, "symbol": ":/symbols/vpcs_guest.svg", "x": -87, @@ -75,7 +74,6 @@ "port_segment_size": 0, "first_port_name": null, "properties": { - "startup_script_path": "startup.vpc" }, "symbol": ":/symbols/vpcs_guest.svg", "x": 123, diff --git a/tests/topologies/dynamips_2_0_0_b2/after/dynamips_2_0_0_b2.gns3 b/tests/topologies/dynamips_2_0_0_b2/after/dynamips_2_0_0_b2.gns3 index 13febe9f..336bb988 100644 --- a/tests/topologies/dynamips_2_0_0_b2/after/dynamips_2_0_0_b2.gns3 +++ b/tests/topologies/dynamips_2_0_0_b2/after/dynamips_2_0_0_b2.gns3 @@ -61,8 +61,6 @@ 1, 1 ], - "private_config": "", - "private_config_content": "", "ram": 512, "sensors": [ 22, @@ -78,8 +76,6 @@ "slot5": null, "slot6": null, "sparsemem": true, - "startup_config": "configs/i1_startup-config.cfg", - "startup_config_content": "!\n!\nservice timestamps debug datetime msec\nservice timestamps log datetime msec\nno service password-encryption\n!\nhostname R1\n!\nip cef\nno ip domain-lookup\nno ip icmp rate-limit unreachable\nip tcp synwait 5\nno cdp log mismatch duplex\n!\nline con 0\n exec-timeout 0 0\n logging synchronous\n privilege level 15\n no login\nline aux 0\n exec-timeout 0 0\n logging synchronous\n privilege level 15\n no login\n!\n!\nend\n", "system_id": "FTX0945W0MY" }, "symbol": ":/symbols/router.svg", @@ -129,8 +125,6 @@ 1, 1 ], - "private_config": "", - "private_config_content": "", "ram": 512, "sensors": [ 22, @@ -146,8 +140,6 @@ "slot5": null, "slot6": null, "sparsemem": true, - "startup_config": "configs/i2_startup-config.cfg", - "startup_config_content": "!\n!\nservice timestamps debug datetime msec\nservice timestamps log datetime msec\nno service password-encryption\n!\nhostname R2\n!\nip cef\nno ip domain-lookup\nno ip icmp rate-limit unreachable\nip tcp synwait 5\nno cdp log mismatch duplex\n!\nline con 0\n exec-timeout 0 0\n logging synchronous\n privilege level 15\n no login\nline aux 0\n exec-timeout 0 0\n logging synchronous\n privilege level 15\n no login\n!\n!\nend\n", "system_id": "FTX0945W0MY" }, "symbol": ":/symbols/router.svg", @@ -160,4 +152,4 @@ }, "type": "topology", "version": "2.0.0dev7" -} \ No newline at end of file +} From 96194cef673546723c1b8c63ad6abb7eef05ca1d Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 3 Feb 2017 14:44:32 +0100 Subject: [PATCH 04/63] Fix import/export of dynamips configuration --- gns3server/handlers/api/controller/node_handler.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/gns3server/handlers/api/controller/node_handler.py b/gns3server/handlers/api/controller/node_handler.py index 67857df7..d9a715f0 100644 --- a/gns3server/handlers/api/controller/node_handler.py +++ b/gns3server/handlers/api/controller/node_handler.py @@ -344,10 +344,7 @@ class NodeHandler: raise aiohttp.web.HTTPForbidden node_type = node.node_type - if node_type == "dynamips": - path = "/project-files/{}/{}".format(node_type, path) - else: - path = "/project-files/{}/{}/{}".format(node_type, node.id, path) + path = "/project-files/{}/{}/{}".format(node_type, node.id, path) res = yield from node.compute.http_query("GET", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), timeout=None, raw=True) response.set_status(200) @@ -384,12 +381,9 @@ class NodeHandler: raise aiohttp.web.HTTPForbidden node_type = node.node_type - if node_type == "dynamips": - path = "/project-files/{}/{}".format(node_type, path) - else: - path = "/project-files/{}/{}/{}".format(node_type, node.id, path) + path = "/project-files/{}/{}/{}".format(node_type, node.id, path) data = yield from request.content.read() - res = yield from node.compute.http_query("POST", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), data=data, timeout=None, raw=True) + yield from node.compute.http_query("POST", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), data=data, timeout=None, raw=True) response.set_status(201) From 959c08449ec44cc8ac1d2fac0596dccac046e4a7 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 3 Feb 2017 14:56:55 +0100 Subject: [PATCH 05/63] Do not crash if you pass {name} in name --- gns3server/controller/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 4debcb73..2e360b15 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -289,7 +289,7 @@ class Project: if '{0}' in base_name or '{id}' in base_name: # base name is a template, replace {0} or {id} by an unique identifier for number in range(1, 1000000): - name = base_name.format(number, id=number) + name = base_name.format(number, id=number, name="Node") if name not in self._allocated_node_names: self._allocated_node_names.add(name) return name From d06af526b29b85db1fbd3d721cf53a7ee5bc5973 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 6 Feb 2017 10:49:09 +0100 Subject: [PATCH 06/63] Fix the server don't start if a remote is unavailable --- gns3server/controller/__init__.py | 2 -- gns3server/controller/compute.py | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 295e6762..825e9455 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -16,7 +16,6 @@ # along with this program. If not, see . import os -import sys import json import socket import asyncio @@ -318,7 +317,6 @@ class Controller: try: return self._computes[compute_id] except KeyError: - server_config = Config.instance().get_section_config("Server") if compute_id == "vm": raise aiohttp.web.HTTPNotFound(text="You try to use a node on the GNS3 VM server but the GNS3 VM is not configured") raise aiohttp.web.HTTPNotFound(text="Compute ID {} doesn't exist".format(compute_id)) diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py index 47b780e8..f3373c46 100644 --- a/gns3server/controller/compute.py +++ b/gns3server/controller/compute.py @@ -22,14 +22,12 @@ import socket import json import uuid import sys -import os import io from ..utils import parse_version -from ..utils.images import list_images, md5sum +from ..utils.images import list_images from ..utils.asyncio import locked_coroutine from ..controller.controller_error import ControllerError -from ..config import Config from ..version import __version__ @@ -381,7 +379,9 @@ class Compute: except aiohttp.web.HTTPNotFound: raise aiohttp.web.HTTPConflict(text="The server {} is not a GNS3 server or it's a 1.X server".format(self._id)) except aiohttp.web.HTTPUnauthorized: - raise aiohttp.web.HTTPConflict(text="Invalid auth for server {} ".format(self._id)) + raise aiohttp.web.HTTPConflict(text="Invalid auth for server {}".format(self._id)) + except aiohttp.web.HTTPServiceUnavailable: + raise aiohttp.web.HTTPConflict(text="The server {} is unavailable".format(self._id)) if "version" not in response.json: self._http_session.close() From fbe26d11cff6e6ea125949b65ec43820f7aa9149 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 6 Feb 2017 11:07:35 +0100 Subject: [PATCH 07/63] Fix a potential crash --- gns3server/controller/__init__.py | 2 +- gns3server/controller/project.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 825e9455..581a9897 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -378,7 +378,7 @@ class Controller: :param load: Load the topology """ topo_data = load_topology(path) - topology = topo_data.pop("topology") + topo_data.pop("topology") topo_data.pop("version") topo_data.pop("revision") topo_data.pop("type") diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 2e360b15..35793034 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -440,7 +440,7 @@ class Project: Create a link. By default the link is empty """ if link_id and link_id in self._links: - return self._links[link.id] + return self._links[link_id] link = UDPLink(self, link_id=link_id) self._links[link.id] = link self.dump() From 0d7157c295c462c49924bbd825f9396d9c005448 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 6 Feb 2017 11:40:00 +0100 Subject: [PATCH 08/63] Improve a lot project loading speed Fix #893 --- gns3server/controller/link.py | 7 +++++-- gns3server/controller/project.py | 26 +++++++++++++++++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/gns3server/controller/link.py b/gns3server/controller/link.py index 5f8bf61e..03c5f12c 100644 --- a/gns3server/controller/link.py +++ b/gns3server/controller/link.py @@ -52,9 +52,11 @@ class Link: return self._created @asyncio.coroutine - def add_node(self, node, adapter_number, port_number, label=None): + def add_node(self, node, adapter_number, port_number, label=None, dump=True): """ Add a node to the link + + :param dump: Dump project on disk """ port = node.get_port(adapter_number, port_number) @@ -101,7 +103,8 @@ class Link: self._created = True self._project.controller.notification.emit("link.created", self.__json__()) - self._project.dump() + if dump: + self._project.dump() @asyncio.coroutine def update_nodes(self, nodes): diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 35793034..38d3c4ed 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -314,10 +314,11 @@ class Project: @open_required @asyncio.coroutine - def add_node(self, compute, name, node_id, node_type=None, **kwargs): + def add_node(self, compute, name, node_id, dump=True, node_type=None, **kwargs): """ Create a node or return an existing node + :param dump: Dump topology to disk :param kwargs: See the documentation of node """ if node_id in self._nodes: @@ -349,7 +350,8 @@ class Project: yield from node.create() self._nodes[node.id] = node self.controller.notification.emit("node.created", node.__json__()) - self.dump() + if dump: + self.dump() return node @locked_coroutine @@ -401,17 +403,19 @@ class Project: @open_required @asyncio.coroutine - def add_drawing(self, drawing_id=None, **kwargs): + def add_drawing(self, drawing_id=None, dump=True, **kwargs): """ Create an drawing or return an existing drawing + :param dump: Dump the topology to disk :param kwargs: See the documentation of drawing """ if drawing_id not in self._drawings: drawing = Drawing(self, drawing_id=drawing_id, **kwargs) self._drawings[drawing.id] = drawing self.controller.notification.emit("drawing.created", drawing.__json__()) - self.dump() + if dump: + self.dump() return drawing return self._drawings[drawing_id] @@ -435,15 +439,18 @@ class Project: @open_required @asyncio.coroutine - def add_link(self, link_id=None): + def add_link(self, link_id=None, dump=True): """ Create a link. By default the link is empty + + :param dump: Dump topology to disk """ if link_id and link_id in self._links: return self._links[link_id] link = UDPLink(self, link_id=link_id) self._links[link.id] = link - self.dump() + if dump: + self.dump() return link @open_required @@ -626,15 +633,16 @@ class Project: compute = self.controller.get_compute(node.pop("compute_id")) name = node.pop("name") node_id = node.pop("node_id") - yield from self.add_node(compute, name, node_id, **node) + yield from self.add_node(compute, name, node_id, **node, dump=False) for link_data in topology.get("links", []): link = yield from self.add_link(link_id=link_data["link_id"]) for node_link in link_data["nodes"]: node = self.get_node(node_link["node_id"]) - yield from link.add_node(node, node_link["adapter_number"], node_link["port_number"], label=node_link.get("label")) + yield from link.add_node(node, node_link["adapter_number"], node_link["port_number"], label=node_link.get("label"), dump=False) for drawing_data in topology.get("drawings", []): - drawing = yield from self.add_drawing(**drawing_data) + yield from self.add_drawing(**drawing_data, dump=False) + 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: From 9c7d2e9915d34e556a203fdcdf25b09a44f864ba Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 6 Feb 2017 15:05:29 +0100 Subject: [PATCH 09/63] Raise an error if you put an invalid key in node name Fix https://github.com/GNS3/gns3-gui/issues/1833 --- gns3server/controller/project.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 38d3c4ed..c0dab9cb 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -289,7 +289,10 @@ class Project: if '{0}' in base_name or '{id}' in base_name: # base name is a template, replace {0} or {id} by an unique identifier for number in range(1, 1000000): - name = base_name.format(number, id=number, name="Node") + try: + name = base_name.format(number, id=number, name="Node") + except KeyError as e: + raise aiohttp.web.HTTPConflict(text="{" + e.args[0] + "} is not a valid replacement string in the node name") if name not in self._allocated_node_names: self._allocated_node_names.add(name) return name From 08c2892295dfae0ad4c7c4ac035501102b7da360 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 6 Feb 2017 16:47:40 +0100 Subject: [PATCH 10/63] If we can't resolve compute name return 0.0.0.0 It's not perfect, but it's rare in most cases it's handle before. Fix #892 --- gns3server/controller/compute.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py index f3373c46..7437d796 100644 --- a/gns3server/controller/compute.py +++ b/gns3server/controller/compute.py @@ -214,7 +214,10 @@ class Compute: """ Return the IP associated to the host """ - return socket.gethostbyname(self._host) + try: + return socket.gethostbyname(self._host) + except socket.gaierror: + return '0.0.0.0' @host.setter def host(self, host): From bcc71b54557647428cd9a96c8de15e69dd7e6d34 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 6 Feb 2017 16:52:13 +0100 Subject: [PATCH 11/63] Fix a crash with Python 3.4 Fix https://github.com/GNS3/gns3-server/issues/876 --- gns3server/controller/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index c0dab9cb..d8b73a99 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -636,7 +636,7 @@ class Project: compute = self.controller.get_compute(node.pop("compute_id")) name = node.pop("name") node_id = node.pop("node_id") - yield from self.add_node(compute, name, node_id, **node, dump=False) + yield from self.add_node(compute, name, node_id, dump=False, **node) for link_data in topology.get("links", []): link = yield from self.add_link(link_id=link_data["link_id"]) for node_link in link_data["nodes"]: From 6ded234681cabfa0f4eb2955e233f3aa8573e7d5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 6 Feb 2017 17:19:02 +0100 Subject: [PATCH 12/63] Fix an error with Python 3.4 --- gns3server/controller/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index d8b73a99..8796e028 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -643,7 +643,7 @@ class Project: node = self.get_node(node_link["node_id"]) yield from link.add_node(node, node_link["adapter_number"], node_link["port_number"], label=node_link.get("label"), dump=False) for drawing_data in topology.get("drawings", []): - yield from self.add_drawing(**drawing_data, dump=False) + yield from self.add_drawing(dump=False, **drawing_data) self.dump() # We catch all error to be able to rollback the .gns3 to the previous state From 94fd4bcbe9acc3f3ae82a0bd172f89fab6e088bd Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 6 Feb 2017 17:56:08 +0100 Subject: [PATCH 13/63] Fix stacktrace display when connecting to remote server Fix #891 --- gns3server/controller/compute.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py index 7437d796..413abba6 100644 --- a/gns3server/controller/compute.py +++ b/gns3server/controller/compute.py @@ -361,6 +361,16 @@ class Compute: response = yield from self._run_http_query(method, path, data=data, **kwargs) return response + @asyncio.coroutine + def _try_reconnect(self): + """ + We catch error during reconnect + """ + try: + yield from self.connect() + except aiohttp.web.HTTPConflict: + pass + @locked_coroutine def connect(self): """ @@ -375,8 +385,10 @@ class Compute: self._connection_failure += 1 # After 5 failure we close the project using the compute to avoid sync issues if self._connection_failure == 5: + log.warning("Can't connect to compute %s", self._id) yield from self._controller.close_compute_projects(self) - asyncio.get_event_loop().call_later(2, lambda: asyncio.async(self.connect())) + + asyncio.get_event_loop().call_later(2, lambda: asyncio.async(self._try_reconnect())) return except aiohttp.web.HTTPNotFound: From 19b70accd553244ab9403fe53a18f5b954a7688d Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 6 Feb 2017 17:59:00 +0100 Subject: [PATCH 14/63] Catch error when we can't access to a unix socket --- gns3server/utils/asyncio/serial.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gns3server/utils/asyncio/serial.py b/gns3server/utils/asyncio/serial.py index 6dc961dd..48d12bc0 100644 --- a/gns3server/utils/asyncio/serial.py +++ b/gns3server/utils/asyncio/serial.py @@ -122,7 +122,10 @@ def _asyncio_open_serial_unix(path): raise NodeError('Pipe file "{}" is missing'.format(path)) output = SerialReaderWriterProtocol() - con = yield from asyncio.get_event_loop().create_unix_connection(lambda: output, path) + try: + yield from asyncio.get_event_loop().create_unix_connection(lambda: output, path) + except ConnectionRefusedError: + raise NodeError('Can\'t open pipe file "{}"'.format(path)) return output From f33e470601c3400d7a9c2761e0b195b46dcf36f9 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 7 Feb 2017 10:36:36 +0100 Subject: [PATCH 15/63] Fix error when you have error on your filesystem during project convertion Fix #894 --- gns3server/controller/topology.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/gns3server/controller/topology.py b/gns3server/controller/topology.py index b9400364..3f7a1540 100644 --- a/gns3server/controller/topology.py +++ b/gns3server/controller/topology.py @@ -163,11 +163,14 @@ def _convert_2_0_0_beta_2(topo, topo_path): dynamips_dir = os.path.join(topo_dir, "project-files", "dynamips") node_dir = os.path.join(dynamips_dir, node_id) - os.makedirs(os.path.join(node_dir, "configs"), exist_ok=True) - for path in glob.glob(os.path.join(glob.escape(dynamips_dir), "*_i{}_*".format(dynamips_id))): - shutil.move(path, os.path.join(node_dir, os.path.basename(path))) - for path in glob.glob(os.path.join(glob.escape(dynamips_dir), "configs", "i{}_*".format(dynamips_id))): - shutil.move(path, os.path.join(node_dir, "configs", os.path.basename(path))) + try: + os.makedirs(os.path.join(node_dir, "configs"), exist_ok=True) + for path in glob.glob(os.path.join(glob.escape(dynamips_dir), "*_i{}_*".format(dynamips_id))): + shutil.move(path, os.path.join(node_dir, os.path.basename(path))) + for path in glob.glob(os.path.join(glob.escape(dynamips_dir), "configs", "i{}_*".format(dynamips_id))): + shutil.move(path, os.path.join(node_dir, "configs", os.path.basename(path))) + except OSError as e: + raise aiohttp.web.HTTPConflict(text="Can't convert project {}: {}".format(topo_path, str(e))) return topo From b0567772f77b5fca10f7a0dc874a907f77119fc9 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 7 Feb 2017 17:04:29 +0100 Subject: [PATCH 16/63] Allow up to 275 adapters for qemu See #889 for more details --- gns3server/compute/qemu/qemu_vm.py | 21 ++++++++++++++++--- gns3server/schemas/qemu.py | 6 +++--- tests/compute/qemu/test_qemu_vm.py | 33 +++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 9ac7eb21..fd9e9360 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -23,12 +23,13 @@ order to run a QEMU VM. import sys import os import re +import math import shutil -import subprocess import shlex import asyncio import socket import gns3server +import subprocess from gns3server.utils import parse_version from .qemu_error import QemuError @@ -1446,6 +1447,14 @@ class QemuVM(BaseNode): # this is a patched Qemu if version is below 1.1.0 patched_qemu = True + # Each 32 PCI device we need to add a PCI bridge with max 9 bridges + pci_devices = 4 + len(self._ethernet_adapters) # 4 PCI devices are use by default by qemu + bridge_id = 0 + for bridge_id in range(1, math.floor(pci_devices / 32) + 1): + network_options.extend(["-device", "i82801b11-bridge,id=dmi_pci_bridge{bridge_id}".format(bridge_id=bridge_id)]) + network_options.extend(["-device", "pci-bridge,id=pci-bridge{bridge_id},bus=dmi_pci_bridge{bridge_id},chassis_nr=0x1,addr=0x{bridge_id},shpc=off".format(bridge_id=bridge_id)]) + + pci_device_id = 4 + bridge_id # Bridge consume PCI ports for adapter_number, adapter in enumerate(self._ethernet_adapters): mac = int_to_macaddress(macaddress_to_int(self._mac_address) + adapter_number) @@ -1483,8 +1492,14 @@ class QemuVM(BaseNode): else: # newer QEMU networking syntax + device_string = "{},mac={}".format(self._adapter_type, mac) + bridge_id = math.floor(pci_device_id / 32) + if bridge_id > 0: + addr = pci_device_id % 32 + device_string = "{},bus=pci-bridge{bridge_id},addr=0x{addr:02x}".format(device_string, bridge_id=bridge_id, addr=addr) + pci_device_id += 1 if nio: - network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_number)]) + network_options.extend(["-device", "{},netdev=gns3-{}".format(device_string, adapter_number)]) if isinstance(nio, NIOUDP): network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_number, nio.rhost, @@ -1494,7 +1509,7 @@ class QemuVM(BaseNode): elif isinstance(nio, NIOTAP): network_options.extend(["-netdev", "tap,id=gns3-{},ifname={},script=no,downscript=no".format(adapter_number, nio.tap_device)]) else: - network_options.extend(["-device", "{},mac={}".format(self._adapter_type, mac)]) + network_options.extend(["-device", device_string]) return network_options diff --git a/gns3server/schemas/qemu.py b/gns3server/schemas/qemu.py index 36e2fa6b..aa16d594 100644 --- a/gns3server/schemas/qemu.py +++ b/gns3server/schemas/qemu.py @@ -147,7 +147,7 @@ QEMU_CREATE_SCHEMA = { "description": "Number of adapters", "type": ["integer", "null"], "minimum": 0, - "maximum": 32, + "maximum": 275, }, "adapter_type": { "description": "QEMU adapter type", @@ -332,7 +332,7 @@ QEMU_UPDATE_SCHEMA = { "description": "Number of adapters", "type": ["integer", "null"], "minimum": 0, - "maximum": 32, + "maximum": 275, }, "adapter_type": { "description": "QEMU adapter type", @@ -520,7 +520,7 @@ QEMU_OBJECT_SCHEMA = { "description": "Number of adapters", "type": "integer", "minimum": 0, - "maximum": 32, + "maximum": 275, }, "adapter_type": { "description": "QEMU adapter type", diff --git a/tests/compute/qemu/test_qemu_vm.py b/tests/compute/qemu/test_qemu_vm.py index 8b01381d..91e40d9c 100644 --- a/tests/compute/qemu/test_qemu_vm.py +++ b/tests/compute/qemu/test_qemu_vm.py @@ -588,7 +588,7 @@ def test_build_command_two_adapters_mac_address(vm, loop, fake_qemu_binary, port vm.adapters = 2 vm.mac_address = "00:00:ab:0e:0f:09" mac_0 = vm._mac_address - mac_1 = int_to_macaddress(macaddress_to_int(vm._mac_address)) + mac_1 = int_to_macaddress(macaddress_to_int(vm._mac_address) + 1) assert mac_0[:8] == "00:00:ab" with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: cmd = loop.run_until_complete(asyncio.async(vm._build_command())) @@ -605,6 +605,37 @@ def test_build_command_two_adapters_mac_address(vm, loop, fake_qemu_binary, port assert "e1000,mac={}".format(mac_1) in cmd +def test_build_command_large_number_of_adapters(vm, loop, fake_qemu_binary, port_manager): + """ + When we have more than 28 interface we need to add a pci bridge for + additionnal interfaces + """ + + vm.adapters = 100 + vm.mac_address = "00:00:ab:0e:0f:09" + mac_0 = vm._mac_address + mac_1 = int_to_macaddress(macaddress_to_int(vm._mac_address) + 1) + assert mac_0[:8] == "00:00:ab" + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: + cmd = loop.run_until_complete(asyncio.async(vm._build_command())) + + assert "e1000,mac={}".format(mac_0) in cmd + assert "e1000,mac={}".format(mac_1) in cmd + assert "pci-bridge,id=pci-bridge0,bus=dmi_pci_bridge0,chassis_nr=0x1,addr=0x0,shpc=off" not in cmd + assert "pci-bridge,id=pci-bridge1,bus=dmi_pci_bridge1,chassis_nr=0x1,addr=0x1,shpc=off" in cmd + assert "pci-bridge,id=pci-bridge2,bus=dmi_pci_bridge2,chassis_nr=0x1,addr=0x2,shpc=off" in cmd + assert "i82801b11-bridge,id=dmi_pci_bridge1" in cmd + + print(cmd) + + mac_29 = int_to_macaddress(macaddress_to_int(vm._mac_address) + 29) + assert "e1000,mac={},bus=pci-bridge1,addr=0x04".format(mac_29) in cmd + mac_30 = int_to_macaddress(macaddress_to_int(vm._mac_address) + 30) + assert "e1000,mac={},bus=pci-bridge1,addr=0x05".format(mac_30) in cmd + mac_74 = int_to_macaddress(macaddress_to_int(vm._mac_address) + 74) + assert "e1000,mac={},bus=pci-bridge2,addr=0x11".format(mac_74) in cmd + + # Windows accept this kind of mistake @pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows") def test_build_command_with_invalid_options(vm, loop, fake_qemu_binary): From 4fed98617bc7fa86ba467e3b9edd7c48c8d1e25b Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 13 Feb 2017 11:39:21 +0100 Subject: [PATCH 17/63] Add Dockerfile for development --- Dockerfile | 34 ++++++++++++++++++++++++++++++++++ README.rst | 10 ++++++++++ 2 files changed, 44 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..5f6dcc4a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# Dockerfile for GNS3 server development + +FROM ubuntu:16.04 + +ENV DEBIAN_FRONTEND noninteractive + +# Set the locale +RUN locale-gen en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 + +RUN apt-get update && apt-get install -y software-properties-common +RUN add-apt-repository ppa:gns3/ppa +RUN apt-get update && apt-get install -y \ + python3-pip \ + python3-dev \ + qemu-system-x86 \ + qemu-system-arm \ + qemu-kvm \ + libvirt-bin \ + x11vnc + +# Install uninstall to install dependencies +RUN apt-get install -y vpcs ubridge + +ADD . /server +WORKDIR /server + +RUN pip3 install -r /server/requirements.txt + +EXPOSE 3080 + +ENTRYPOINT python3 -m gns3server --local diff --git a/README.rst b/README.rst index 81ffc9d5..6da27c4f 100644 --- a/README.rst +++ b/README.rst @@ -71,6 +71,16 @@ To run tests use: py.test -v +Docker container +**************** + +For development you can run the GNS3 server in a container + +.. code:: bash + docker build -t gns3-server . + docker run -i --expose=8001 -p 8001:8001/tcp -t gns3-server python3 -m gns3server --local --port 8001 + + Run as daemon (Unix only) ************************** From defcf8261048cc17acd8d85a54884ff96b0083c4 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 13 Feb 2017 15:18:00 +0100 Subject: [PATCH 18/63] Fix a rare error when closing a project Fix #897 --- gns3server/controller/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 8796e028..5bf23744 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -536,7 +536,7 @@ class Project: @asyncio.coroutine def close(self, ignore_notification=False): yield from self.stop_all() - 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), dont_connect=True) # We don't care if a compute is down at this step From 5639cbe8601c51b125f1efc86e52075e922c0b41 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 13 Feb 2017 15:24:22 +0100 Subject: [PATCH 19/63] Fix a rare crash when closing a project Fix #900 --- gns3server/controller/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 581a9897..d0726713 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -367,7 +367,8 @@ class Controller: return project def remove_project(self, project): - del self._projects[project.id] + if project.id in self._projects: + del self._projects[project.id] @asyncio.coroutine def load_project(self, path, load=True): From a576c57873ea2019f4dde867b91d396966c25608 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 13 Feb 2017 15:30:02 +0100 Subject: [PATCH 20/63] Catch permission error when restoring a snapshot Fix #899 --- gns3server/controller/snapshot.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/gns3server/controller/snapshot.py b/gns3server/controller/snapshot.py index 217a16b7..4163a6b1 100644 --- a/gns3server/controller/snapshot.py +++ b/gns3server/controller/snapshot.py @@ -20,6 +20,7 @@ import os import uuid import shutil import asyncio +import aiohttp.web from datetime import datetime, timezone @@ -80,10 +81,13 @@ class Snapshot: # We don't send close notif to clients because the close / open dance is purely internal yield from self._project.close(ignore_notification=True) self._project.controller.notification.emit("snapshot.restored", self.__json__()) - if os.path.exists(os.path.join(self._project.path, "project-files")): - shutil.rmtree(os.path.join(self._project.path, "project-files")) - with open(self._path, "rb") as f: - project = yield from import_project(self._project.controller, self._project.id, f, location=self._project.path) + try: + if os.path.exists(os.path.join(self._project.path, "project-files")): + shutil.rmtree(os.path.join(self._project.path, "project-files")) + with open(self._path, "rb") as f: + project = yield from import_project(self._project.controller, self._project.id, f, location=self._project.path) + except (OSError, PermissionError) as e: + raise aiohttp.web.HTTPConflict(text=str(e)) yield from project.open() return project From 0dbd92db1156049e18ce4ec352146cc4b36da81e Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 13 Feb 2017 19:11:29 +0100 Subject: [PATCH 21/63] Fix disk lost when save as a project using linked clone VirtualBox Fix https://github.com/GNS3/gns3-gui/issues/1824 --- .../compute/virtualbox/virtualbox_vm.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/gns3server/compute/virtualbox/virtualbox_vm.py b/gns3server/compute/virtualbox/virtualbox_vm.py index 801988e2..e1d520dc 100644 --- a/gns3server/compute/virtualbox/virtualbox_vm.py +++ b/gns3server/compute/virtualbox/virtualbox_vm.py @@ -19,14 +19,16 @@ VirtualBox VM instance. """ -import sys -import shlex import re import os -import tempfile +import sys import json +import uuid +import shlex +import shutil import socket import asyncio +import tempfile import xml.etree.ElementTree as ET from gns3server.utils import parse_version @@ -209,7 +211,16 @@ class VirtualBoxVM(BaseNode): if os.path.exists(self._linked_vbox_file()): tree = ET.parse(self._linked_vbox_file()) machine = tree.getroot().find("{http://www.virtualbox.org/}Machine") - if machine is not None: + if machine is not None and machine.get("uuid") != "{" + self.id + "}": + + for image in tree.getroot().findall("{http://www.virtualbox.org/}Image"): + currentSnapshot = machine.get("currentSnapshot") + if currentSnapshot: + newSnapshot = re.sub("\{.*\}", "{" + str(uuid.uuid4()) + "}", currentSnapshot) + shutil.move(os.path.join(self.working_dir, self._vmname, "Snapshots", currentSnapshot) + ".vdi", + os.path.join(self.working_dir, self._vmname, "Snapshots", newSnapshot) + ".vdi") + image.set("uuid", newSnapshot) + machine.set("uuid", "{" + self.id + "}") tree.write(self._linked_vbox_file()) From a191029c4f364ee9bcbc28fb6adf9fae9aaedb63 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 14 Feb 2017 14:45:48 +0100 Subject: [PATCH 22/63] Fix linked_clone property lost during topology convert --- gns3server/controller/topology.py | 2 ++ tests/controller/test_export_project.py | 24 +++++++++---------- .../1_5_virtualbox/after/1_5_virtualbox.gns3 | 1 + .../1_5_vmware/after/1_5_vmware.gns3 | 1 + 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/gns3server/controller/topology.py b/gns3server/controller/topology.py index 3f7a1540..c188e9a9 100644 --- a/gns3server/controller/topology.py +++ b/gns3server/controller/topology.py @@ -321,10 +321,12 @@ def _convert_1_3_later(topo, topo_path): node["properties"]["ram"] = PLATFORMS_DEFAULT_RAM[old_node["type"].lower()] elif old_node["type"] == "VMwareVM": node["node_type"] = "vmware" + node["properties"]["linked_clone"] = old_node.get("linked_clone", False) if node["symbol"] is None: node["symbol"] = ":/symbols/vmware_guest.svg" elif old_node["type"] == "VirtualBoxVM": node["node_type"] = "virtualbox" + node["properties"]["linked_clone"] = old_node.get("linked_clone", False) if node["symbol"] is None: node["symbol"] = ":/symbols/vbox_guest.svg" elif old_node["type"] == "IOUDevice": diff --git a/tests/controller/test_export_project.py b/tests/controller/test_export_project.py index 65b1e80f..da7c3c1d 100644 --- a/tests/controller/test_export_project.py +++ b/tests/controller/test_export_project.py @@ -202,7 +202,7 @@ def test_export_disallow_some_type(tmpdir, project, async_run): with pytest.raises(aiohttp.web.HTTPConflict): z = async_run(export_project(project, str(tmpdir))) - z = async_run(export_project(project, str(tmpdir), allow_all_nodes=True)) + z = async_run(export_project(project, str(tmpdir), allow_all_nodes=True)) def test_export_fix_path(tmpdir, project, async_run): @@ -215,18 +215,18 @@ def test_export_fix_path(tmpdir, project, async_run): topology = { "topology": { "nodes": [ - { - "properties": { - "image": "/tmp/c3725-adventerprisek9-mz.124-25d.image" - }, - "node_type": "dynamips" - }, { - "properties": { - "image": "gns3/webterm:lastest" - }, - "node_type": "docker" - } + "properties": { + "image": "/tmp/c3725-adventerprisek9-mz.124-25d.image" + }, + "node_type": "dynamips" + }, + { + "properties": { + "image": "gns3/webterm:lastest" + }, + "node_type": "docker" + } ] } } diff --git a/tests/topologies/1_5_virtualbox/after/1_5_virtualbox.gns3 b/tests/topologies/1_5_virtualbox/after/1_5_virtualbox.gns3 index 1f591b24..ba17e46d 100644 --- a/tests/topologies/1_5_virtualbox/after/1_5_virtualbox.gns3 +++ b/tests/topologies/1_5_virtualbox/after/1_5_virtualbox.gns3 @@ -34,6 +34,7 @@ "port_segment_size": 0, "first_port_name": null, "properties": { + "linked_clone": false, "acpi_shutdown": false, "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", "adapters": 1, diff --git a/tests/topologies/1_5_vmware/after/1_5_vmware.gns3 b/tests/topologies/1_5_vmware/after/1_5_vmware.gns3 index 98a25f62..8238605c 100644 --- a/tests/topologies/1_5_vmware/after/1_5_vmware.gns3 +++ b/tests/topologies/1_5_vmware/after/1_5_vmware.gns3 @@ -34,6 +34,7 @@ "port_segment_size": 0, "first_port_name": null, "properties": { + "linked_clone": false, "acpi_shutdown": false, "adapter_type": "e1000", "adapters": 1, From e04eb44a15227cf5901f685ddc010464ae7a8dfa Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 14 Feb 2017 16:41:31 +0100 Subject: [PATCH 23/63] Disallow export of project with VirtualBox linked clone Fix https://github.com/GNS3/gns3-gui/issues/1824 --- gns3server/controller/export_project.py | 2 ++ tests/controller/test_export_project.py | 24 +++++++++++++++++++++--- tests/controller/test_project.py | 2 +- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/gns3server/controller/export_project.py b/gns3server/controller/export_project.py index 6fb0dd64..25c167e2 100644 --- a/gns3server/controller/export_project.py +++ b/gns3server/controller/export_project.py @@ -136,6 +136,8 @@ def _export_project_file(project, path, z, include_images, keep_compute_id, allo if "topology" in topology: if "nodes" in topology["topology"]: for node in topology["topology"]["nodes"]: + if node["node_type"] == "virtualbox" and node.get("properties", {}).get("linked_clone"): + raise aiohttp.web.HTTPConflict(text="Topology with a linked {} clone could not be exported. Use qemu instead.".format(node["node_type"])) if not allow_all_nodes and node["node_type"] in ["virtualbox", "vmware", "cloud"]: raise aiohttp.web.HTTPConflict(text="Topology with a {} could not be exported".format(node["node_type"])) diff --git a/tests/controller/test_export_project.py b/tests/controller/test_export_project.py index da7c3c1d..2b6b6610 100644 --- a/tests/controller/test_export_project.py +++ b/tests/controller/test_export_project.py @@ -190,9 +190,9 @@ def test_export_disallow_some_type(tmpdir, project, async_run): topology = { "topology": { "nodes": [ - { - "node_type": "virtualbox" - } + { + "node_type": "cloud" + } ] } } @@ -204,6 +204,24 @@ def test_export_disallow_some_type(tmpdir, project, async_run): z = async_run(export_project(project, str(tmpdir))) z = async_run(export_project(project, str(tmpdir), allow_all_nodes=True)) + # VirtualBox is always disallowed + topology = { + "topology": { + "nodes": [ + { + "node_type": "virtualbox", + "properties": { + "linked_clone": True + } + } + ] + } + } + with open(os.path.join(path, "test.gns3"), 'w+') as f: + json.dump(topology, f) + with pytest.raises(aiohttp.web.HTTPConflict): + z = async_run(export_project(project, str(tmpdir), allow_all_nodes=True)) + def test_export_fix_path(tmpdir, project, async_run): """ diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index 1fec7320..4422c0d1 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -427,7 +427,7 @@ def test_duplicate(project, async_run, controller): remote_vpcs = async_run(project.add_node(compute, "test", None, node_type="vpcs", properties={"startup_config": "test.cfg"})) # We allow node not allowed for standard import / export - remote_virtualbox = async_run(project.add_node(compute, "test", None, node_type="virtualbox", properties={"startup_config": "test.cfg"})) + remote_virtualbox = async_run(project.add_node(compute, "test", None, node_type="vmware", properties={"startup_config": "test.cfg"})) new_project = async_run(project.duplicate(name="Hello")) assert new_project.id != project.id From b7e5c08fdfce4451cec99c50c1a8361d9fb7e904 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 15 Feb 2017 12:58:12 +0100 Subject: [PATCH 24/63] Display git version in commit --- gns3server/version.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/gns3server/version.py b/gns3server/version.py index fda1c3dd..34300d3c 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -24,4 +24,16 @@ # number has been incremented) __version__ = "2.0.0dev8" + +# If it's a git checkout try to add the commit +if "dev" in __version__: + try: + import os + import subprocess + if os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".git")): + r = subprocess.run(["git", "rev-parse", "--short", "HEAD"], stdout=subprocess.PIPE).stdout.decode().strip("\n") + __version__ += "-" + r + except Exception as e: + print(e) + __version_info__ = (2, 0, 0, -99) From a2337ed6c65cb8985e7bb14aef67fda3eff82482 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 15 Feb 2017 16:41:23 +0100 Subject: [PATCH 25/63] Force installation of last aiohttp --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index f79cf434..629ad599 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ jsonschema>=2.4.0 -aiohttp>=1.2.0 +aiohttp>=1.3.1 aiohttp_cors>=0.4.0 -yarl>=0.8.1 +yarl>=0.9.6 typing>=3.5.3.0 # Otherwise yarl fail with python 3.4 Jinja2>=2.7.3 raven>=5.23.0 From 897c9cb42c24005076f78d8dc9d7d9b6cd6cc977 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 15 Feb 2017 19:31:18 +0100 Subject: [PATCH 26/63] Improve docker container for development --- Dockerfile | 2 +- README.rst | 4 ++-- scripts/docker_dev_server.sh | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 scripts/docker_dev_server.sh diff --git a/Dockerfile b/Dockerfile index 5f6dcc4a..fec7d333 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,4 +31,4 @@ RUN pip3 install -r /server/requirements.txt EXPOSE 3080 -ENTRYPOINT python3 -m gns3server --local +CMD python3 -m gns3server --local diff --git a/README.rst b/README.rst index 6da27c4f..e3ebfde5 100644 --- a/README.rst +++ b/README.rst @@ -77,8 +77,8 @@ Docker container For development you can run the GNS3 server in a container .. code:: bash - docker build -t gns3-server . - docker run -i --expose=8001 -p 8001:8001/tcp -t gns3-server python3 -m gns3server --local --port 8001 + + bash scripts/docker_dev_server.sh Run as daemon (Unix only) diff --git a/scripts/docker_dev_server.sh b/scripts/docker_dev_server.sh new file mode 100644 index 00000000..a2f6e784 --- /dev/null +++ b/scripts/docker_dev_server.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# +# Copyright (C) 2016 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 . + +# A docker server use for localy test a remote GNS3 server + +docker build -t gns3-server . +docker run -i -h gns3vm -p 8001:8001/tcp -t gns3-server python3 -m gns3server --local --port 8001 + + From 10b039074f8aa164a0ecf7d1c8be17af39ba6a82 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 15 Feb 2017 19:31:38 +0100 Subject: [PATCH 27/63] Avoid a crash in some conditions when reading the serial console --- gns3server/utils/asyncio/serial.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gns3server/utils/asyncio/serial.py b/gns3server/utils/asyncio/serial.py index 48d12bc0..c118a87e 100644 --- a/gns3server/utils/asyncio/serial.py +++ b/gns3server/utils/asyncio/serial.py @@ -34,6 +34,7 @@ class SerialReaderWriterProtocol(asyncio.Protocol): def __init__(self): self._output = asyncio.StreamReader() + self._closed = False self.transport = None def read(self, n=-1): @@ -54,9 +55,11 @@ class SerialReaderWriterProtocol(asyncio.Protocol): self.transport = transport def data_received(self, data): - self._output.feed_data(data) + if not self._closed: + self._output.feed_data(data) def close(self): + self._closed = True self._output.feed_eof() From 8d2c27eafdb4c47dc3b42fa475d0163e67a88a69 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 15 Feb 2017 20:10:41 +0100 Subject: [PATCH 28/63] Lock aiohttp to see if it's the reason of Unkown error from Qt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 629ad599..222e4b5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ jsonschema>=2.4.0 -aiohttp>=1.3.1 +aiohttp==1.2.0 aiohttp_cors>=0.4.0 yarl>=0.9.6 typing>=3.5.3.0 # Otherwise yarl fail with python 3.4 From 0f33448af2f433fa5f1299b9a0e6638d9059c035 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 16 Feb 2017 11:19:27 +0100 Subject: [PATCH 29/63] 2.0.0 beta 4 --- CHANGELOG | 35 ++++++++++++++++++++++++++++++++++- gns3server/version.py | 2 +- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f598184c..9762117b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,38 @@ # Change Log +## 2.0.0 beta 4 16/02/2017 + +* Lock aiohttp to 1.2.0 because 1.3 create bug with Qt +* Avoid a crash in some conditions when reading the serial console +* Disallow export of project with VirtualBox linked clone +* Fix linked_clone property lost during topology convert +* Catch permission error when restoring a snapshot +* Fix a rare crash when closing a project +* Fix error when you have error on your filesystem during project convertion +* Catch error when we can't access to a unix socket +* If we can't resolve compute name return 0.0.0.0 +* Raise an error if you put an invalid key in node name +* Improve a lot project loading speed +* Fix a potential crash +* Fix the server don't start if a remote is unavailable +* Do not crash if you pass {name} in name +* Fix import/export of dynamips configuration +* Simplify conversion process from 1.3 to 2.0 +* Prevent corruption of VM in VirtualBox when using linked clone +* Fix creation of qemu img +* Fix rare race condition when stopping ubridge +* Prevent renaming of a running VirtualBox linked VM +* Avoid crash when you broke your system permissions +* Do not crash when you broke permission on your file system during execution +* Fix a crash when you broke permission on your file system +* Fix a rare race condition when exporting debug informations +* Do not try to start the GNS3 VM if the name is none +* Fix version check for VPCS +* Fix pcap for PPP link with IOU +* Correct link are not connected to the correct ethernet switch port after conversion +* Fix an error if you don't have permissions on your symbols directory +* Fix an error when converting some topologies from 1.3 + ## 2.0.0 beta 3 19/01/2017 * Force the dependency on typing because otherwise it's broke on 3.4 @@ -45,7 +78,7 @@ * Replace JSONDecodeError by ValueError (Python 3.4 compatibility) * Catch an error when we can't create the IOU directory -## 1.5.3 12/01/2016 +## 1.5.3 12/01/2017 * Fix sporadically systemd is unable to start gns3-server diff --git a/gns3server/version.py b/gns3server/version.py index 34300d3c..5e460b9c 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,7 +23,7 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.0.0dev8" +__version__ = "2.0.0b4" # If it's a git checkout try to add the commit if "dev" in __version__: From 5796f47a55faa0ad5d23e8130be5b2914636127a Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 16 Feb 2017 11:21:39 +0100 Subject: [PATCH 30/63] Crash report key for b4 --- gns3server/crash_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index ecd23733..7edc2865 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -53,7 +53,7 @@ class CrashReport: Report crash to a third party service """ - DSN = "sync+https://b7430bad849c4b88b3a928032d6cce5e:f140bfdd2ebb4bf4b929c002b45b2357@sentry.io/38482" + DSN = "sync+https://83564b27a6f6475488a3eb74c78f1760:ed5ac7c6d3f7428d960a84da98450b69@sentry.io/38482" if hasattr(sys, "frozen"): cacert = get_resource("cacert.pem") if cacert is not None and os.path.isfile(cacert): From a730fce5143429d6cec9bd42ac1fa18f42f10ac5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 16 Feb 2017 11:23:19 +0100 Subject: [PATCH 31/63] 2.0.0dev9 --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index 5e460b9c..f30973c3 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,7 +23,7 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.0.0b4" +__version__ = "2.0.0dev9" # If it's a git checkout try to add the commit if "dev" in __version__: From 99f817392db82ae2f383748c7bc7a8d0d98a4d68 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 17 Feb 2017 09:55:50 +0100 Subject: [PATCH 32/63] Raise an error if you use Qemu < 2.4 and try to use large number of adapters --- gns3server/compute/qemu/qemu_vm.py | 6 ++++++ tests/compute/qemu/test_qemu_vm.py | 15 +++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index fd9e9360..30c4d061 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -1454,7 +1454,13 @@ class QemuVM(BaseNode): network_options.extend(["-device", "i82801b11-bridge,id=dmi_pci_bridge{bridge_id}".format(bridge_id=bridge_id)]) network_options.extend(["-device", "pci-bridge,id=pci-bridge{bridge_id},bus=dmi_pci_bridge{bridge_id},chassis_nr=0x1,addr=0x{bridge_id},shpc=off".format(bridge_id=bridge_id)]) + if bridge_id > 1: + qemu_version = yield from self.manager.get_qemu_version(self.qemu_path) + if qemu_version and parse_version(qemu_version) < parse_version("2.4.0"): + raise QemuError("You need to Qemu 2.4 or later in order to support large number of adapters") + pci_device_id = 4 + bridge_id # Bridge consume PCI ports + for adapter_number, adapter in enumerate(self._ethernet_adapters): mac = int_to_macaddress(macaddress_to_int(self._mac_address) + adapter_number) diff --git a/tests/compute/qemu/test_qemu_vm.py b/tests/compute/qemu/test_qemu_vm.py index 91e40d9c..854f47e4 100644 --- a/tests/compute/qemu/test_qemu_vm.py +++ b/tests/compute/qemu/test_qemu_vm.py @@ -611,6 +611,9 @@ def test_build_command_large_number_of_adapters(vm, loop, fake_qemu_binary, port additionnal interfaces """ + # It's supported only with Qemu 2.4 and later + vm.manager.get_qemu_version = AsyncioMagicMock(return_value="2.4.0") + vm.adapters = 100 vm.mac_address = "00:00:ab:0e:0f:09" mac_0 = vm._mac_address @@ -626,8 +629,6 @@ def test_build_command_large_number_of_adapters(vm, loop, fake_qemu_binary, port assert "pci-bridge,id=pci-bridge2,bus=dmi_pci_bridge2,chassis_nr=0x1,addr=0x2,shpc=off" in cmd assert "i82801b11-bridge,id=dmi_pci_bridge1" in cmd - print(cmd) - mac_29 = int_to_macaddress(macaddress_to_int(vm._mac_address) + 29) assert "e1000,mac={},bus=pci-bridge1,addr=0x04".format(mac_29) in cmd mac_30 = int_to_macaddress(macaddress_to_int(vm._mac_address) + 30) @@ -635,8 +636,18 @@ def test_build_command_large_number_of_adapters(vm, loop, fake_qemu_binary, port mac_74 = int_to_macaddress(macaddress_to_int(vm._mac_address) + 74) assert "e1000,mac={},bus=pci-bridge2,addr=0x11".format(mac_74) in cmd + # Qemu < 2.4 doesn't support large number of adapters + vm.manager.get_qemu_version = AsyncioMagicMock(return_value="2.0.0") + with pytest.raises(QemuError): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: + cmd = loop.run_until_complete(asyncio.async(vm._build_command())) + vm.adapters = 5 + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: + cmd = loop.run_until_complete(asyncio.async(vm._build_command())) # Windows accept this kind of mistake + + @pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows") def test_build_command_with_invalid_options(vm, loop, fake_qemu_binary): From 635e163954a6141e5e49fc15436ccfb812ef0f52 Mon Sep 17 00:00:00 2001 From: Jeremy Grossmann Date: Fri, 17 Feb 2017 17:37:06 +0800 Subject: [PATCH 33/63] Update qemu_vm.py --- gns3server/compute/qemu/qemu_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 30c4d061..bfe2f2bf 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -1457,7 +1457,7 @@ class QemuVM(BaseNode): if bridge_id > 1: qemu_version = yield from self.manager.get_qemu_version(self.qemu_path) if qemu_version and parse_version(qemu_version) < parse_version("2.4.0"): - raise QemuError("You need to Qemu 2.4 or later in order to support large number of adapters") + raise QemuError("Qemu version 2.4 or later is required to run this VM with a large number of network adapters") pci_device_id = 4 + bridge_id # Bridge consume PCI ports From 7fea6f0e2e265bdfce91256105be0ad0d5029136 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 20 Feb 2017 10:48:03 +0100 Subject: [PATCH 34/63] Fix conversion issue for old IOU projects Fix https://github.com/GNS3/gns3-gui/issues/1868 --- gns3server/controller/topology.py | 8 ++++++++ tests/topologies/1_5_iou/after/1_5_iou.gns3 | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/gns3server/controller/topology.py b/gns3server/controller/topology.py index c188e9a9..726ef519 100644 --- a/gns3server/controller/topology.py +++ b/gns3server/controller/topology.py @@ -331,6 +331,14 @@ def _convert_1_3_later(topo, topo_path): node["symbol"] = ":/symbols/vbox_guest.svg" elif old_node["type"] == "IOUDevice": node["node_type"] = "iou" + node["port_name_format"] = old_node.get("port_name_format", "Ethernet{segment0}/{port0}") + node["port_segment_size"] = int(old_node.get("port_segment_size", "4")) + if node["symbol"] is None: + if "l2" in node["properties"].get("path", ""): + node["symbol"] = ":/symbols/multilayer_switch.svg" + else: + node["symbol"] = ":/symbols/router.svg" + elif old_node["type"] == "Cloud": old_node["ports"] = _create_cloud(node, old_node, ":/symbols/cloud.svg") elif old_node["type"] == "Host": diff --git a/tests/topologies/1_5_iou/after/1_5_iou.gns3 b/tests/topologies/1_5_iou/after/1_5_iou.gns3 index f45baa1c..9e7f6b0a 100644 --- a/tests/topologies/1_5_iou/after/1_5_iou.gns3 +++ b/tests/topologies/1_5_iou/after/1_5_iou.gns3 @@ -30,8 +30,8 @@ "name": "IOU1", "node_id": "aaeb2288-a7d8-42a9-b9d8-c42ab464a390", "node_type": "iou", - "port_name_format": "Ethernet{0}", - "port_segment_size": 0, + "port_name_format": "Ethernet{segment0}/{port0}", + "port_segment_size": 4, "first_port_name": null, "properties": { "ethernet_adapters": 2, From 7407ab88b38a49a21e3b0a671e1f54ded5d76534 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 20 Feb 2017 10:56:48 +0100 Subject: [PATCH 35/63] Fix a failing test on Python 3.6 --- .travis.yml | 2 ++ gns3server/compute/vpcs/vpcs_vm.py | 15 +++++++-------- tests/compute/vpcs/test_vpcs_vm.py | 6 +++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index a8a4eb75..c6870778 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: python python: + - '3.4' - '3.5' + - '3.6' sudo: false cache: pip install: diff --git a/gns3server/compute/vpcs/vpcs_vm.py b/gns3server/compute/vpcs/vpcs_vm.py index 85952be3..e63d8436 100644 --- a/gns3server/compute/vpcs/vpcs_vm.py +++ b/gns3server/compute/vpcs/vpcs_vm.py @@ -104,7 +104,7 @@ class VPCSVM(BaseNode): Check if VPCS is available with the correct version. """ - path = self.vpcs_path + path = self._vpcs_path() if not path: raise VPCSError("No path to a VPCS executable has been set") @@ -146,8 +146,7 @@ class VPCSVM(BaseNode): else: return None - @property - def vpcs_path(self): + def _vpcs_path(self): """ Returns the VPCS executable path. @@ -217,7 +216,7 @@ class VPCSVM(BaseNode): Checks if the VPCS executable version is >= 0.8b or == 0.6.1. """ try: - output = yield from subprocess_check_output(self.vpcs_path, "-v", cwd=self.working_dir) + output = yield from subprocess_check_output(self._vpcs_path(), "-v", cwd=self.working_dir) match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output) if match: version = match.group(1) @@ -225,7 +224,7 @@ class VPCSVM(BaseNode): if self._vpcs_version < parse_version("0.6.1"): raise VPCSError("VPCS executable version must be >= 0.6.1 but not a 0.8") else: - raise VPCSError("Could not determine the VPCS version for {}".format(self.vpcs_path)) + raise VPCSError("Could not determine the VPCS version for {}".format(self._vpcs_path())) except (OSError, subprocess.SubprocessError) as e: raise VPCSError("Error while looking for the VPCS version: {}".format(e)) @@ -270,8 +269,8 @@ class VPCSVM(BaseNode): self.status = "started" except (OSError, subprocess.SubprocessError) as e: vpcs_stdout = self.read_vpcs_stdout() - log.error("Could not start VPCS {}: {}\n{}".format(self.vpcs_path, e, vpcs_stdout)) - raise VPCSError("Could not start VPCS {}: {}\n{}".format(self.vpcs_path, e, vpcs_stdout)) + log.error("Could not start VPCS {}: {}\n{}".format(self._vpcs_path(), e, vpcs_stdout)) + raise VPCSError("Could not start VPCS {}: {}\n{}".format(self._vpcs_path(), e, vpcs_stdout)) def _termination_callback(self, returncode): """ @@ -514,7 +513,7 @@ class VPCSVM(BaseNode): """ - command = [self.vpcs_path] + command = [self._vpcs_path()] command.extend(["-p", str(self._internal_console_port)]) # listen to console port command.extend(["-m", str(self._manager.get_mac_id(self.id))]) # the unique ID is used to set the MAC address offset command.extend(["-i", "1"]) # option to start only one VPC instance diff --git a/tests/compute/vpcs/test_vpcs_vm.py b/tests/compute/vpcs/test_vpcs_vm.py index 6eedbd93..ce0d9257 100644 --- a/tests/compute/vpcs/test_vpcs_vm.py +++ b/tests/compute/vpcs/test_vpcs_vm.py @@ -75,7 +75,7 @@ def test_vm_invalid_vpcs_version(loop, manager, vm): def test_vm_invalid_vpcs_path(vm, manager, loop): - with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.vpcs_path", return_value="/tmp/fake/path/vpcs"): + with patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM._vpcs_path", return_value="/tmp/fake/path/vpcs"): with pytest.raises(VPCSError): nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) vm.port_add_nio_binding(0, nio) @@ -97,7 +97,7 @@ def test_start(loop, vm, async_run): nio = VPCS.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) async_run(vm.port_add_nio_binding(0, nio)) loop.run_until_complete(asyncio.async(vm.start())) - assert mock_exec.call_args[0] == (vm.vpcs_path, + assert mock_exec.call_args[0] == (vm._vpcs_path(), '-p', str(vm._internal_console_port), '-m', '1', @@ -133,7 +133,7 @@ def test_start_0_6_1(loop, vm, async_run): nio = VPCS.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) async_run(vm.port_add_nio_binding(0, nio)) async_run(vm.start()) - assert mock_exec.call_args[0] == (vm.vpcs_path, + assert mock_exec.call_args[0] == (vm._vpcs_path(), '-p', str(vm._internal_console_port), '-m', '1', From 132a7bfeb7c11e11173127e9bb34fede674521b6 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 20 Feb 2017 12:19:38 +0100 Subject: [PATCH 36/63] Catch an error in etherswitch when ubridge die Fix #907 --- gns3server/compute/dynamips/nodes/ethernet_switch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gns3server/compute/dynamips/nodes/ethernet_switch.py b/gns3server/compute/dynamips/nodes/ethernet_switch.py index 037a837e..52d7c4ab 100644 --- a/gns3server/compute/dynamips/nodes/ethernet_switch.py +++ b/gns3server/compute/dynamips/nodes/ethernet_switch.py @@ -211,7 +211,8 @@ class EthernetSwitch(Device): nio = self._nios[port_number] if isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport, self._project) - yield from self._hypervisor.send('ethsw remove_nio "{name}" {nio}'.format(name=self._name, nio=nio)) + if self._hypervisor: + yield from self._hypervisor.send('ethsw remove_nio "{name}" {nio}'.format(name=self._name, nio=nio)) log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, id=self._id, From 961c209ab1c0a6366ff7b0dcd26b458338c6efac Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 20 Feb 2017 17:25:26 +0100 Subject: [PATCH 37/63] Fix an issue when getting size from some SVG file Fix https://github.com/GNS3/gns3-gui/issues/1866 --- gns3server/utils/picture.py | 3 +- tests/resources/firefox.svg | 420 ++++++++++++++++++++++++++++++++++++ tests/utils/test_picture.py | 4 + 3 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 tests/resources/firefox.svg diff --git a/gns3server/utils/picture.py b/gns3server/utils/picture.py index 4424ad9a..0fd740d8 100644 --- a/gns3server/utils/picture.py +++ b/gns3server/utils/picture.py @@ -120,7 +120,8 @@ def _svg_convert_size(size): "pc": 15, "mm": 3.543307, "cm": 35.43307, - "in": 90 + "in": 90, + "px": 1 } if len(size) > 3: if size[-2:] in conversion_table: diff --git a/tests/resources/firefox.svg b/tests/resources/firefox.svg new file mode 100644 index 00000000..2d3178f9 --- /dev/null +++ b/tests/resources/firefox.svg @@ -0,0 +1,420 @@ + + + + diff --git a/tests/utils/test_picture.py b/tests/utils/test_picture.py index d2764c40..a592d500 100644 --- a/tests/utils/test_picture.py +++ b/tests/utils/test_picture.py @@ -39,3 +39,7 @@ def test_get_size(): with open("gns3server/symbols/cloud.svg", "rb") as f: res = get_size(f.read()) assert res == (159, 71, "svg") + # Size with px + with open("tests/resources/firefox.svg", "rb") as f: + res = get_size(f.read()) + assert res == (66, 70, "svg") From b132d95a04ad34c98544215522371fb216a01b94 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 20 Feb 2017 18:28:49 +0100 Subject: [PATCH 38/63] Fix error when you delete the builtin symbols directory Fix #908 --- gns3server/controller/symbols.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/gns3server/controller/symbols.py b/gns3server/controller/symbols.py index 389f3ac0..5c473d8b 100644 --- a/gns3server/controller/symbols.py +++ b/gns3server/controller/symbols.py @@ -39,16 +39,17 @@ class Symbols: def list(self): self._symbols_path = {} symbols = [] - for file in os.listdir(get_resource("symbols")): - if file.startswith('.'): - continue - symbol_id = ':/symbols/' + file - symbols.append({ - 'symbol_id': symbol_id, - 'filename': file, - 'builtin': True, - }) - self._symbols_path[symbol_id] = os.path.join(get_resource("symbols"), file) + if get_resource("symbols"): + for file in os.listdir(get_resource("symbols")): + if file.startswith('.'): + continue + symbol_id = ':/symbols/' + file + symbols.append({ + 'symbol_id': symbol_id, + 'filename': file, + 'builtin': True, + }) + self._symbols_path[symbol_id] = os.path.join(get_resource("symbols"), file) directory = self.symbols_path() if directory: for file in os.listdir(directory): From 2884a40769039c451be233d95b48c3005b9b5056 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 22 Feb 2017 09:28:34 +0100 Subject: [PATCH 39/63] Fix error when the startup config file is missing Fix https://github.com/GNS3/gns3-gui/issues/1877 --- gns3server/compute/dynamips/nodes/router.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gns3server/compute/dynamips/nodes/router.py b/gns3server/compute/dynamips/nodes/router.py index e43b827f..e0f2b673 100644 --- a/gns3server/compute/dynamips/nodes/router.py +++ b/gns3server/compute/dynamips/nodes/router.py @@ -1551,8 +1551,11 @@ class Router(BaseNode): try: startup_config_path = os.path.join(self._working_directory, startup_config) - with open(startup_config_path) as f: - self._startup_config_content = f.read() + if os.path.exists(startup_config_path): + with open(startup_config_path) as f: + self._startup_config_content = f.read() + else: + self._startup_config_content = '' except OSError as e: raise DynamipsError("Cannot access the startup-config {}: {}".format(startup_config_path, e)) From ebe8c1e53666a379c04fd6078cb564d4ed8d978f Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 22 Feb 2017 10:28:43 +0100 Subject: [PATCH 40/63] Update aiohttp from 1.2.0 to 1.3.3 (#905) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 222e4b5a..2a11681b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ jsonschema>=2.4.0 -aiohttp==1.2.0 +aiohttp==1.3.3 aiohttp_cors>=0.4.0 yarl>=0.9.6 typing>=3.5.3.0 # Otherwise yarl fail with python 3.4 From 6a9180411611a5c53dceaaebdfd85b81dd6da632 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 22 Feb 2017 18:01:59 +0100 Subject: [PATCH 41/63] Catch error when you provide an invalid port name formating Fix #909 --- gns3server/controller/ports/port_factory.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/gns3server/controller/ports/port_factory.py b/gns3server/controller/ports/port_factory.py index e0174564..bc4d509b 100644 --- a/gns3server/controller/ports/port_factory.py +++ b/gns3server/controller/ports/port_factory.py @@ -15,6 +15,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import aiohttp + from .atm_port import ATMPort from .frame_relay_port import FrameRelayPort from .gigabitethernet_port import GigabitEthernetPort @@ -64,11 +66,14 @@ class StandardPortFactory: port_name = first_port_name port = PortFactory(port_name, segment_number, adapter_number, port_number, "ethernet") else: - port_name = port_name_format.format( - interface_number, - segment_number, - adapter=adapter_number, - **cls._generate_replacement(interface_number, segment_number)) + try: + port_name = port_name_format.format( + interface_number, + segment_number, + adapter=adapter_number, + **cls._generate_replacement(interface_number, segment_number)) + except (ValueError, KeyError) as e: + raise aiohttp.web.HTTPConflict(text="Invalid port name format {}: {}".format(port_name_format, str(e))) port = PortFactory(port_name, segment_number, adapter_number, port_number, "ethernet") interface_number += 1 if port_segment_size: From 40be22bc58547763b15ed271db03a00f7744fb38 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 23 Feb 2017 09:13:27 +0100 Subject: [PATCH 42/63] Fix run missing function Fix https://github.com/GNS3/gns3-gui/issues/1878 --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index f30973c3..aa32f3b8 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -31,7 +31,7 @@ if "dev" in __version__: import os import subprocess if os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".git")): - r = subprocess.run(["git", "rev-parse", "--short", "HEAD"], stdout=subprocess.PIPE).stdout.decode().strip("\n") + r = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).decode().strip("\n") __version__ += "-" + r except Exception as e: print(e) From 726480f67660f651845f7b562f1d7eed1dd3ccf2 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 23 Feb 2017 09:21:49 +0100 Subject: [PATCH 43/63] Disable Keep Alive because it's bug with old Qt versions --- gns3server/web/response.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gns3server/web/response.py b/gns3server/web/response.py index c057c6a9..65d85d53 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -39,6 +39,7 @@ class Response(aiohttp.web.Response): self._route = route self._output_schema = output_schema self._request = request + headers['Connection'] = "close" # Disable keep alive because create trouble with old Qt (5.2, 5.3 and 5.4) headers['X-Route'] = self._route headers['Server'] = "Python/{0[0]}.{0[1]} GNS3/{1}".format(sys.version_info, __version__) super().__init__(headers=headers, **kwargs) From afa3f12e30120cd1c4dea092ba7a7b46cab602ac Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 23 Feb 2017 11:48:34 +0100 Subject: [PATCH 44/63] Allow any 1.3 aiohttp release but not 1.4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2a11681b..da2ab028 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ jsonschema>=2.4.0 -aiohttp==1.3.3 +aiohttp>=1.3.0,<=1.4.0 aiohttp_cors>=0.4.0 yarl>=0.9.6 typing>=3.5.3.0 # Otherwise yarl fail with python 3.4 From 30db4d8b5cd68220c635c47e5a2c4aaf0bf97148 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 23 Feb 2017 11:49:46 +0100 Subject: [PATCH 45/63] Yarl 0.9.8 is require by aiohttp 1.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index da2ab028..c5400455 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ jsonschema>=2.4.0 aiohttp>=1.3.0,<=1.4.0 aiohttp_cors>=0.4.0 -yarl>=0.9.6 +yarl>=0.9.8 typing>=3.5.3.0 # Otherwise yarl fail with python 3.4 Jinja2>=2.7.3 raven>=5.23.0 From 8aca3c7b99ea51aa9e76de5a45885ac0591391bf Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 23 Feb 2017 14:34:21 +0100 Subject: [PATCH 46/63] Do not crash at startup if local server as the same name as remote server Fix #910 --- gns3server/controller/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index d0726713..e159b1ac 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -254,7 +254,7 @@ class Controller: return None for compute in self._computes.values(): - if name and compute.name == name: + if name and compute.name == name and not force: raise aiohttp.web.HTTPConflict(text='Compute name "{}" already exists'.format(name)) compute = Compute(compute_id=compute_id, controller=self, name=name, **kwargs) From 0d96471f2939c786ee85e8b9eaba1bbb6f6f8b64 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 23 Feb 2017 15:35:30 +0100 Subject: [PATCH 47/63] Fix headless startup of the GNS3 VM --- gns3server/controller/gns3vm/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gns3server/controller/gns3vm/__init__.py b/gns3server/controller/gns3vm/__init__.py index dec8a8c3..f72c7aec 100644 --- a/gns3server/controller/gns3vm/__init__.py +++ b/gns3server/controller/gns3vm/__init__.py @@ -267,6 +267,7 @@ class GNS3VM: engine.vmname = self._settings["vmname"] engine.ram = self._settings["ram"] engine.vpcus = self._settings["vcpus"] + engine.headless = self._settings["headless"] compute = yield from self._controller.add_compute(compute_id="vm", name="GNS3 VM is starting ({})".format(engine.vmname), host=None, From 5bfa864f0c9ae2f3392a21980458a26280fe68e6 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 23 Feb 2017 16:19:20 +0100 Subject: [PATCH 48/63] Increase timeout for detecting VirtualBox GNS3 VM --- gns3server/controller/gns3vm/virtualbox_gns3_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/controller/gns3vm/virtualbox_gns3_vm.py b/gns3server/controller/gns3vm/virtualbox_gns3_vm.py index c6788034..07dcb72d 100644 --- a/gns3server/controller/gns3vm/virtualbox_gns3_vm.py +++ b/gns3server/controller/gns3vm/virtualbox_gns3_vm.py @@ -221,7 +221,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM): second to a GNS3 endpoint in order to get the list of the interfaces and their IP and after that match it with VirtualBox host only. """ - remaining_try = 240 + remaining_try = 300 while remaining_try > 0: json_data = None session = aiohttp.ClientSession() From 3fb24dd89551db1ca4c79a704776f9a8e79215f5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 23 Feb 2017 17:54:01 +0100 Subject: [PATCH 49/63] Avoid a crash when the connection with the server close --- gns3server/controller/compute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py index 413abba6..85f10575 100644 --- a/gns3server/controller/compute.py +++ b/gns3server/controller/compute.py @@ -426,7 +426,7 @@ class Compute: except aiohttp.errors.WSServerHandshakeError: self._ws = None break - if response.tp == aiohttp.MsgType.closed or response.tp == aiohttp.MsgType.error: + if response.tp == aiohttp.MsgType.closed or response.tp == aiohttp.MsgType.error or response.data is None: self._connected = False break msg = json.loads(response.data) From 366c567864e87255cf6c3cf490b4c1b26e7b80f0 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 23 Feb 2017 18:21:00 +0100 Subject: [PATCH 50/63] Fix restoration of private config when using dynamips Fix #906 --- gns3server/compute/dynamips/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gns3server/compute/dynamips/__init__.py b/gns3server/compute/dynamips/__init__.py index 4ad0472f..d46f321d 100644 --- a/gns3server/compute/dynamips/__init__.py +++ b/gns3server/compute/dynamips/__init__.py @@ -534,6 +534,8 @@ class Dynamips(BaseManager): elif private_config_content: private_config_path = self._create_config(vm, default_private_config_path, private_config_content) yield from vm.set_configs(vm.startup_config, private_config_path) + elif os.path.isfile(default_private_config_path) and os.path.getsize(default_private_config_path) > 0: + yield from vm.set_configs(vm.startup_config, default_private_config_path) def _create_config(self, vm, path, content=None): """ From 65b75a92123db6198d59a98d83ae4bbbbb929387 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 24 Feb 2017 11:55:41 +0100 Subject: [PATCH 51/63] Fix an issue with serial capture for IOU Fix https://github.com/GNS3/gns3-gui/issues/1886 --- gns3server/compute/iou/iou_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py index fc5ce499..5bd8590e 100644 --- a/gns3server/compute/iou/iou_vm.py +++ b/gns3server/compute/iou/iou_vm.py @@ -1167,7 +1167,7 @@ class IOUVM(BaseNode): bay=adapter_number, unit=port_number, output_file=output_file, - data_link_type=data_link_type)) + data_link_type=re.sub("^DLT_", "", data_link_type))) @asyncio.coroutine def stop_capture(self, adapter_number, port_number): From 2e0f012952259f3b6995f1af7cfef497ba4cf4d7 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 24 Feb 2017 13:58:03 +0100 Subject: [PATCH 52/63] Improve ACPI shutdown for virtualbox --- gns3server/compute/virtualbox/virtualbox_vm.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/gns3server/compute/virtualbox/virtualbox_vm.py b/gns3server/compute/virtualbox/virtualbox_vm.py index e1d520dc..77466ec9 100644 --- a/gns3server/compute/virtualbox/virtualbox_vm.py +++ b/gns3server/compute/virtualbox/virtualbox_vm.py @@ -303,6 +303,16 @@ class VirtualBoxVM(BaseNode): if self.acpi_shutdown: # use ACPI to shutdown the VM result = yield from self._control_vm("acpipowerbutton") + trial = 0 + while True: + vm_state = yield from self._get_vm_state() + if vm_state == "poweroff": + break + yield from asyncio.sleep(1) + trial += 1 + if trial >= 120: + yield from self._control_vm("poweroff") + break self.status = "stopped" log.debug("ACPI shutdown result: {}".format(result)) else: From 34d07369468ea04147476d380565dbe2a5324ca6 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 27 Feb 2017 08:45:45 +0100 Subject: [PATCH 53/63] Update sphinx from 1.5.2 to 1.5.3 (#916) --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 4cfcf60e..54339617 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,6 +1,6 @@ -rrequirements.txt -sphinx==1.5.2 +sphinx==1.5.3 pytest==3.0.6 pep8==1.7.0 pytest-catchlog==1.2.2 From 53dd1bd6e19ccdbe3e6f3318cd32773abf3cc21a Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 27 Feb 2017 11:08:58 +0100 Subject: [PATCH 54/63] Ensure we dump a .gns3 before exporting it Fix #915 --- gns3server/controller/export_project.py | 3 +++ tests/controller/test_export_project.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/gns3server/controller/export_project.py b/gns3server/controller/export_project.py index 25c167e2..1cc93a12 100644 --- a/gns3server/controller/export_project.py +++ b/gns3server/controller/export_project.py @@ -47,6 +47,9 @@ def export_project(project, temporary_dir, include_images=False, keep_compute_id if project.is_running(): raise aiohttp.web.HTTPConflict(text="Running topology could not be exported") + # Make sure we save the project + project.dump() + z = zipstream.ZipFile(allowZip64=True) if not os.path.exists(project._path): diff --git a/tests/controller/test_export_project.py b/tests/controller/test_export_project.py index 2b6b6610..f48df7c8 100644 --- a/tests/controller/test_export_project.py +++ b/tests/controller/test_export_project.py @@ -33,7 +33,9 @@ from gns3server.controller.export_project import export_project, _filter_files @pytest.fixture def project(controller): - return Project(controller=controller, name="Test") + p = Project(controller=controller, name="Test") + p.dump = MagicMock() + return p @pytest.fixture From 8e9c480d8dfe142a3a02aacdb87a8b624cbc35ab Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 27 Feb 2017 11:24:06 +0100 Subject: [PATCH 55/63] Catch some invalid node name formatting Fix #917 --- gns3server/controller/project.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 5bf23744..da884f31 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -293,6 +293,8 @@ class Project: name = base_name.format(number, id=number, name="Node") except KeyError as e: raise aiohttp.web.HTTPConflict(text="{" + e.args[0] + "} is not a valid replacement string in the node name") + except ValueError as e: + raise aiohttp.web.HTTPConflict(text="{} is not a valid replacement string in the node name".format(base_name)) if name not in self._allocated_node_names: self._allocated_node_names.add(name) return name From 9c71e96fd4bb55bf24b95c623b478e09488c628f Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 27 Feb 2017 11:31:51 +0100 Subject: [PATCH 56/63] Report aiohttp version in crash report --- gns3server/crash_report.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 7edc2865..7302207b 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -18,6 +18,7 @@ import os import sys import struct +import aiohttp import platform @@ -94,6 +95,7 @@ class CrashReport: "os:win_32": " ".join(platform.win32_ver()), "os:mac": "{} {}".format(platform.mac_ver()[0], platform.mac_ver()[2]), "os:linux": " ".join(platform.linux_distribution()), + "aiohttp:version": aiohttp.__version__, "python:version": "{}.{}.{}".format(sys.version_info[0], sys.version_info[1], sys.version_info[2]), From 8fd59c7967fc3d764e052e0713e88c90dd90b911 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 27 Feb 2017 12:03:26 +0100 Subject: [PATCH 57/63] If the GNS3 VM as failed to start reset his status --- gns3server/controller/gns3vm/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gns3server/controller/gns3vm/__init__.py b/gns3server/controller/gns3vm/__init__.py index f72c7aec..e069e2dc 100644 --- a/gns3server/controller/gns3vm/__init__.py +++ b/gns3server/controller/gns3vm/__init__.py @@ -278,6 +278,7 @@ class GNS3VM: except Exception as e: yield from self._controller.delete_compute("vm") log.error("Can't start the GNS3 VM: {}", str(e)) + yield from compute.update(name="GNS3 VM ({})".format(engine.vmname)) raise e yield from compute.update(name="GNS3 VM ({})".format(engine.vmname), protocol=self.protocol, From 70e2b87ff08182beaa51d7f3453b51585e0b4a35 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 27 Feb 2017 12:48:05 +0100 Subject: [PATCH 58/63] Patch hostname in configuration file even if name is unsync Ref https://github.com/GNS3/gns3-gui/issues/1889 --- gns3server/compute/dynamips/nodes/router.py | 3 ++- gns3server/compute/iou/iou_vm.py | 2 +- gns3server/compute/vpcs/vpcs_vm.py | 1 + tests/compute/iou/test_iou_vm.py | 8 ++++++++ tests/compute/vpcs/test_vpcs_vm.py | 15 +++++++++++---- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/gns3server/compute/dynamips/nodes/router.py b/gns3server/compute/dynamips/nodes/router.py index e0f2b673..b264ca64 100644 --- a/gns3server/compute/dynamips/nodes/router.py +++ b/gns3server/compute/dynamips/nodes/router.py @@ -24,6 +24,7 @@ import asyncio import time import sys import os +import re import glob import shlex import base64 @@ -1493,7 +1494,7 @@ class Router(BaseNode): try: with open(startup_config_path, "r+", encoding="utf-8", errors="replace") as f: old_config = f.read() - new_config = old_config.replace(self.name, new_name) + new_config = re.sub(r"^hostname .+$", "hostname " + new_name, old_config, flags=re.MULTILINE) f.seek(0) self._startup_config_content = new_config f.write(new_config) diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py index 5bd8590e..8a12aa9b 100644 --- a/gns3server/compute/iou/iou_vm.py +++ b/gns3server/compute/iou/iou_vm.py @@ -307,7 +307,7 @@ class IOUVM(BaseNode): if self.startup_config_file: content = self.startup_config_content - content = content.replace(self._name, new_name) + content = re.sub(r"^hostname .+$", "hostname " + new_name, content, flags=re.MULTILINE) self.startup_config_content = content super(IOUVM, IOUVM).name.__set__(self, new_name) diff --git a/gns3server/compute/vpcs/vpcs_vm.py b/gns3server/compute/vpcs/vpcs_vm.py index e63d8436..0c8c9ebc 100644 --- a/gns3server/compute/vpcs/vpcs_vm.py +++ b/gns3server/compute/vpcs/vpcs_vm.py @@ -171,6 +171,7 @@ class VPCSVM(BaseNode): if self.script_file: content = self.startup_script content = content.replace(self._name, new_name) + content = re.sub(r"^set pcname .+$", "set pcname " + new_name, content, flags=re.MULTILINE) self.startup_script = content super(VPCSVM, VPCSVM).name.__set__(self, new_name) diff --git a/tests/compute/iou/test_iou_vm.py b/tests/compute/iou/test_iou_vm.py index 3f00875e..83e65041 100644 --- a/tests/compute/iou/test_iou_vm.py +++ b/tests/compute/iou/test_iou_vm.py @@ -298,6 +298,14 @@ def test_change_name(vm, tmpdir): assert vm.name == "hello" with open(path) as f: assert f.read() == "hostname hello" + # support hostname not sync + vm.name = "alpha" + with open(path, 'w+') as f: + f.write("no service password-encryption\nhostname beta\nno ip icmp rate-limit unreachable") + vm.name = "charlie" + assert vm.name == "charlie" + with open(path) as f: + assert f.read() == "no service password-encryption\nhostname charlie\nno ip icmp rate-limit unreachable" def test_library_check(loop, vm): diff --git a/tests/compute/vpcs/test_vpcs_vm.py b/tests/compute/vpcs/test_vpcs_vm.py index ce0d9257..b36ac1c9 100644 --- a/tests/compute/vpcs/test_vpcs_vm.py +++ b/tests/compute/vpcs/test_vpcs_vm.py @@ -243,12 +243,12 @@ def test_update_startup_script(vm): def test_update_startup_script_h(vm): - content = "setname %h\n" + content = "set pcname %h\n" vm.name = "pc1" vm.startup_script = content assert os.path.exists(vm.script_file) with open(vm.script_file) as f: - assert f.read() == "setname pc1\n" + assert f.read() == "set pcname pc1\n" def test_get_startup_script(vm): @@ -275,11 +275,18 @@ def test_change_name(vm, tmpdir): path = os.path.join(vm.working_dir, 'startup.vpc') vm.name = "world" with open(path, 'w+') as f: - f.write("name world") + f.write("set pcname world") vm.name = "hello" assert vm.name == "hello" with open(path) as f: - assert f.read() == "name hello" + assert f.read() == "set pcname hello" + # Support when the name is not sync with config + with open(path, 'w+') as f: + f.write("set pcname alpha") + vm.name = "beta" + assert vm.name == "beta" + with open(path) as f: + assert f.read() == "set pcname beta" def test_close(vm, port_manager, loop): From ec6411f7300b1d9fdf96f14880bdacd9289732bd Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 28 Feb 2017 11:22:53 +0100 Subject: [PATCH 59/63] Fix an error with some SVG Fix #919 --- gns3server/controller/drawing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gns3server/controller/drawing.py b/gns3server/controller/drawing.py index 078d67b0..39a4d158 100644 --- a/gns3server/controller/drawing.py +++ b/gns3server/controller/drawing.py @@ -43,6 +43,7 @@ class Drawing: self._id = str(uuid.uuid4()) else: self._id = drawing_id + self._svg = "" self.svg = svg self._x = x self._y = y From da8811515ddd7a7d96b9be76ea3e8bc9bbafc535 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 28 Feb 2017 11:42:07 +0100 Subject: [PATCH 60/63] Remove noise from log when VMware is not installed --- gns3server/controller/gns3vm/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/gns3server/controller/gns3vm/__init__.py b/gns3server/controller/gns3vm/__init__.py index e069e2dc..73a759fb 100644 --- a/gns3server/controller/gns3vm/__init__.py +++ b/gns3server/controller/gns3vm/__init__.py @@ -222,8 +222,14 @@ class GNS3VM: """ engine = self._get_engine(engine) vms = [] - for vm in (yield from engine.list()): - vms.append({"vmname": vm["vmname"]}) + try: + for vm in (yield from engine.list()): + vms.append({"vmname": vm["vmname"]}) + except GNS3VMError as e: + # We raise error only if user activated the GNS3 VM + # otherwise you have noise when VMware is not installed + if self.enable: + raise e return vms @asyncio.coroutine From 41d7570b24082c145270cfcdda7579c3704edc1e Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 28 Feb 2017 12:08:47 +0100 Subject: [PATCH 61/63] Load local server before anything else --- gns3server/controller/__init__.py | 6 ++++-- tests/controller/test_controller.py | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index e159b1ac..efc62bca 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -53,9 +53,10 @@ class Controller: @asyncio.coroutine def start(self): log.info("Start controller") - yield from self.load() + server_config = Config.instance().get_section_config("Server") host = server_config.get("host", "localhost") + # If console_host is 0.0.0.0 client will use the ip they use # to connect to the controller console_host = host @@ -70,6 +71,7 @@ class Controller: user=server_config.get("user", ""), password=server_config.get("password", ""), force=True) + yield from self._load_controller_settings() yield from self.load_projects() yield from self.gns3vm.auto_start_vm() yield from self._project_auto_open() @@ -116,7 +118,7 @@ class Controller: json.dump(data, f, indent=4) @asyncio.coroutine - def load(self): + def _load_controller_settings(self): """ Reload the controller configuration from disk """ diff --git a/tests/controller/test_controller.py b/tests/controller/test_controller.py index 3227c54f..3ae347a2 100644 --- a/tests/controller/test_controller.py +++ b/tests/controller/test_controller.py @@ -42,7 +42,7 @@ def test_save(controller, controller_config_path): assert data["gns3vm"] == controller.gns3vm.__json__() -def test_load(controller, controller_config_path, async_run): +def test_load_controller_settings(controller, controller_config_path, async_run): controller.save() with open(controller_config_path) as f: data = json.load(f) @@ -60,7 +60,7 @@ def test_load(controller, controller_config_path, async_run): data["gns3vm"] = {"vmname": "Test VM"} with open(controller_config_path, "w+") as f: json.dump(data, f) - async_run(controller.load()) + async_run(controller._load_controller_settings()) assert controller.settings["IOU"] assert controller.computes["test1"].__json__() == { "compute_id": "test1", @@ -104,7 +104,7 @@ def test_import_computes_1_x(controller, controller_config_path, async_run): with open(os.path.join(config_dir, "gns3_gui.conf"), "w+") as f: json.dump(gns3_gui_conf, f) - async_run(controller.load()) + async_run(controller._load_controller_settings()) for compute in controller.computes.values(): if compute.id != "local": assert len(compute.id) == 36 @@ -146,7 +146,7 @@ def test_import_gns3vm_1_x(controller, controller_config_path, async_run): json.dump(gns3_gui_conf, f) controller.gns3vm.settings["engine"] = None - async_run(controller.load()) + async_run(controller._load_controller_settings()) assert controller.gns3vm.settings["engine"] == "vmware" assert controller.gns3vm.settings["enable"] assert controller.gns3vm.settings["headless"] @@ -202,7 +202,7 @@ def test_import_remote_gns3vm_1_x(controller, controller_config_path, async_run) json.dump(gns3_gui_conf, f) with asyncio_patch("gns3server.controller.compute.Compute.connect"): - async_run(controller.load()) + async_run(controller._load_controller_settings()) assert controller.gns3vm.settings["engine"] == "remote" assert controller.gns3vm.settings["vmname"] == "http://127.0.0.1:3081" From 39106ac36bf7d1f7f07bf6d4177b03b6757e19ff Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 28 Feb 2017 13:11:03 +0100 Subject: [PATCH 62/63] Do not prevent the creation of a local server on a machine named gns3vm Fix #920 --- gns3server/controller/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index efc62bca..ee917327 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -62,8 +62,13 @@ class Controller: console_host = host if host == "0.0.0.0": host = "127.0.0.1" + + name = socket.gethostname() + if name == "gns3vm": + name = "Main server" + yield from self.add_compute(compute_id="local", - name=socket.gethostname(), + name=name, protocol=server_config.get("protocol", "http"), host=host, console_host=console_host, From 7e1e63cd09897693050c2b64013e32127b20a793 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 28 Feb 2017 14:31:52 +0100 Subject: [PATCH 63/63] Catch error when we can't save the settings Fix #921 --- gns3server/handlers/api/controller/server_handler.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gns3server/handlers/api/controller/server_handler.py b/gns3server/handlers/api/controller/server_handler.py index b66f2e83..f6551550 100644 --- a/gns3server/handlers/api/controller/server_handler.py +++ b/gns3server/handlers/api/controller/server_handler.py @@ -114,7 +114,10 @@ class ServerHandler: def write_settings(request, response): controller = Controller.instance() controller.settings = request.json - controller.save() + try: + controller.save() + except (OSError, PermissionError) as e: + raise HTTPConflict(text="Can't save the settings {}".format(str(e))) response.json(controller.settings) response.set_status(201)