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")