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/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/Dockerfile b/Dockerfile new file mode 100644 index 00000000..fec7d333 --- /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 + +CMD python3 -m gns3server --local diff --git a/README.rst b/README.rst index 81ffc9d5..e3ebfde5 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 + + bash scripts/docker_dev_server.sh + + Run as daemon (Unix only) ************************** 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 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/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, diff --git a/gns3server/compute/dynamips/nodes/router.py b/gns3server/compute/dynamips/nodes/router.py index e43b827f..4f230d6a 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 @@ -78,8 +79,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 +101,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 +149,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 +166,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 +282,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 +1461,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 +1469,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 = re.sub(r"^hostname .+$", "hostname " + new_name, old_config, flags=re.MULTILINE) + 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 +1522,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..200c0b20 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} @@ -307,7 +301,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) @@ -1167,7 +1161,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): diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 9ac7eb21..bfe2f2bf 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,20 @@ 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)]) + + 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("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 + for adapter_number, adapter in enumerate(self._ethernet_adapters): mac = int_to_macaddress(macaddress_to_int(self._mac_address) + adapter_number) @@ -1483,8 +1498,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 +1515,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/compute/virtualbox/virtualbox_vm.py b/gns3server/compute/virtualbox/virtualbox_vm.py index 801988e2..77466ec9 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()) @@ -292,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: diff --git a/gns3server/compute/vpcs/vpcs_vm.py b/gns3server/compute/vpcs/vpcs_vm.py index 85952be3..7765af74 100644 --- a/gns3server/compute/vpcs/vpcs_vm.py +++ b/gns3server/compute/vpcs/vpcs_vm.py @@ -104,12 +104,12 @@ 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") # 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 @@ -146,8 +144,7 @@ class VPCSVM(BaseNode): else: return None - @property - def vpcs_path(self): + def _vpcs_path(self): """ Returns the VPCS executable path. @@ -172,6 +169,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) @@ -217,7 +215,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 +223,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 +268,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 +512,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/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 7afe9c14..837ef19f 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -18,6 +18,7 @@ import os import json import socket +import shutil import asyncio import aiohttp @@ -68,16 +69,22 @@ class Controller: @asyncio.coroutine def start(self): log.info("Start controller") - yield from self.load() + self.load_base_files() 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 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, @@ -85,6 +92,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() @@ -131,7 +139,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 """ @@ -177,6 +185,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 @@ -186,6 +208,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): """ @@ -269,7 +300,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) @@ -332,7 +363,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)) @@ -383,7 +413,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): @@ -394,7 +425,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/compute.py b/gns3server/controller/compute.py index 47b780e8..85f10575 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__ @@ -216,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): @@ -360,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): """ @@ -374,14 +385,18 @@ 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: 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() @@ -411,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) 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 diff --git a/gns3server/controller/export_project.py b/gns3server/controller/export_project.py index 6fb0dd64..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): @@ -136,6 +139,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/gns3server/controller/gns3vm/__init__.py b/gns3server/controller/gns3vm/__init__.py index dec8a8c3..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 @@ -267,6 +273,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, @@ -277,6 +284,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, 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() 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/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/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: diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 4debcb73..da884f31 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -289,7 +289,12 @@ 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) + 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") + 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 @@ -314,10 +319,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 +355,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 +408,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 +444,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] + 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 @@ -526,7 +538,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 @@ -626,15 +638,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, 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"]: 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(dump=False, **drawing_data) + 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: 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 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): diff --git a/gns3server/controller/topology.py b/gns3server/controller/topology.py index 7842c536..875aaa52 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 @@ -37,7 +36,7 @@ import logging log = logging.getLogger(__name__) -GNS3_FILE_FORMAT_REVISION = 7 +GNS3_FILE_FORMAT_REVISION = 8 def _check_topology_schema(topo): @@ -117,34 +116,65 @@ 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) + + # Version before GNS3 2.1 + if topo["revision"] < 8: + topo = _convert_2_0_0(topo, path) + + _check_topology_schema(topo) + + if changed: with open(path, "w+", encoding="utf-8") as f: json.dump(topo, f, indent=4, sort_keys=True) + return topo - 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) + +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 @@ -165,11 +195,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 @@ -320,14 +353,24 @@ 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": 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/gns3server/crash_report.py b/gns3server/crash_report.py index ecd23733..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 @@ -53,7 +54,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): @@ -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]), 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/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/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/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) 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 5b14bc97..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" @@ -218,23 +186,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, -} 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/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/gns3server/utils/asyncio/serial.py b/gns3server/utils/asyncio/serial.py index 6dc961dd..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() @@ -122,7 +125,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 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/gns3server/version.py b/gns3server/version.py index aff38c2d..68fe29f4 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -25,3 +25,14 @@ __version__ = "2.1.0dev1" __version_info__ = (2, 1, 0, -99) + +# 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.check_output(["git", "rev-parse", "--short", "HEAD"]).decode().strip("\n") + __version__ += "-" + r + except Exception as e: + print(e) 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) diff --git a/requirements.txt b/requirements.txt index f79cf434..c5400455 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ jsonschema>=2.4.0 -aiohttp>=1.2.0 +aiohttp>=1.3.0,<=1.4.0 aiohttp_cors>=0.4.0 -yarl>=0.8.1 +yarl>=0.9.8 typing>=3.5.3.0 # Otherwise yarl fail with python 3.4 Jinja2>=2.7.3 raven>=5.23.0 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 + + 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/qemu/test_qemu_vm.py b/tests/compute/qemu/test_qemu_vm.py index 8b01381d..854f47e4 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,7 +605,49 @@ 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 + """ + + # 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 + 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 + + 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 + + # 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): diff --git a/tests/compute/vpcs/test_vpcs_vm.py b/tests/compute/vpcs/test_vpcs_vm.py index 6eedbd93..b36ac1c9 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', @@ -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): diff --git a/tests/controller/test_controller.py b/tests/controller/test_controller.py index e1e00e17..ce765b2b 100644 --- a/tests/controller/test_controller.py +++ b/tests/controller/test_controller.py @@ -39,7 +39,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) @@ -57,7 +57,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", @@ -101,7 +101,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 @@ -143,7 +143,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"] @@ -199,7 +199,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" @@ -466,3 +466,17 @@ def test_get_free_project_name(controller, async_run): def test_appliance_templates(controller): assert len(controller.appliance_templates) > 0 + + +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_export_project.py b/tests/controller/test_export_project.py index 65b1e80f..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 @@ -190,9 +192,9 @@ def test_export_disallow_some_type(tmpdir, project, async_run): topology = { "topology": { "nodes": [ - { - "node_type": "virtualbox" - } + { + "node_type": "cloud" + } ] } } @@ -202,6 +204,24 @@ 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)) + + # 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)) @@ -215,18 +235,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/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..9d6af268 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 @@ -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 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/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/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..cc417d25 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, @@ -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_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, 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 +} 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")