Compare commits

...
46 Commits
Author SHA1 Message Date
ziajka eafe27e684 Release v2.1.6 2018-05-22 14:11:48 +02:00
ziajka c52342907a Merge pull request #1332 from GNS3/global-vars
Docker `ExtraHosts`, global variables for project and supplier support, Fixes: #2482
2018-05-15 09:23:54 +02:00
ziajka 5a0e21f58d Locks down async-timeout<3.0.0 for P3.4 support; Fixes: #1331 2018-05-11 13:37:04 +02:00
ziajka f3c6faed75 Removed unnecessary line 2018-05-09 15:53:50 +02:00
ziajka 7b3d5ae5e3 Create/update project on compute when variables changes 2018-05-09 15:29:35 +02:00
ziajka 83445214be Support for nested global variables 2018-05-09 11:25:55 +02:00
ziajka 0bcc657bf1 Don't clean logo images when applied to the project 2018-05-08 16:22:35 +02:00
ziajka f2700ed445 Support of supplier and variables in topology 2018-05-08 10:07:42 +02:00
ziajka e4436096c2 Fix tests 2018-05-07 15:18:19 +02:00
ziajka 132c7c8f5b Fix tests 2018-05-07 12:55:32 +02:00
ziajka e267f8a8b8 Project global variables 2018-05-04 14:34:44 +02:00
grossmj 202e7362a5 Add command information when uBridge has an error. Ref #1289 2018-04-28 18:48:52 +07:00
grossmj 305fe2e817 Handle asyncio timeouts. Ref #1307. 2018-04-28 17:42:02 +07:00
grossmj a56d5b453f Fix project export tests. 2018-04-28 16:46:47 +07:00
grossmj 3b94484914 Fix bug with export project. Ref #1187 #1307. 2018-04-28 16:38:52 +07:00
grossmj 50a922f83e Offload slow file operations to threads for snapshots and project "save as". Ref #1187 #1307. 2018-04-28 16:01:43 +07:00
ziajka 80958e8a6d support based on init.sh, Ref: #2482 2018-04-27 14:33:07 +02:00
grossmj 20294e284c Fix exception from send_signal() on Windows. 2018-04-27 17:00:28 +07:00
ziajka 43081152ef Add support of ExtraHosts for Docker, Ref. gns3-gui#2482 2018-04-25 16:03:01 +02:00
ziajka 747814f083 Development on v2.1.6 2018-04-18 11:41:30 +02:00
ziajka 2e9c5590a8 Release v2.1.5 2018-04-18 11:29:02 +02:00
grossmj cfd0216554 Disable TraceNG for version 2.1.5 2018-04-18 17:19:44 +08:00
grossmj 9ecb058343 Fix tests. 2018-04-18 16:13:59 +08:00
grossmj 013c74f6d8 Sync appliance files. 2018-04-18 16:12:30 +08:00
grossmj fff2e128eb Set the first byte to 0C when generating a random MAC address for a Qemu VM. Ref #1267. 2018-04-18 16:06:51 +08:00
grossmj 5690d350bd Update appliance files. 2018-04-17 12:58:37 +08:00
grossmj 1fea086f13 Do not use VMnet0 when allocating VMnet adapters. 2018-04-17 11:47:25 +08:00
grossmj dd9c18b9f8 Use SO_REUSEADDR before calling bind() where missing. Fixes #1289. 2018-04-16 16:36:36 +08:00
grossmj 5c4f6dd3dc Do not fail a Dynamips project conversion if a file being used. 2018-04-16 15:45:43 +08:00
grossmj a12c5a95b0 Catch exceptions when using AsyncioTelnetServer. Fixes #1321. 2018-04-16 15:30:06 +08:00
grossmj 025441f027 Add default destination setting for traceng + some checks. 2018-04-16 15:03:22 +08:00
grossmj 801547983a Fix tests. 2018-04-13 17:04:19 +08:00
grossmj 2de182eff4 Grid size support for projects. 2018-04-13 16:54:57 +08:00
grossmj 9adfb1820d Remove 'include INSTALL' from MANIFEST. 2018-04-13 14:17:18 +08:00
grossmj 297bbd91ec Fix traceng tests. 2018-03-30 12:10:57 +07:00
grossmj 47e5dfabd8 Merge branch 'traceng' into 2.1 2018-03-29 15:17:41 +07:00
grossmj 898f180310 Fix issue with start all. 2018-03-29 13:32:56 +07:00
grossmj b61bd2dc67 Merge remote-tracking branch 'origin/traceng' into traceng 2018-03-29 13:29:56 +07:00
grossmj b7780ba7cc Check for valid IP address and prevent to run on non-Windows platforms. 2018-03-29 13:26:43 +07:00
grossmj c053a2f350 Enable UDP tunnel option and use ICMP probing by default. 2018-03-28 04:26:40 -07:00
grossmj 05ec14e888 Use the configured IP address to trace. 2018-03-27 03:43:50 -07:00
grossmj d08c08617c Support for source and destination for traceNG. 2018-03-27 16:58:49 +07:00
grossmj 8e695c8af1 Have TraceNG start without needing cmd.exe 2018-03-27 01:19:24 -07:00
grossmj a4626a2b79 Custom icons and small fixes for TraceNG integration. 2018-03-14 16:56:37 +07:00
grossmj 397c1322b4 Base support for TraceNG. 2018-03-12 17:57:13 +07:00
ziajka 148baaf465 Back to development on v2.1.5dev1 2018-03-12 11:27:02 +01:00
71 changed files with 3076 additions and 383 deletions

No files matched your search

+1
View File
@@ -39,6 +39,7 @@ nosetests.xml
.project
.pydevproject
.settings
.vscode
# Pycharm
.idea
+32
View File
@@ -1,5 +1,37 @@
# Change Log
## 2.1.6 22/05/2018
* Locks down async-timeout<3.0.0 for P3.4 support; Fixes: #1331
* Create/update project on compute when variables changes
* Support for nested global variables
* Don't clean logo images when applied to the project
* Support of supplier and variables in topology
* Project global variables
* Add command information when uBridge has an error. Ref #1289
* Handle asyncio timeouts. Ref #1307.
* Fix bug with export project. Ref #1187 #1307.
* Offload slow file operations to threads for snapshots and project "save as". Ref #1187 #1307.
* support based on init.sh, Ref: #2482
* Fix exception from send_signal() on Windows.
* Add support of ExtraHosts for Docker, Ref. gns3-gui#2482
## 2.1.5 18/04/2018
* Set the first byte to 0C when generating a random MAC address for a Qemu VM. Ref #1267.
* Update appliance files.
* Do not use VMnet0 when allocating VMnet adapters.
* Use SO_REUSEADDR before calling bind() where missing. Fixes #1289.
* Do not fail a Dynamips project conversion if a file being used.
* Catch exceptions when using AsyncioTelnetServer. Fixes #1321.
* Grid size support for projects.
* Remove 'include INSTALL' from MANIFEST.
* Fix issue with start all.
* Check for valid IP address and prevent to run on non-Windows platforms.
* Enable UDP tunnel option and use ICMP probing by default.
* Use the configured IP address to trace.
* Have TraceNG start without needing cmd.exe
## 2.1.4 12/03/2018
* Add Juniper JunOS space appliance.
-1
View File
@@ -1,6 +1,5 @@
include README.rst
include AUTHORS
include INSTALL
include LICENSE
include MANIFEST.in
include tox.ini
+2 -2
View File
@@ -22,14 +22,14 @@
"images": [
{
"filename": "c7200-adventerprisek9-mz.124-24.T5.image",
"version": "124-25.T5",
"version": "124-24.T5",
"md5sum": "6b89d0d804e1f2bb5b8bda66b5692047",
"filesize": 102345240
}
],
"versions": [
{
"name": "124-25.T5",
"name": "124-24.T5",
"idlepc": "0x606df838",
"images": {
"image": "c7200-adventerprisek9-mz.124-24.T5.image"
+14
View File
@@ -25,6 +25,13 @@
"kvm": "require"
},
"images": [
{
"filename": "nxosv-final.7.0.3.I7.3.qcow2",
"version": "7.0.3.I7.3",
"md5sum": "9d7a20367bf681a239f14097bbce470a",
"filesize": 983629824,
"download_url": "https://software.cisco.com/download/"
},
{
"filename": "nxosv-final.7.0.3.I7.2.qcow2",
"version": "7.0.3.I7.2",
@@ -71,6 +78,13 @@
}
],
"versions": [
{
"name": "7.0.3.I7.3",
"images": {
"bios_image": "OVMF-20160813.fd",
"hda_disk_image": "nxosv-final.7.0.3.I7.3.qcow2"
}
},
{
"name": "7.0.3.I7.2",
"images": {
@@ -26,6 +26,13 @@
"options": "-smp 2 -cpu host"
},
"images": [
{
"filename": "NSVPX-KVM-12.0-56.20_nc_32.qcow2",
"version": "12.0-56.20",
"md5sum": "0ea1c23e3b8eb8451037d46ee472cfa6",
"filesize": 739704832,
"download_url": "https://www.citrix.com/lp/try/netscaler-vpx-express.html"
},
{
"filename": "NSVPX-KVM-11.1-47.14_nc.raw",
"version": "11.1-47.14 F",
@@ -42,6 +49,12 @@
}
],
"versions": [
{
"name": "12.0-56.20",
"images": {
"hda_disk_image": "NSVPX-KVM-12.0-56.20_nc_32.qcow2"
}
},
{
"name": "11.1-47.14 F",
"images": {
+1
View File
@@ -12,6 +12,7 @@
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"usage": "Initial username is root, no password.",
"first_port_name": "fxp0",
"port_name_format": "ge-0/0/{0}",
"qemu": {
"adapter_type": "e1000",
+39
View File
@@ -26,6 +26,33 @@
"options": "-nographic"
},
"images": [
{
"filename": "chr-6.42.img",
"version": "6.42",
"md5sum": "279bb518497b40f41c8585128916a2fb",
"filesize": 134217728,
"download_url": "http://www.mikrotik.com/download",
"direct_download_url": "https://download2.mikrotik.com/routeros/6.42/chr-6.42.img.zip",
"compression": "zip"
},
{
"filename": "chr-6.41.4.img",
"version": "6.41.4",
"md5sum": "63b555b2b7f0d78b79edb92f7e7d2ed7",
"filesize": 134217728,
"download_url": "http://www.mikrotik.com/download",
"direct_download_url": "https://download2.mikrotik.com/routeros/6.41.4/chr-6.41.4.img.zip",
"compression": "zip"
},
{
"filename": "chr-6.40.7.img",
"version": "6.40.7",
"md5sum": "424b897d631c4cac4324ca310e81b494",
"filesize": 134217728,
"download_url": "http://www.mikrotik.com/download",
"direct_download_url": "https://download2.mikrotik.com/routeros/6.40.7/chr-6.40.7.img.zip",
"compression": "zip"
},
{
"filename": "chr-6.40.5.img",
"version": "6.40.5",
@@ -235,6 +262,18 @@
}
],
"versions": [
{
"name": "6.41.4",
"images": {
"hda_disk_image": "chr-6.41.4.img"
}
},
{
"name": "6.40.7",
"images": {
"hda_disk_image": "chr-6.40.7.img"
}
},
{
"name": "6.40.5",
"images": {
+61 -23
View File
@@ -27,17 +27,31 @@
},
"images": [
{
"filename": "PA-VM-ESX-6.1.0-disk1.vmdk",
"version": "6.1.0 (ESX)",
"md5sum": "64b1e81cd54008318235832ea6d71424",
"filesize": 2959736832,
"filename": "PA-VM-ESX-8.1.0-disk1.vmdk",
"version": "8.1.0",
"md5sum": "49af8e8225c2e90414bde0be15eaf421",
"filesize": 2281454080,
"download_url": "https://support.paloaltonetworks.com/Updates/SoftwareUpdates/"
},
{
"filename": "PA-VM-KVM-7.1.0.qcow2",
"version": "7.1.0",
"md5sum": "da300253709740068927408239c2e321",
"filesize": 1858797568,
"filename": "PA-VM-KVM-8.1.0.qcow2",
"version": "8.1.0",
"md5sum": "459558515b965b2e43fde2842abbae66",
"filesize": 2260467712,
"download_url": "https://support.paloaltonetworks.com/Updates/SoftwareUpdates/"
},
{
"filename": "PA-VM-ESX-8.0.0-disk1.vmdk",
"version": "8.0.0",
"md5sum": "a505fb1dbcc855ecf98630fd5d329f9a",
"filesize": 2002713088,
"download_url": "https://support.paloaltonetworks.com/Updates/SoftwareUpdates/"
},
{
"filename": "PA-VM-KVM-8.0.0.qcow2",
"version": "8.0.0",
"md5sum": "b6a1ddc8552aff87f05f9c0d4cb54dc3",
"filesize": 1987444736,
"download_url": "https://support.paloaltonetworks.com/Updates/SoftwareUpdates/"
},
{
@@ -48,31 +62,37 @@
"download_url": "https://support.paloaltonetworks.com/Updates/SoftwareUpdates/"
},
{
"filename": "PA-VM-KVM-8.0.0.qcow2",
"version": "8.0.0",
"md5sum": "b6a1ddc8552aff87f05f9c0d4cb54dc3",
"filesize": 1987444736,
"filename": "PA-VM-KVM-7.1.0.qcow2",
"version": "7.1.0",
"md5sum": "da300253709740068927408239c2e321",
"filesize": 1858797568,
"download_url": "https://support.paloaltonetworks.com/Updates/SoftwareUpdates/"
}
},
{
"filename": "PA-VM-ESX-6.1.0-disk1.vmdk",
"version": "6.1.0 (ESX)",
"md5sum": "64b1e81cd54008318235832ea6d71424",
"filesize": 2959736832,
"download_url": "https://support.paloaltonetworks.com/Updates/SoftwareUpdates/"
}
],
"versions": [
{
"name": "6.1.0 (ESX)",
"name": "8.1.0 (ESX)",
"images": {
"hda_disk_image": "PA-VM-ESX-6.1.0-disk1.vmdk"
"hda_disk_image": "PA-VM-ESX-8.1.0-disk1.vmdk"
}
},
},
{
"name": "7.1.0",
"name": "8.1.0",
"images": {
"hda_disk_image": "PA-VM-KVM-7.1.0.qcow2"
"hda_disk_image": "PA-VM-KVM-8.1.0.qcow2"
}
},
},
{
"name": "7.1.0 (ESX)",
"name": "8.0.0 (ESX)",
"images": {
"hda_disk_image": "PA-VM-ESX-7.1.0-disk1.vmdk"
"hda_disk_image": "PA-VM-ESX-8.0.0-disk1.vmdk2"
}
},
{
@@ -80,6 +100,24 @@
"images": {
"hda_disk_image": "PA-VM-KVM-8.0.0.qcow2"
}
}
},
{
"name": "7.1.0 (ESX)",
"images": {
"hda_disk_image": "PA-VM-ESX-7.1.0-disk1.vmdk"
}
},
{
"name": "7.1.0",
"images": {
"hda_disk_image": "PA-VM-KVM-7.1.0.qcow2"
}
},
{
"name": "6.1.0 (ESX)",
"images": {
"hda_disk_image": "PA-VM-ESX-6.1.0-disk1.vmdk"
}
}
]
}
+12 -26
View File
@@ -28,37 +28,30 @@
{
"filename": "ubuntu-17.10-server-cloudimg-amd64.img",
"version": "17.10",
"md5sum": "5d221878d8b2e49c5de7ebb58a2b35e3",
"filesize": 318373888,
"download_url": "https://cloud-images.ubuntu.com/releases/17.10/release/"
},
{
"filename": "ubuntu-17.04-server-cloudimg-amd64.img",
"version": "17.04",
"md5sum": "d4da8157dbf2e64f2fa1fb5d121398e5",
"filesize": 351993856,
"download_url": "https://cloud-images.ubuntu.com/releases/17.04/release/"
"md5sum": "331b44f2b05858c251b3ea92c8b65152",
"filesize": 320405504,
"download_url": "https://cloud-images.ubuntu.com/releases/17.10/release-20180404/ubuntu-17.10-server-cloudimg-amd64.img"
},
{
"filename": "ubuntu-16.04-server-cloudimg-amd64-disk1.img",
"version": "16.04.3",
"md5sum": "bd0c168a83b1f483bd240b3d874edd6c",
"filesize": 288686080,
"download_url": "https://cloud-images.ubuntu.com/releases/16.04/release/"
"version": "16.04",
"md5sum": "22c124ba65ea096cdef8b0a197dd613a",
"filesize": 290193408,
"download_url": "https://cloud-images.ubuntu.com/releases/16.04/release-20180405/ubuntu-16.04-server-cloudimg-amd64-disk1.img"
},
{
"filename": "ubuntu-14.04-server-cloudimg-amd64-disk1.img",
"version": "14.04.5",
"md5sum": "d7b4112c7d797e5e77ef9995d06a76f1",
"filesize": 262406656,
"download_url": "https://cloud-images.ubuntu.com/releases/14.04/release/"
"version": "14.04",
"md5sum": "d11b89321d41d0eeddcacf73bf0d2262",
"filesize": 262668800,
"download_url": "https://cloud-images.ubuntu.com/releases/14.04/release-20180404/ubuntu-14.04-server-cloudimg-amd64-disk1.img"
},
{
"filename": "ubuntu-cloud-init-data.iso",
"version": "1.0",
"md5sum": "328469100156ae8dbf262daa319c27ff",
"filesize": 131072,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/ubuntu-cloud-init-data.iso/download"
"download_url": "https://github.com/asenci/gns3-ubuntu-cloud-init-data/raw/master/ubuntu-cloud-init-data.iso"
}
],
"versions": [
@@ -69,13 +62,6 @@
"cdrom_image": "ubuntu-cloud-init-data.iso"
}
},
{
"name": "17.04",
"images": {
"hda_disk_image": "ubuntu-17.04-server-cloudimg-amd64.img",
"cdrom_image": "ubuntu-cloud-init-data.iso"
}
},
{
"name": "16.04 (LTS)",
"images": {
+7
View File
@@ -80,6 +80,13 @@
"cdrom_image": "vyos-1.2.0-beta1-amd64.iso"
}
},
{
"name": "1.1.8",
"images": {
"hda_disk_image": "empty8G.qcow2",
"cdrom_image": "vyos-1.1.8-amd64.iso"
}
},
{
"name": "1.1.7",
"images": {
+5 -6
View File
@@ -24,15 +24,14 @@ from .virtualbox import VirtualBox
from .dynamips import Dynamips
from .qemu import Qemu
from .vmware import VMware
from .traceng import TraceNG
MODULES = [Builtin, VPCS, VirtualBox, Dynamips, Qemu, VMware]
MODULES = [Builtin, VPCS, VirtualBox, Dynamips, Qemu, VMware, TraceNG]
if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1":
from .docker import Docker
MODULES.append(Docker)
# IOU runs only on Linux but testsuite work on UNIX platform
# IOU & Docker only runs on Linux but test suite works on UNIX platform
if not sys.platform.startswith("win"):
from .docker import Docker
MODULES.append(Docker)
from .iou import IOU
MODULES.append(IOU)
+2 -1
View File
@@ -346,6 +346,7 @@ class BaseNode:
remaining_trial -= 1
yield from AsyncioTelnetServer.write_client_intro(writer, echo=True)
server = AsyncioTelnetServer(reader=reader, writer=writer, binary=True, echo=True)
# warning: this will raise OSError exception if there is a problem...
self._wrapper_telnet_server = yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console)
@property
@@ -513,7 +514,7 @@ class BaseNode:
try:
yield from self._ubridge_hypervisor.send(command)
except UbridgeError as e:
raise UbridgeError("{}: {}".format(e, self._ubridge_hypervisor.read_stdout()))
raise UbridgeError("Error while sending command '{}': {}: {}".format(command, e, self._ubridge_hypervisor.read_stdout()))
@locked_coroutine
def _start_ubridge(self):
+5 -1
View File
@@ -198,7 +198,7 @@ class Docker(BaseManager):
pass
if progress_callback:
progress_callback("Pull {} from docker hub".format(image))
progress_callback("Pulling '{}' from docker hub".format(image))
response = yield from self.http_query("POST", "images/create", params={"fromImage": image}, timeout=None)
# The pull api will stream status via an HTTP JSON stream
content = ""
@@ -206,6 +206,10 @@ class Docker(BaseManager):
try:
chunk = yield from response.content.read(1024)
except aiohttp.ServerDisconnectedError:
log.error("Disconnected from server while pulling Docker image '{}' from docker hub".format(image))
break
except asyncio.TimeoutError:
log.error("Timeout while pulling Docker image '{}' from docker hub".format(image))
break
if not chunk:
break
+57 -6
View File
@@ -61,11 +61,12 @@ class DockerVM(BaseNode):
:param console_resolution: Resolution of the VNC display
:param console_http_port: Port to redirect HTTP queries
:param console_http_path: Url part with the path of the web interface
:param extra_hosts: Hosts which will be written into /etc/hosts into docker conainer
"""
def __init__(self, name, node_id, project, manager, image, console=None, aux=None, start_command=None,
adapters=None, environment=None, console_type="telnet", console_resolution="1024x768",
console_http_port=80, console_http_path="/"):
console_http_port=80, console_http_path="/", extra_hosts=None):
super().__init__(name, node_id, project, manager, console=console, aux=aux, allocate_aux=True, console_type=console_type)
@@ -84,6 +85,8 @@ class DockerVM(BaseNode):
self._console_http_path = console_http_path
self._console_http_port = console_http_port
self._console_websocket = None
self._extra_hosts = extra_hosts
self._volumes = []
# Keep a list of created bridge
self._bridges = set()
@@ -114,7 +117,8 @@ class DockerVM(BaseNode):
"start_command": self.start_command,
"status": self.status,
"environment": self.environment,
"node_directory": self.working_path
"node_directory": self.working_path,
"extra_hosts": self.extra_hosts
}
def _get_free_display_port(self):
@@ -178,6 +182,14 @@ class DockerVM(BaseNode):
def environment(self, command):
self._environment = command
@property
def extra_hosts(self):
return self._extra_hosts
@extra_hosts.setter
def extra_hosts(self, extra_hosts):
self._extra_hosts = extra_hosts
@asyncio.coroutine
def _get_container_state(self):
"""Returns the container state (e.g. running, paused etc.)
@@ -288,7 +300,7 @@ class DockerVM(BaseNode):
"HostConfig": {
"CapAdd": ["ALL"],
"Privileged": True,
"Binds": self._mount_binds(image_infos)
"Binds": self._mount_binds(image_infos),
},
"Volumes": {},
"Env": ["container=docker"], # Systemd compliant: https://github.com/GNS3/gns3-server/issues/573
@@ -313,11 +325,20 @@ class DockerVM(BaseNode):
# Give the information to the container the list of volume path mounted
params["Env"].append("GNS3_VOLUMES={}".format(":".join(self._volumes)))
variables = self.project.variables
if not variables:
variables = []
for var in variables:
formatted = self._format_env(variables, var.get('value', ''))
params["Env"].append("{}={}".format(var["name"], formatted))
if self._environment:
for e in self._environment.strip().split("\n"):
e = e.strip()
if not e.startswith("GNS3_"):
params["Env"].append(e)
formatted = self._format_env(variables, e)
params["Env"].append(formatted)
if self._console_type == "vnc":
yield from self._start_vnc()
@@ -325,12 +346,36 @@ class DockerVM(BaseNode):
params["Env"].append("DISPLAY=:{}".format(self._display))
params["HostConfig"]["Binds"].append("/tmp/.X11-unix/:/tmp/.X11-unix/")
if self._extra_hosts:
extra_hosts = self._format_extra_hosts(self._extra_hosts)
if extra_hosts:
params["Env"].append("GNS3_EXTRA_HOSTS={}".format(extra_hosts))
result = yield from self.manager.query("POST", "containers/create", data=params)
self._cid = result['Id']
log.info("Docker container '{name}' [{id}] created".format(
name=self._name, id=self._id))
return True
def _format_env(self, variables, env):
for variable in variables:
env = env.replace('${' + variable["name"] + '}', variable.get("value", ""))
return env
def _format_extra_hosts(self, extra_hosts):
lines = [h.strip() for h in self._extra_hosts.split("\n") if h.strip() != ""]
hosts = []
try:
for host in lines:
hostname, ip = host.split(":")
hostname = hostname.strip()
ip = ip.strip()
if hostname and ip:
hosts.append((hostname, ip))
except ValueError:
raise DockerError("Can't apply `ExtraHosts`, wrong format: {}".format(extra_hosts))
return "\n".join(["{}\t{}".format(h[1], h[0]) for h in hosts])
@asyncio.coroutine
def update(self):
"""
@@ -412,7 +457,10 @@ class DockerVM(BaseNode):
stderr=asyncio.subprocess.STDOUT,
stdin=asyncio.subprocess.PIPE)
server = AsyncioTelnetServer(reader=process.stdout, writer=process.stdin, binary=True, echo=True)
self._telnet_servers.append((yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.aux)))
try:
self._telnet_servers.append((yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.aux)))
except OSError as e:
raise DockerError("Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, self.aux, e))
log.debug("Docker container '%s' started listen for auxilary telnet on %d", self.name, self.aux)
@asyncio.coroutine
@@ -508,7 +556,10 @@ class DockerVM(BaseNode):
input_stream = InputStream()
telnet = AsyncioTelnetServer(reader=output_stream, writer=input_stream, echo=True)
self._telnet_servers.append((yield from asyncio.start_server(telnet.run, self._manager.port_manager.console_host, self.console)))
try:
self._telnet_servers.append((yield from asyncio.start_server(telnet.run, self._manager.port_manager.console_host, self.console)))
except OSError as e:
raise DockerError("Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, self.console, e))
self._console_websocket = yield from self.manager.websocket_query("containers/{}/attach/ws?stream=1&stdin=1&stdout=1&stderr=1".format(self._cid))
input_stream.ws = self._console_websocket
@@ -60,6 +60,14 @@ ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
__EOF__
# imitate docker's `ExtraHosts` behaviour
sed -i '/GNS3_EXTRA_HOSTS_START/,/GNS3_EXTRA_HOSTS_END/d' /etc/hosts
[ -n "$GNS3_EXTRA_HOSTS" ] && cat >> /etc/hosts << __EOF__
# GNS3_EXTRA_HOSTS_START
$GNS3_EXTRA_HOSTS
# GNS3_EXTRA_HOSTS_END
__EOF__
# configure loopback interface
ip link set dev lo up
@@ -167,8 +167,10 @@ class EthernetSwitch(Device):
self._telnet_shell = EthernetSwitchConsole(self)
self._telnet_shell.prompt = self._name + '> '
self._telnet = create_telnet_shell(self._telnet_shell)
self._telnet_server = (yield from asyncio.start_server(self._telnet.run, self._manager.port_manager.console_host, self.console))
try:
self._telnet_server = (yield from asyncio.start_server(self._telnet.run, self._manager.port_manager.console_host, self.console))
except OSError as e:
self.project.emit("log.warning", {"message": "Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, self.console, e)})
self._hypervisor.devices.append(self)
@asyncio.coroutine
+4 -2
View File
@@ -131,14 +131,16 @@ class Router(BaseNode):
try:
shutil.move(path, dst)
except OSError as e:
raise DynamipsError("Can't move {}: {}".format(path, str(e)))
log.error("Can't move {}: {}".format(path, str(e)))
continue
for path in glob.glob(os.path.join(glob.escape(dynamips_dir), "*_i{}_*".format(dynamips_id))):
dst = os.path.join(self._working_directory, os.path.basename(path))
if not os.path.exists(dst):
try:
shutil.move(path, dst)
except OSError as e:
raise DynamipsError("Can't move {}: {}".format(path, str(e)))
log.error("Can't move {}: {}".format(path, str(e)))
continue
def __json__(self):
+5 -1
View File
@@ -541,7 +541,11 @@ class IOUVM(BaseNode):
raise IOUError("Could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout))
server = AsyncioTelnetServer(reader=self._iou_process.stdout, writer=self._iou_process.stdin, binary=True, echo=True)
self._telnet_server = yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console)
try:
self._telnet_server = yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console)
except OSError as e:
yield from self.stop()
raise IOUError("Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, self.console, e))
# configure networking support
yield from self._networking()
+24 -3
View File
@@ -25,13 +25,13 @@ import zipfile
import json
from uuid import UUID, uuid4
from .port_manager import PortManager
from .notification_manager import NotificationManager
from ..config import Config
from ..utils.asyncio import wait_run_in_executor
from ..utils.path import check_path_allowed, get_default_project_directory
import logging
log = logging.getLogger(__name__)
@@ -46,7 +46,7 @@ class Project:
:param path: path of the project. (None use the standard directory)
"""
def __init__(self, name=None, project_id=None, path=None):
def __init__(self, name=None, project_id=None, path=None, variables=None):
self._name = name
if project_id:
@@ -61,6 +61,7 @@ class Project:
self._nodes = set()
self._used_tcp_ports = set()
self._used_udp_ports = set()
self._variables = variables
if path is None:
location = get_default_project_directory()
@@ -83,7 +84,8 @@ class Project:
return {
"name": self._name,
"project_id": self._id
"project_id": self._id,
"variables": self._variables
}
def _config(self):
@@ -131,6 +133,14 @@ class Project:
return self._nodes
@property
def variables(self):
return self._variables
@variables.setter
def variables(self, variables):
self._variables = variables
def record_tcp_port(self, port):
"""
Associate a reserved TCP port number with this project.
@@ -287,6 +297,17 @@ class Project:
yield from node.delete()
self._nodes.remove(node)
@asyncio.coroutine
def update(self, variables=None, **kwargs):
original_variables = self.variables
self.variables = variables
# we need to update docker nodes when variables changes
if original_variables != variables:
for node in self.nodes:
if hasattr(node, 'update'):
yield from node.update()
@asyncio.coroutine
def close(self):
"""
+4 -3
View File
@@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import aiohttp
import asyncio
import psutil
import platform
from .project import Project
@@ -95,16 +96,16 @@ class ProjectManager:
log.warning(message)
project.emit("log.warning", {"message": message})
def create_project(self, name=None, project_id=None, path=None):
def create_project(self, name=None, project_id=None, path=None, variables=None):
"""
Create a project and keep a references to it in project manager.
See documentation of Project for arguments
"""
if project_id is not None and project_id in self._projects:
return self._projects[project_id]
project = Project(name=name, project_id=project_id, path=path)
project = Project(name=name, project_id=project_id,
path=path, variables=variables)
self._check_available_disk_space(project)
self._projects[project.id] = project
return project
+2 -1
View File
@@ -533,7 +533,7 @@ class QemuVM(BaseNode):
if not mac_address:
# use the node UUID to generate a random MAC address
self._mac_address = "52:%s:%s:%s:%s:00" % (self.project.id[-4:-2], self.project.id[-2:], self.id[-4:-2], self.id[-2:])
self._mac_address = "0c:%s:%s:%s:%s:00" % (self.project.id[-4:-2], self.project.id[-2:], self.id[-4:-2], self.id[-2:])
else:
self._mac_address = mac_address
@@ -894,6 +894,7 @@ class QemuVM(BaseNode):
af, socktype, proto, _, sa = res
# let the OS find an unused port for the Qemu monitor
with socket.socket(af, socktype, proto) as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(sa)
self._monitor = sock.getsockname()[1]
except OSError as e:
+45
View File
@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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 <http://www.gnu.org/licenses/>.
"""
TraceNG server module.
"""
import asyncio
from ..base_manager import BaseManager
from .traceng_error import TraceNGError
from .traceng_vm import TraceNGVM
class TraceNG(BaseManager):
_NODE_CLASS = TraceNGVM
def __init__(self):
super().__init__()
@asyncio.coroutine
def create_node(self, *args, **kwargs):
"""
Creates a new TraceNG VM.
:returns: TraceNGVM instance
"""
return (yield from super().create_node(*args, **kwargs))
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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 <http://www.gnu.org/licenses/>.
"""
Custom exceptions for the TraceNG module.
"""
from ..error import NodeError
class TraceNGError(NodeError):
pass
+458
View File
@@ -0,0 +1,458 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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 <http://www.gnu.org/licenses/>.
"""
TraceNG VM management in order to run a TraceNG VM.
"""
import sys
import os
import socket
import subprocess
import asyncio
import shutil
import ipaddress
from gns3server.utils.asyncio import wait_for_process_termination
from gns3server.utils.asyncio import monitor_process
from .traceng_error import TraceNGError
from ..adapters.ethernet_adapter import EthernetAdapter
from ..nios.nio_udp import NIOUDP
from ..base_node import BaseNode
import logging
log = logging.getLogger(__name__)
class TraceNGVM(BaseNode):
module_name = 'traceng'
"""
TraceNG VM implementation.
:param name: TraceNG VM name
:param node_id: Node identifier
:param project: Project instance
:param manager: Manager instance
:param console: TCP console port
"""
def __init__(self, name, node_id, project, manager, console=None, console_type="none"):
super().__init__(name, node_id, project, manager, console=console, console_type=console_type)
self._process = None
self._started = False
self._ip_address = None
self._default_destination = None
self._destination = None
self._local_udp_tunnel = None
self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface
@property
def ethernet_adapter(self):
return self._ethernet_adapter
@asyncio.coroutine
def close(self):
"""
Closes this TraceNG VM.
"""
if not (yield from super().close()):
return False
nio = self._ethernet_adapter.get_nio(0)
if isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project)
if self._local_udp_tunnel:
self.manager.port_manager.release_udp_port(self._local_udp_tunnel[0].lport, self._project)
self.manager.port_manager.release_udp_port(self._local_udp_tunnel[1].lport, self._project)
self._local_udp_tunnel = None
yield from self._stop_ubridge()
if self.is_running():
self._terminate_process()
return True
@asyncio.coroutine
def _check_requirements(self):
"""
Check if TraceNG is available.
"""
path = self._traceng_path()
if not path:
raise TraceNGError("No path to a TraceNG executable has been set")
# This raise an error if ubridge is not available
self.ubridge_path
if not os.path.isfile(path):
raise TraceNGError("TraceNG program '{}' is not accessible".format(path))
if not os.access(path, os.X_OK):
raise TraceNGError("TraceNG program '{}' is not executable".format(path))
def __json__(self):
return {"name": self.name,
"ip_address": self.ip_address,
"default_destination": self._default_destination,
"node_id": self.id,
"node_directory": self.working_path,
"status": self.status,
"console": self._console,
"console_type": "none",
"project_id": self.project.id,
"command_line": self.command_line}
def _traceng_path(self):
"""
Returns the TraceNG executable path.
:returns: path to TraceNG
"""
search_path = self._manager.config.get_section_config("TraceNG").get("traceng_path", "traceng")
path = shutil.which(search_path)
# shutil.which return None if the path doesn't exists
if not path:
return search_path
return path
@property
def ip_address(self):
"""
Returns the IP address for this node.
:returns: IP address
"""
return self._ip_address
@ip_address.setter
def ip_address(self, ip_address):
"""
Sets the IP address of this node.
:param ip_address: IP address
"""
try:
if ip_address:
ipaddress.IPv4Address(ip_address)
except ipaddress.AddressValueError:
raise TraceNGError("Invalid IP address: {}\n".format(ip_address))
self._ip_address = ip_address
log.info("{module}: {name} [{id}] set IP address to {ip_address}".format(module=self.manager.module_name,
name=self.name,
id=self.id,
ip_address=ip_address))
@property
def default_destination(self):
"""
Returns the default destination IP/host for this node.
:returns: destination IP/host
"""
return self._default_destination
@default_destination.setter
def default_destination(self, destination):
"""
Sets the destination IP/host for this node.
:param destination: destination IP/host
"""
self._default_destination = destination
log.info("{module}: {name} [{id}] set default destination to {destination}".format(module=self.manager.module_name,
name=self.name,
id=self.id,
destination=destination))
@asyncio.coroutine
def start(self, destination=None):
"""
Starts the TraceNG process.
"""
if not sys.platform.startswith("win"):
raise TraceNGError("Sorry, TraceNG can only run on Windows")
yield from self._check_requirements()
if not self.is_running():
nio = self._ethernet_adapter.get_nio(0)
command = self._build_command(destination)
yield from self._stop_ubridge() # make use we start with a fresh uBridge instance
try:
log.info("Starting TraceNG: {}".format(command))
flags = 0
if hasattr(subprocess, "CREATE_NEW_CONSOLE"):
flags = subprocess.CREATE_NEW_CONSOLE
self.command_line = ' '.join(command)
self._process = yield from asyncio.create_subprocess_exec(*command,
cwd=self.working_dir,
creationflags=flags)
monitor_process(self._process, self._termination_callback)
yield from self._start_ubridge()
if nio:
yield from self.add_ubridge_udp_connection("TraceNG-{}".format(self._id), self._local_udp_tunnel[1], nio)
log.info("TraceNG instance {} started PID={}".format(self.name, self._process.pid))
self._started = True
self.status = "started"
except (OSError, subprocess.SubprocessError) as e:
log.error("Could not start TraceNG {}: {}\n".format(self._traceng_path(), e))
raise TraceNGError("Could not start TraceNG {}: {}\n".format(self._traceng_path(), e))
def _termination_callback(self, returncode):
"""
Called when the process has stopped.
:param returncode: Process returncode
"""
if self._started:
log.info("TraceNG process has stopped, return code: %d", returncode)
self._started = False
self.status = "stopped"
self._process = None
if returncode != 0:
self.project.emit("log.error", {"message": "TraceNG process has stopped, return code: {}\n".format(returncode)})
@asyncio.coroutine
def stop(self):
"""
Stops the TraceNG process.
"""
yield from self._stop_ubridge()
if self.is_running():
self._terminate_process()
if self._process.returncode is None:
try:
yield from wait_for_process_termination(self._process, timeout=3)
except asyncio.TimeoutError:
if self._process.returncode is None:
try:
self._process.kill()
except OSError as e:
log.error("Cannot stop the TraceNG process: {}".format(e))
if self._process.returncode is None:
log.warning('TraceNG VM "{}" with PID={} is still running'.format(self._name, self._process.pid))
self._process = None
self._started = False
yield from super().stop()
@asyncio.coroutine
def reload(self):
"""
Reloads the TraceNG process (stop & start).
"""
yield from self.stop()
yield from self.start(self._destination)
def _terminate_process(self):
"""
Terminate the process if running
"""
log.info("Stopping TraceNG instance {} PID={}".format(self.name, self._process.pid))
#if sys.platform.startswith("win32"):
# self._process.send_signal(signal.CTRL_BREAK_EVENT)
#else:
try:
self._process.terminate()
# Sometime the process may already be dead when we garbage collect
except ProcessLookupError:
pass
def is_running(self):
"""
Checks if the TraceNG process is running
:returns: True or False
"""
if self._process and self._process.returncode is None:
return True
return False
@asyncio.coroutine
def port_add_nio_binding(self, port_number, nio):
"""
Adds a port NIO binding.
:param port_number: port number
:param nio: NIO instance to add to the slot/port
"""
if not self._ethernet_adapter.port_exists(port_number):
raise TraceNGError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
port_number=port_number))
if self.is_running():
yield from self.add_ubridge_udp_connection("TraceNG-{}".format(self._id), self._local_udp_tunnel[1], nio)
self._ethernet_adapter.add_nio(port_number, nio)
log.info('TraceNG "{name}" [{id}]: {nio} added to port {port_number}'.format(name=self._name,
id=self.id,
nio=nio,
port_number=port_number))
return nio
@asyncio.coroutine
def port_update_nio_binding(self, port_number, nio):
if not self._ethernet_adapter.port_exists(port_number):
raise TraceNGError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
port_number=port_number))
if self.is_running():
yield from self.update_ubridge_udp_connection("TraceNG-{}".format(self._id), self._local_udp_tunnel[1], nio)
@asyncio.coroutine
def port_remove_nio_binding(self, port_number):
"""
Removes a port NIO binding.
:param port_number: port number
:returns: NIO instance
"""
if not self._ethernet_adapter.port_exists(port_number):
raise TraceNGError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
port_number=port_number))
if self.is_running():
yield from self._ubridge_send("bridge delete {name}".format(name="TraceNG-{}".format(self._id)))
nio = self._ethernet_adapter.get_nio(port_number)
if isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project)
self._ethernet_adapter.remove_nio(port_number)
log.info('TraceNG "{name}" [{id}]: {nio} removed from port {port_number}'.format(name=self._name,
id=self.id,
nio=nio,
port_number=port_number))
return nio
@asyncio.coroutine
def start_capture(self, port_number, output_file):
"""
Starts a packet capture.
:param port_number: port number
:param output_file: PCAP destination file for the capture
"""
if not self._ethernet_adapter.port_exists(port_number):
raise TraceNGError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
port_number=port_number))
nio = self._ethernet_adapter.get_nio(0)
if not nio:
raise TraceNGError("Port {} is not connected".format(port_number))
if nio.capturing:
raise TraceNGError("Packet capture is already activated on port {port_number}".format(port_number=port_number))
nio.startPacketCapture(output_file)
if self.ubridge:
yield from self._ubridge_send('bridge start_capture {name} "{output_file}"'.format(name="TraceNG-{}".format(self._id),
output_file=output_file))
log.info("TraceNG '{name}' [{id}]: starting packet capture on port {port_number}".format(name=self.name,
id=self.id,
port_number=port_number))
@asyncio.coroutine
def stop_capture(self, port_number):
"""
Stops a packet capture.
:param port_number: port number
"""
if not self._ethernet_adapter.port_exists(port_number):
raise TraceNGError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
port_number=port_number))
nio = self._ethernet_adapter.get_nio(0)
if not nio:
raise TraceNGError("Port {} is not connected".format(port_number))
nio.stopPacketCapture()
if self.ubridge:
yield from self._ubridge_send('bridge stop_capture {name}'.format(name="TraceNG-{}".format(self._id)))
log.info("TraceNG '{name}' [{id}]: stopping packet capture on port {port_number}".format(name=self.name,
id=self.id,
port_number=port_number))
def _build_command(self, destination):
"""
Command to start the TraceNG process.
(to be passed to subprocess.Popen())
"""
if not destination:
# use the default destination if no specific destination provided
destination = self.default_destination
if not destination:
raise TraceNGError("Please provide a host or IP address to trace")
if not self.ip_address:
raise TraceNGError("Please configure an IP address for this TraceNG node")
if self.ip_address == destination:
raise TraceNGError("Destination cannot be the same as the IP address")
self._destination = destination
command = [self._traceng_path()]
# use the local UDP tunnel to uBridge instead
if not self._local_udp_tunnel:
self._local_udp_tunnel = self._create_local_udp_tunnel()
nio = self._local_udp_tunnel[0]
if nio and isinstance(nio, NIOUDP):
# UDP tunnel
command.extend(["-u"]) # enable UDP tunnel
command.extend(["-c", str(nio.lport)]) # source UDP port
command.extend(["-v", str(nio.rport)]) # destination UDP port
try:
command.extend(["-b", socket.gethostbyname(nio.rhost)]) # destination host, we need to resolve the hostname because TraceNG doesn't support it
except socket.gaierror as e:
raise TraceNGError("Can't resolve hostname {}: {}".format(nio.rhost, e))
command.extend(["-s", "ICMP"]) # Use ICMP probe type by default
command.extend(["-f", self._ip_address]) # source IP address to trace from
command.extend([destination]) # host or IP to trace
return command
@@ -942,7 +942,10 @@ class VirtualBoxVM(BaseNode):
writer=self._remote_pipe,
binary=True,
echo=True)
self._telnet_server = yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console)
try:
self._telnet_server = yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console)
except OSError as e:
self.project.emit("log.warning", {"message": "Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, self.console, e)})
@asyncio.coroutine
def _stop_remote_console(self):
+4 -4
View File
@@ -253,7 +253,7 @@ class VMware(BaseManager):
if winreg.QueryInfoKey(hkeyvmnet)[1]:
# the vmnet has not been configure if the key has no values
vmnet = vmnet.replace("vm", "VM")
if vmnet not in ("VMnet1", "VMnet8"):
if vmnet not in ("VMnet0", "VMnet1", "VMnet8"):
vmnet_interfaces.append(vmnet)
winreg.CloseKey(hkeyvmnet)
winreg.CloseKey(hkey)
@@ -278,7 +278,7 @@ class VMware(BaseManager):
match = re.search("VNET_([0-9]+)_VIRTUAL_ADAPTER", line)
if match:
vmnet = "vmnet{}".format(match.group(1))
if vmnet not in ("vmnet1", "vmnet8"):
if vmnet not in ("vmnet0", "vmnet1", "vmnet8"):
vmnet_interfaces.append(vmnet)
except OSError as e:
raise VMwareError("Cannot open {}: {}".format(vmware_networking_file, e))
@@ -297,11 +297,11 @@ class VMware(BaseManager):
match = re.search("(VMnet[0-9]+)", windows_name)
if match:
vmnet = match.group(1)
if vmnet not in ("VMnet1", "VMnet8"):
if vmnet not in ("VMnet0", "VMnet1", "VMnet8"):
vmnet_interfaces.append(vmnet)
elif interface["name"].startswith("vmnet"):
vmnet = interface["name"]
if vmnet not in ("vmnet1", "vmnet8"):
if vmnet not in ("vmnet0", "vmnet1", "vmnet8"):
vmnet_interfaces.append(interface["name"])
return vmnet_interfaces
+15 -11
View File
@@ -24,7 +24,6 @@ import os
import asyncio
import tempfile
from gns3server.utils.interfaces import interfaces
from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer
from gns3server.utils.asyncio.serial import asyncio_open_serial
from gns3server.utils.asyncio import locked_coroutine
@@ -277,6 +276,7 @@ class VMwareVM(BaseNode):
continue
self._vmx_pairs["ethernet{}.connectiontype".format(adapter_number)] = "custom"
# make sure we have a vmnet per adapter if we use uBridge
allocate_vmnet = False
@@ -285,7 +285,7 @@ class VMwareVM(BaseNode):
if vnet in self._vmx_pairs:
vmnet = os.path.basename(self._vmx_pairs[vnet])
if self.manager.is_managed_vmnet(vmnet) or vmnet in ("vmnet0", "vmnet1", "vmnet8"):
# vmnet already managed, try to allocate a new one
# vmnet already managed or a special vmnet, try to allocate a new one
allocate_vmnet = True
else:
# otherwise allocate a new one
@@ -299,7 +299,7 @@ class VMwareVM(BaseNode):
self._vmnets.clear()
raise
# mark the vmnet managed by us
# mark the vmnet as managed by us
if vmnet not in self._vmnets:
self._vmnets.append(vmnet)
self._vmx_pairs["ethernet{}.vnet".format(adapter_number)] = vmnet
@@ -739,17 +739,18 @@ class VMwareVM(BaseNode):
if self._get_vmx_setting("ethernet{}.present".format(adapter_number), "TRUE"):
# check for the connection type
connection_type = "ethernet{}.connectiontype".format(adapter_number)
if connection_type in self._vmx_pairs and self._vmx_pairs[connection_type] in ("nat", "bridged", "hostonly"):
if not self._use_any_adapter:
raise VMwareError("Attachment '{attachment}' is already configured on network adapter {adapter_number}. "
"Please remove it or allow VMware VM '{name}' to use any adapter.".format(attachment=self._vmx_pairs[connection_type],
adapter_number=adapter_number,
name=self.name))
elif (yield from self.is_running()):
if not self._use_any_adapter and connection_type in self._vmx_pairs and self._vmx_pairs[connection_type] in ("nat", "bridged", "hostonly"):
if (yield from self.is_running()):
raise VMwareError("Attachment '{attachment}' is configured on network adapter {adapter_number}. "
"Please stop VMware VM '{name}' to link to this adapter and allow GNS3 to change the attachment type.".format(attachment=self._vmx_pairs[connection_type],
adapter_number=adapter_number,
name=self.name))
else:
raise VMwareError("Attachment '{attachment}' is already configured on network adapter {adapter_number}. "
"Please remove it or allow VMware VM '{name}' to use any adapter.".format(attachment=self._vmx_pairs[connection_type],
adapter_number=adapter_number,
name=self.name))
adapter.add_nio(0, nio)
if self._started and self._ubridge_hypervisor:
@@ -848,7 +849,10 @@ class VMwareVM(BaseNode):
writer=self._remote_pipe,
binary=True,
echo=True)
self._telnet_server = yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console)
try:
self._telnet_server = yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console)
except OSError as e:
self.project.emit("log.warning", {"message": "Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, self.console, e)})
@asyncio.coroutine
def _stop_remote_console(self):
+4 -1
View File
@@ -313,7 +313,10 @@ class VPCSVM(BaseNode):
log.info("Stopping VPCS instance {} PID={}".format(self.name, self._process.pid))
if sys.platform.startswith("win32"):
self._process.send_signal(signal.CTRL_BREAK_EVENT)
try:
self._process.send_signal(signal.CTRL_BREAK_EVENT)
except OSError:
pass
else:
try:
self._process.terminate()
+11 -4
View File
@@ -117,6 +117,9 @@ class Controller:
for vm in self._settings.get("VPCS", {}).get("nodes", []):
vm["node_type"] = "vpcs"
vms.append(vm)
for vm in self._settings.get("TraceNG", {}).get("nodes", []):
vm["node_type"] = "traceng"
vms.append(vm)
for vm in vms:
# remove deprecated properties
@@ -155,6 +158,10 @@ class Controller:
builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_hub"), {"node_type": "ethernet_hub", "name": "Ethernet hub", "category": 1, "symbol": ":/symbols/hub.svg"}, builtin=True))
builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "frame_relay_switch"), {"node_type": "frame_relay_switch", "name": "Frame Relay switch", "category": 1, "symbol": ":/symbols/frame_relay_switch.svg"}, builtin=True))
builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "atm_switch"), {"node_type": "atm_switch", "name": "ATM switch", "category": 1, "symbol": ":/symbols/atm_switch.svg"}, builtin=True))
#FIXME: disable TraceNG
#if sys.platform.startswith("win"):
# builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "traceng"), {"node_type": "traceng", "name": "TraceNG", "default_name_format": "TraceNG-{0}", "category": 2, "symbol": ":/symbols/traceng.svg", "properties": {}}, builtin=True))
for b in builtins:
self._appliances[b.id] = b
@@ -187,13 +194,13 @@ class Controller:
user=server_config.get("user", ""),
password=server_config.get("password", ""),
force=True)
except aiohttp.web_exceptions.HTTPConflict as e:
except aiohttp.web.HTTPConflict as e:
log.fatal("Can't access to the local server, make sure anything else is not running on the same port")
sys.exit(1)
for c in computes:
try:
yield from self.add_compute(**c)
except (aiohttp.web_exceptions.HTTPConflict, KeyError):
except (aiohttp.web.HTTPConflict, KeyError):
pass # Skip not available servers at loading
yield from self.load_projects()
try:
@@ -221,7 +228,7 @@ class Controller:
try:
yield from compute.close()
# We don't care if a compute is down at this step
except (ComputeError, aiohttp.web_exceptions.HTTPError, OSError):
except (ComputeError, aiohttp.web.HTTPError, OSError):
pass
yield from self.gns3vm.exit_vm()
self._computes = {}
@@ -301,7 +308,7 @@ class Controller:
if file.endswith(".gns3"):
try:
yield from self.load_project(os.path.join(project_dir, file), load=False)
except (aiohttp.web_exceptions.HTTPConflict, NotImplementedError):
except (aiohttp.web.HTTPConflict, NotImplementedError):
pass # Skip not compatible projects
except OSError as e:
log.error(str(e))
-1
View File
@@ -460,7 +460,6 @@ class Compute:
msg = json.loads(response.data)
action = msg.pop("action")
event = msg.pop("event")
if action == "ping":
self._cpu_usage_percent = event["cpu_usage_percent"]
self._memory_usage_percent = event["memory_usage_percent"]
+74 -75
View File
@@ -29,122 +29,128 @@ log = logging.getLogger(__name__)
@asyncio.coroutine
def export_project(project, temporary_dir, include_images=False, keep_compute_id=False,
allow_all_nodes=False, ignore_prefixes=None):
def export_project(project, temporary_dir, include_images=False, keep_compute_id=False, allow_all_nodes=False):
"""
Export the project as zip. It's a ZipStream object.
The file will be read chunk by chunk when you iterate on
the zip.
Export a project to a zip file.
It will ignore some files like snapshots and
The file will be read chunk by chunk when you iterate over the zip stream.
Some files like snapshots and packet captures are ignored.
:param temporary_dir: A temporary dir where to store intermediate data
:param keep_compute_id: If false replace all compute id by local it's the standard behavior for .gns3project to make them portable
:param allow_all_nodes: Allow all nodes type to be include in the zip even if not portable default False
:param include images: save OS images to the zip file
:param keep_compute_id: If false replace all compute id by local (standard behavior for .gns3project to make it portable)
:param allow_all_nodes: Allow all nodes type to be include in the zip even if not portable
:returns: ZipStream object
"""
# To avoid issue with data not saved we disallow the export of a running topologie
# To avoid issue with data not saved we disallow the export of a running project
if project.is_running():
raise aiohttp.web.HTTPConflict(text="Running topology could not be exported")
raise aiohttp.web.HTTPConflict(text="Project must be stopped in order to export it")
# Make sure we save the project
project.dump()
z = zipstream.ZipFile(allowZip64=True)
zstream = zipstream.ZipFile(allowZip64=True)
if not os.path.exists(project._path):
raise aiohttp.web.HTTPNotFound(text="The project doesn't exist at location {}".format(project._path))
raise aiohttp.web.HTTPNotFound(text="Project could not be found at '{}'".format(project._path))
# First we process the .gns3 in order to be sure we don't have an error
for file in os.listdir(project._path):
if file.endswith(".gns3"):
images = yield from _export_project_file(project, os.path.join(project._path, file),
z, include_images, keep_compute_id, allow_all_nodes, temporary_dir)
yield from _patch_project_file(project, os.path.join(project._path, file), zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir)
# Export the local files
for root, dirs, files in os.walk(project._path, topdown=True):
files = [f for f in files if not _filter_files(os.path.join(root, f))]
files = [f for f in files if _is_exportable(os.path.join(root, f))]
for file in files:
path = os.path.join(root, file)
# Try open the file
# check if we can export the file
try:
open(path).close()
except OSError as e:
msg = "Could not export file {}: {}".format(path, e)
log.warn(msg)
msg = "Could not export file '{}': {}".format(path, e)
log.warning(msg)
project.controller.notification.emit("log.warning", {"message": msg})
continue
# ignore the .gns3 file
if file.endswith(".gns3"):
pass
else:
z.write(path, os.path.relpath(path, project._path), compress_type=zipfile.ZIP_DEFLATED)
continue
zstream.write(path, os.path.relpath(path, project._path), compress_type=zipfile.ZIP_DEFLATED)
# Export files from remote computes
downloaded_files = set()
for compute in project.computes:
if compute.id != "local":
compute_files = yield from compute.list_files(project)
for compute_file in compute_files:
if not _filter_files(compute_file["path"]):
if _is_exportable(compute_file["path"]):
(fd, temp_path) = tempfile.mkstemp(dir=temporary_dir)
f = open(fd, "wb", closefd=True)
response = yield from compute.download_file(project, compute_file["path"])
while True:
data = yield from response.content.read(512)
try:
data = yield from response.content.read(1024)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when downloading file '{}' from remote compute server {}:{}".format(compute_file["path"], compute.host, compute.port))
if not data:
break
f.write(data)
response.close()
f.close()
z.write(temp_path, arcname=compute_file["path"], compress_type=zipfile.ZIP_DEFLATED)
zstream.write(temp_path, arcname=compute_file["path"], compress_type=zipfile.ZIP_DEFLATED)
downloaded_files.add(compute_file['path'])
return z
return zstream
def _filter_files(path):
def _is_exportable(path):
"""
:returns: True if file should not be included in the final archive
"""
s = os.path.normpath(path).split(os.path.sep)
# do not export snapshots
if path.endswith("snapshots"):
return True
return False
# filter directory of snapshots
# do not export directories of snapshots
if "{sep}snapshots{sep}".format(sep=os.path.sep) in path:
return True
return False
try:
# do not export captures and other temporary directory
s = os.path.normpath(path).split(os.path.sep)
i = s.index("project-files")
if s[i + 1] in ("tmp", "captures", "snapshots"):
return True
return False
except (ValueError, IndexError):
pass
file_name = os.path.basename(path)
# Ignore log files and OS noises
if file_name.endswith('_log.txt') or file_name.endswith('.log') or file_name == '.DS_Store':
return True
return False
# do not export log files and OS noise
filename = os.path.basename(path)
if filename.endswith('_log.txt') or filename.endswith('.log') or filename == '.DS_Store':
return False
return True
@asyncio.coroutine
def _export_project_file(project, path, z, include_images, keep_compute_id, allow_all_nodes, temporary_dir):
def _patch_project_file(project, path, zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir):
"""
Take a project file (.gns3) and patch it for the export
Patch a project file (.gns3) to export a project.
The .gns3 file is renamed to project.gns3
We rename the .gns3 project.gns3 to avoid the task to the client to guess the file name
:param path: Path of the .gns3
:param path: path of the .gns3 file
"""
# Image file that we need to include in the exported archive
# image files that we need to include in the exported archive
images = []
with open(path) as f:
topology = json.load(f)
try:
with open(path) as f:
topology = json.load(f)
except (OSError, ValueError) as e:
raise aiohttp.web.HTTPConflict(text="Project file '{}' cannot be read: {}".format(path, e))
if "topology" in topology:
if "nodes" in topology["topology"]:
@@ -152,9 +158,9 @@ def _export_project_file(project, path, z, include_images, keep_compute_id, allo
compute_id = node.get('compute_id', 'local')
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"]))
raise aiohttp.web.HTTPConflict(text="Projects with a linked {} clone node cannot not be exported. Please 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"]))
raise aiohttp.web.HTTPConflict(text="Projects with a {} node cannot be exported".format(node["node_type"]))
if not keep_compute_id:
node["compute_id"] = "local" # To make project portable all node by default run on local
@@ -186,78 +192,71 @@ def _export_project_file(project, path, z, include_images, keep_compute_id, allo
local_images = set([i['image'] for i in images if i['compute_id'] == 'local'])
for image in local_images:
_export_local_images(project, image, z)
_export_local_image(image, zstream)
remote_images = set([
(i['compute_id'], i['image_type'], i['image'])
for i in images if i['compute_id'] != 'local'])
for compute_id, image_type, image in remote_images:
yield from _export_remote_images(project, compute_id, image_type, image, z, temporary_dir)
z.writestr("project.gns3", json.dumps(topology).encode())
yield from _export_remote_images(project, compute_id, image_type, image, zstream, temporary_dir)
zstream.writestr("project.gns3", json.dumps(topology).encode())
return images
def _export_local_images(project, image, z):
def _export_local_image(image, zstream):
"""
Take a project file (.gns3) and export images to the zip
Exports a local image to the zip file.
:param image: Image path
:param z: Zipfile instance for the export
:param image: image path
:param zstream: Zipfile instance for the export
"""
from ..compute import MODULES
for module in MODULES:
try:
img_directory = module.instance().get_images_directory()
images_directory = module.instance().get_images_directory()
except NotImplementedError:
# Some modules don't have images
continue
directory = os.path.split(img_directory)[-1:][0]
directory = os.path.split(images_directory)[-1:][0]
if os.path.exists(image):
path = image
else:
path = os.path.join(img_directory, image)
path = os.path.join(images_directory, image)
if os.path.exists(path):
arcname = os.path.join("images", directory, os.path.basename(image))
z.write(path, arcname)
zstream.write(path, arcname)
return
@asyncio.coroutine
def _export_remote_images(project, compute_id, image_type, image, project_zipfile, temporary_dir):
"""
Export specific image from remote compute
:param project:
:param compute_id:
:param image_type:
:param image:
:param project_zipfile:
:return:
Export specific image from remote compute.
"""
log.info("Obtaining image `{}` from `{}`".format(image, compute_id))
log.info("Downloading image '{}' from compute server '{}'".format(image, compute_id))
try:
compute = [compute for compute in project.computes if compute.id == compute_id][0]
except IndexError:
raise aiohttp.web.HTTPConflict(
text="Cannot export image from `{}` compute. Compute doesn't exist.".format(compute_id))
raise aiohttp.web.HTTPConflict(text="Cannot export image from '{}' compute. Compute doesn't exist.".format(compute_id))
(fd, temp_path) = tempfile.mkstemp(dir=temporary_dir)
f = open(fd, "wb", closefd=True)
response = yield from compute.download_image(image_type, image)
if response.status != 200:
raise aiohttp.web.HTTPConflict(
text="Cannot export image from `{}` compute. Compute sent `{}` status.".format(
compute_id, response.status))
raise aiohttp.web.HTTPConflict(text="Cannot export image from '{}' compute. Compute returned status code {}.".format(compute_id, response.status))
while True:
data = yield from response.content.read(512)
try:
data = yield from response.content.read(1024)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when downloading image '{}' from remote compute server {}:{}".format(image, compute.host, compute.port))
if not data:
break
f.write(data)
@@ -191,6 +191,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
try:
# get a random port on localhost
with socket.socket() as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((ip_address, 0))
api_port = s.getsockname()[1]
except OSError as e:
+111 -106
View File
@@ -26,7 +26,7 @@ import aiohttp
import itertools
from .topology import load_topology
from ..utils.asyncio import wait_run_in_executor
"""
Handle the import of project from a .gns3project
@@ -38,7 +38,7 @@ def import_project(controller, project_id, stream, location=None, name=None, kee
"""
Import a project contain in a zip file
You need to handle OSerror exceptions
You need to handle OSError exceptions
:param controller: GNS3 Controller
:param project_id: ID of the project to import
@@ -46,6 +46,7 @@ def import_project(controller, project_id, stream, location=None, name=None, kee
:param location: Directory for the project if None put in the default directory
:param name: Wanted project name, generate one from the .gns3 if None
:param keep_compute_id: If true do not touch the compute id
:returns: Project
"""
@@ -53,115 +54,118 @@ def import_project(controller, project_id, stream, location=None, name=None, kee
raise aiohttp.web.HTTPConflict(text="The destination path should not contain .gns3")
try:
with zipfile.ZipFile(stream) as myzip:
try:
topology = json.loads(myzip.read("project.gns3").decode())
# We import the project on top of an existing project (snapshots)
if topology["project_id"] == project_id:
project_name = topology["name"]
else:
# If the project name is already used we generate a new one
if name:
project_name = controller.get_free_project_name(name)
else:
project_name = controller.get_free_project_name(topology["name"])
except KeyError:
raise aiohttp.web.HTTPConflict(text="Can't import topology the .gns3 is corrupted or missing")
if location:
path = location
else:
projects_path = controller.projects_directory()
path = os.path.join(projects_path, project_id)
try:
os.makedirs(path, exist_ok=True)
except UnicodeEncodeError as e:
raise aiohttp.web.HTTPConflict(text="The project name contain non supported or invalid characters")
myzip.extractall(path)
topology = load_topology(os.path.join(path, "project.gns3"))
topology["name"] = project_name
# To avoid unexpected behavior (project start without manual operations just after import)
topology["auto_start"] = False
topology["auto_open"] = False
topology["auto_close"] = True
# Generate a new node id
node_old_to_new = {}
for node in topology["topology"]["nodes"]:
if "node_id" in node:
node_old_to_new[node["node_id"]] = str(uuid.uuid4())
_move_node_file(path, node["node_id"], node_old_to_new[node["node_id"]])
node["node_id"] = node_old_to_new[node["node_id"]]
else:
node["node_id"] = str(uuid.uuid4())
# Update link to use new id
for link in topology["topology"]["links"]:
link["link_id"] = str(uuid.uuid4())
for node in link["nodes"]:
node["node_id"] = node_old_to_new[node["node_id"]]
# Generate new drawings id
for drawing in topology["topology"]["drawings"]:
drawing["drawing_id"] = str(uuid.uuid4())
# Modify the compute id of the node depending of compute capacity
if not keep_compute_id:
# For some VM type we move them to the GNS3 VM if possible
# unless it's a linux host without GNS3 VM
if not sys.platform.startswith("linux") or controller.has_compute("vm"):
for node in topology["topology"]["nodes"]:
if node["node_type"] in ("docker", "qemu", "iou", "nat"):
node["compute_id"] = "vm"
else:
# Round-robin through available compute resources.
compute_nodes = itertools.cycle(controller.computes)
for node in topology["topology"]["nodes"]:
node["compute_id"] = next(compute_nodes)
compute_created = set()
for node in topology["topology"]["nodes"]:
if node["compute_id"] != "local":
# Project created on the remote GNS3 VM?
if node["compute_id"] not in compute_created:
compute = controller.get_compute(node["compute_id"])
yield from compute.post("/projects", data={
"name": project_name,
"project_id": project_id,
})
compute_created.add(node["compute_id"])
yield from _move_files_to_compute(compute, project_id, path, os.path.join("project-files", node["node_type"], node["node_id"]))
# And we dump the updated.gns3
dot_gns3_path = os.path.join(path, project_name + ".gns3")
# We change the project_id to avoid erasing the project
topology["project_id"] = project_id
with open(dot_gns3_path, "w+") as f:
json.dump(topology, f, indent=4)
os.remove(os.path.join(path, "project.gns3"))
if os.path.exists(os.path.join(path, "images")):
_import_images(controller, path)
project = yield from controller.load_project(dot_gns3_path, load=False)
return project
with zipfile.ZipFile(stream) as zip_file:
project_file = zip_file.read("project.gns3").decode()
except zipfile.BadZipFile:
raise aiohttp.web.HTTPConflict(text="Can't import topology the file is corrupted or not a GNS3 project (invalid zip)")
raise aiohttp.web.HTTPConflict(text="Cannot import project, not a GNS3 project (invalid zip)")
except KeyError:
raise aiohttp.web.HTTPConflict(text="Cannot import project, project.gns3 file could not be found")
try:
topology = json.loads(project_file)
# We import the project on top of an existing project (snapshots)
if topology["project_id"] == project_id:
project_name = topology["name"]
else:
# If the project name is already used we generate a new one
if name:
project_name = controller.get_free_project_name(name)
else:
project_name = controller.get_free_project_name(topology["name"])
except (ValueError, KeyError):
raise aiohttp.web.HTTPConflict(text="Cannot import project, the project.gns3 file is corrupted")
if location:
path = location
else:
projects_path = controller.projects_directory()
path = os.path.join(projects_path, project_id)
try:
os.makedirs(path, exist_ok=True)
except UnicodeEncodeError:
raise aiohttp.web.HTTPConflict(text="The project name contain non supported or invalid characters")
try:
with zipfile.ZipFile(stream) as zip_file:
yield from wait_run_in_executor(zip_file.extractall, path)
except zipfile.BadZipFile:
raise aiohttp.web.HTTPConflict(text="Cannot extract files from GNS3 project (invalid zip)")
topology = load_topology(os.path.join(path, "project.gns3"))
topology["name"] = project_name
# To avoid unexpected behavior (project start without manual operations just after import)
topology["auto_start"] = False
topology["auto_open"] = False
topology["auto_close"] = True
# Generate a new node id
node_old_to_new = {}
for node in topology["topology"]["nodes"]:
if "node_id" in node:
node_old_to_new[node["node_id"]] = str(uuid.uuid4())
_move_node_file(path, node["node_id"], node_old_to_new[node["node_id"]])
node["node_id"] = node_old_to_new[node["node_id"]]
else:
node["node_id"] = str(uuid.uuid4())
# Update link to use new id
for link in topology["topology"]["links"]:
link["link_id"] = str(uuid.uuid4())
for node in link["nodes"]:
node["node_id"] = node_old_to_new[node["node_id"]]
# Generate new drawings id
for drawing in topology["topology"]["drawings"]:
drawing["drawing_id"] = str(uuid.uuid4())
# Modify the compute id of the node depending of compute capacity
if not keep_compute_id:
# For some VM type we move them to the GNS3 VM if possible
# unless it's a linux host without GNS3 VM
if not sys.platform.startswith("linux") or controller.has_compute("vm"):
for node in topology["topology"]["nodes"]:
if node["node_type"] in ("docker", "qemu", "iou", "nat"):
node["compute_id"] = "vm"
else:
# Round-robin through available compute resources.
compute_nodes = itertools.cycle(controller.computes)
for node in topology["topology"]["nodes"]:
node["compute_id"] = next(compute_nodes)
compute_created = set()
for node in topology["topology"]["nodes"]:
if node["compute_id"] != "local":
# Project created on the remote GNS3 VM?
if node["compute_id"] not in compute_created:
compute = controller.get_compute(node["compute_id"])
yield from compute.post("/projects", data={"name": project_name, "project_id": project_id,})
compute_created.add(node["compute_id"])
yield from _move_files_to_compute(compute, project_id, path, os.path.join("project-files", node["node_type"], node["node_id"]))
# And we dump the updated.gns3
dot_gns3_path = os.path.join(path, project_name + ".gns3")
# We change the project_id to avoid erasing the project
topology["project_id"] = project_id
with open(dot_gns3_path, "w+") as f:
json.dump(topology, f, indent=4)
os.remove(os.path.join(path, "project.gns3"))
if os.path.exists(os.path.join(path, "images")):
_import_images(controller, path)
project = yield from controller.load_project(dot_gns3_path, load=False)
return project
def _move_node_file(path, old_id, new_id):
"""
Move the files from a node when changing his id
Move a file from a node when changing its id
:param path: Path of the project
:param old_id: ID before change
:param new_id: New node UUID
"""
root = os.path.join(path, "project-files")
if os.path.exists(root):
for dirname in os.listdir(root):
@@ -175,8 +179,9 @@ def _move_node_file(path, old_id, new_id):
@asyncio.coroutine
def _move_files_to_compute(compute, project_id, directory, files_path):
"""
Move the files to a remote compute
Move files to a remote compute
"""
location = os.path.join(directory, files_path)
if os.path.exists(location):
for (dirpath, dirnames, filenames) in os.walk(location):
@@ -184,7 +189,7 @@ def _move_files_to_compute(compute, project_id, directory, files_path):
path = os.path.join(dirpath, filename)
dst = os.path.relpath(path, directory)
yield from _upload_file(compute, project_id, path, dst)
shutil.rmtree(os.path.join(directory, files_path))
yield from wait_run_in_executor(shutil.rmtree, os.path.join(directory, files_path))
@asyncio.coroutine
@@ -195,6 +200,7 @@ def _upload_file(compute, project_id, file_path, path):
:param file_path: File path on the controller file system
:param path: File path on the remote system relative to project directory
"""
path = "/projects/{}/files/{}".format(project_id, path.replace("\\", "/"))
with open(file_path, "rb") as f:
yield from compute.http_query("POST", path, f, timeout=None)
@@ -202,11 +208,10 @@ def _upload_file(compute, project_id, file_path, path):
def _import_images(controller, path):
"""
Copy images to the images directory or delete them if they
already exists.
Copy images to the images directory or delete them if they already exists.
"""
image_dir = controller.images_path()
image_dir = controller.images_path()
root = os.path.join(path, "images")
for (dirpath, dirnames, filenames) in os.walk(root):
for filename in filenames:
+1
View File
@@ -414,6 +414,7 @@ class Link:
"""
for node in self._nodes:
if node["node"].node_type in ('vpcs',
'traceng',
'vmware',
'dynamips',
'qemu',
+4 -4
View File
@@ -121,7 +121,7 @@ class Node:
return self.node_type not in (
"qemu", "docker", "dynamips",
"vpcs", "vmware", "virtualbox",
"iou")
"iou", "traceng")
@property
def id(self):
@@ -454,7 +454,7 @@ class Node:
yield from self.delete()
@asyncio.coroutine
def start(self):
def start(self, data=None):
"""
Start a node
"""
@@ -467,7 +467,7 @@ class Node:
raise aiohttp.web.HTTPConflict(text="IOU licence is not configured")
yield from self.post("/start", timeout=240, data={"iourc_content": licence})
else:
yield from self.post("/start", timeout=240)
yield from self.post("/start", data=data, timeout=240)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when starting {}".format(self._name))
@@ -622,7 +622,7 @@ class Node:
for port in self._properties["ports_mapping"]:
self._ports.append(PortFactory(port["name"], 0, 0, port_number, "ethernet", short_name="e{}".format(port_number)))
port_number += 1
elif self._node_type in ("vpcs"):
elif self._node_type in ("vpcs", "traceng"):
self._ports.append(PortFactory("Ethernet0", 0, 0, 0, "ethernet", short_name="e0"))
elif self._node_type in ("cloud", "nat"):
port_number = 0
+92 -27
View File
@@ -37,6 +37,7 @@ from ..config import Config
from ..utils.path import check_path_allowed, get_default_project_directory
from ..utils.asyncio.pool import Pool
from ..utils.asyncio import locked_coroutine
from ..utils.asyncio import wait_run_in_executor
from .export_project import export_project
from .import_project import import_project
@@ -68,7 +69,7 @@ class Project:
def __init__(self, name=None, project_id=None, path=None, controller=None, status="opened",
filename=None, auto_start=False, auto_open=False, auto_close=True,
scene_height=1000, scene_width=2000, zoom=100, show_layers=False, snap_to_grid=False, show_grid=False,
show_interface_labels=False):
grid_size=0, show_interface_labels=False, variables=None, supplier=None):
self._controller = controller
assert name is not None
@@ -83,7 +84,11 @@ class Project:
self._show_layers = show_layers
self._snap_to_grid = snap_to_grid
self._show_grid = show_grid
self._grid_size = grid_size
self._show_interface_labels = show_interface_labels
self._variables = variables
self._supplier = supplier
self._loading = False
# Disallow overwrite of existing project
@@ -132,6 +137,15 @@ class Project:
self.controller.notification.emit("project.updated", self.__json__())
self.dump()
# update on computes
for compute in list(self._project_created_on_compute):
yield from compute.put(
"/projects/{}".format(self._id), {
"variables": self.variables
}
)
def reset(self):
"""
Called when open/close a project. Cleanup internal stuff
@@ -235,6 +249,21 @@ class Project:
"""
self._show_grid = show_grid
@property
def grid_size(self):
"""
Grid size
:return: integer
"""
return self._grid_size
@grid_size.setter
def grid_size(self, grid_size):
"""
Setter for grid size
"""
self._grid_size = grid_size
@property
def show_interface_labels(self):
"""
@@ -250,6 +279,36 @@ class Project:
"""
self._show_interface_labels = show_interface_labels
@property
def variables(self):
"""
Variables applied to the project
:return: list
"""
return self._variables
@variables.setter
def variables(self, variables):
"""
Setter for variables applied to the project
"""
self._variables = variables
@property
def supplier(self):
"""
Supplier of the project
:return: dict
"""
return self._supplier
@supplier.setter
def supplier(self, supplier):
"""
Setter for supplier of the project
"""
self._supplier = supplier
@property
def auto_start(self):
"""
@@ -444,12 +503,14 @@ class Project:
yield from compute.post("/projects", data={
"name": self._name,
"project_id": self._id,
"path": self._path
"path": self._path,
"variables": self._variables
})
else:
yield from compute.post("/projects", data={
"name": self._name,
"project_id": self._id,
"variables": self._variables
})
self._project_created_on_compute.add(compute)
@@ -615,27 +676,10 @@ class Project:
:param name: Name of the snapshot
"""
if name in [snap.name for snap in self.snapshots.values()]:
raise aiohttp.web_exceptions.HTTPConflict(text="The snapshot {} already exist".format(name))
if name in [snap.name for snap in self._snapshots.values()]:
raise aiohttp.web.HTTPConflict(text="The snapshot name {} already exists".format(name))
snapshot = Snapshot(self, name=name)
try:
if os.path.exists(snapshot.path):
raise aiohttp.web_exceptions.HTTPConflict(text="The snapshot {} already exist".format(name))
os.makedirs(os.path.join(self.path, "snapshots"), exist_ok=True)
with tempfile.TemporaryDirectory() as tmpdir:
zipstream = yield from export_project(self, tmpdir, keep_compute_id=True, allow_all_nodes=True)
try:
with open(snapshot.path, "wb") as f:
for data in zipstream:
f.write(data)
except OSError as e:
raise aiohttp.web.HTTPConflict(text="Could not write snapshot file '{}': {}".format(snapshot.path, e))
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e))
yield from snapshot.create()
self._snapshots[snapshot.id] = snapshot
return snapshot
@@ -676,6 +720,15 @@ class Project:
except KeyError:
pass
# don't remove supplier's logo
if self.supplier:
try:
logo = self.supplier['logo']
pictures.remove(logo)
except KeyError:
pass
for pict in pictures:
os.remove(os.path.join(self.pictures_directory, pict))
except OSError as e:
@@ -760,6 +813,7 @@ class Project:
"show_layers",
"snap_to_grid",
"show_grid",
"grid_size",
"show_interface_labels"
]
@@ -840,6 +894,15 @@ class Project:
while self._loading:
yield from asyncio.sleep(0.5)
def _create_duplicate_project_file(self, path, zipstream):
"""
Creates the project file (to be run in its own thread)
"""
with open(path, "wb") as f:
for data in zipstream:
f.write(data)
@asyncio.coroutine
def duplicate(self, name=None, location=None):
"""
@@ -861,10 +924,9 @@ class Project:
try:
with tempfile.TemporaryDirectory() as tmpdir:
zipstream = yield from export_project(self, tmpdir, keep_compute_id=True, allow_all_nodes=True)
with open(os.path.join(tmpdir, "project.gns3p"), "wb") as f:
for data in zipstream:
f.write(data)
with open(os.path.join(tmpdir, "project.gns3p"), "rb") as f:
project_path = os.path.join(tmpdir, "project.gns3p")
yield from wait_run_in_executor(self._create_duplicate_project_file, project_path, zipstream)
with open(project_path, "rb") as f:
project = yield from import_project(self._controller, str(uuid.uuid4()), f, location=location, name=name, keep_compute_id=True)
except (OSError, UnicodeEncodeError) as e:
raise aiohttp.web.HTTPConflict(text="Can not duplicate project: {}".format(str(e)))
@@ -994,7 +1056,10 @@ class Project:
"show_layers": self._show_layers,
"snap_to_grid": self._snap_to_grid,
"show_grid": self._show_grid,
"show_interface_labels": self._show_interface_labels
"grid_size": self._grid_size,
"show_interface_labels": self._show_interface_labels,
"supplier": self._supplier,
"variables": self._variables
}
def __repr__(self):
+42 -6
View File
@@ -19,11 +19,13 @@
import os
import uuid
import shutil
import tempfile
import asyncio
import aiohttp.web
from datetime import datetime, timezone
from ..utils.asyncio import wait_run_in_executor
from .export_project import export_project
from .import_project import import_project
@@ -71,6 +73,37 @@ class Snapshot:
def created_at(self):
return int(self._created_at)
def _create_snapshot_file(self, zipstream):
"""
Creates the snapshot file (to be run in its own thread)
"""
with open(self.path, "wb") as f:
for data in zipstream:
f.write(data)
@asyncio.coroutine
def create(self):
"""
Create the snapshot
"""
if os.path.exists(self.path):
raise aiohttp.web.HTTPConflict(text="The snapshot file '{}' already exists".format(self.name))
snapshot_directory = os.path.join(self._project.path, "snapshots")
try:
os.makedirs(snapshot_directory, exist_ok=True)
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not create the snapshot directory '{}': {}".format(snapshot_directory, e))
try:
with tempfile.TemporaryDirectory() as tmpdir:
zipstream = yield from export_project(self._project, tmpdir, keep_compute_id=True, allow_all_nodes=True)
yield from wait_run_in_executor(self._create_snapshot_file, zipstream)
except OSError as e:
raise aiohttp.web.HTTPConflict(text="Could not create snapshot file '{}': {}".format(self.path, e))
@asyncio.coroutine
def restore(self):
"""
@@ -78,18 +111,21 @@ class Snapshot:
"""
yield from self._project.delete_on_computes()
# We don't send close notif to clients because the close / open dance is purely internal
# We don't send close notification 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__())
try:
if os.path.exists(os.path.join(self._project.path, "project-files")):
shutil.rmtree(os.path.join(self._project.path, "project-files"))
# delete the current project files
project_files_path = os.path.join(self._project.path, "project-files")
if os.path.exists(project_files_path):
yield from wait_run_in_executor(shutil.rmtree, project_files_path)
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
self._project.controller.notification.emit("snapshot.restored", self.__json__())
return self._project
def __json__(self):
return {
+3
View File
@@ -83,7 +83,10 @@ def project_to_topology(project):
"show_layers": project.show_layers,
"snap_to_grid": project.snap_to_grid,
"show_grid": project.show_grid,
"grid_size": project.grid_size,
"show_interface_labels": project.show_interface_labels,
"variables": project.variables,
"supplier": project.supplier,
"topology": {
"nodes": [],
"links": [],
+1 -1
View File
@@ -57,7 +57,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "sync+https://6b6c2ce19b8545278f7ee00c333175a6:be17229ec8da460e9a126d02b82de5dc@sentry.io/38482"
DSN = "sync+https://b0dcf456eaa24e97b46d789ecf927d41:f2541a589b1b492fb79f8bee5b9d799d@sentry.io/38482"
if hasattr(sys, "frozen"):
cacert = get_resource("cacert.pem")
if cacert is not None and os.path.isfile(cacert):
+2 -1
View File
@@ -34,9 +34,10 @@ from .ethernet_hub_handler import EthernetHubHandler
from .ethernet_switch_handler import EthernetSwitchHandler
from .frame_relay_switch_handler import FrameRelaySwitchHandler
from .atm_switch_handler import ATMSwitchHandler
from .traceng_handler import TraceNGHandler
if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1":
# IOU runs only on Linux but test suite works on UNIX platform
# IOU & Docker only runs on Linux but test suite works on UNIX platform
if not sys.platform.startswith("win"):
from .iou_handler import IOUHandler
from .docker_handler import DockerHandler
@@ -60,7 +60,8 @@ class DockerHandler:
console_resolution=request.json.get("console_resolution", "1024x768"),
console_http_port=request.json.get("console_http_port", 80),
console_http_path=request.json.get("console_http_path", "/"),
aux=request.json.get("aux"))
aux=request.json.get("aux"),
extra_hosts=request.json.get("extra_hosts"))
for name, value in request.json.items():
if name != "node_id":
if hasattr(container, name) and getattr(container, name) != value:
@@ -312,7 +313,7 @@ class DockerHandler:
props = [
"name", "console", "aux", "console_type", "console_resolution",
"console_http_port", "console_http_path", "start_command",
"environment", "adapters"
"environment", "adapters", "extra_hosts"
]
changed = False
@@ -73,11 +73,31 @@ class ProjectHandler:
p = pm.create_project(
name=request.json.get("name"),
path=request.json.get("path"),
project_id=request.json.get("project_id")
project_id=request.json.get("project_id"),
variables=request.json.get("variables", None)
)
response.set_status(201)
response.json(p)
@Route.put(
r"/projects/{project_id}",
description="Update the project on the server",
status_codes={
201: "Project updated",
403: "Forbidden to update a project"
},
output=PROJECT_OBJECT_SCHEMA,
input=PROJECT_UPDATE_SCHEMA)
def update_project(request, response):
pm = ProjectManager.instance()
project = pm.get_project(request.match_info["project_id"])
yield from project.update(
variables=request.json.get("variables", None)
)
response.set_status(200)
response.json(project)
@Route.get(
r"/projects/{project_id}",
description="Get project information",
@@ -319,10 +339,13 @@ class ProjectHandler:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'wb+') as f:
while True:
packet = yield from request.content.read(512)
if not packet:
try:
chunk = yield from request.content.read(1024)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when writing to file '{}'".format(path))
if not chunk:
break
f.write(packet)
f.write(chunk)
except FileNotFoundError:
raise aiohttp.web.HTTPNotFound()
@@ -380,10 +403,10 @@ class ProjectHandler:
try:
with tempfile.SpooledTemporaryFile(max_size=10000) as temp:
while True:
packet = yield from request.content.read(512)
if not packet:
chunk = yield from request.content.read(1024)
if not chunk:
break
temp.write(packet)
temp.write(chunk)
project.import_zip(temp, gns3vm=bool(int(request.GET.get("gns3vm", "1"))))
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not import the project: {}".format(e))
@@ -0,0 +1,341 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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 <http://www.gnu.org/licenses/>.
import os
from aiohttp.web import HTTPConflict
from gns3server.web.route import Route
from gns3server.schemas.nio import NIO_SCHEMA
from gns3server.schemas.node import NODE_CAPTURE_SCHEMA
from gns3server.compute.traceng import TraceNG
from gns3server.schemas.traceng import (
TRACENG_CREATE_SCHEMA,
TRACENG_UPDATE_SCHEMA,
TRACENG_START_SCHEMA,
TRACENG_OBJECT_SCHEMA
)
class TraceNGHandler:
"""
API entry points for TraceNG.
"""
@Route.post(
r"/projects/{project_id}/traceng/nodes",
parameters={
"project_id": "Project UUID"
},
status_codes={
201: "Instance created",
400: "Invalid request",
409: "Conflict"
},
description="Create a new TraceNG instance",
input=TRACENG_CREATE_SCHEMA,
output=TRACENG_OBJECT_SCHEMA)
def create(request, response):
traceng = TraceNG.instance()
vm = yield from traceng.create_node(request.json["name"],
request.match_info["project_id"],
request.json.get("node_id"),
console=request.json.get("console"))
vm.ip_address = request.json.get("ip_address", "")
vm.default_destination = request.json.get("default_destination", "")
response.set_status(201)
response.json(vm)
@Route.get(
r"/projects/{project_id}/traceng/nodes/{node_id}",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
200: "Success",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Get a TraceNG instance",
output=TRACENG_OBJECT_SCHEMA)
def show(request, response):
traceng_manager = TraceNG.instance()
vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
response.json(vm)
@Route.put(
r"/projects/{project_id}/traceng/nodes/{node_id}",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
200: "Instance updated",
400: "Invalid request",
404: "Instance doesn't exist",
409: "Conflict"
},
description="Update a TraceNG instance",
input=TRACENG_UPDATE_SCHEMA,
output=TRACENG_OBJECT_SCHEMA)
def update(request, response):
traceng_manager = TraceNG.instance()
vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
vm.name = request.json.get("name", vm.name)
vm.ip_address = request.json.get("ip_address", vm.ip_address)
vm.default_destination = request.json.get("default_destination", vm.default_destination)
vm.updated()
response.json(vm)
@Route.delete(
r"/projects/{project_id}/traceng/nodes/{node_id}",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
204: "Instance deleted",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Delete a TraceNG instance")
def delete(request, response):
yield from TraceNG.instance().delete_node(request.match_info["node_id"])
response.set_status(204)
@Route.post(
r"/projects/{project_id}/traceng/nodes/{node_id}/duplicate",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
201: "Instance duplicated",
404: "Instance doesn't exist"
},
description="Duplicate a TraceNG instance")
def duplicate(request, response):
new_node = yield from TraceNG.instance().duplicate_node(
request.match_info["node_id"],
request.json["destination_node_id"]
)
response.set_status(201)
response.json(new_node)
@Route.post(
r"/projects/{project_id}/traceng/nodes/{node_id}/start",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
204: "Instance started",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Start a TraceNG instance",
input=TRACENG_START_SCHEMA,
output=TRACENG_OBJECT_SCHEMA)
def start(request, response):
traceng_manager = TraceNG.instance()
vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
yield from vm.start(request.get("destination"))
response.json(vm)
@Route.post(
r"/projects/{project_id}/traceng/nodes/{node_id}/stop",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
204: "Instance stopped",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Stop a TraceNG instance")
def stop(request, response):
traceng_manager = TraceNG.instance()
vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
yield from vm.stop()
response.set_status(204)
@Route.post(
r"/projects/{project_id}/traceng/nodes/{node_id}/suspend",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID"
},
status_codes={
204: "Instance suspended",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Suspend a TraceNG instance (does nothing)")
def suspend(request, response):
traceng_manager = TraceNG.instance()
traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
response.set_status(204)
@Route.post(
r"/projects/{project_id}/traceng/nodes/{node_id}/reload",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID",
},
status_codes={
204: "Instance reloaded",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Reload a TraceNG instance")
def reload(request, response):
traceng_manager = TraceNG.instance()
vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
yield from vm.reload()
response.set_status(204)
@Route.post(
r"/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID",
"adapter_number": "Network adapter where the nio is located",
"port_number": "Port where the nio should be added"
},
status_codes={
201: "NIO created",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Add a NIO to a TraceNG instance",
input=NIO_SCHEMA,
output=NIO_SCHEMA)
def create_nio(request, response):
traceng_manager = TraceNG.instance()
vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
nio_type = request.json["type"]
if nio_type not in ("nio_udp"):
raise HTTPConflict(text="NIO of type {} is not supported".format(nio_type))
nio = traceng_manager.create_nio(request.json)
yield from vm.port_add_nio_binding(int(request.match_info["port_number"]), nio)
response.set_status(201)
response.json(nio)
@Route.put(
r"/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID",
"adapter_number": "Network adapter where the nio is located",
"port_number": "Port from where the nio should be updated"
},
status_codes={
201: "NIO updated",
400: "Invalid request",
404: "Instance doesn't exist"
},
input=NIO_SCHEMA,
output=NIO_SCHEMA,
description="Update a NIO from a TraceNG instance")
def update_nio(request, response):
traceng_manager = TraceNG.instance()
vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
nio = vm.ethernet_adapter.get_nio(int(request.match_info["port_number"]))
if "filters" in request.json and nio:
nio.filters = request.json["filters"]
yield from vm.port_update_nio_binding(int(request.match_info["port_number"]), nio)
response.set_status(201)
response.json(request.json)
@Route.delete(
r"/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID",
"adapter_number": "Network adapter where the nio is located",
"port_number": "Port from where the nio should be removed"
},
status_codes={
204: "NIO deleted",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Remove a NIO from a TraceNG instance")
def delete_nio(request, response):
traceng_manager = TraceNG.instance()
vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
yield from vm.port_remove_nio_binding(int(request.match_info["port_number"]))
response.set_status(204)
@Route.post(
r"/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID",
"adapter_number": "Adapter to start a packet capture",
"port_number": "Port on the adapter"
},
status_codes={
200: "Capture started",
400: "Invalid request",
404: "Instance doesn't exist",
},
description="Start a packet capture on a TraceNG instance",
input=NODE_CAPTURE_SCHEMA)
def start_capture(request, response):
traceng_manager = TraceNG.instance()
vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
port_number = int(request.match_info["port_number"])
pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"])
yield from vm.start_capture(port_number, pcap_file_path)
response.json({"pcap_file_path": pcap_file_path})
@Route.post(
r"/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture",
parameters={
"project_id": "Project UUID",
"node_id": "Node UUID",
"adapter_number": "Adapter to stop a packet capture",
"port_number": "Port on the adapter"
},
status_codes={
204: "Capture stopped",
400: "Invalid request",
404: "Instance doesn't exist",
},
description="Stop a packet capture on a TraceNG instance")
def stop_capture(request, response):
traceng_manager = TraceNG.instance()
vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
port_number = int(request.match_info["port_number"])
yield from vm.stop_capture(port_number)
response.set_status(204)
@@ -73,7 +73,7 @@ class LinkHandler:
node.get("adapter_number", 0),
node.get("port_number", 0),
label=node.get("label"))
except aiohttp.web_exceptions.HTTPException as e:
except aiohttp.web.HTTPException as e:
yield from project.delete_link(link.id)
raise e
response.set_status(201)
@@ -223,7 +223,7 @@ class NodeHandler:
project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
yield from node.start()
yield from node.start(data=request.json)
response.json(node)
response.set_status(201)
@@ -409,8 +409,6 @@ class NodeHandler:
node_type = node.node_type
path = "/project-files/{}/{}/{}".format(node_type, node.id, path)
data = yield from request.content.read()
data = yield from request.content.read() #FIXME: are we handling timeout or large files correctly?
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)
@@ -335,10 +335,10 @@ class ProjectHandler:
try:
with tempfile.SpooledTemporaryFile(max_size=10000) as temp:
while True:
packet = yield from request.content.read(512)
if not packet:
chunk = yield from request.content.read(1024)
if not chunk:
break
temp.write(packet)
temp.write(chunk)
project = yield from import_project(controller, request.match_info["project_id"], temp, location=path, name=name)
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not import the project: {}".format(e))
@@ -448,10 +448,13 @@ class ProjectHandler:
try:
with open(path, 'wb+') as f:
while True:
packet = yield from request.content.read(512)
if not packet:
try:
chunk = yield from request.content.read(1024)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when writing to file '{}'".format(path))
if not chunk:
break
f.write(packet)
f.write(chunk)
except FileNotFoundError:
raise aiohttp.web.HTTPNotFound()
except PermissionError:
@@ -133,7 +133,7 @@ class ServerHandler:
@Route.post(
r"/debug",
description="Dump debug informations to disk (debug directory in config directory). Work only for local server",
description="Dump debug information to disk (debug directory in config directory). Work only for local server",
status_codes={
201: "Writed"
})
@@ -17,6 +17,8 @@
import os
import aiohttp
import asyncio
from gns3server.web.route import Route
from gns3server.controller import Controller
@@ -66,10 +68,13 @@ class SymbolHandler:
try:
with open(path, 'wb') as f:
while True:
packet = yield from request.content.read(512)
if not packet:
try:
chunk = yield from request.content.read(1024)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when writing to symbol '{}'".format(path))
if not chunk:
break
f.write(packet)
f.write(chunk)
except OSError as e:
raise aiohttp.web.HTTPConflict(text="Could not write symbol file '{}': {}".format(path, e))
# Reset the symbol list
+1 -1
View File
@@ -183,7 +183,7 @@ def kill_ghosts():
"""
Kill process from previous GNS3 session
"""
detect_process = ["vpcs", "ubridge", "dynamips"]
detect_process = ["vpcs", "traceng", "ubridge", "dynamips"]
for proc in psutil.process_iter():
try:
name = proc.name().lower().split(".")[0]
+10
View File
@@ -87,6 +87,11 @@ DOCKER_CREATE_SCHEMA = {
"type": ["string", "null"],
"minLength": 0,
},
"extra_hosts": {
"description": "Docker extra hosts (added to /etc/hosts)",
"type": ["string", "null"],
"minLength": 0,
},
"container_id": {
"description": "Docker container ID Read only",
"type": "string",
@@ -184,6 +189,11 @@ DOCKER_OBJECT_SCHEMA = {
"type": ["string", "null"],
"minLength": 0,
},
"extra_hosts": {
"description": "Docker extra hosts (added to /etc/hosts)",
"type": ["string", "null"],
"minLength": 0,
},
"node_directory": {
"description": "Path to the node working directory Read only",
"type": "string"
+2 -1
View File
@@ -30,6 +30,7 @@ NODE_TYPE_SCHEMA = {
"docker",
"dynamips",
"vpcs",
"traceng",
"virtualbox",
"vmware",
"iou",
@@ -144,7 +145,7 @@ NODE_OBJECT_SCHEMA = {
},
"console_type": {
"description": "Console type",
"enum": ["vnc", "telnet", "http", "https", "spice", None]
"enum": ["vnc", "telnet", "http", "https", "spice", "none", None]
},
"properties": {
"description": "Properties specific to an emulator",
+55 -3
View File
@@ -15,6 +15,40 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
SUPPLIER_OBJECT_SCHEMA = {
"type": ["object", "null"],
"description": "Supplier of the project",
"properties": {
"logo": {
"type": "string",
"description": "Path to the project supplier logo"
},
"url": {
"type": "string",
"description": "URL to the project supplier site"
}
}
}
VARIABLES_OBJECT_SCHEMA = {
"type": ["array", "null"],
"description": "Variables required to run the project",
"items": {
"properties": {
"name": {
"type": "string",
"description": "Variable name"
},
"value": {
"type": "string",
"description": "Variable value"
}
},
"required": ["name"]
}
}
PROJECT_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
@@ -66,10 +100,16 @@ PROJECT_CREATE_SCHEMA = {
"type": "boolean",
"description": "Show the grid on the drawing area"
},
"grid_size": {
"type": "integer",
"description": "Grid size for the drawing area"
},
"show_interface_labels": {
"type": "boolean",
"description": "Show interface labels on the drawing area"
}
},
"supplier": SUPPLIER_OBJECT_SCHEMA,
"variables": VARIABLES_OBJECT_SCHEMA
},
"additionalProperties": False,
"required": ["name"]
@@ -125,10 +165,16 @@ PROJECT_UPDATE_SCHEMA = {
"type": "boolean",
"description": "Show the grid on the drawing area"
},
"grid_size": {
"type": "integer",
"description": "Grid size for the drawing area"
},
"show_interface_labels": {
"type": "boolean",
"description": "Show interface labels on the drawing area"
}
},
"supplier": SUPPLIER_OBJECT_SCHEMA,
"variables": VARIABLES_OBJECT_SCHEMA
},
"additionalProperties": False,
}
@@ -200,10 +246,16 @@ PROJECT_OBJECT_SCHEMA = {
"type": "boolean",
"description": "Show the grid on the drawing area"
},
"grid_size": {
"type": "integer",
"description": "Grid size for the drawing area"
},
"show_interface_labels": {
"type": "boolean",
"description": "Show interface labels on the drawing area"
}
},
"supplier": SUPPLIER_OBJECT_SCHEMA,
"variables": VARIABLES_OBJECT_SCHEMA
},
"additionalProperties": False,
"required": ["project_id"]
+8
View File
@@ -23,6 +23,8 @@ from gns3server.schemas.compute import COMPUTE_OBJECT_SCHEMA
from gns3server.schemas.drawing import DRAWING_OBJECT_SCHEMA
from gns3server.schemas.link import LINK_OBJECT_SCHEMA
from gns3server.schemas.node import NODE_OBJECT_SCHEMA
from gns3server.schemas.project import VARIABLES_OBJECT_SCHEMA
from gns3server.schemas.project import SUPPLIER_OBJECT_SCHEMA
TOPOLOGY_SCHEMA = {
@@ -89,10 +91,16 @@ TOPOLOGY_SCHEMA = {
"type": "boolean",
"description": "Show the grid on the drawing area"
},
"grid_size": {
"type": "integer",
"description": "Grid size for the drawing area"
},
"show_interface_labels": {
"type": "boolean",
"description": "Show interface labels on the drawing area"
},
"supplier": SUPPLIER_OBJECT_SCHEMA,
"variables": VARIABLES_OBJECT_SCHEMA,
"topology": {
"description": "The topology content",
"type": "object",
+162
View File
@@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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 <http://www.gnu.org/licenses/>.
TRACENG_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to create a new TraceNG instance",
"type": "object",
"properties": {
"name": {
"description": "TraceNG VM name",
"type": "string",
"minLength": 1,
},
"node_id": {
"description": "Node UUID",
"oneOf": [
{"type": "string",
"minLength": 36,
"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}$"}
]
},
"console": {
"description": "Console TCP port",
"minimum": 1,
"maximum": 65535,
"type": ["integer", "null"]
},
"console_type": {
"description": "Console type",
"enum": ["none"]
},
"ip_address": {
"description": "Source IP address for tracing",
"type": ["string"]
},
"default_destination": {
"description": "Default destination IP address or hostname for tracing",
"type": ["string"]
}
},
"additionalProperties": False,
"required": ["name"]
}
TRACENG_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update a TraceNG instance",
"type": "object",
"properties": {
"name": {
"description": "TraceNG VM name",
"type": ["string", "null"],
"minLength": 1,
},
"console": {
"description": "Console TCP port",
"minimum": 1,
"maximum": 65535,
"type": ["integer", "null"]
},
"console_type": {
"description": "Console type",
"enum": ["none"]
},
"ip_address": {
"description": "Source IP address for tracing",
"type": ["string"]
},
"default_destination": {
"description": "Default destination IP address or hostname for tracing",
"type": ["string"]
}
},
"additionalProperties": False,
}
TRACENG_START_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a TraceNG instance",
"type": "object",
"properties": {
"destination": {
"description": "Host or IP address to trace",
"type": ["string"]
}
},
}
TRACENG_OBJECT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "TraceNG instance",
"type": "object",
"properties": {
"name": {
"description": "TraceNG VM name",
"type": "string",
"minLength": 1,
},
"node_id": {
"description": "Node UUID",
"type": "string",
"minLength": 36,
"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}$"
},
"node_directory": {
"description": "Path to the VM working directory",
"type": "string"
},
"status": {
"description": "VM status",
"enum": ["started", "stopped", "suspended"]
},
"console": {
"description": "Console TCP port",
"minimum": 1,
"maximum": 65535,
"type": ["integer", "null"]
},
"console_type": {
"description": "Console type",
"enum": ["none"]
},
"project_id": {
"description": "Project UUID",
"type": "string",
"minLength": 36,
"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}$"
},
"command_line": {
"description": "Last command line used by GNS3 to start TraceNG",
"type": "string"
},
"ip_address": {
"description": "Source IP address for tracing",
"type": ["string"]
},
"default_destination": {
"description": "Default destination IP address or hostname for tracing",
"type": ["string"]
}
},
"additionalProperties": False,
"required": ["name", "node_id", "status", "console", "console_type", "project_id", "command_line", "ip_address", "default_destination"]
}
+610
View File
@@ -0,0 +1,610 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="59.253815"
id="Andysvg"
inkscape:version="0.91 r13725"
sodipodi:docname="traceng.svg"
sodipodi:version="0.32"
version="1.0"
width="65.414536"
x="0.00000000"
y="0.00000000"
inkscape:output_extension="org.inkscape.output.svg.inkscape">
<metadata
id="metadata3">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:title />
<dc:description />
<dc:subject>
<rdf:Bag>
<rdf:li>hash</rdf:li>
<rdf:li />
<rdf:li>hardware</rdf:li>
<rdf:li>computer</rdf:li>
</rdf:Bag>
</dc:subject>
<dc:publisher>
<cc:Agent
rdf:about="http://www.openclipart.org">
<dc:title>Andy Fitzsimon</dc:title>
</cc:Agent>
</dc:publisher>
<dc:creator>
<cc:Agent>
<dc:title>Andy Fitzsimon</dc:title>
</cc:Agent>
</dc:creator>
<dc:rights>
<cc:Agent>
<dc:title>Andy Fitzsimon</dc:title>
</cc:Agent>
</dc:rights>
<dc:date />
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://web.resource.org/cc/PublicDomain" />
<dc:language>en</dc:language>
</cc:Work>
<cc:License
rdf:about="http://web.resource.org/cc/PublicDomain">
<cc:permits
rdf:resource="http://web.resource.org/cc/Reproduction" />
<cc:permits
rdf:resource="http://web.resource.org/cc/Distribution" />
<cc:permits
rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
</cc:License>
</rdf:RDF>
</metadata>
<defs
id="defs3">
<linearGradient
id="linearGradient1806">
<stop
id="stop1807"
offset="0.0000000"
style="stop-color:#000000;stop-opacity:0.35051546;" />
<stop
id="stop3276"
offset="0.64999998"
style="stop-color:#000000;stop-opacity:0.13402061;" />
<stop
id="stop1808"
offset="1.0000000"
style="stop-color:#000000;stop-opacity:0.0000000;" />
</linearGradient>
<radialGradient
cx="42.007256"
cy="39.007645"
fx="42.280807"
fy="39.410465"
id="radialGradient1977"
r="11.574221"
xlink:href="#linearGradient1806"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient893">
<stop
id="stop895"
offset="0"
style="stop-color:#000;stop-opacity:1;" />
<stop
id="stop896"
offset="1"
style="stop-color:#fff;stop-opacity:1;" />
</linearGradient>
<linearGradient
id="linearGradient1317">
<stop
id="stop1318"
offset="0.00000000"
style="stop-color:#000000;stop-opacity:0.52892560;" />
<stop
id="stop1320"
offset="0.50000000"
style="stop-color:#000000;stop-opacity:0.17355372;" />
<stop
id="stop1319"
offset="1.0000000"
style="stop-color:#000000;stop-opacity:0.00000000;" />
</linearGradient>
<linearGradient
id="linearGradient1133">
<stop
id="stop1134"
offset="0.00000000"
style="stop-color:#8bb7df;stop-opacity:1.0000000;" />
<stop
id="stop1136"
offset="0.76209301"
style="stop-color:#2a6092;stop-opacity:1.0000000;" />
<stop
id="stop1135"
offset="1.0000000"
style="stop-color:#375e82;stop-opacity:1.0000000;" />
</linearGradient>
<linearGradient
id="linearGradient1098">
<stop
id="stop1099"
offset="0.00000000"
style="stop-color:#ffffff;stop-opacity:1.0000000;" />
<stop
id="stop1101"
offset="0.50000000"
style="stop-color:#ffffff;stop-opacity:0.22314049;" />
<stop
id="stop1102"
offset="0.59930235"
style="stop-color:#ffffff;stop-opacity:0.00000000;" />
<stop
id="stop1100"
offset="1.0000000"
style="stop-color:#ffffff;stop-opacity:0.60330576;" />
</linearGradient>
<linearGradient
id="linearGradient902">
<stop
id="stop903"
offset="0.00000000"
style="stop-color:#000000;stop-opacity:0.00000000;" />
<stop
id="stop904"
offset="1.0000000"
style="stop-color:#000000;stop-opacity:0.22000000;" />
</linearGradient>
<linearGradient
id="linearGradient892">
<stop
id="stop893"
offset="0.00000000"
style="stop-color:#ffffff;stop-opacity:0.00000000;" />
<stop
id="stop894"
offset="1"
style="stop-color:#fff;stop-opacity:1;" />
</linearGradient>
<linearGradient
id="linearGradient888">
<stop
id="stop889"
offset="0.0000000"
style="stop-color:#626262;stop-opacity:1.0000000;" />
<stop
id="stop890"
offset="1"
style="stop-color:#fff;stop-opacity:1;" />
</linearGradient>
<linearGradient
id="linearGradient891"
x1="92.437965"
x2="27.674332"
xlink:href="#linearGradient888"
y1="-3.9104078"
y2="91.076988"
gradientTransform="matrix(0.8184166,0,0,0.530237,-9.8827678,-14.589608)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient901"
xlink:href="#linearGradient1806"
gradientTransform="matrix(0.8517959,0,0,0.5754549,70.875335,-17.351492)"
x1="9.4921856"
y1="22.862282"
x2="41.719688"
y2="22.862282"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient905"
x1="-77.726181"
x2="95.64444"
xlink:href="#linearGradient888"
y1="208.43991"
y2="11.699047"
gradientTransform="matrix(0.7314635,0,0,0.5932693,-9.8827694,-14.589616)"
gradientUnits="userSpaceOnUse" />
<radialGradient
cx="60.004654"
cy="56.485935"
fx="72.10788"
fy="39.288475"
id="radialGradient1132"
r="68.589226"
xlink:href="#linearGradient1133"
gradientTransform="matrix(0.8184166,0,0,0.530237,-9.8827678,-14.589608)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient1138"
x1="-249.72067"
x2="-268.25406"
xlink:href="#linearGradient1806"
y1="375.922"
y2="75.912529"
gradientTransform="scale(1.087146,0.91984)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient1140"
x1="45.685757"
x2="41.96706"
xlink:href="#linearGradient888"
y1="110.4447"
y2="232.24953"
gradientTransform="matrix(1.2743811,0,0,0.3405213,-9.8827694,-14.589616)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient1141"
x1="0"
x2="0.92957747"
xlink:href="#linearGradient888"
y1="3.3012049"
y2="-0.45783132" />
<linearGradient
id="linearGradient1144"
x1="31.449743"
x2="31.617281"
xlink:href="#linearGradient892"
y1="203.49899"
y2="251.21892"
gradientTransform="matrix(1.4044089,0,0,0.3089952,-9.8827694,-14.589616)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient1146"
x1="39.810947"
x2="17.87653"
xlink:href="#linearGradient892"
y1="90.197021"
y2="113.71949"
gradientTransform="matrix(0.8811179,0,0,0.4925045,-9.8827694,-14.589616)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient1148"
x1="39.690613"
x2="70.224304"
xlink:href="#linearGradient892"
y1="49.507656"
y2="20.481863"
gradientTransform="scale(1.329144,0.752364)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient1150"
x1="35.190361"
x2="8.3460579"
xlink:href="#linearGradient892"
y1="76.277557"
y2="105.42543"
gradientTransform="scale(1.328386,0.752793)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient1156"
x1="25.220816"
x2="25.220816"
xlink:href="#linearGradient888"
y1="178.48862"
y2="234.26866"
gradientTransform="matrix(1.6156202,0,0,0.2685999,-9.8827678,-14.589608)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient1157"
x1="51.46093"
x2="-16.224497"
xlink:href="#linearGradient888"
y1="269.85831"
y2="176.28694"
gradientTransform="matrix(1.6156202,0,0,0.2685999,-9.8827678,-14.589608)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient1166"
x1="111.49758"
x2="107.04918"
xlink:href="#linearGradient1317"
y1="131.25249"
y2="148.78619"
gradientTransform="matrix(0.562951,0,0,0.378005,1.021178,-9.426161)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient1167"
x1="141.60217"
x2="88.447014"
xlink:href="#linearGradient888"
y1="228.39311"
y2="133.5471"
gradientTransform="matrix(0.544555,0,0,0.390775,1.021178,-9.426161)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient1169"
x1="146.69923"
x2="74.533691"
xlink:href="#linearGradient893"
y1="224.57898"
y2="81.4776"
gradientTransform="matrix(0.546024,0,0,0.389723,1.021178,-9.426161)"
gradientUnits="userSpaceOnUse" />
<linearGradient
gradientTransform="scale(0.998371,1.001632)"
id="linearGradient1170"
x1="0.47284532"
x2="0.48655096"
xlink:href="#linearGradient902"
y1="-0.016295359"
y2="1.8378206" />
<linearGradient
id="linearGradient1171"
x1="101.10657"
x2="95.100159"
xlink:href="#linearGradient902"
y1="177.77768"
y2="173.03152"
gradientTransform="matrix(0.62565,0,0,0.340123,1.021178,-9.426161)"
gradientUnits="userSpaceOnUse" />
<radialGradient
cx="0.47887325"
cy="0.53333336"
fx="0.47535211"
fy="0.26666668"
id="radialGradient1315"
r="0.41197181"
xlink:href="#linearGradient1317" />
<radialGradient
cx="0.5"
cy="0.50000006"
fx="0.50352114"
fy="0.18269235"
id="radialGradient1316"
r="0.34964636"
xlink:href="#linearGradient1317" />
<linearGradient
id="linearGradient1404"
x1="88.755692"
x2="88.996956"
xlink:href="#linearGradient892"
y1="169.09755"
y2="182.99155"
gradientTransform="matrix(0.629979,0,0,0.337786,1.021178,-9.426161)"
gradientUnits="userSpaceOnUse" />
<linearGradient
gradientTransform="scale(0.997825,1.00218)"
id="linearGradient1505"
x1="0.47157744"
x2="0.48548824"
xlink:href="#linearGradient902"
y1="-0.02485317"
y2="1.8570156" />
<linearGradient
gradientTransform="scale(0.995847,1.00417)"
id="linearGradient1506"
x1="0.4704251"
x2="0.48481107"
xlink:href="#linearGradient902"
y1="-0.04365262"
y2="1.9025002" />
<linearGradient
gradientTransform="scale(0.997153,1.002855)"
id="linearGradient2740"
x1="0.47041038"
x2="0.48453596"
xlink:href="#linearGradient902"
y1="-0.033741195"
y2="1.8771822" />
<linearGradient
id="linearGradient4283"
x1="-0.77314812"
x2="0.99074072"
xlink:href="#linearGradient893"
y1="2.0837989"
y2="-0.033519555" />
<linearGradient
id="linearGradient4284"
x1="-2.3960868e-17"
x2="0.92957747"
xlink:href="#linearGradient893"
y1="3.3012049"
y2="-0.45783132" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1806"
id="linearGradient1948"
gradientUnits="userSpaceOnUse"
gradientTransform="scale(1.087146,0.91984)"
x1="-249.72067"
y1="375.922"
x2="-268.25406"
y2="75.912529" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1806"
id="linearGradient1950"
gradientUnits="userSpaceOnUse"
gradientTransform="scale(1.087146,0.91984)"
x1="-249.72067"
y1="375.922"
x2="-268.25406"
y2="75.912529" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1806"
id="linearGradient1952"
gradientUnits="userSpaceOnUse"
gradientTransform="scale(1.087146,0.91984)"
x1="-249.72067"
y1="375.922"
x2="-268.25406"
y2="75.912529" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient892"
id="linearGradient2625"
gradientUnits="userSpaceOnUse"
gradientTransform="scale(1.329144,0.752364)"
x1="39.690613"
y1="49.507656"
x2="70.224304"
y2="20.481863" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient892"
id="linearGradient2627"
gradientUnits="userSpaceOnUse"
gradientTransform="scale(1.328386,0.752793)"
x1="35.190361"
y1="76.277557"
x2="8.3460579"
y2="105.42543" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient888"
id="linearGradient2633"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.3870648,0,0,0.2694829,71.57581,-24.087318)"
x1="92.437965"
y1="-3.9104078"
x2="27.674332"
y2="91.076988" />
</defs>
<sodipodi:namedview
bordercolor="#666666"
borderopacity="1.0"
id="base"
inkscape:cx="26.459162"
inkscape:cy="28.090626"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:window-height="1405"
inkscape:window-width="2280"
inkscape:window-x="696"
inkscape:window-y="149"
inkscape:zoom="16"
pagecolor="#ffffff"
showborder="true"
inkscape:current-layer="Andysvg"
showgrid="false"
inkscape:window-maximized="0" />
<rect
height="5.4088969"
id="rect1155"
style="fill:url(#linearGradient1156);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient1157);stroke-width:0.95344429pt"
width="38.405891"
x="12.350429"
y="36.575089" />
<path
d="M 0.19287398,54.885213 C -0.67386281,56.46268 1.5579198,59.253939 3.3933636,59.253939 L 62.069027,59.253939 C 63.628857,59.253939 65.976201,57.281205 65.269522,55.925378 L 58.655165,43.235277 C 58.145696,42.257819 57.005954,41.779036 55.881408,41.779036 L 8.9408812,41.779036 C 8.0135624,41.779036 7.0317541,42.438291 6.5938539,43.235277 L 0.19287398,54.885213 z "
id="path1139"
sodipodi:nodetypes="czzzzzzzz"
style="fill:url(#linearGradient1140);fill-opacity:1;fill-rule:evenodd;stroke-width:1.44734821pt" />
<rect
height="39.110481"
id="rect1137"
rx="3.6272225"
ry="3.536587"
style="fill:url(#linearGradient905);fill-opacity:1;fill-rule:evenodd;stroke-width:1.62826681"
width="48.220726"
x="7.4732022"
y="-0.010164791" />
<rect
height="24.74659"
id="rect1131"
style="fill:#ff9955;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient891);stroke-width:0.96503658pt"
width="38.813015"
x="12.390426"
y="6.027596" />
<path
d="M 8.5906358,19.782157 L 8.3772707,34.032521 C 8.3432557,36.304242 9.6712558,37.75887 12.004493,37.777143 L 38.568566,37.985175 L 41.128957,32.160207 L 11.471076,31.952177 L 11.257713,19.678141 L 8.5906358,19.782157 z "
id="path1145"
sodipodi:nodetypes="czzccccc"
style="fill:url(#linearGradient1146);fill-opacity:1;fill-rule:evenodd;stroke-width:1pt" />
<path
d="M 2.7205883,54.675124 C 2.1113822,55.866786 2.9089235,57.975349 4.9701318,57.975349 L 60.179981,57.975349 C 61.276346,57.975349 62.926231,56.48511 62.429524,55.460899 L 57.78047,45.874547 C 57.422369,45.136152 56.621274,44.774474 55.830865,44.774474 L 8.8693353,44.774468 C 8.2175462,44.774468 7.5274604,45.272486 7.2196723,45.874547 L 2.7205883,54.675124 z "
id="path1143"
sodipodi:nodetypes="czzzzzzzz"
style="fill:url(#linearGradient1144);fill-opacity:1;fill-rule:evenodd;stroke-width:1pt" />
<ellipse
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path4212"
cx="21.5625"
cy="11.253814"
rx="3.3125"
ry="3.375" />
<ellipse
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path4212-3"
cx="41.1875"
cy="11.128816"
rx="3.3125"
ry="3.375" />
<ellipse
style="opacity:1;fill:#ff9955;fill-opacity:1;stroke:#ffffff;stroke-width:1.85415959;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path4212-3-6"
cx="30.46245"
cy="25.978863"
rx="2.3854203"
ry="2.4479203" />
<path
inkscape:connector-curvature="0"
style="fill:#ffffff;stroke:#ffffff;stroke-width:0.38147008;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 37.747841,25.720332 c 2.423268,-1.218576 3.927625,-3.912271 3.812948,-7.621135 l 1.642658,1.638437 1.08515,-1.049406 -3.462731,-3.556066 -3.963528,3.384577 1.033228,0.870639 1.703184,-1.46973 c 0.452288,3.542567 -0.666814,5.13406 -2.97277,6.29367 -1.596753,0.802958 -2.928888,1.433178 -4.2106,-0.23115 l -0.510936,0.914481 c 0.714057,0.927235 0.370391,1.816393 1.288764,2.104435 0.947222,0.297085 3.631209,-0.814386 4.554633,-1.278752 z"
id="path3"
sodipodi:nodetypes="scccccccsccss" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g5" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g7" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g9" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g11" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g13" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g15" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g17" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g19" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g21" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g23" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g25" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g27" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g29" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g31" />
<g
transform="matrix(1.3039536,0,0,1.3039536,-225.75033,-238.74618)"
id="g33" />
<path
inkscape:connector-curvature="0"
style="fill:#ffffff;stroke:#ffffff;stroke-width:0.38147008;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 27.747543,11.203969 c 2.374543,1.310993 3.774049,4.060623 3.516628,7.762319 l 1.704535,-1.573962 1.043932,1.090417 -3.597106,3.420077 -3.830247,-3.534702 1.06599,-0.830203 1.645321,1.534229 c 0.588378,-3.522521 -0.468606,-5.15593 -2.728194,-6.403483 -1.564647,-0.863854 -2.871524,-1.544907 -4.216378,0.06883 l -0.475341,-0.933479 c 0.749236,-0.899048 0.440066,-1.800781 1.368851,-2.053243 0.95796,-0.2603868 3.597153,0.953621 4.502009,1.453203 z"
id="path3-7"
sodipodi:nodetypes="scccccccsccss" />
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

+1
View File
@@ -62,6 +62,7 @@ class Hypervisor(UBridgeHypervisor):
af, socktype, proto, _, sa = res
# let the OS find an unused port for the uBridge hypervisor
with socket.socket(af, socktype, proto) as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(sa)
port = sock.getsockname()[1]
break
+8 -8
View File
@@ -208,8 +208,8 @@ class UBridgeHypervisor:
self._writer.write(command.encode())
yield from self._writer.drain()
except OSError as e:
raise UbridgeError("Lost communication with {host}:{port} :{error}, Dynamips process running: {run}"
.format(host=self._host, port=self._port, error=e, run=self.is_running()))
raise UbridgeError("Lost communication with {host}:{port} when sending command '{command}': {error}, uBridge process running: {run}"
.format(host=self._host, port=self._port, command=command, error=e, run=self.is_running()))
# Now retrieve the result
data = []
@@ -232,8 +232,8 @@ class UBridgeHypervisor:
continue
if not chunk:
if retries > max_retries:
raise UbridgeError("No data returned from {host}:{port}, uBridge process running: {run}"
.format(host=self._host, port=self._port, run=self.is_running()))
raise UbridgeError("No data returned from {host}:{port} after sending command '{command}', uBridge process running: {run}"
.format(host=self._host, port=self._port, command=command, run=self.is_running()))
else:
retries += 1
yield from asyncio.sleep(0.1)
@@ -241,16 +241,16 @@ class UBridgeHypervisor:
retries = 0
buf += chunk.decode("utf-8")
except OSError as e:
raise UbridgeError("Lost communication with {host}:{port} :{error}, uBridge process running: {run}"
.format(host=self._host, port=self._port, error=e, run=self.is_running()))
raise UbridgeError("Lost communication with {host}:{port} after sending command '{command}': {error}, uBridge process running: {run}"
.format(host=self._host, port=self._port, command=command, error=e, run=self.is_running()))
# If the buffer doesn't end in '\n' then we can't be done
try:
if buf[-1] != '\n':
continue
except IndexError:
raise UbridgeError("Could not communicate with {host}:{port}, uBridge process running: {run}"
.format(host=self._host, port=self._port, run=self.is_running()))
raise UbridgeError("Could not communicate with {host}:{port} after sending command '{command}', uBridge process running: {run}"
.format(host=self._host, port=self._port, command=command, run=self.is_running()))
data += buf.split('\r\n')
if data[-1] == '':
+2 -2
View File
@@ -23,8 +23,8 @@
# or negative for a release candidate or beta (after the base version
# number has been incremented)
__version__ = "2.1.4"
__version_info__ = (2, 1, 4, 0)
__version__ = "2.1.6"
__version_info__ = (2, 1, 6, 0)
# If it's a git checkout try to add the commit
if "dev" in __version__:
+1
View File
@@ -8,3 +8,4 @@ psutil>=3.0.0
zipstream>=1.1.4
typing>=3.5.3.0 # Otherwise yarl fails with python 3.4
prompt-toolkit
async-timeout<3.0.0 # pyup: ignore; 3.0 drops support for python 3.4
+72
View File
@@ -61,6 +61,7 @@ def test_json(vm, project):
'console_resolution': '1024x768',
'console_http_port': 80,
'console_http_path': '/',
'extra_hosts': None,
'aux': vm.aux,
'start_command': vm.start_command,
'environment': vm.environment,
@@ -202,6 +203,77 @@ def test_create_vnc(loop, project, manager):
assert vm._console_type == "vnc"
def test_create_with_extra_hosts(loop, project, manager):
extra_hosts = "test:199.199.199.1\ntest2:199.199.199.1"
response = {
"Id": "e90e34656806",
"Warnings": []
}
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), project, manager, "ubuntu", extra_hosts=extra_hosts)
loop.run_until_complete(asyncio.async(vm.create()))
called_kwargs = mock.call_args[1]
assert "GNS3_EXTRA_HOSTS=199.199.199.1\ttest\n199.199.199.1\ttest2" in called_kwargs["data"]["Env"]
assert vm._extra_hosts == extra_hosts
def test_create_with_extra_hosts_wrong_format(loop, project, manager):
extra_hosts = "test"
response = {
"Id": "e90e34656806",
"Warnings": []
}
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response):
vm = DockerVM("test", str(uuid.uuid4()), project, manager, "ubuntu", extra_hosts=extra_hosts)
with pytest.raises(DockerError):
loop.run_until_complete(asyncio.async(vm.create()))
def test_create_with_empty_extra_hosts(loop, project, manager):
extra_hosts = "test:\n"
response = {
"Id": "e90e34656806",
"Warnings": []
}
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), project, manager, "ubuntu", extra_hosts=extra_hosts)
loop.run_until_complete(asyncio.async(vm.create()))
called_kwargs = mock.call_args[1]
assert len([ e for e in called_kwargs["data"]["Env"] if "GNS3_EXTRA_HOSTS" in e]) == 0
def test_create_with_project_variables(loop, project, manager):
response = {
"Id": "e90e34656806",
"Warnings": []
}
project.variables = [
{"name": "VAR1"},
{"name": "VAR2", "value": "VAL1"},
{"name": "VAR3", "value": "2x${VAR2}"}
]
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), project, manager, "ubuntu")
loop.run_until_complete(asyncio.async(vm.create()))
called_kwargs = mock.call_args[1]
assert "VAR1=" in called_kwargs["data"]["Env"]
assert "VAR2=VAL1" in called_kwargs["data"]["Env"]
assert "VAR3=2xVAL1" in called_kwargs["data"]["Env"]
project.variables = None
def test_create_start_cmd(loop, project, manager):
response = {
+28 -1
View File
@@ -92,9 +92,29 @@ def test_changing_path_not_allowed(tmpdir):
p.path = str(tmpdir)
def test_variables(tmpdir):
variables = [{"name": "VAR1", "value": "VAL1"}]
p = Project(project_id=str(uuid4()), variables=variables)
assert p.variables == variables
def test_json(tmpdir):
p = Project(project_id=str(uuid4()))
assert p.__json__() == {"name": p.name, "project_id": p.id}
assert p.__json__() == {
"name": p.name,
"project_id": p.id,
"variables": None
}
def test_json_with_variables(tmpdir):
variables = [{"name": "VAR1", "value": "VAL1"}]
p = Project(project_id=str(uuid4()), variables=variables)
assert p.__json__() == {
"name": p.name,
"project_id": p.id,
"variables": variables
}
def test_node_working_directory(tmpdir, node):
@@ -185,3 +205,10 @@ def test_emit(async_run):
(action, event, context) = async_run(queue.get(0.5))
assert action == "test"
assert context["project_id"] == project.id
def test_update_project(loop):
variables = [{"name": "TEST", "value": "VAL"}]
project = Project(project_id=str(uuid.uuid4()))
loop.run_until_complete(asyncio.async(project.update(variables=variables)))
assert project.variables == variables
+179
View File
@@ -0,0 +1,179 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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 <http://www.gnu.org/licenses/>.
import pytest
import asyncio
import sys
from tests.utils import asyncio_patch, AsyncioMagicMock
from unittest.mock import patch, MagicMock, ANY, PropertyMock
from gns3server.compute.traceng.traceng_vm import TraceNGVM
from gns3server.compute.traceng.traceng_error import TraceNGError
from gns3server.compute.traceng import TraceNG
from gns3server.compute.notification_manager import NotificationManager
@pytest.fixture
def manager(port_manager):
m = TraceNG.instance()
m.port_manager = port_manager
return m
@pytest.fixture(scope="function")
def vm(project, manager, ubridge_path):
vm = TraceNGVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager)
vm._start_ubridge = AsyncioMagicMock()
vm._ubridge_hypervisor = MagicMock()
vm._ubridge_hypervisor.is_running.return_value = True
return vm
def test_vm(project, manager):
vm = TraceNGVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager)
assert vm.name == "test"
assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0f"
def test_vm_invalid_traceng_path(vm, manager, loop):
with patch("gns3server.compute.traceng.traceng_vm.TraceNGVM._traceng_path", return_value="/tmp/fake/path/traceng"):
with pytest.raises(TraceNGError):
nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"})
vm.port_add_nio_binding(0, nio)
loop.run_until_complete(asyncio.async(vm.start()))
assert vm.name == "test"
assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0e"
def test_start(loop, vm, async_run):
process = MagicMock()
process.returncode = None
with NotificationManager.instance().queue() as queue:
async_run(queue.get(0)) # Ping
vm.ip_address = "192.168.1.1"
with patch("sys.platform", return_value="win"):
with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM._check_requirements", return_value=True):
with asyncio_patch("asyncio.create_subprocess_exec", return_value=process) as mock_exec:
loop.run_until_complete(asyncio.async(vm.start("192.168.1.2")))
assert mock_exec.call_args[0] == (vm._traceng_path(),
'-u',
'-c',
ANY,
'-v',
ANY,
'-b',
'127.0.0.1',
'-s',
'ICMP',
'-f',
'192.168.1.1',
'192.168.1.2')
assert vm.is_running()
assert vm.command_line == ' '.join(mock_exec.call_args[0])
(action, event, kwargs) = async_run(queue.get(0))
assert action == "node.updated"
assert event == vm
def test_stop(loop, vm, async_run):
process = MagicMock()
# Wait process kill success
future = asyncio.Future()
future.set_result(True)
process.wait.return_value = future
process.returncode = None
vm.ip_address = "192.168.1.1"
with NotificationManager.instance().queue() as queue:
with patch("sys.platform", return_value="win"):
with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM._check_requirements", return_value=True):
with asyncio_patch("asyncio.create_subprocess_exec", return_value=process):
nio = TraceNG.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}})
async_run(vm.port_add_nio_binding(0, nio))
vm._ubridge_send = AsyncioMagicMock()
async_run(vm.start("192.168.1.2"))
assert vm.is_running()
with asyncio_patch("gns3server.utils.asyncio.wait_for_process_termination"):
loop.run_until_complete(asyncio.async(vm.stop()))
assert vm.is_running() is False
process.terminate.assert_called_with()
async_run(queue.get(0)) #  Ping
async_run(queue.get(0)) #  Started
(action, event, kwargs) = async_run(queue.get(0))
assert action == "node.updated"
assert event == vm
def test_reload(loop, vm, async_run):
process = MagicMock()
# Wait process kill success
future = asyncio.Future()
future.set_result(True)
process.wait.return_value = future
process.returncode = None
vm.ip_address = "192.168.1.1"
with patch("sys.platform", return_value="win"):
with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM._check_requirements", return_value=True):
with asyncio_patch("asyncio.create_subprocess_exec", return_value=process):
nio = TraceNG.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}})
async_run(vm.port_add_nio_binding(0, nio))
vm._ubridge_send = AsyncioMagicMock()
async_run(vm.start("192.168.1.2"))
assert vm.is_running()
with asyncio_patch("gns3server.utils.asyncio.wait_for_process_termination"):
async_run(vm.reload())
assert vm.is_running() is True
#if sys.platform.startswith("win"):
# process.send_signal.assert_called_with(1)
#else:
process.terminate.assert_called_with()
def test_add_nio_binding_udp(vm, async_run):
nio = TraceNG.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}})
async_run(vm.port_add_nio_binding(0, nio))
assert nio.lport == 4242
def test_port_remove_nio_binding(vm):
nio = TraceNG.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"})
vm.port_add_nio_binding(0, nio)
vm.port_remove_nio_binding(0)
assert vm._ethernet_adapter.ports[0] is None
def test_close(vm, port_manager, loop):
with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM._check_requirements", return_value=True):
with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()):
vm.start()
loop.run_until_complete(asyncio.async(vm.close()))
assert vm.is_running() is False
+9 -9
View File
@@ -28,7 +28,7 @@ from unittest.mock import MagicMock
from tests.utils import AsyncioMagicMock, AsyncioBytesIO
from gns3server.controller.project import Project
from gns3server.controller.export_project import export_project, _filter_files
from gns3server.controller.export_project import export_project, _is_exportable
@pytest.fixture
@@ -51,14 +51,14 @@ def node(controller, project, async_run):
return node
def test_filter_files():
assert not _filter_files("hello/world")
assert _filter_files("project-files/tmp")
assert _filter_files("project-files/test_log.txt")
assert _filter_files("project-files/test.log")
assert _filter_files("test/snapshots")
assert _filter_files("test/project-files/snapshots")
assert _filter_files("test/project-files/snapshots/test.gns3p")
def test_exportable_files():
assert _is_exportable("hello/world")
assert not _is_exportable("project-files/tmp")
assert not _is_exportable("project-files/test_log.txt")
assert not _is_exportable("project-files/test.log")
assert not _is_exportable("test/snapshots")
assert not _is_exportable("test/project-files/snapshots")
assert not _is_exportable("test/project-files/snapshots/test.gns3p")
def test_export(tmpdir, project, async_run):
+45 -5
View File
@@ -75,20 +75,37 @@ def test_json(tmpdir):
"show_grid": False,
"show_interface_labels": False,
"show_layers": False,
"snap_to_grid": False
"snap_to_grid": False,
"grid_size": 0,
"supplier": None,
"variables": None
}
def test_update(controller, async_run):
project = Project(controller=controller, name="Hello")
controller._notification = MagicMock()
assert project.name == "Hello"
async_run(project.update(name="World"))
assert project.name == "World"
controller.notification.emit.assert_any_call("project.updated", project.__json__())
def test_update_on_compute(controller, async_run):
variables = [{"name": "TEST", "value": "VAL1"}]
compute = MagicMock()
compute.id = "local"
project = Project(controller=controller, name="Test")
project._project_created_on_compute = [compute]
controller._notification = MagicMock()
async_run(project.update(variables=variables))
compute.put.assert_any_call('/projects/{}'.format(project.id), {
"variables": variables
})
def test_path(tmpdir):
directory = Config.instance().get_section_config("Server").get("projects_path")
@@ -147,7 +164,8 @@ def test_add_node_local(async_run, controller):
compute.post.assert_any_call('/projects', data={
"name": project._name,
"project_id": project._id,
"path": project._path
"path": project._path,
"variables": None
})
compute.post.assert_any_call('/projects/{}/vpcs/nodes'.format(project.id),
data={'node_id': node.id,
@@ -175,7 +193,8 @@ def test_add_node_non_local(async_run, controller):
compute.post.assert_any_call('/projects', data={
"name": project._name,
"project_id": project._id
"project_id": project._id,
"variables": None
})
compute.post.assert_any_call('/projects/{}/vpcs/nodes'.format(project.id),
data={'node_id': node.id,
@@ -215,7 +234,8 @@ def test_add_node_from_appliance(async_run, controller):
compute.post.assert_any_call('/projects', data={
"name": project._name,
"project_id": project._id,
"path": project._path
"path": project._path,
"variables": None
})
compute.post.assert_any_call('/projects/{}/vpcs/nodes'.format(project.id),
data={'node_id': node.id,
@@ -394,6 +414,26 @@ def test_clean_pictures(async_run, project, controller):
assert not os.path.exists(os.path.join(project.pictures_directory, "test2.png"))
def test_clean_pictures_and_keep_supplier_logo(async_run, project, controller):
"""
When a project is close old pictures should be removed
"""
project.supplier = {
'logo': 'logo.png'
}
drawing = async_run(project.add_drawing())
drawing._svg = "test.png"
open(os.path.join(project.pictures_directory, "test.png"), "w+").close()
open(os.path.join(project.pictures_directory, "test2.png"), "w+").close()
open(os.path.join(project.pictures_directory, "logo.png"), "w+").close()
async_run(project.close())
assert os.path.exists(os.path.join(project.pictures_directory, "test.png"))
assert not os.path.exists(os.path.join(project.pictures_directory, "test2.png"))
assert os.path.exists(os.path.join(project.pictures_directory, "logo.png"))
def test_delete(async_run, project, controller):
assert os.path.exists(project.path)
async_run(project.delete())
+75
View File
@@ -45,6 +45,7 @@ def test_project_to_topology_empty(tmpdir):
"show_interface_labels": False,
"show_layers": False,
"snap_to_grid": False,
"grid_size": 0,
"topology": {
"nodes": [],
"links": [],
@@ -52,6 +53,8 @@ def test_project_to_topology_empty(tmpdir):
"drawings": []
},
"type": "topology",
"supplier": None,
"variables": None,
"version": __version__
}
@@ -80,6 +83,26 @@ def test_basic_topology(tmpdir, async_run, controller):
assert topo["topology"]["drawings"][0] == drawing.__json__(topology_dump=True)
def test_project_to_topology(tmpdir, controller):
variables = [
{"name": "TEST1"},
{"name": "TEST2", "value": "value1"}
]
supplier = {
'logo': 'logo.png',
'url': 'http://example.com'
}
project = Project(name="Test", controller=controller)
compute = Compute("my_compute", controller)
compute.http_query = MagicMock()
project.variables = variables
project.supplier = supplier
topo = project_to_topology(project)
assert topo["variables"] == variables
assert topo["supplier"] == supplier
def test_load_topology(tmpdir):
data = {
"project_id": "69f26504-7aa3-48aa-9f29-798d44841211",
@@ -136,3 +159,55 @@ def test_load_newer_topology(tmpdir):
json.dump(data, f)
with pytest.raises(aiohttp.web.HTTPConflict):
topo = load_topology(path)
def test_load_topology_with_variables(tmpdir):
variables = [
{"name": "TEST1"},
{"name": "TEST2", "value": "value1"}
]
data = {
"project_id": "69f26504-7aa3-48aa-9f29-798d44841211",
"name": "Test",
"revision": GNS3_FILE_FORMAT_REVISION,
"topology": {
"nodes": [],
"links": [],
"computes": [],
"drawings": []
},
"variables": variables,
"type": "topology",
"version": __version__}
path = str(tmpdir / "test.gns3")
with open(path, "w+") as f:
json.dump(data, f)
topo = load_topology(path)
assert topo == data
def test_load_topology_with_supplier(tmpdir):
supplier = {
'logo': 'logo.png',
'url': 'http://example.com'
}
data = {
"project_id": "69f26504-7aa3-48aa-9f29-798d44841211",
"name": "Test",
"revision": GNS3_FILE_FORMAT_REVISION,
"topology": {
"nodes": [],
"links": [],
"computes": [],
"drawings": []
},
"supplier": supplier,
"type": "topology",
"version": __version__}
path = str(tmpdir / "test.gns3")
with open(path, "w+") as f:
json.dump(data, f)
topo = load_topology(path)
assert topo == data
@@ -31,11 +31,11 @@ from gns3server.version import __version__
def test_get(http_compute, windows_platform):
response = http_compute.get('/capabilities', example=True)
assert response.status == 200
assert response.json == {'node_types': ['cloud', 'ethernet_hub', 'ethernet_switch', 'nat', 'vpcs', 'virtualbox', 'dynamips', 'frame_relay_switch', 'atm_switch', 'qemu', 'vmware', 'docker', 'iou'], 'version': __version__, 'platform': sys.platform}
assert response.json == {'node_types': ['cloud', 'ethernet_hub', 'ethernet_switch', 'nat', 'vpcs', 'virtualbox', 'dynamips', 'frame_relay_switch', 'atm_switch', 'qemu', 'vmware', 'traceng', 'docker', 'iou'], 'version': __version__, 'platform': sys.platform}
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows")
def test_get_on_gns3vm(http_compute, on_gns3vm):
response = http_compute.get('/capabilities', example=True)
assert response.status == 200
assert response.json == {'node_types': ['cloud', 'ethernet_hub', 'ethernet_switch', 'nat', 'vpcs', 'virtualbox', 'dynamips', 'frame_relay_switch', 'atm_switch', 'qemu', 'vmware', 'docker', 'iou'], 'version': __version__, 'platform': sys.platform}
assert response.json == {'node_types': ['cloud', 'ethernet_hub', 'ethernet_switch', 'nat', 'vpcs', 'virtualbox', 'dynamips', 'frame_relay_switch', 'atm_switch', 'qemu', 'vmware', 'traceng', 'docker', 'iou'], 'version': __version__, 'platform': sys.platform}
+5 -3
View File
@@ -32,7 +32,7 @@ pytestmark = pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supp
@pytest.fixture
def base_params():
"""Return standard parameters"""
return {"name": "PC TEST 1", "image": "nginx", "start_command": "nginx-daemon", "adapters": 2, "environment": "YES=1\nNO=0", "console_type": "telnet", "console_resolution": "1280x1024"}
return {"name": "PC TEST 1", "image": "nginx", "start_command": "nginx-daemon", "adapters": 2, "environment": "YES=1\nNO=0", "console_type": "telnet", "console_resolution": "1280x1024", "extra_hosts": "test:127.0.0.1"}
@pytest.yield_fixture(autouse=True)
@@ -69,7 +69,7 @@ def test_docker_create(http_compute, project, base_params):
assert response.json["adapters"] == 2
assert response.json["environment"] == "YES=1\nNO=0"
assert response.json["console_resolution"] == "1280x1024"
assert response.json["extra_hosts"] == "test:127.0.0.1"
def test_docker_start(http_compute, vm):
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.start", return_value=True) as mock:
@@ -150,7 +150,8 @@ def test_docker_update(http_compute, vm, tmpdir, free_console_port):
response = http_compute.put("/projects/{project_id}/docker/nodes/{node_id}".format(project_id=vm["project_id"], node_id=vm["node_id"]), {"name": "test",
"console": free_console_port,
"start_command": "yes",
"environment": "GNS3=1\nGNS4=0"},
"environment": "GNS3=1\nGNS4=0",
"extra_hosts": "test:127.0.0.1"},
example=True)
assert mock.called
assert response.status == 200
@@ -158,6 +159,7 @@ def test_docker_update(http_compute, vm, tmpdir, free_console_port):
assert response.json["console"] == free_console_port
assert response.json["start_command"] == "yes"
assert response.json["environment"] == "GNS3=1\nGNS4=0"
assert response.json["extra_hosts"] == "test:127.0.0.1"
def test_docker_start_capture(http_compute, vm, tmpdir, project):
+19 -4
View File
@@ -21,9 +21,6 @@ This test suite check /project endpoint
import uuid
import os
import asyncio
import aiohttp
import zipfile
from unittest.mock import patch
from tests.utils import asyncio_patch
@@ -60,9 +57,10 @@ def test_show_project(http_compute):
response = http_compute.post("/projects", query)
assert response.status == 201
response = http_compute.get("/projects/40010203-0405-0607-0809-0a0b0c0d0e02", example=True)
assert len(response.json.keys()) == 2
assert len(response.json.keys()) == 3
assert response.json["project_id"] == "40010203-0405-0607-0809-0a0b0c0d0e02"
assert response.json["name"] == "test"
assert response.json["variables"] is None
def test_show_project_invalid_uuid(http_compute):
@@ -93,6 +91,23 @@ def test_delete_project(http_compute, project):
assert mock.called
def test_update_project(http_compute):
query = {"name": "test", "project_id": "51010203-0405-0607-0809-0a0b0c0d0e0f"}
response = http_compute.post("/projects", query)
assert response.status == 201
query = {
"variables": [{"name": "TEST1", "value": "VAL1"}]
}
response = http_compute.put(
"/projects/{project_id}".format(project_id="51010203-0405-0607-0809-0a0b0c0d0e0f"),
query,
example=True
)
assert response.status == 200
assert response.json["variables"] == [{"name": "TEST1", "value": "VAL1"}]
def test_delete_project_invalid_uuid(http_compute):
response = http_compute.delete("/projects/{project_id}".format(project_id=uuid.uuid4()))
assert response.status == 404
+147
View File
@@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 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 <http://www.gnu.org/licenses/>.
import pytest
import uuid
import sys
import os
from tests.utils import asyncio_patch
from unittest.mock import patch
@pytest.fixture(scope="function")
def vm(http_compute, project):
response = http_compute.post("/projects/{project_id}/traceng/nodes".format(project_id=project.id), {"name": "TraceNG TEST 1"})
assert response.status == 201
return response.json
def test_traceng_create(http_compute, project):
response = http_compute.post("/projects/{project_id}/traceng/nodes".format(project_id=project.id), {"name": "TraceNG TEST 1"}, example=True)
assert response.status == 201
assert response.route == "/projects/{project_id}/traceng/nodes"
assert response.json["name"] == "TraceNG TEST 1"
assert response.json["project_id"] == project.id
def test_traceng_get(http_compute, project, vm):
response = http_compute.get("/projects/{project_id}/traceng/nodes/{node_id}".format(project_id=vm["project_id"], node_id=vm["node_id"]), example=True)
assert response.status == 200
assert response.route == "/projects/{project_id}/traceng/nodes/{node_id}"
assert response.json["name"] == "TraceNG TEST 1"
assert response.json["project_id"] == project.id
assert response.json["status"] == "stopped"
def test_traceng_create_port(http_compute, project, free_console_port):
response = http_compute.post("/projects/{project_id}/traceng/nodes".format(project_id=project.id), {"name": "TraceNG TEST 1", "console": free_console_port})
assert response.status == 201
assert response.route == "/projects/{project_id}/traceng/nodes"
assert response.json["name"] == "TraceNG TEST 1"
assert response.json["project_id"] == project.id
assert response.json["console"] == free_console_port
def test_traceng_nio_create_udp(http_compute, vm):
with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM.add_ubridge_udp_connection"):
response = http_compute.post("/projects/{project_id}/traceng/nodes/{node_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], node_id=vm["node_id"]), {"type": "nio_udp",
"lport": 4242,
"rport": 4343,
"rhost": "127.0.0.1"},
example=True)
assert response.status == 201
assert response.route == "/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio"
assert response.json["type"] == "nio_udp"
def test_traceng_nio_update_udp(http_compute, vm):
response = http_compute.put("/projects/{project_id}/traceng/nodes/{node_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], node_id=vm["node_id"]),
{
"type": "nio_udp",
"lport": 4242,
"rport": 4343,
"rhost": "127.0.0.1",
"filters": {}},
example=True)
assert response.status == 201, response.body.decode("utf-8")
assert response.route == "/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio"
assert response.json["type"] == "nio_udp"
def test_traceng_delete_nio(http_compute, vm):
with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM._ubridge_send"):
http_compute.post("/projects/{project_id}/traceng/nodes/{node_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], node_id=vm["node_id"]), {"type": "nio_udp",
"lport": 4242,
"rport": 4343,
"rhost": "127.0.0.1"})
response = http_compute.delete("/projects/{project_id}/traceng/nodes/{node_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], node_id=vm["node_id"]), example=True)
assert response.status == 204, response.body.decode()
assert response.route == "/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio"
def test_traceng_start(http_compute, vm):
with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM.start", return_value=True) as mock:
response = http_compute.post("/projects/{project_id}/traceng/nodes/{node_id}/start".format(project_id=vm["project_id"], node_id=vm["node_id"]), {"destination": "192.168.1.2"}, example=True)
assert mock.called
assert response.status == 200
assert response.json["name"] == "TraceNG TEST 1"
def test_traceng_stop(http_compute, vm):
with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM.stop", return_value=True) as mock:
response = http_compute.post("/projects/{project_id}/traceng/nodes/{node_id}/stop".format(project_id=vm["project_id"], node_id=vm["node_id"]), example=True)
assert mock.called
assert response.status == 204
def test_traceng_reload(http_compute, vm):
with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM.reload", return_value=True) as mock:
response = http_compute.post("/projects/{project_id}/traceng/nodes/{node_id}/reload".format(project_id=vm["project_id"], node_id=vm["node_id"]), example=True)
assert mock.called
assert response.status == 204
def test_traceng_delete(http_compute, vm):
with asyncio_patch("gns3server.compute.traceng.TraceNG.delete_node", return_value=True) as mock:
response = http_compute.delete("/projects/{project_id}/traceng/nodes/{node_id}".format(project_id=vm["project_id"], node_id=vm["node_id"]), example=True)
assert mock.called
assert response.status == 204
def test_traceng_duplicate(http_compute, vm):
with asyncio_patch("gns3server.compute.traceng.TraceNG.duplicate_node", return_value=True) as mock:
response = http_compute.post(
"/projects/{project_id}/traceng/nodes/{node_id}/duplicate".format(
project_id=vm["project_id"],
node_id=vm["node_id"]),
body={
"destination_node_id": str(uuid.uuid4())
},
example=True)
assert mock.called
assert response.status == 201
def test_traceng_update(http_compute, vm, tmpdir, free_console_port):
response = http_compute.put("/projects/{project_id}/traceng/nodes/{node_id}".format(project_id=vm["project_id"], node_id=vm["node_id"]), {"name": "test",
"ip_address": "192.168.1.1",
},
example=True)
assert response.status == 200
assert response.json["name"] == "test"
assert response.json["ip_address"] == "192.168.1.1"
@@ -67,6 +67,31 @@ def test_create_project_with_uuid(http_controller):
assert response.json["name"] == "test"
def test_create_project_with_variables(http_controller):
variables = [
{"name": "TEST1"},
{"name": "TEST2", "value": "value1"}
]
query = {"name": "test", "project_id": "30010203-0405-0607-0809-0a0b0c0d0e0f", "variables": variables}
response = http_controller.post("/projects", query)
assert response.status == 201
assert response.json["variables"] == [
{"name": "TEST1"},
{"name": "TEST2", "value": "value1"}
]
def test_create_project_with_supplier(http_controller):
supplier = {
'logo': 'logo.png',
'url': 'http://example.com'
}
query = {"name": "test", "project_id": "30010203-0405-0607-0809-0a0b0c0d0e0f", "supplier": supplier}
response = http_controller.post("/projects", query)
assert response.status == 201
assert response.json["supplier"] == supplier
def test_update_project(http_controller):
query = {"name": "test", "project_id": "10010203-0405-0607-0809-0a0b0c0d0e0f"}
response = http_controller.post("/projects", query)
@@ -79,6 +104,20 @@ def test_update_project(http_controller):
assert response.json["name"] == "test2"
def test_update_project_with_variables(http_controller):
variables = [
{"name": "TEST1"},
{"name": "TEST2", "value": "value1"}
]
query = {"name": "test", "project_id": "10010203-0405-0607-0809-0a0b0c0d0e0f", "variables": variables}
response = http_controller.post("/projects", query)
assert response.status == 201
query = {"name": "test2"}
response = http_controller.put("/projects/10010203-0405-0607-0809-0a0b0c0d0e0f", query, example=True)
assert response.status == 200
assert response.json["variables"] == variables
def test_list_projects(http_controller, tmpdir):
http_controller.post("/projects", {"name": "test", "path": str(tmpdir), "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"})
response = http_controller.get("/projects", example=True)