Compare commits

...
79 Commits
Author SHA1 Message Date
grossmj 8ad632976f Release v2.2.0a2 2019-03-14 17:09:53 +07:00
ziajka b8bf2bf465 Web-UI v2019.1.0-alpha.1 2019-03-14 10:44:09 +01:00
ziajka 55be74f93c Update docs for update-bundled-web-ui.sh 2019-03-14 10:33:35 +01:00
grossmj 7d591b0228 Fix issue when loading and quickly closing a project and opening it again. Fixes #1501. 2019-03-14 15:15:27 +07:00
grossmj a059e6b943 Disable unreliable nested virtualization check. 2019-03-13 23:13:54 +07:00
grossmj c6990dfeee Merge remote-tracking branch 'origin/2.2' into 2.2 2019-03-13 12:56:50 +07:00
grossmj aff67b324f Fix issue not checking build number on Windows. 2019-03-13 12:56:33 +07:00
Jeremy Grossmann f7e90a7d7f Merge pull request #1550 from GNS3/pyup-update-pytest-4.3.0-to-4.3.1
Update pytest to 4.3.1
2019-03-13 02:21:49 +07:00
pyup-bot ed6b1d909d Update pytest from 4.3.0 to 4.3.1 2019-03-12 20:16:34 +01:00
grossmj 010d65114d Hyper-V with nested virtualization is only supported on Windows 10 Anniversary Update or later. 2019-03-13 02:15:58 +07:00
grossmj 3dcb7799e3 Use accurate info to detect Windows version. 2019-03-12 22:40:37 +07:00
grossmj aaaeb410d5 Fix exception when loading and quickly closing a project. 2019-03-12 21:50:09 +07:00
grossmj d506962bdb Debugging why sys.getwindowsversion() does not work with frozen app. 2019-03-12 21:18:46 +07:00
grossmj 4d9728b6f5 Correctly rename symbol. 2019-03-12 18:54:05 +07:00
grossmj ff9048b5a8 Revert "Rename symbol."
This reverts commit 6c96be2acc.
2019-03-12 18:53:02 +07:00
grossmj 6c96be2acc Rename symbol. 2019-03-12 18:42:54 +07:00
Jeremy Grossmann 21afaf31f9 Merge pull request #1549 from GNS3/symbol-management-refactoring
Symbol management refactoring
2019-03-12 18:21:30 +07:00
grossmj 8e5ac228d5 Merge remote-tracking branch 'origin/2.2' into 2.2 2019-03-12 18:17:41 +07:00
grossmj 94f63f54ba Change Hyper-V requirement checks. 2019-03-12 18:17:31 +07:00
grossmj cf91e904f7 Early support for symbol themes. 2019-03-12 18:13:33 +07:00
ziajka 767bb770ca Re-order handlers in order to prevent CORS 2019-03-11 16:16:10 +01:00
grossmj bae3fb84b9 Download custom appliance symbols from GitHub
Fix symbol cache issue. Ref https://github.com/GNS3/gns3-gui/issues/2671
Fix temporary directory for symbols was not deleted
Fix temporary appliance file was not deleted
2019-03-11 16:55:16 +07:00
grossmj 889d29ec55 Update appliances. 2019-03-11 09:48:54 +07:00
grossmj d3ec2a7275 Merge remote-tracking branch 'origin/2.2' into 2.2 2019-03-07 18:55:59 +07:00
grossmj 2d01ef489b Option to export snapshots. 2019-03-07 18:55:38 +07:00
ziajka d64d7de541 Support tags versioned WebUI when bundling 2019-03-07 11:56:15 +01:00
Jeremy Grossmann 372017f467 Merge pull request #1537 from GNS3/import-export-refactoring
Project import/export refactoring. Fixes #1349
2019-03-07 17:38:56 +07:00
grossmj 826f6b2d57 Fix include images test. 2019-03-07 17:17:07 +07:00
grossmj 0ae66a2608 Support selecting a compression type when exporting a project. 2019-03-07 17:05:32 +07:00
grossmj 380224dca1 Change how VPCS executable is searched. 2019-03-06 23:15:05 +07:00
grossmj af80b0bb6e Use aiofiles where relevant. 2019-03-06 23:00:01 +07:00
grossmj b0df7ecabf Merge branch '2.2' into import-export-refactoring 2019-03-05 17:29:01 +07:00
grossmj 4c83fc1243 Update paths for binaries moved to the MacOS directory in GNS3.app 2019-03-04 16:07:04 +07:00
grossmj 69ba0ccf77 Bump version to 2.2.0dev2 2019-03-04 14:48:57 +07:00
Jeremy Grossmann 0681c296a7 Merge pull request #1541 from GNS3/lock-unlock-items
Refactoring for locking/unlocking items
2019-03-02 16:44:28 +07:00
grossmj 3f679bd106 Locked state should not be used when duplicating a node. 2019-03-02 16:39:05 +07:00
grossmj 724eda1f35 Handle locking/unlocking items independently from the layer position. 2019-03-02 16:26:40 +07:00
Jeremy Grossmann 3f7c4c0474 Update qemu_vm.py 2019-03-01 23:40:32 +07:00
grossmj 5b6011a248 Stay with jsonschema 2.6.0 2019-03-01 17:26:23 +07:00
grossmj 80eb06f67c Merge 2.1 into 2.2 2019-03-01 17:23:49 +07:00
grossmj 9132ba8d05 Remove zipstream from tests. 2019-02-28 23:25:05 +07:00
grossmj 85ea1cf7a9 Remove import zipstream from tests. 2019-02-28 17:50:09 +07:00
grossmj 8cc8269842 Use aiozipstream for snapshots.
Fix tests.
2019-02-28 17:25:05 +07:00
grossmj 52bfa636c1 Project duplication support. 2019-02-27 17:57:07 +07:00
grossmj 4eca82174d Development on 2.1.15dev1 2019-02-27 15:59:16 +07:00
grossmj 32f5dbb645 Release v2.1.14 2019-02-27 14:58:52 +07:00
grossmj 7aae682f0a Fix issue when setting cpuid.corespersocket for the GNS3 VM. Fixes https://github.com/GNS3/gns3-gui/issues/2723 2019-02-27 14:47:45 +07:00
grossmj 879e32eb9a Bump ACPI Shutdown Timeout to 120 seconds. Ref #1536 2019-02-26 23:19:33 +07:00
grossmj 01481cde1f Development on 2.1.14dev1 2019-02-26 18:10:23 +07:00
grossmj b5deb9d177 Release v2.1.13 2019-02-26 16:46:02 +07:00
grossmj a8990c9e89 Non blocking project exportation. 2019-02-26 15:55:07 +07:00
Jeremy Grossmann e1aa13fa73 Force jsonschema dependency to 2.6.0
This is to fix this issue when starting the (frozen) application on Windows:

```
  File "C:\Python36-x64\lib\site-packages\jsonschema\validators.py", line 505, in <module>
  File "C:\Python36-x64\lib\site-packages\jsonschema\_utils.py", line 57, in load_schema
  File "C:\Python36-x64\lib\pkgutil.py", line 634, in get_data
OSError: [Errno 34] Result too large: 'jsonschema\\schemas\\draft6.json'
```
2019-02-26 15:28:11 +07:00
grossmj 135d56371d Fix tests for project notifications. 2019-02-23 23:20:11 +07:00
grossmj 7fe8f7e716 Fix long-polling request for project notifications. 2019-02-23 21:08:52 +07:00
grossmj 10702f87bc Fix export test. 2019-02-23 11:13:50 +07:00
grossmj 2fb5e47c8d Allow cloud nodes to be exported. Fixes #1519 2019-02-23 11:07:01 +07:00
grossmj 4487e578bc Fix create a node from a template test. 2019-02-22 18:44:12 +07:00
grossmj 5754747a90 Merging 2.1 into 2.2 2019-02-22 18:04:49 +07:00
grossmj 62c51edbae Less aggressive connections to uBridge. Ref #1289 2019-02-22 16:05:31 +07:00
grossmj a13d063aa1 Fix topology images (Pictures) disappearing from projects. Fixes #1514. 2019-02-21 23:58:54 +07:00
grossmj 1ef1872f8e Reset MAC addresses when duplicating a project. Fixes #1522 2019-02-20 16:38:43 +07:00
grossmj 657698a961 Fix create a node from an appliance test. 2019-02-20 11:21:29 +07:00
grossmj 081ba31b50 Fix API call to create a node from an appliance doesn't return the new node data. Fixes #1527 2019-02-20 10:47:33 +07:00
grossmj aea4ae808f Detect invalid environment variable and send a warning when creating a Docker node. Ref #2683 2019-02-19 17:34:10 +07:00
Jeremy Grossmann 3e601a8a24 Merge pull request #1526 from GNS3/pyup-update-pytest-4.1.1-to-4.3.0
Update pytest to 4.3.0
2019-02-19 15:57:13 +07:00
grossmj ae3515434c Do not export/import symlinks for projects. Fixes #2699 2019-02-19 12:43:44 +07:00
pyup-bot 8e2d8f3962 Update pytest from 4.1.1 to 4.3.0 2019-02-19 04:27:57 +01:00
grossmj 589c9754e8 Fix symlink not being created for duplicated IOU devices. Fixes https://github.com/GNS3/gns3-gui/issues/2699 2019-02-19 00:09:59 +08:00
grossmj 4ecd3b2015 Configure coresPerSocket value in VMX file for the GNS3 VM. Fixes https://github.com/GNS3/gns3-gui/issues/2688 2019-02-17 23:16:48 +08:00
grossmj 84ee3263ba Count logical CPUs to detect if the number of vCPUs is too high when configuring the GNS3 VM. Fixes #2688. 2019-02-17 23:07:33 +08:00
grossmj 174624121d Fix Qemu VM tests. Ref #1516 2019-02-17 19:53:46 +08:00
grossmj d9a9abf845 Add explicit error when trying to pull a Docker image from Docker Hub without Internet access. Fixes #1506. 2019-02-17 19:21:21 +08:00
grossmj 0b07299472 Fixes double display output in GRUB in QEMU v3.1. Fixes #1516. 2019-02-17 19:03:36 +08:00
ziajka f7d0f54b6d Use production env for bundled web-ui 2019-01-29 10:02:39 +01:00
grossmj 7fb192699b Development on 2.1.13dev1 2019-01-23 15:42:10 +08:00
grossmj bccdfc97d1 Release 2.1.12 2019-01-23 15:40:38 +08:00
Jeremy Grossmann 792c4310eb Merge pull request #1502 from GNS3/svg-symbols-fixes
Tune how to get the size of SVG images.
2019-01-22 00:41:26 +07:00
grossmj 3e21f96bf9 Fix indentation issue. Ref https://github.com/GNS3/gns3-gui/issues/2674
(cherry picked from commit c14d79a3d5)
2019-01-21 23:51:46 +07:00
grossmj 5f932fee9f Tune how to get the size of SVG images. Ref https://github.com/GNS3/gns3-gui/issues/2674.
* Default for missing height/width is "100%" as defined in the SVG specification
* Better error message, if viewBox attribute is missing
* Removal of "%" in percent more fault tolerant by using rstrip("%")

(cherry picked from commit e3757a8955)
2019-01-21 23:49:43 +07:00
146 changed files with 53557 additions and 721 deletions

No files matched your search

+54
View File
@@ -1,5 +1,27 @@
# Change Log
## 2.2.0a2 14/03/2019
* Web-UI v2019.1.0-alpha.1
* Update docs for update-bundled-web-ui.sh
* Fix issue when loading and quickly closing a project and opening it again. Fixes #1501.
* Disable unreliable nested virtualization check.
* Fix issue not checking build number on Windows.
* Change Hyper-V requirement checks.
* Early support for symbol themes.
* Re-order handlers in order to prevent CORS
* Download custom appliance symbols from GitHub Fix symbol cache issue. Ref https://github.com/GNS3/gns3-gui/issues/2671 Fix temporary directory for symbols was not deleted Fix temporary appliance file was not deleted
* Option to export snapshots.
* Support tags versioned WebUI when bundling
* Support selecting a compression type when exporting a project.
* Change how VPCS executable is searched.
* Use aiofiles where relevant.
* Update paths for binaries moved to the MacOS directory in GNS3.app
* Locked state should not be used when duplicating a node.
* Handle locking/unlocking items independently from the layer position.
* Use aiozipstream for snapshots. Fix tests.
* Project duplication support.
## 2.2.0a1 29/01/2019
* Restore reload support for nodes.
@@ -140,6 +162,38 @@
* Implement #1153 into 2.2 branch.
* Pin prompt-toolkit to latest version 1.0.15
## 2.1.14 27/02/2019
* Fix issue when setting cpuid.corespersocket for the GNS3 VM. Fixes https://github.com/GNS3/gns3-gui/issues/2723
* Bump ACPI Shutdown Timeout to 120 seconds. Ref #1536
## 2.1.13 26/02/2019
* Force jsonschema dependency to 2.6.0
* Less aggressive connections to uBridge. Ref #1289
* Fix topology images (Pictures) disappearing from projects. Fixes #1514.
* Reset MAC addresses when duplicating a project. Fixes #1522
* Fix API call to create a node from an appliance doesn't return the new node data. Fixes #1527
* Detect invalid environment variable and send a warning when creating a Docker node. Ref #2683
* Do not export/import symlinks for projects. Fixes #2699
* Fix symlink not being created for duplicated IOU devices. Fixes https://github.com/GNS3/gns3-gui/issues/2699
* Configure coresPerSocket value in VMX file for the GNS3 VM. Fixes https://github.com/GNS3/gns3-gui/issues/2688
* Count logical CPUs to detect if the number of vCPUs is too high when configuring the GNS3 VM. Fixes #2688.
* Add explicit error when trying to pull a Docker image from Docker Hub without Internet access. Fixes #1506.
* Fixes double display output in GRUB in QEMU v3.1. Fixes #1516.
## 2.1.12 23/01/2019
* Tune how to get the size of SVG images. Ref https://github.com/GNS3/gns3-gui/issues/2674.
* Automatically create a symbolic link to the IOU image in the IOU working directory. Fixes #1484
* Fix link pause/filters only work for the first interface of Docker containers. Fixes #1482
* Telnet console resize support for Docker VM.
* Fix _fix_permissions() garbles permissions in Docker VM. Ref #1428
* Fix "None is not of type 'integer'" when opening project containing a Qemu VM. Fixes #2610.
* Only require Xtigervnc or Xvfb+x11vnc for Docker with vnc console. Ref #1438
* Support tigervnc in Docker VM. Ref #1438
* Update minimum VIX version requirements for VMware. Ref #1415.
## 2.1.11 28/09/2018
* Catch some exceptions.
+1 -1
View File
@@ -1,6 +1,6 @@
-rrequirements.txt
sphinx==1.8.3
pytest==4.1.1
pytest==4.3.1
pep8==1.7.1
pytest-timeout==1.3.3
@@ -0,0 +1,53 @@
{
"name": "ArubaOS-CX Simulation Software",
"category": "multilayer_switch",
"status": "stable",
"product_name": "ArubaOS-CX Simulation Software",
"description": "The ArubaOS-CX Simulation Software OVA is a virtual platform to enable simulation of the ArubaOS-CX Network Operating System. Simulated networks can be created using many of the protocols in the ArubaOS-CX Operating system like OSPF and BGP. Key features like the Aruba Network Analytics Engine and the REST API can be simulated, providing a lightweight development platform to building the modern network. This software can be easily implemented in the GNS3 simulation software to enable drag and drop network design for building complex simulated topologies.",
"maintainer_email": "alloytm@gmail.com",
"vendor_url": "arubanetworks.com",
"vendor_name": "HPE Aruba",
"availability": "service-contract",
"maintainer": "Tak Mem Loy",
"registry_version": 4,
"usage": "Default username admin with blank password.",
"symbol": ":/symbols/route_switch_processor.svg",
"first_port_name": "mgmt",
"port_name_format": "1/1/{0}",
"qemu": {
"arch": "x86_64",
"ram": 4096,
"adapters": 8,
"hdb_disk_interface": "ide",
"hdc_disk_interface": "ide",
"hda_disk_interface": "ide",
"cpus": 2,
"kvm": "require",
"adapter_type": "virtio-net-pci",
"console_type": "vnc",
"options": "-nographic",
"process_priority": "normal"
},
"images": [
{
"filename": "arubaoscx-disk-image-genericx86-p4-20180712161119.vmdk",
"version": "10.01.0001",
"md5sum": "9146ae0ac650d8206c9600e03753f022",
"filesize": 287734784,
"download_url": "http://support.arubanetworks.com/"
}
],
"versions": [
{
"name": "10.01.0001",
"images": {
"hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20180712161119.vmdk"
}
}
]
}
@@ -22,6 +22,13 @@
"kvm": "require"
},
"images": [
{
"filename": "csr1000v-universalk9.16.10.01b-serial.qcow2",
"version": "16.10.1b",
"md5sum": "0d3d647f83631c7955ad7899e6cce293",
"filesize": 950468608,
"download_url": "https://software.cisco.com/download/home/284364978/type/282046477/release/Gibraltar-16.10.1b"
},
{
"filename": "csr1000v-universalk9.16.09.01-serial.qcow2",
"version": "16.9.1",
@@ -108,6 +115,12 @@
}
],
"versions": [
{
"name": "16.10.1b",
"images": {
"hda_disk_image": "csr1000v-universalk9.16.10.01b-serial.qcow2"
}
},
{
"name": "16.9.1",
"images": {
+42
View File
@@ -25,6 +25,20 @@
"kvm": "require"
},
"images": [
{
"filename": "nxosv-final.9.2.3.qcow2",
"version": "9.2.3",
"md5sum": "74bd9a5b4970e868685f753e48979194",
"filesize": 1357643776,
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.2%25283%2529"
},
{
"filename": "nxosv-final.9.2.2.qcow2",
"version": "9.2.2",
"md5sum": "2119702c488552fc4d1b4210a04d4f64",
"filesize": 1344077824,
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.2%25281%2529"
},
{
"filename": "nxosv-final.9.2.1.qcow2",
"version": "9.2.1",
@@ -32,6 +46,13 @@
"filesize": 1330315264,
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.2%25281%2529"
},
{
"filename": "nxosv-final.7.0.3.I7.6.qcow2",
"version": "7.0.3.I7.6",
"md5sum": "a122ee954b4c11761abd99291b70544e",
"filesize": 1031995392,
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/7.0%25283%2529I7%25286%2529"
},
{
"filename": "nxosv-final.7.0.3.I7.5.qcow2",
"version": "7.0.3.I7.5",
@@ -99,6 +120,20 @@
}
],
"versions": [
{
"name": "9.2.3",
"images": {
"bios_image": "OVMF-20160813.fd",
"hda_disk_image": "nxosv-final.9.2.3.qcow2"
}
},
{
"name": "9.2.2",
"images": {
"bios_image": "OVMF-20160813.fd",
"hda_disk_image": "nxosv-final.9.2.2.qcow2"
}
},
{
"name": "9.2.1",
"images": {
@@ -106,6 +141,13 @@
"hda_disk_image": "nxosv-final.9.2.1.qcow2"
}
},
{
"name": "7.0.3.I7.6",
"images": {
"bios_image": "OVMF-20160813.fd",
"hda_disk_image": "nxosv-final.7.0.3.I7.6.qcow2"
}
},
{
"name": "7.0.3.I7.5",
"images": {
@@ -11,7 +11,7 @@
"maintainer": "Extreme Networks",
"maintainer_email": "voss@extremenetworks.com",
"usage": "Boot up and login is rwa/rwa",
"symbol": "ethernet_switch.svg",
"symbol": ":/symbols/multilayer_switch.svg",
"first_port_name": "Mgmt",
"port_name_format": "1/{port1}",
"qemu":
+46
View File
@@ -0,0 +1,46 @@
{
"category": "guest",
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"description": "Mininet creates a realistic virtual network, running real kernel, switch and application code, on a single machine (VM, cloud or native), in seconds, with a single command.",
"product_name": "Mininet",
"registry_version": 4,
"product_url": "http://mininet.org/",
"documentation_url": "http://mininet.org/walkthrough/",
"first_port_name": "eth0",
"usage": "Username: mininet\nPassword: mininet",
"port_name_format": "eth{0}",
"vendor_name": "Mininet Team",
"qemu": {
"adapters": 1,
"adapter_type": "virtio-net-pci",
"arch": "x86_64",
"ram": 2048,
"console_type": "vnc",
"hda_disk_interface": "virtio",
"kvm": "allow"
},
"availability": "free",
"vendor_url": "http://mininet.org/",
"name": "Mininet",
"images": [
{
"filename": "mininet-vm-x86_64.vmdk",
"version": "2.2.2",
"md5sum": "a683441642300bdaf37b8e614de85342",
"filesize": 2047868928,
"download_url": "https://github.com/mininet/mininet/releases/",
"direct_download_url": "https://github.com/mininet/mininet/releases/download/2.2.2/mininet-2.2.2-170321-ubuntu-14.04.4-server-amd64.zip",
"compression": "zip"
}
],
"versions": [
{
"images": {
"hda_disk_image": "mininet-vm-x86_64.vmdk"
},
"name": "2.2.2"
}
]
}
+15
View File
@@ -21,6 +21,15 @@
"kvm": "allow"
},
"images": [
{
"filename": "openwrt-18.06.2-x86-64-combined-ext4.img",
"version": "18.06.2",
"md5sum": "d112cd432bf51e2ddadbf9513f272fd9",
"filesize": 285736960,
"download_url": "https://downloads.openwrt.org/releases/18.06.2/targets/x86/64/",
"direct_download_url": "https://downloads.openwrt.org/releases/18.06.2/targets/x86/64/openwrt-18.06.2-x86-64-combined-ext4.img.gz",
"compression": "gzip"
},
{
"filename": "openwrt-18.06.1-x86-64-combined-ext4.img",
"version": "18.06.1",
@@ -51,6 +60,12 @@
],
"versions": [
{
"name": "openwrt 18.06.2",
"images": {
"hda_disk_image": "openwrt-18.06.2-x86-64-combined-ext4.img"
}
},
{
"name": "openwrt 18.06.1",
"images": {
"hda_disk_image": "openwrt-18.06.1-x86-64-combined-ext4.img"
+20 -21
View File
@@ -2,51 +2,50 @@
"name": "pfSense",
"category": "firewall",
"description": "The pfSense project is a free network firewall distribution, based on the FreeBSD operating system with a custom kernel and including third party free software packages for additional functionality. pfSense software, with the help of the package system, is able to provide the same functionality or more of common commercial firewalls, without any of the artificial limitations. It has successfully replaced every big name commercial firewall you can imagine in numerous installations around the world, including Check Point, Cisco PIX, Cisco ASA, Juniper, Sonicwall, Netgear, Watchguard, Astaro, and more.",
"vendor_name": "Electric Sheep Fencing LLC",
"vendor_name": "Rubicon Communications, LLC (Netgate)",
"vendor_url": "https://www.pfsense.org",
"documentation_url": "https://doc.pfsense.org/index.php/Main_Page",
"product_name": "pfSense",
"registry_version": 3,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"maintainer": "Jose Phillips",
"maintainer_email": "jose@latinol.com",
"usage": "Credentials: admin / pfsense\nTo install pfSense use all the defaults settings.\n\nNote:The default LAN port is em1.",
"port_name_format": "em{0}",
"qemu": {
"adapter_type": "e1000",
"adapters": 6,
"ram": 2048,
"arch": "x86_64",
"console_type": "telnet",
"console_type": "vnc",
"hda_disk_interface": "virtio",
"kvm": "allow",
"process_priority": "normal"
},
"images": [
{
"filename": "pfSense-CE-memstick-2.4.3-RELEASE-amd64.img",
"version": "2.4.3",
"md5sum": "b754d7e75dece5e756b6539c95714a74",
"filesize": 650392576,
"filename": "pfSense-CE-2.4.4-RELEASE-amd64.iso",
"version": "2.4.4",
"md5sum": "71386433238f96fc167d14cec9c708c6",
"filesize": 685557760,
"download_url": "https://www.pfsense.org/download/mirror.php?section=downloads"
},
{
"filename": "pfSense-CE-2.3.5-RELEASE-2g-amd64-nanobsd.img",
"version": "2.3.5",
"md5sum": "b6cb76adba3e1113892f84ea01894228",
"filesize": 1989969408,
"download_url": "https://www.pfsense.org/download/mirror.php?section=downloads"
"filename": "empty100G.qcow2",
"version": "1.0",
"md5sum": "1e6409a4523ada212dea2ebc50e50a65",
"filesize": 197120,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty100G.qcow2/download"
}
],
"versions": [
{
"name": "2.4.3",
"name": "2.4.4",
"images": {
"hda_disk_image": "pfSense-CE-memstick-2.4.3-RELEASE-amd64.img"
}
},
{
"name": "2.3.5",
"images": {
"hda_disk_image": "pfSense-CE-2.3.5-RELEASE-2g-amd64-nanobsd.img"
"hda_disk_image": "empty100G.qcow2",
"cdrom_image": "pfSense-CE-2.4.4-RELEASE-amd64.iso"
}
}
]
+46
View File
@@ -0,0 +1,46 @@
{
"name": "VPP",
"category": "router",
"description": "Vector Packet Processing (VPP) platform",
"vendor_name": "FD.IO VPP router",
"vendor_url": "https://fd.io/",
"documentation_url": "https://fd.io/resources/",
"product_name": "VPP",
"registry_version": 4,
"status": "experimental",
"availability": "free",
"maintainer": "Virginijus Magelinskas",
"maintainer_email": "virginijus.m@gmail.com",
"usage": "Login: root , pass: vpp. This appliance requires >2 vCPUs and 4GB of RAM to run",
"port_name_format": "eth{0}",
"qemu": {
"adapter_type": "virtio-net-pci",
"adapters": 5,
"ram": 4096,
"hda_disk_interface": "ide",
"arch": "x86_64",
"console_type": "telnet",
"boot_priority": "c",
"kvm": "require",
"options": "-nographic -cpu host -smp 2"
},
"images": [
{
"filename": "vpp_public-18.10-07.qcow2",
"version": "0.7",
"md5sum": "3e962985e5bbda0de4dc7893e60f6366",
"filesize": 2065825792,
"direct_download_url": "https://sigaba.net/vpp/vpp_public-18.10-07.qcow2"
}
],
"versions": [
{
"name": "18.10-07",
"images": {
"hda_disk_image": "vpp_public-18.10-07.qcow2"
}
}
]
}
+11 -11
View File
@@ -20,6 +20,7 @@ import os
import struct
import stat
import asyncio
import aiofiles
import aiohttp
import socket
@@ -46,6 +47,8 @@ from .nios.nio_ethernet import NIOEthernet
from ..utils.images import md5sum, remove_checksum, images_directories, default_images_directory, list_images
from .error import NodeError, ImageMissingError
CHUNK_SIZE = 1024 * 8 # 8KB
class BaseManager:
@@ -279,7 +282,7 @@ class BaseManager:
destination_dir = destination_node.working_dir
try:
shutil.rmtree(destination_dir)
shutil.copytree(source_node.working_dir, destination_dir)
shutil.copytree(source_node.working_dir, destination_dir, symlinks=True, ignore_dangling_symlinks=True)
except OSError as e:
raise aiohttp.web.HTTPConflict(text="Cannot duplicate node data: {}".format(e))
@@ -456,14 +459,11 @@ class BaseManager:
with open(path, "rb") as f:
await response.prepare(request)
while nio.capturing:
data = f.read(4096)
data = f.read(CHUNK_SIZE)
if not data:
await asyncio.sleep(0.1)
continue
try:
await response.write(data)
except ConnectionError:
break
await response.write(data)
except FileNotFoundError:
raise aiohttp.web.HTTPNotFound()
except PermissionError:
@@ -597,18 +597,18 @@ class BaseManager:
path = os.path.abspath(os.path.join(directory, *os.path.split(filename)))
if os.path.commonprefix([directory, path]) != directory:
raise aiohttp.web.HTTPForbidden(text="Could not write image: {}, {} is forbidden".format(filename, path))
log.info("Writing image file %s", path)
log.info("Writing image file to '{}'".format(path))
try:
remove_checksum(path)
# We store the file under his final name only when the upload is finished
tmp_path = path + ".tmp"
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(tmp_path, 'wb') as f:
async with aiofiles.open(tmp_path, 'wb') as f:
while True:
packet = await stream.read(4096)
if not packet:
chunk = await stream.read(CHUNK_SIZE)
if not chunk:
break
f.write(packet)
await f.write(chunk)
os.chmod(tmp_path, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
shutil.move(tmp_path, path)
await cancellable_wait_run_in_executor(md5sum, path)
+5 -11
View File
@@ -292,9 +292,9 @@ class Cloud(BaseNode):
if not isinstance(nio, NIOUDP):
raise NodeError("Source NIO is not UDP")
await self._ubridge_send('bridge add_nio_udp {name} {lport} {rhost} {rport}'.format(name=bridge_name,
lport=nio.lport,
rhost=nio.rhost,
rport=nio.rport))
lport=nio.lport,
rhost=nio.rhost,
rport=nio.rport))
await self._ubridge_apply_filters(bridge_name, nio.filters)
if port_info["type"] in ("ethernet", "tap"):
@@ -309,7 +309,7 @@ class Cloud(BaseNode):
if port_info["type"] == "ethernet":
network_interfaces = [interface["name"] for interface in self._interfaces()]
if not port_info["interface"] in network_interfaces:
raise NodeError("Interface '{}' could not be found on this system".format(port_info["interface"]))
raise NodeError("Interface '{}' could not be found on this system, please update '{}'".format(port_info["interface"], self.name))
if sys.platform.startswith("linux"):
await self._add_linux_ethernet(port_info, bridge_name)
@@ -401,17 +401,11 @@ class Cloud(BaseNode):
await self.start()
await self._add_ubridge_connection(nio, port_number)
self._nios[port_number] = nio
except NodeError as e:
self.project.emit("log.error", {"message": str(e)})
except (NodeError, UbridgeError) as e:
await self._stop_ubridge()
self.status = "stopped"
self._nios[port_number] = nio
# Cleanup stuff
except UbridgeError as e:
self.project.emit("log.error", {"message": str(e)})
await self._stop_ubridge()
self.status = "stopped"
self._nios[port_number] = nio
async def update_nio(self, port_number, nio):
"""
+6 -2
View File
@@ -37,6 +37,7 @@ log = logging.getLogger(__name__)
DOCKER_MINIMUM_API_VERSION = "1.25"
DOCKER_MINIMUM_VERSION = "1.13"
DOCKER_PREFERRED_API_VERSION = "1.30"
CHUNK_SIZE = 1024 * 8 # 8KB
class Docker(BaseManager):
@@ -198,12 +199,15 @@ class Docker(BaseManager):
if progress_callback:
progress_callback("Pulling '{}' from docker hub".format(image))
response = await self.http_query("POST", "images/create", params={"fromImage": image}, timeout=None)
try:
response = await self.http_query("POST", "images/create", params={"fromImage": image}, timeout=None)
except DockerError as e:
raise DockerError("Could not pull the '{}' image from Docker Hub, please check your Internet connection (original error: {})".format(image, e))
# The pull api will stream status via an HTTP JSON stream
content = ""
while True:
try:
chunk = await response.content.read(1024)
chunk = await response.content.read(CHUNK_SIZE)
except aiohttp.ServerDisconnectedError:
log.error("Disconnected from server while pulling Docker image '{}' from docker hub".format(image))
break
+3
View File
@@ -353,6 +353,9 @@ class DockerVM(BaseNode):
if self._environment:
for e in self._environment.strip().split("\n"):
e = e.strip()
if e.split("=")[0] == "":
self.project.emit("log.warning", {"message": "{} has invalid environment variable: {}".format(self.name, e)})
continue
if not e.startswith("GNS3_"):
formatted = self._format_env(variables, e)
params["Env"].append(formatted)
+3 -2
View File
@@ -535,8 +535,9 @@ class IOUVM(BaseNode):
# on newer images, see https://github.com/GNS3/gns3-server/issues/1484
try:
symlink = os.path.join(self.working_dir, os.path.basename(self.path))
if not os.path.islink(symlink):
os.symlink(self.path, symlink)
if os.path.islink(symlink):
os.unlink(symlink)
os.symlink(self.path, symlink)
except OSError as e:
raise IOUError("Could not create symbolic link: {}".format(e))
+4 -4
View File
@@ -100,10 +100,10 @@ class Qemu(BaseManager):
paths.update(["/usr/bin", "/usr/local/bin", "/opt/local/bin"])
try:
exec_dir = os.path.dirname(os.path.abspath(sys.executable))
paths.add(os.path.abspath(os.path.join(exec_dir, "../Resources/qemu/bin/")))
paths.add(os.path.abspath(os.path.join(exec_dir, "qemu/bin")))
# If the user run the server by hand from outside
except FileNotFoundError:
paths.add("/Applications/GNS3.app/Contents/Resources/qemu/bin")
paths.add("/Applications/GNS3.app/Contents/MacOS/qemu/bin")
return paths
@staticmethod
@@ -185,8 +185,8 @@ class Qemu(BaseManager):
return ""
else:
try:
output = await subprocess_check_output(qemu_path, "-version")
match = re.search(r"version\s+([0-9a-z\-\.]+)", output)
output = await subprocess_check_output(qemu_path, "-version", "-nographic")
match = re.search("version\s+([0-9a-z\-\.]+)", output)
if match:
version = match.group(1)
return version
+11 -11
View File
@@ -984,7 +984,7 @@ class QemuVM(BaseNode):
log.info("QEMU process has stopped, return code: %d", returncode)
await self.stop()
# A return code of 1 seem fine on Windows
if returncode != 0 and (returncode != 1 or not sys.platform.startswith("win")):
if returncode != 0 and (not sys.platform.startswith("win") or returncode != 1):
self.project.emit("log.error", {"message": "QEMU process has stopped, return code: {}\n{}".format(returncode, self.read_stdout())})
async def stop(self):
@@ -1013,7 +1013,7 @@ class QemuVM(BaseNode):
if self.on_close == "shutdown_signal":
await self._control_vm("system_powerdown")
await gns3server.utils.asyncio.wait_for_process_termination(self._process, timeout=30)
await gns3server.utils.asyncio.wait_for_process_termination(self._process, timeout=120)
else:
self._process.terminate()
await gns3server.utils.asyncio.wait_for_process_termination(self._process, timeout=3)
@@ -1736,18 +1736,18 @@ class QemuVM(BaseNode):
return network_options
def _graphic(self):
async def _disable_graphics(self):
"""
Adds the correct graphic options depending of the OS
Disable graphics depending of the QEMU version
"""
if sys.platform.startswith("win"):
if any(opt in self._options for opt in ["-display", "-nographic", "-curses", "-sdl" "-spice", "-vnc"]):
return []
if len(os.environ.get("DISPLAY", "")) > 0:
return []
if "-nographic" not in self._options:
version = await self.manager.get_qemu_version(self.qemu_path)
if version and parse_version(version) >= parse_version("3.0"):
return ["-display", "none"]
else:
return ["-nographic"]
return []
async def _run_with_hardware_acceleration(self, qemu_path, options):
"""
@@ -1920,12 +1920,12 @@ class QemuVM(BaseNode):
raise QemuError("Console type {} is unknown".format(self._console_type))
command.extend(self._monitor_options())
command.extend((await self._network_options()))
command.extend(self._graphic())
if self.on_close != "save_vm_state":
await self._clear_save_vm_stated()
else:
command.extend((await self._saved_state_option()))
if self._console_type == "telnet":
command.extend((await self._disable_graphics()))
if additional_options:
try:
command.extend(shlex.split(additional_options))
+4 -7
View File
@@ -37,7 +37,6 @@ from gns3server.utils import parse_version
from .vpcs_error import VPCSError
from ..adapters.ethernet_adapter import EthernetAdapter
from ..nios.nio_udp import NIOUDP
from ..nios.nio_tap import NIOTAP
from ..base_node import BaseNode
@@ -139,12 +138,10 @@ class VPCSVM(BaseNode):
:returns: path to VPCS
"""
search_path = self._manager.config.get_section_config("VPCS").get("vpcs_path", "vpcs")
path = shutil.which(search_path)
# shutil.which return None if the path doesn't exists
if not path:
return search_path
return path
vpcs_path = self._manager.config.get_section_config("VPCS").get("vpcs_path", "vpcs")
if not os.path.isabs(vpcs_path):
vpcs_path = shutil.which(vpcs_path)
return vpcs_path
@BaseNode.name.setter
def name(self, new_name):
+8
View File
@@ -41,6 +41,14 @@ class Appliance:
def status(self):
return self._data["status"]
@property
def symbol(self):
return self._data.get("symbol")
@symbol.setter
def symbol(self, new_symbol):
self._data["symbol"] = new_symbol
def __json__(self):
"""
Appliance data (a hash)
+67 -5
View File
@@ -74,7 +74,7 @@ class ApplianceManager:
os.makedirs(appliances_path, exist_ok=True)
return appliances_path
def load_appliances(self):
def load_appliances(self, symbol_theme="Classic"):
"""
Loads appliance files from disk.
"""
@@ -90,13 +90,72 @@ class ApplianceManager:
try:
with open(path, 'r', encoding='utf-8') as f:
appliance = Appliance(appliance_id, json.load(f), builtin=builtin)
appliance.__json__() # Check if loaded without error
if appliance.status != 'broken':
self._appliances[appliance.id] = appliance
json_data = appliance.__json__() # Check if loaded without error
if appliance.status != 'broken':
self._appliances[appliance.id] = appliance
if not appliance.symbol or appliance.symbol.startswith(":/symbols/"):
# apply a default symbol if the appliance has none or a default symbol
default_symbol = self._get_default_symbol(json_data, symbol_theme)
if default_symbol:
appliance.symbol = default_symbol
except (ValueError, OSError, KeyError) as e:
log.warning("Cannot load appliance file '%s': %s", path, str(e))
continue
def _get_default_symbol(self, appliance, symbol_theme):
"""
Returns the default symbol for a given appliance.
"""
from . import Controller
controller = Controller.instance()
category = appliance["category"]
if category == "guest":
if "docker" in appliance:
return controller.symbols.get_default_symbol("docker_guest", symbol_theme)
elif "qemu" in appliance:
return controller.symbols.get_default_symbol("qemu_guest", symbol_theme)
return controller.symbols.get_default_symbol(category, symbol_theme)
async def download_custom_symbols(self):
"""
Download custom appliance symbols from our GitHub registry repository.
"""
from . import Controller
symbol_dir = Controller.instance().symbols.symbols_path()
self.load_appliances()
for appliance in self._appliances.values():
symbol = appliance.symbol
if symbol and not symbol.startswith(":/symbols/"):
destination_path = os.path.join(symbol_dir, symbol)
if not os.path.exists(destination_path):
await self._download_symbol(symbol, destination_path)
# refresh the symbol cache
Controller.instance().symbols.list()
async def _download_symbol(self, symbol, destination_path):
"""
Download a custom appliance symbol from our GitHub registry repository.
"""
symbol_url = "https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{}".format(symbol)
async with aiohttp.ClientSession() as session:
async with session.get(symbol_url) as response:
if response.status != 200:
log.warning("Could not retrieve appliance symbol {} from GitHub due to HTTP error code {}".format(symbol, response.status))
else:
try:
symbol_data = await response.read()
log.info("Saving {} symbol to {}".format(symbol, destination_path))
with open(destination_path, 'wb') as f:
f.write(symbol_data)
except asyncio.TimeoutError:
log.warning("Timeout while downloading '{}'".format(symbol_url))
except OSError as e:
log.warning("Could not write appliance symbol '{}': {}".format(destination_path, e))
@locking
async def download_appliances(self):
"""
@@ -114,7 +173,7 @@ class ApplianceManager:
log.info("Appliances are already up-to-date (ETag {})".format(self._appliances_etag))
return
elif response.status != 200:
raise aiohttp.web.HTTPConflict(text="Could not retrieve appliances on GitHub due to HTTP error code {}".format(response.status))
raise aiohttp.web.HTTPConflict(text="Could not retrieve appliances from GitHub due to HTTP error code {}".format(response.status))
etag = response.headers.get("ETag")
if etag:
self._appliances_etag = etag
@@ -144,3 +203,6 @@ class ApplianceManager:
raise aiohttp.web.HTTPConflict(text="Could not write appliance file '{}': {}".format(path, e))
except ValueError as e:
raise aiohttp.web.HTTPConflict(text="Could not read appliances information from GitHub: {}".format(e))
# download the custom symbols
await self.download_custom_symbols()
+2 -23
View File
@@ -320,28 +320,6 @@ class Compute:
raise aiohttp.web.HTTPNotFound(text="{} not found on compute".format(image))
return response
async def stream_file(self, project, path, timeout=None):
"""
Read file of a project and stream it
:param project: A project object
:param path: The path of the file in the project
:param timeout: timeout
:returns: A file stream
"""
url = self._getUrl("/projects/{}/stream/{}".format(project.id, path))
response = await self._session().request("GET", url, auth=self._auth, timeout=timeout)
if response.status == 404:
raise aiohttp.web.HTTPNotFound(text="file '{}' not found on compute".format(path))
elif response.status == 403:
raise aiohttp.web.HTTPForbidden(text="forbidden to open '{}' on compute".format(path))
elif response.status != 200:
raise aiohttp.web.HTTPInternalServerError(text="Unexpected error {}: {}: while opening {} on compute".format(response.status,
response.reason,
path))
return response
async def http_query(self, method, path, data=None, dont_connect=False, **kwargs):
"""
:param dont_connect: If true do not reconnect if not connected
@@ -446,13 +424,14 @@ class Compute:
msg = json.loads(response.data)
action = msg.pop("action")
event = msg.pop("event")
project_id = msg.pop("project_id", None)
if action == "ping":
self._cpu_usage_percent = event["cpu_usage_percent"]
self._memory_usage_percent = event["memory_usage_percent"]
#FIXME: slow down number of compute events
self._controller.notification.controller_emit("compute.updated", self.__json__())
else:
await self._controller.notification.dispatch(action, event, compute_id=self.id)
await self._controller.notification.dispatch(action, event, project_id=project_id, compute_id=self.id)
elif response.type == aiohttp.WSMsgType.CLOSED or response.type == aiohttp.WSMsgType.ERROR or response.data is None:
self._connected = False
break
+14 -3
View File
@@ -37,7 +37,7 @@ class Drawing:
text, images, rectangle... They are pure SVG elements.
"""
def __init__(self, project, drawing_id=None, svg="<svg></svg>", x=0, y=0, z=2, rotation=0):
def __init__(self, project, drawing_id=None, svg="<svg></svg>", x=0, y=0, z=2, locked=False, rotation=0):
self._project = project
if drawing_id is None:
self._id = str(uuid.uuid4())
@@ -49,13 +49,14 @@ class Drawing:
self._y = y
self._z = z
self._rotation = rotation
self._locked = locked
@property
def id(self):
return self._id
@property
def ressource_filename(self):
def resource_filename(self):
"""
If the svg content has been dump to an external file return is name otherwise None
"""
@@ -157,6 +158,14 @@ class Drawing:
def z(self, val):
self._z = val
@property
def locked(self):
return self._locked
@locked.setter
def locked(self, val):
self._locked = val
@property
def rotation(self):
return self._rotation
@@ -185,7 +194,7 @@ class Drawing:
data = self.__json__()
if not svg_changed:
del data["svg"]
self._project.controller.notification.project_emit("drawing.updated", data)
self._project.emit_notification("drawing.updated", data)
self._project.dump()
def __json__(self, topology_dump=False):
@@ -198,6 +207,7 @@ class Drawing:
"x": self._x,
"y": self._y,
"z": self._z,
"locked": self._locked,
"rotation": self._rotation,
"svg": self._svg
}
@@ -207,6 +217,7 @@ class Drawing:
"x": self._x,
"y": self._y,
"z": self._z,
"locked": self._locked,
"rotation": self._rotation,
"svg": self.svg
}
+60 -52
View File
@@ -19,30 +19,34 @@ import os
import sys
import json
import asyncio
import aiofiles
import aiohttp
import zipfile
import tempfile
import zipstream
from datetime import datetime
import logging
log = logging.getLogger(__name__)
CHUNK_SIZE = 1024 * 8 # 8KB
async def export_project(project, temporary_dir, include_images=False, keep_compute_id=False, allow_all_nodes=False):
async def export_project(zstream, project, temporary_dir, include_images=False, include_snapshots=False, keep_compute_id=False, allow_all_nodes=False, reset_mac_addresses=False):
"""
Export a project to a zip file.
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 zstream: ZipStream object
:param project: Project instance
:param temporary_dir: A temporary dir where to store intermediate data
:param include images: save OS images to the zip file
:param include_images: save OS images to the zip file
:param include_snapshots: save snapshots 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
:param reset_mac_addresses: Reset MAC addresses for every nodes.
"""
# To avoid issue with data not saved we disallow the export of a running project
@@ -52,19 +56,17 @@ async def export_project(project, temporary_dir, include_images=False, keep_comp
# Make sure we save the project
project.dump()
zstream = zipstream.ZipFile(allowZip64=True)
if not os.path.exists(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"):
await _patch_project_file(project, os.path.join(project._path, file), zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir)
await _patch_project_file(project, os.path.join(project._path, file), zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir, reset_mac_addresses)
# Export the local files
for root, dirs, files in os.walk(project._path, topdown=True):
files = [f for f in files if _is_exportable(os.path.join(root, f))]
for root, dirs, files in os.walk(project._path, topdown=True, followlinks=False):
files = [f for f in files if _is_exportable(os.path.join(root, f), include_snapshots)]
for file in files:
path = os.path.join(root, file)
# check if we can export the file
@@ -73,39 +75,37 @@ async def export_project(project, temporary_dir, include_images=False, keep_comp
except OSError as e:
msg = "Could not export file {}: {}".format(path, e)
log.warning(msg)
project.controller.notification.project_emit("log.warning", {"message": msg})
project.emit_notification("log.warning", {"message": msg})
continue
# ignore the .gns3 file
if file.endswith(".gns3"):
continue
_patch_mtime(path)
zstream.write(path, os.path.relpath(path, project._path), compress_type=zipfile.ZIP_DEFLATED)
zstream.write(path, os.path.relpath(path, project._path))
# Export files from remote computes
downloaded_files = set()
for compute in project.computes:
if compute.id != "local":
compute_files = await compute.list_files(project)
for compute_file in compute_files:
if _is_exportable(compute_file["path"]):
(fd, temp_path) = tempfile.mkstemp(dir=temporary_dir)
f = open(fd, "wb", closefd=True)
if _is_exportable(compute_file["path"], include_snapshots):
log.debug("Downloading file '{}' from compute '{}'".format(compute_file["path"], compute.id))
response = await compute.download_file(project, compute_file["path"])
while True:
try:
data = await response.content.read(1024)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when downloading file '{}' from remote compute {}:{}".format(compute_file["path"], compute.host, compute.port))
if not data:
break
f.write(data)
#if response.status != 200:
# raise aiohttp.web.HTTPConflict(text="Cannot export file from compute '{}'. Compute returned status code {}.".format(compute.id, response.status))
(fd, temp_path) = tempfile.mkstemp(dir=temporary_dir)
async with aiofiles.open(fd, 'wb') as f:
while True:
try:
data = await response.content.read(CHUNK_SIZE)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when downloading file '{}' from remote compute {}:{}".format(compute_file["path"], compute.host, compute.port))
if not data:
break
await f.write(data)
response.close()
f.close()
_patch_mtime(temp_path)
zstream.write(temp_path, arcname=compute_file["path"], compress_type=zipfile.ZIP_DEFLATED)
downloaded_files.add(compute_file['path'])
return zstream
zstream.write(temp_path, arcname=compute_file["path"])
def _patch_mtime(path):
@@ -124,24 +124,31 @@ def _patch_mtime(path):
new_mtime = file_date.replace(year=1980).timestamp()
os.utime(path, (st.st_atime, new_mtime))
def _is_exportable(path):
def _is_exportable(path, include_snapshots=False):
"""
:returns: True if file should not be included in the final archive
"""
# do not export snapshots
if path.endswith("snapshots"):
# do not export snapshots by default
if include_snapshots is False and path.endswith("snapshots"):
return False
# do not export symlinks
if os.path.islink(path):
return False
# do not export directories of snapshots
if "{sep}snapshots{sep}".format(sep=os.path.sep) in path:
if include_snapshots is False and "{sep}snapshots{sep}".format(sep=os.path.sep) in path:
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"):
if include_snapshots is False and s[i + 1] == "snapshots":
return False
if s[i + 1] in ("tmp", "captures"):
return False
except (ValueError, IndexError):
pass
@@ -153,7 +160,7 @@ def _is_exportable(path):
return True
async def _patch_project_file(project, path, zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir):
async def _patch_project_file(project, path, zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir, reset_mac_addresses):
"""
Patch a project file (.gns3) to export a project.
The .gns3 file is renamed to project.gns3
@@ -177,7 +184,7 @@ async def _patch_project_file(project, path, zstream, include_images, keep_compu
if node["node_type"] == "virtualbox" and node.get("properties", {}).get("linked_clone"):
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"]:
if not allow_all_nodes and node["node_type"] in ["virtualbox", "vmware"]:
raise aiohttp.web.HTTPConflict(text="Projects with a {} node cannot be exported".format(node["node_type"]))
if not keep_compute_id:
@@ -186,6 +193,10 @@ async def _patch_project_file(project, path, zstream, include_images, keep_compu
if "properties" in node and node["node_type"] != "docker":
for prop, value in node["properties"].items():
# reset the MAC address
if reset_mac_addresses and prop in ("mac_addr", "mac_address"):
node["properties"][prop] = None
if node["node_type"] == "iou":
if not prop == "path":
continue
@@ -222,6 +233,7 @@ async def _patch_project_file(project, path, zstream, include_images, keep_compu
zstream.writestr("project.gns3", json.dumps(topology).encode())
return images
def _export_local_image(image, zstream):
"""
Exports a local image to the zip file.
@@ -256,30 +268,26 @@ async def _export_remote_images(project, compute_id, image_type, image, project_
Export specific image from remote compute.
"""
log.info("Downloading image '{}' from compute '{}'".format(image, compute_id))
log.debug("Downloading image '{}' from compute '{}'".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))
(fd, temp_path) = tempfile.mkstemp(dir=temporary_dir)
f = open(fd, "wb", closefd=True)
response = await compute.download_image(image_type, image)
if response.status != 200:
raise aiohttp.web.HTTPConflict(text="Cannot export image from '{}' compute. Compute returned status code {}.".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:
try:
data = await response.content.read(1024)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when downloading image '{}' from remote compute {}:{}".format(image, compute.host, compute.port))
if not data:
break
f.write(data)
(fd, temp_path) = tempfile.mkstemp(dir=temporary_dir)
async with aiofiles.open(fd, 'wb') as f:
while True:
try:
data = await response.content.read(CHUNK_SIZE)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when downloading image '{}' from remote compute {}:{}".format(image, compute.host, compute.port))
if not data:
break
await f.write(data)
response.close()
f.close()
arcname = os.path.join("images", image_type, image)
log.info("Saved {}".format(arcname))
project_zipfile.write(temp_path, arcname=arcname, compress_type=zipfile.ZIP_DEFLATED)
+15 -7
View File
@@ -53,22 +53,30 @@ class HyperVGNS3VM(BaseGNS3VM):
Checks if the GNS3 VM can run on Hyper-V.
"""
if not sys.platform.startswith("win") or sys.getwindowsversion().major < 10:# or sys.getwindowsversion().build < 14393:
raise GNS3VMError("Hyper-V nested virtualization is only supported on Windows 10 and Windows Server 2016 or later")
if not sys.platform.startswith("win"):
raise GNS3VMError("Hyper-V is only supported on Windows")
if sys.getwindowsversion().platform_version[0] < 10:
raise GNS3VMError("Windows 10/Windows Server 2016 or a later version is required to run Hyper-V with nested virtualization enabled (version {} detected)".format(sys.getwindowsversion().platform_version[0]))
if sys.getwindowsversion().platform_version[0] == 10 and sys.getwindowsversion().platform_version[1] == 0:
if sys.getwindowsversion().platform_version[2] < 14393:
raise GNS3VMError("Hyper-V with nested virtualization is only supported on Windows 10 Anniversary Update (build 10.0.14393) or later")
try:
conn = wmi.WMI()
except wmi.x_wmi as e:
raise GNS3VMError("Could not connect to WMI: {}".format(e))
if not conn.Win32_ComputerSystem()[0].HypervisorPresent:
raise GNS3VMError("Hyper-V is not installed or activated")
if conn.Win32_Processor()[0].Manufacturer != "GenuineIntel":
raise GNS3VMError("An Intel processor is required by Hyper-V to support nested virtualization")
if not conn.Win32_ComputerSystem()[0].HypervisorPresent:
raise GNS3VMError("Hyper-V is not installed")
if not conn.Win32_Processor()[0].VirtualizationFirmwareEnabled:
raise GNS3VMError("Nested Virtualization (VT-x) is not enabled on this system")
# This is not reliable
#if not conn.Win32_Processor()[0].VirtualizationFirmwareEnabled:
# raise GNS3VMError("Nested Virtualization (VT-x) is not enabled on this system")
def _connect(self):
"""
@@ -69,13 +69,16 @@ class VMwareGNS3VM(BaseGNS3VM):
if ram % 4 != 0:
raise GNS3VMError("Allocated memory {} for the GNS3 VM must be a multiple of 4".format(ram))
available_vcpus = psutil.cpu_count(logical=False)
available_vcpus = psutil.cpu_count()
if vcpus > available_vcpus:
raise GNS3VMError("You have allocated too many vCPUs for the GNS3 VM! (max available is {} vCPUs)".format(available_vcpus))
try:
pairs = VMware.parse_vmware_file(self._vmx_path)
pairs["numvcpus"] = str(vcpus)
cores_per_sockets = int(available_vcpus / psutil.cpu_count(logical=False))
if cores_per_sockets >= 1:
pairs["cpuid.corespersocket"] = str(cores_per_sockets)
pairs["memsize"] = str(ram)
VMware.write_vmx_file(self._vmx_path, pairs)
log.info("GNS3 VM vCPU count set to {} and RAM amount set to {}".format(vcpus, ram))
+6 -3
View File
@@ -20,7 +20,6 @@ import sys
import json
import uuid
import shutil
import asyncio
import zipfile
import aiohttp
import itertools
@@ -182,9 +181,11 @@ async def _move_files_to_compute(compute, project_id, directory, files_path):
location = os.path.join(directory, files_path)
if os.path.exists(location):
for (dirpath, dirnames, filenames) in os.walk(location):
for (dirpath, dirnames, filenames) in os.walk(location, followlinks=False):
for filename in filenames:
path = os.path.join(dirpath, filename)
if os.path.islink(path):
continue
dst = os.path.relpath(path, directory)
await _upload_file(compute, project_id, path, dst)
await wait_run_in_executor(shutil.rmtree, os.path.join(directory, files_path))
@@ -210,9 +211,11 @@ def _import_images(controller, path):
image_dir = controller.images_path()
root = os.path.join(path, "images")
for (dirpath, dirnames, filenames) in os.walk(root):
for (dirpath, dirnames, filenames) in os.walk(root, followlinks=False):
for filename in filenames:
path = os.path.join(dirpath, filename)
if os.path.islink(path):
continue
dst = os.path.join(image_dir, os.path.relpath(path, root))
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.move(path, dst)
+6 -6
View File
@@ -199,14 +199,14 @@ class Link:
self._filters = new_filters
if self._created:
await self.update()
self._project.controller.notification.project_emit("link.updated", self.__json__())
self._project.emit_notification("link.updated", self.__json__())
self._project.dump()
async def update_suspend(self, value):
if value != self._suspended:
self._suspended = value
await self.update()
self._project.controller.notification.project_emit("link.updated", self.__json__())
self._project.emit_notification("link.updated", self.__json__())
self._project.dump()
@property
@@ -269,7 +269,7 @@ class Link:
n["node"].add_link(self)
n["port"].link = self
self._created = True
self._project.controller.notification.project_emit("link.created", self.__json__())
self._project.emit_notification("link.created", self.__json__())
if dump:
self._project.dump()
@@ -282,7 +282,7 @@ class Link:
label = node_data.get("label")
if label:
port["label"] = label
self._project.controller.notification.project_emit("link.updated", self.__json__())
self._project.emit_notification("link.updated", self.__json__())
self._project.dump()
async def create(self):
@@ -317,7 +317,7 @@ class Link:
self._capturing = True
self._capture_file_name = capture_file_name
self._project.controller.notification.project_emit("link.updated", self.__json__())
self._project.emit_notification("link.updated", self.__json__())
async def stop_capture(self):
"""
@@ -325,7 +325,7 @@ class Link:
"""
self._capturing = False
self._project.controller.notification.project_emit("link.updated", self.__json__())
self._project.emit_notification("link.updated", self.__json__())
def pcap_streaming_url(self):
"""
+15 -4
View File
@@ -34,7 +34,7 @@ log = logging.getLogger(__name__)
class Node:
# This properties are used only on controller and are not forwarded to the compute
CONTROLLER_ONLY_PROPERTIES = ["x", "y", "z", "width", "height", "symbol", "label", "console_host",
CONTROLLER_ONLY_PROPERTIES = ["x", "y", "z", "locked", "width", "height", "symbol", "label", "console_host",
"port_name_format", "first_port_name", "port_segment_size", "ports",
"category", "console_auto_start"]
@@ -74,6 +74,7 @@ class Node:
self._x = 0
self._y = 0
self._z = 1 # default z value is 1
self._locked = False
self._ports = None
self._symbol = None
self._custom_adapters = []
@@ -236,6 +237,14 @@ class Node:
def z(self, val):
self._z = val
@property
def locked(self):
return self._locked
@locked.setter
def locked(self, val):
self._locked = val
@property
def width(self):
return self._width
@@ -405,7 +414,7 @@ class Node:
await self.parse_node_response(response.json)
elif old_json != self.__json__():
# We send notif only if object has changed
self.project.controller.notification.project_emit("node.updated", self.__json__())
self.project.emit_notification("node.updated", self.__json__())
self.project.dump()
async def parse_node_response(self, response):
@@ -563,13 +572,13 @@ class Node:
for directory in images_directories(type):
image = os.path.join(directory, img)
if os.path.exists(image):
self.project.controller.notification.project_emit("log.info", {"message": "Uploading missing image {}".format(img)})
self.project.emit_notification("log.info", {"message": "Uploading missing image {}".format(img)})
try:
with open(image, 'rb') as f:
await self._compute.post("/{}/images/{}".format(self._node_type, os.path.basename(img)), data=f, timeout=None)
except OSError as e:
raise aiohttp.web.HTTPConflict(text="Can't upload {}: {}".format(image, str(e)))
self.project.controller.notification.project_emit("log.info", {"message": "Upload finished for {}".format(img)})
self.project.emit_notification("log.info", {"message": "Upload finished for {}".format(img)})
return True
return False
@@ -681,6 +690,7 @@ class Node:
"x": self._x,
"y": self._y,
"z": self._z,
"locked": self._locked,
"width": self._width,
"height": self._height,
"symbol": self._symbol,
@@ -708,6 +718,7 @@ class Node:
"x": self._x,
"y": self._y,
"z": self._z,
"locked": self._locked,
"width": self._width,
"height": self._height,
"symbol": self._symbol,
+13 -13
View File
@@ -33,19 +33,19 @@ class Notification:
self._controller_listeners = []
@contextmanager
def project_queue(self, project):
def project_queue(self, project_id):
"""
Get a queue of notifications
Use it with Python with
"""
queue = NotificationQueue()
self._project_listeners.setdefault(project.id, set())
self._project_listeners[project.id].add(queue)
self._project_listeners.setdefault(project_id, set())
self._project_listeners[project_id].add(queue)
try:
yield queue
finally:
self._project_listeners[project.id].remove(queue)
self._project_listeners[project_id].remove(queue)
@contextmanager
def controller_queue(self):
@@ -84,14 +84,14 @@ class Notification:
for controller_listener in self._controller_listeners:
controller_listener.put_nowait((action, event, {}))
def project_has_listeners(self, project):
def project_has_listeners(self, project_id):
"""
:param project_id: Project object
:returns: True if client listen this project
"""
return project.id in self._project_listeners and len(self._project_listeners[project.id]) > 0
return project_id in self._project_listeners and len(self._project_listeners[project_id]) > 0
async def dispatch(self, action, event, compute_id):
async def dispatch(self, action, event, project_id, compute_id):
"""
Notification received from compute node. Send it directly
to clients or process it
@@ -111,12 +111,12 @@ class Notification:
except (aiohttp.web.HTTPNotFound, aiohttp.web.HTTPForbidden): # Project closing
return
elif action == "ping":
event["compute_id"] = compute_id
self.project_emit(action, event)
event["compute_id"] = compute_id
self.project_emit(action, event)
else:
self.project_emit(action, event)
self.project_emit(action, event, project_id)
def project_emit(self, action, event):
def project_emit(self, action, event, project_id=None):
"""
Send a notification to clients scoped by projects
@@ -136,8 +136,8 @@ class Notification:
except TypeError: # If we receive a mock as an event it will raise TypeError when using json dump
pass
if "project_id" in event:
self._send_event_to_project(event["project_id"], action, event)
if "project_id" in event or project_id:
self._send_event_to_project(event.get("project_id", project_id), action, event)
else:
self._send_event_to_all_projects(action, event)
+64 -32
View File
@@ -21,9 +21,12 @@ import json
import uuid
import copy
import shutil
import time
import asyncio
import aiohttp
import aiofiles
import tempfile
import zipfile
from uuid import UUID, uuid4
@@ -37,7 +40,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 locking
from ..utils.asyncio import wait_run_in_executor
from ..utils.asyncio import aiozipstream
from .export_project import export_project
from .import_project import import_project
@@ -91,6 +94,7 @@ class Project:
self._supplier = supplier
self._loading = False
self._closing = False
# Disallow overwrite of existing project
if project_id is None and path is not None:
@@ -122,6 +126,16 @@ class Project:
assert self._status != "closed"
self.dump()
def emit_notification(self, action, event):
"""
Emit a notification to all clients using this project.
:param action: Action name
:param event: Event to send
"""
self.controller.notification.project_emit(action, event, project_id=self.id)
async def update(self, **kwargs):
"""
Update the project
@@ -135,7 +149,7 @@ class Project:
# We send notif only if object has changed
if old_json != self.__json__():
self.controller.notification.project_emit("project.updated", self.__json__())
self.emit_notification("project.updated", self.__json__())
self.dump()
# update on computes
@@ -533,7 +547,7 @@ class Project:
self._project_created_on_compute.add(compute)
await node.create()
self._nodes[node.id] = node
self.controller.notification.project_emit("node.created", node.__json__())
self.emit_notification("node.created", node.__json__())
if dump:
self.dump()
return node
@@ -558,7 +572,7 @@ class Project:
del self._nodes[node.id]
await node.destroy()
self.dump()
self.controller.notification.project_emit("node.deleted", node.__json__())
self.emit_notification("node.deleted", node.__json__())
@open_required
def get_node(self, node_id):
@@ -623,7 +637,7 @@ class Project:
if drawing_id not in self._drawings:
drawing = Drawing(self, drawing_id=drawing_id, **kwargs)
self._drawings[drawing.id] = drawing
self.controller.notification.project_emit("drawing.created", drawing.__json__())
self.emit_notification("drawing.created", drawing.__json__())
if dump:
self.dump()
return drawing
@@ -644,7 +658,7 @@ class Project:
drawing = self.get_drawing(drawing_id)
del self._drawings[drawing.id]
self.dump()
self.controller.notification.project_emit("drawing.deleted", drawing.__json__())
self.emit_notification("drawing.deleted", drawing.__json__())
@open_required
async def add_link(self, link_id=None, dump=True):
@@ -671,7 +685,7 @@ class Project:
if force_delete is False:
raise
self.dump()
self.controller.notification.project_emit("link.deleted", link.__json__())
self.emit_notification("link.deleted", link.__json__())
@open_required
def get_link(self, link_id):
@@ -730,9 +744,14 @@ class Project:
del self._snapshots[snapshot.id]
os.remove(snapshot.path)
@locking
async def close(self, ignore_notification=False):
if self._status == "closed":
if self._status == "closed" or self._closing:
return
if self._loading:
log.warning("Closing project '{}' ignored because it is being loaded".format(self.name))
return
self._closing = True
await self.stop_all()
for compute in list(self._project_created_on_compute):
try:
@@ -740,25 +759,28 @@ class Project:
# We don't care if a compute is down at this step
except (ComputeError, aiohttp.web.HTTPError, aiohttp.ClientResponseError, TimeoutError):
pass
self._cleanPictures()
self._clean_pictures()
self._status = "closed"
if not ignore_notification:
self.controller.notification.project_emit("project.closed", self.__json__())
self.emit_notification("project.closed", self.__json__())
self.reset()
self._closing = False
def _cleanPictures(self):
def _clean_pictures(self):
"""
Delete unused images
Delete unused pictures.
"""
# Project have been deleted
if not os.path.exists(self.path):
# Project have been deleted or is loading or is not opened
if not os.path.exists(self.path) or self._loading or self._status != "opened":
return
try:
pictures = set(os.listdir(self.pictures_directory))
for drawing in self._drawings.values():
try:
pictures.remove(drawing.ressource_filename)
resource_filename = drawing.resource_filename
if resource_filename:
pictures.remove(resource_filename)
except KeyError:
pass
@@ -770,10 +792,12 @@ class Project:
except KeyError:
pass
for pict in pictures:
os.remove(os.path.join(self.pictures_directory, pict))
for pic_filename in pictures:
path = os.path.join(self.pictures_directory, pic_filename)
log.info("Deleting unused picture '{}'".format(path))
os.remove(path)
except OSError as e:
log.warning(str(e))
log.warning("Could not delete unused pictures: {}".format(e))
async def delete(self):
@@ -823,6 +847,10 @@ class Project:
"""
Load topology elements
"""
if self._closing is True:
raise aiohttp.web.HTTPConflict(text="Project is closing, please try again in a few seconds...")
if self._status == "opened":
return
@@ -933,15 +961,6 @@ class Project:
while self._loading:
await 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)
async def duplicate(self, name=None, location=None):
"""
Duplicate a project
@@ -961,12 +980,24 @@ class Project:
self.dump()
assert self._status != "closed"
try:
begin = time.time()
with tempfile.TemporaryDirectory() as tmpdir:
zipstream = await export_project(self, tmpdir, keep_compute_id=True, allow_all_nodes=True)
project_path = os.path.join(tmpdir, "project.gns3p")
await wait_run_in_executor(self._create_duplicate_project_file, project_path, zipstream)
with open(project_path, "rb") as f:
project = await import_project(self._controller, str(uuid.uuid4()), f, location=location, name=name, keep_compute_id=True)
# Do not compress the exported project when duplicating
with aiozipstream.ZipFile(compression=zipfile.ZIP_STORED) as zstream:
await export_project(zstream, self, tmpdir, keep_compute_id=True, allow_all_nodes=True, reset_mac_addresses=True)
# export the project to a temporary location
project_path = os.path.join(tmpdir, "project.gns3p")
log.info("Exporting project to '{}'".format(project_path))
async with aiofiles.open(project_path, 'wb') as f:
async for chunk in zstream:
await f.write(chunk)
# import the temporary project
with open(project_path, "rb") as f:
project = await import_project(self._controller, str(uuid.uuid4()), f, location=location, name=name, keep_compute_id=True)
log.info("Project '{}' duplicated in {:.4f} seconds".format(project.name, time.time() - begin))
except (ValueError, OSError, UnicodeEncodeError) as e:
raise aiohttp.web.HTTPConflict(text="Cannot duplicate project: {}".format(str(e)))
@@ -1056,6 +1087,7 @@ class Project:
data['x'] = x
data['y'] = y
data['z'] = z
data['locked'] = False # duplicated node must not be locked
new_node_uuid = str(uuid.uuid4())
new_node = await self.add_node(
node.compute,
+16 -13
View File
@@ -20,14 +20,20 @@ import os
import uuid
import shutil
import tempfile
import asyncio
import aiofiles
import zipfile
import time
import aiohttp.web
from datetime import datetime, timezone
from ..utils.asyncio import wait_run_in_executor
from ..utils.asyncio import aiozipstream
from .export_project import export_project
from .import_project import import_project
import logging
log = logging.getLogger(__name__)
# The string use to extract the date from the filename
FILENAME_TIME_FORMAT = "%d%m%y_%H%M%S"
@@ -73,15 +79,6 @@ 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)
async def create(self):
"""
Create the snapshot
@@ -97,9 +94,15 @@ class Snapshot:
raise aiohttp.web.HTTPInternalServerError(text="Could not create the snapshot directory '{}': {}".format(snapshot_directory, e))
try:
begin = time.time()
with tempfile.TemporaryDirectory() as tmpdir:
zipstream = await export_project(self._project, tmpdir, keep_compute_id=True, allow_all_nodes=True)
await wait_run_in_executor(self._create_snapshot_file, zipstream)
# Do not compress the snapshots
with aiozipstream.ZipFile(compression=zipfile.ZIP_STORED) as zstream:
await export_project(zstream, self._project, tmpdir, keep_compute_id=True, allow_all_nodes=True)
async with aiofiles.open(self.path, 'wb') as f:
async for chunk in zstream:
await f.write(chunk)
log.info("Snapshot '{}' created in {:.4f} seconds".format(self.name, time.time() - begin))
except (ValueError, OSError, RuntimeError) as e:
raise aiohttp.web.HTTPConflict(text="Could not create snapshot file '{}': {}".format(self.path, e))
@@ -122,7 +125,7 @@ class Snapshot:
except (OSError, PermissionError) as e:
raise aiohttp.web.HTTPConflict(text=str(e))
await project.open()
self._project.controller.notification.project_emit("snapshot.restored", self.__json__())
self._project.emit_notification("snapshot.restored", self.__json__())
return self._project
def __json__(self):
+18 -18
View File
@@ -31,13 +31,13 @@ CLASSIC_SYMBOL_THEME = {"cloud": ":/symbols/classic/cloud.svg",
"vmware_guest": ":/symbols/classic/vmware_guest.svg",
"docker_guest": ":/symbols/classic/docker_guest.svg"}
INFINITY_SQUARE_BLUE_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/blue/cloud.svg",
AFFINITY_SQUARE_BLUE_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/blue/cloud.svg",
"ethernet_switch": ":/symbols/affinity/square/blue/switch.svg",
"ethernet_hub": ":/symbols/affinity/square/blue/hub.svg",
"frame_relay_switch.svg": ":/symbols/affinity/square/blue/isdn.svg",
"atm_switch": ":/symbols/affinity/square/blue/atm.svg",
"router": ":/symbols/affinity/square/blue/router.svg",
"multilayer_switch": ":/symbols/affinity/square/blue/multilayer_switch.svg",
"multilayer_switch": ":/symbols/affinity/square/blue/switch_multilayer.svg",
"firewall": ":/symbols/affinity/square/blue/firewall3.svg",
"computer": ":/symbols/affinity/square/blue/client.svg",
"vpcs_guest": ":/symbols/affinity/square/blue/client.svg",
@@ -46,13 +46,13 @@ INFINITY_SQUARE_BLUE_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/blue/cl
"vmware_guest": ":/symbols/affinity/square/blue/vmware.svg",
"docker_guest": ":/symbols/affinity/square/blue/docker.svg"}
INFINITY_SQUARE_RED_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/red/cloud.svg",
AFFINITY_SQUARE_RED_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/red/cloud.svg",
"ethernet_switch": ":/symbols/affinity/square/red/switch.svg",
"ethernet_hub": ":/symbols/affinity/square/red/hub.svg",
"frame_relay_switch": ":/symbols/affinity/square/red/isdn.svg",
"atm_switch": ":/symbols/affinity/square/red/atm.svg",
"router": ":/symbols/affinity/square/red/router.svg",
"multilayer_switch": ":/symbols/affinity/square/red/multilayer_switch.svg",
"multilayer_switch": ":/symbols/affinity/square/red/switch_multilayer.svg",
"firewall": ":/symbols/affinity/square/red/firewall3.svg",
"computer": ":/symbols/affinity/square/red/client.svg",
"vpcs_guest": ":/symbols/affinity/square/red/client.svg",
@@ -61,13 +61,13 @@ INFINITY_SQUARE_RED_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/red/clou
"vmware_guest": ":/symbols/affinity/square/red/vmware.svg",
"docker_guest": ":/symbols/affinity/square/red/docker.svg"}
INFINITY_SQUARE_GRAY_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/gray/cloud.svg",
AFFINITY_SQUARE_GRAY_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/gray/cloud.svg",
"ethernet_switch": ":/symbols/affinity/square/gray/switch.svg",
"ethernet_hub": ":/symbols/affinity/square/gray/hub.svg",
"frame_relay_switch": ":/symbols/affinity/square/gray/isdn.svg",
"atm_switch": ":/symbols/affinity/square/gray/atm.svg",
"router": ":/symbols/affinity/square/gray/router.svg",
"multilayer_switch": ":/symbols/affinity/square/gray/multilayer_switch.svg",
"multilayer_switch": ":/symbols/affinity/square/gray/switch_multilayer.svg",
"firewall": ":/symbols/affinity/square/gray/firewall3.svg",
"computer": ":/symbols/affinity/square/gray/client.svg",
"vpcs_guest": ":/symbols/affinity/square/gray/client.svg",
@@ -76,13 +76,13 @@ INFINITY_SQUARE_GRAY_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/gray/cl
"vmware_guest": ":/symbols/affinity/square/gray/vmware.svg",
"docker_guest": ":/symbols/affinity/square/gray/docker.svg"}
INFINITY_CIRCLE_BLUE_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/blue/cloud.svg",
AFFINITY_CIRCLE_BLUE_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/blue/cloud.svg",
"ethernet_switch": ":/symbols/affinity/circle/blue/switch.svg",
"ethernet_hub": ":/symbols/affinity/circle/blue/hub.svg",
"frame_relay_switch": ":/symbols/affinity/circle/blue/isdn.svg",
"atm_switch": ":/symbols/affinity/circle/blue/atm.svg",
"router": ":/symbols/affinity/circle/blue/router.svg",
"multilayer_switch": ":/symbols/affinity/circle/blue/multilayer_switch.svg",
"multilayer_switch": ":/symbols/affinity/circle/blue/switch_multilayer.svg",
"firewall": ":/symbols/affinity/circle/blue/firewall3.svg",
"computer": ":/symbols/affinity/circle/blue/client.svg",
"vpcs_guest": ":/symbols/affinity/circle/blue/client.svg",
@@ -91,13 +91,13 @@ INFINITY_CIRCLE_BLUE_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/blue/cl
"vmware_guest": ":/symbols/affinity/circle/blue/vmware.svg",
"docker_guest": ":/symbols/affinity/circle/blue/docker.svg"}
INFINITY_CIRCLE_RED_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/red/cloud.svg",
AFFINITY_CIRCLE_RED_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/red/cloud.svg",
"ethernet_switch": ":/symbols/affinity/circle/red/switch.svg",
"ethernet_hub": ":/symbols/affinity/circle/red/hub.svg",
"frame_relay_switch": ":/symbols/affinity/circle/red/isdn.svg",
"atm_switch": ":/symbols/affinity/circle/red/atm.svg",
"router": ":/symbols/affinity/circle/red/router.svg",
"multilayer_switch": ":/symbols/affinity/circle/red/multilayer_switch.svg",
"multilayer_switch": ":/symbols/affinity/circle/red/switch_multilayer.svg",
"firewall": ":/symbols/affinity/circle/red/firewall3.svg",
"computer": ":/symbols/affinity/circle/red/client.svg",
"vpcs_guest": ":/symbols/affinity/circle/red/client.svg",
@@ -106,13 +106,13 @@ INFINITY_CIRCLE_RED_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/red/clou
"vmware_guest": ":/symbols/affinity/circle/red/vmware.svg",
"docker_guest": ":/symbols/affinity/circle/red/docker.svg"}
INFINITY_CIRCLE_GRAY_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/gray/cloud.svg",
AFFINITY_CIRCLE_GRAY_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/gray/cloud.svg",
"ethernet_switch": ":/symbols/affinity/circle/gray/switch.svg",
"ethernet_hub": ":/symbols/affinity/circle/gray/hub.svg",
"frame_relay_switch": ":/symbols/affinity/circle/gray/isdn.svg",
"atm_switch": ":/symbols/affinity/circle/gray/atm.svg",
"router": ":/symbols/affinity/circle/gray/router.svg",
"multilayer_switch": ":/symbols/affinity/circle/gray/multilayer_switch.svg",
"multilayer_switch": ":/symbols/affinity/circle/gray/switch_multilayer.svg",
"firewall": ":/symbols/affinity/circle/gray/firewall3.svg",
"computer": ":/symbols/affinity/circle/gray/client.svg",
"vpcs_guest": ":/symbols/affinity/circle/gray/client.svg",
@@ -122,9 +122,9 @@ INFINITY_CIRCLE_GRAY_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/gray/cl
"docker_guest": ":/symbols/affinity/circle/gray/docker.svg"}
BUILTIN_SYMBOL_THEMES = {"Classic": CLASSIC_SYMBOL_THEME,
"Infinity-square-blue": INFINITY_SQUARE_BLUE_SYMBOL_THEME,
"Infinity-square-red": INFINITY_SQUARE_RED_SYMBOL_THEME,
"Infinity-square-gray": INFINITY_SQUARE_GRAY_SYMBOL_THEME,
"Infinity-circle-blue": INFINITY_CIRCLE_BLUE_SYMBOL_THEME,
"Infinity-circle-red": INFINITY_CIRCLE_RED_SYMBOL_THEME,
"Infinity-circle-gray": INFINITY_CIRCLE_GRAY_SYMBOL_THEME}
"Affinity-square-blue": AFFINITY_SQUARE_BLUE_SYMBOL_THEME,
"Affinity-square-red": AFFINITY_SQUARE_RED_SYMBOL_THEME,
"Affinity-square-gray": AFFINITY_SQUARE_GRAY_SYMBOL_THEME,
"Affinity-circle-blue": AFFINITY_CIRCLE_BLUE_SYMBOL_THEME,
"Affinity-circle-red": AFFINITY_CIRCLE_RED_SYMBOL_THEME,
"Affinity-circle-gray": AFFINITY_CIRCLE_GRAY_SYMBOL_THEME}
+32 -14
View File
@@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import aiohttp
import posixpath
from .symbol_themes import BUILTIN_SYMBOL_THEMES
@@ -53,10 +54,24 @@ class Symbols:
def theme(self, theme):
if not self._themes.get(theme):
log.error("Could not find symbol theme '{}'".format(theme))
return
raise aiohttp.web.HTTPNotFound(text="Could not find symbol theme '{}'".format(theme))
self._current_theme = theme
def default_symbols(self):
return BUILTIN_SYMBOL_THEMES
def get_default_symbol(self, symbol, symbol_theme):
theme = self._themes.get(symbol_theme, None)
if not theme:
raise aiohttp.web.HTTPNotFound(text="Could not find symbol theme '{}'".format(symbol_theme))
symbol_path = theme.get(symbol)
if symbol_path not in self._symbols_path:
log.warning("Default symbol {} was not found".format(symbol_path))
return None
return symbol_path
def list(self):
self._symbols_path = {}
@@ -67,9 +82,11 @@ class Symbols:
if filename.startswith('.'):
continue
symbol_file = posixpath.normpath(os.path.relpath(os.path.join(root, filename), get_resource("symbols"))).replace('\\', '/')
theme = posixpath.dirname(symbol_file).replace('/', '-').capitalize()
symbol_id = ':/symbols/' + symbol_file
symbols.append({'symbol_id': symbol_id,
'filename': symbol_file,
'filename': filename,
'theme': theme,
'builtin': True})
self._symbols_path[symbol_id] = os.path.join(root, filename)
@@ -81,8 +98,9 @@ class Symbols:
continue
symbol_file = posixpath.normpath(os.path.relpath(os.path.join(root, filename), directory)).replace('\\', '/')
symbols.append({'symbol_id': symbol_file,
'filename': symbol_file,
'builtin': False,})
'filename': filename,
'builtin': False,
'theme': "Custom symbols"})
self._symbols_path[symbol_file] = os.path.join(root, filename)
symbols.sort(key=lambda x: x["filename"])
@@ -99,21 +117,21 @@ class Symbols:
return directory
def get_path(self, symbol_id):
symbol_filename = os.path.splitext(os.path.basename(symbol_id))[0]
theme = self._themes.get(self._current_theme, {})
if not theme:
log.error("Could not find symbol theme '{}'".format(self._current_theme))
try:
return self._symbols_path[theme.get(symbol_filename, symbol_id)]
return self._symbols_path[symbol_id]
except KeyError:
# Symbol not found, let's refresh the cache
try:
self.list()
return self._symbols_path[symbol_id]
except (OSError, KeyError):
log.warning("Could not retrieve symbol '{}'".format(symbol_id))
symbols_path = self._symbols_path
return symbols_path.get(":/symbols/classic/{}".format(os.path.basename(symbol_id)), symbols_path[":/symbols/classic/computer.svg"])
# try to return a symbol with the same name from the classic theme
symbol = self._symbols_path.get(":/symbols/classic/{}".format(os.path.basename(symbol_id)))
if symbol:
return symbol
else:
# return the default computer symbol
log.warning("Could not retrieve symbol '{}'".format(symbol_id))
return self._symbols_path[":/symbols/classic/computer.svg"]
def get_size(self, symbol_id):
try:
+2 -2
View File
@@ -160,7 +160,7 @@ def load_topology(path):
topo = _convert_2_1_0(topo, path)
# Version GNS3 2.2 dev (for project created with 2.2dev).
# Appliance ID has been repleace by Template ID
# Appliance ID has been replaced by Template ID
if topo["revision"] == 9:
for node in topo.get("topology", {}).get("nodes", []):
if "appliance_id" in node:
@@ -177,7 +177,7 @@ def load_topology(path):
try:
with open(path, "w+", encoding="utf-8") as f:
json.dump(topo, f, indent=4, sort_keys=True)
except (OSError) as e:
except OSError as e:
raise aiohttp.web.HTTPConflict(text="Can't write the topology {}: {}".format(path, str(e)))
return topo
+1 -1
View File
@@ -58,7 +58,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "https://f5e2d00b16764b7d8bb996a9c8980185:4c01020e90ee4657956bc680a25f9959@sentry.io/38482"
DSN = "https://b53e4cbcb01c453f9728dbf891424d18:061bfafc9c184f9bb2012d394f860fc4@sentry.io/38482"
if hasattr(sys, "frozen"):
cacert = get_resource("cacert.pem")
if cacert is not None and os.path.isfile(cacert):
@@ -493,7 +493,7 @@ class DynamipsVMHandler:
if filename[0] == ".":
raise aiohttp.web.HTTPForbidden()
await response.file(image_path)
await response.stream_file(image_path)
@Route.post(
r"/projects/{project_id}/dynamips/nodes/{node_id}/duplicate",
@@ -451,4 +451,4 @@ class IOUHandler:
if filename[0] == ".":
raise aiohttp.web.HTTPForbidden()
await response.file(image_path)
await response.stream_file(image_path)
@@ -37,6 +37,8 @@ from gns3server.schemas.project import (
import logging
log = logging.getLogger()
CHUNK_SIZE = 1024 * 8 # 8KB
class ProjectHandler:
@@ -248,64 +250,7 @@ class ProjectHandler:
raise aiohttp.web.HTTPForbidden()
path = os.path.join(project.path, path)
response.content_type = "application/octet-stream"
response.set_status(200)
response.enable_chunked_encoding()
try:
with open(path, "rb") as f:
await response.prepare(request)
while True:
data = f.read(4096)
if not data:
break
await response.write(data)
except FileNotFoundError:
raise aiohttp.web.HTTPNotFound()
except PermissionError:
raise aiohttp.web.HTTPForbidden()
@Route.get(
r"/projects/{project_id}/stream/{path:.+}",
description="Stream a file from a project",
parameters={
"project_id": "Project UUID",
},
status_codes={
200: "File returned",
403: "Permission denied",
404: "The file doesn't exist"
})
async def stream_file(request, response):
pm = ProjectManager.instance()
project = pm.get_project(request.match_info["project_id"])
path = request.match_info["path"]
path = os.path.normpath(path)
# Raise an error if user try to escape
if path[0] == ".":
raise aiohttp.web.HTTPForbidden()
path = os.path.join(project.path, path)
response.content_type = "application/octet-stream"
response.set_status(200)
response.enable_chunked_encoding()
# FIXME: file streaming is never stopped
try:
with open(path, "rb") as f:
await response.prepare(request)
while True:
data = f.read(4096)
if not data:
await asyncio.sleep(0.1)
await response.write(data)
except FileNotFoundError:
raise aiohttp.web.HTTPNotFound()
except PermissionError:
raise aiohttp.web.HTTPForbidden()
await response.stream_file(path)
@Route.post(
r"/projects/{project_id}/files/{path:.+}",
@@ -338,7 +283,7 @@ class ProjectHandler:
with open(path, 'wb+') as f:
while True:
try:
chunk = await request.content.read(1024)
chunk = await request.content.read(CHUNK_SIZE)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when writing to file '{}'".format(path))
if not chunk:
@@ -349,64 +294,3 @@ class ProjectHandler:
raise aiohttp.web.HTTPNotFound()
except PermissionError:
raise aiohttp.web.HTTPForbidden()
@Route.get(
r"/projects/{project_id}/export",
description="Export a project as a portable archive",
parameters={
"project_id": "Project UUID",
},
raw=True,
status_codes={
200: "File returned",
404: "The project doesn't exist"
})
async def export_project(request, response):
pm = ProjectManager.instance()
project = pm.get_project(request.match_info["project_id"])
response.content_type = 'application/gns3project'
response.headers['CONTENT-DISPOSITION'] = 'attachment; filename="{}.gns3project"'.format(project.name)
response.enable_chunked_encoding()
await response.prepare(request)
include_images = bool(int(request.json.get("include_images", "0")))
for data in project.export(include_images=include_images):
await response.write(data)
#await response.write_eof() #FIXME: shound't be needed anymore
@Route.post(
r"/projects/{project_id}/import",
description="Import a project from a portable archive",
parameters={
"project_id": "Project UUID",
},
raw=True,
output=PROJECT_OBJECT_SCHEMA,
status_codes={
200: "Project imported",
403: "Forbidden to import project"
})
async def import_project(request, response):
pm = ProjectManager.instance()
project_id = request.match_info["project_id"]
project = pm.create_project(project_id=project_id)
# We write the content to a temporary location and after we extract it all.
# It could be more optimal to stream this but it is not implemented in Python.
# Spooled means the file is temporary kept in memory until max_size is reached
try:
with tempfile.SpooledTemporaryFile(max_size=10000) as temp:
while True:
chunk = await request.content.read(1024)
if not chunk:
break
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))
response.json(project)
response.set_status(201)
@@ -576,4 +576,4 @@ class QEMUHandler:
if filename[0] == ".":
raise aiohttp.web.HTTPForbidden()
await response.file(image_path)
await response.stream_file(image_path)
@@ -36,7 +36,8 @@ class ApplianceHandler:
async def list_appliances(request, response):
controller = Controller.instance()
if request.query.get("update", "no") == "yes":
if request.query.get("update", "no").lower() == "yes":
await controller.appliance_manager.download_appliances()
controller.appliance_manager.load_appliances()
symbol_theme = request.query.get("symbol_theme", "Classic")
controller.appliance_manager.load_appliances(symbol_theme=symbol_theme)
response.json([c for c in controller.appliance_manager.appliances.values()])
@@ -68,61 +68,6 @@ class NodeHandler:
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
response.json([v for v in project.nodes.values()])
@Route.get(
r"/projects/{project_id}/nodes/{node_id}",
status_codes={
200: "Node found",
400: "Invalid request",
404: "Node doesn't exist"
},
description="Get a node",
output=NODE_OBJECT_SCHEMA)
def get_node(request, response):
project = Controller.instance().get_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
response.set_status(200)
response.json(node)
@Route.put(
r"/projects/{project_id}/nodes/{node_id}",
status_codes={
200: "Instance updated",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Update a node instance",
input=NODE_UPDATE_SCHEMA,
output=NODE_OBJECT_SCHEMA)
async def update(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
# Ignore these because we only use them when creating a node
request.json.pop("node_id", None)
request.json.pop("node_type", None)
request.json.pop("compute_id", None)
await node.update(**request.json)
response.set_status(200)
response.json(node)
@Route.delete(
r"/projects/{project_id}/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 node instance")
async def delete(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
await project.delete_node(request.match_info["node_id"])
response.set_status(204)
@Route.post(
r"/projects/{project_id}/nodes/start",
@@ -197,6 +142,61 @@ class NodeHandler:
await project.start_all()
response.set_status(204)
@Route.get(
r"/projects/{project_id}/nodes/{node_id}",
status_codes={
200: "Node found",
400: "Invalid request",
404: "Node doesn't exist"
},
description="Get a node",
output=NODE_OBJECT_SCHEMA)
def get_node(request, response):
project = Controller.instance().get_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
response.set_status(200)
response.json(node)
@Route.put(
r"/projects/{project_id}/nodes/{node_id}",
status_codes={
200: "Instance updated",
400: "Invalid request",
404: "Instance doesn't exist"
},
description="Update a node instance",
input=NODE_UPDATE_SCHEMA,
output=NODE_OBJECT_SCHEMA)
async def update(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
node = project.get_node(request.match_info["node_id"])
# Ignore these because we only use them when creating a node
request.json.pop("node_id", None)
request.json.pop("node_type", None)
request.json.pop("compute_id", None)
await node.update(**request.json)
response.set_status(200)
response.json(node)
@Route.delete(
r"/projects/{project_id}/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 node instance")
async def delete(request, response):
project = await Controller.instance().get_loaded_project(request.match_info["project_id"])
await project.delete_node(request.match_info["node_id"])
response.set_status(204)
@Route.post(
r"/projects/{project_id}/nodes/{node_id}/duplicate",
parameters={
@@ -16,15 +16,18 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import aiohttp
import asyncio
import tempfile
import zipfile
import aiofiles
import time
from gns3server.web.route import Route
from gns3server.controller import Controller
from gns3server.controller.import_project import import_project
from gns3server.controller.export_project import export_project
from gns3server.utils.asyncio import aiozipstream
from gns3server.config import Config
@@ -48,6 +51,8 @@ async def process_websocket(ws):
except aiohttp.WSServerHandshakeError:
pass
CHUNK_SIZE = 1024 * 8 # 8KB
class ProjectHandler:
@@ -221,23 +226,27 @@ class ProjectHandler:
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
response.content_type = "application/json"
response.set_status(200)
response.enable_chunked_encoding()
await response.prepare(request)
with controller.notification.project_queue(project) as queue:
while True:
msg = await queue.get_json(5)
await response.write(("{}\n".format(msg)).encode("utf-8"))
log.info("New client has connected to the notification stream for project ID '{}' (HTTP long-polling method)".format(project.id))
try:
with controller.notification.project_queue(project.id) as queue:
while True:
msg = await queue.get_json(5)
await response.write(("{}\n".format(msg)).encode("utf-8"))
finally:
log.info("Client has disconnected from notification for project ID '{}' (HTTP long-polling method)".format(project.id))
if project.auto_close:
# To avoid trouble with client connecting disconnecting we sleep few seconds before checking
# if someone else is not connected
await asyncio.sleep(5)
if not controller.notification.project_has_listeners(project.id):
log.info("Project '{}' is automatically closing due to no client listening".format(project.id))
await project.close()
if project.auto_close:
# To avoid trouble with client connecting disconnecting we sleep few seconds before checking
# if someone else is not connected
await asyncio.sleep(5)
if not controller.notification.project_has_listeners(project):
await project.close()
@Route.get(
r"/projects/{project_id}/notifications/ws",
@@ -253,29 +262,30 @@ class ProjectHandler:
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
ws = aiohttp.web.WebSocketResponse()
await ws.prepare(request)
request.app['websockets'].add(ws)
asyncio.ensure_future(process_websocket(ws))
log.info("New client has connected to the notification stream for project ID '{}' (WebSocket method)".format(project.id))
try:
with controller.notification.project_queue(project) as queue:
with controller.notification.project_queue(project.id) as queue:
while True:
notification = await queue.get_json(5)
if ws.closed:
break
await ws.send_str(notification)
finally:
log.info("Client has disconnected from notification stream for project ID '{}' (WebSocket method)".format(project.id))
if not ws.closed:
await ws.close()
request.app['websockets'].discard(ws)
if project.auto_close:
# To avoid trouble with client connecting disconnecting we sleep few seconds before checking
# if someone else is not connected
await asyncio.sleep(5)
if not controller.notification.project_has_listeners(project):
if not controller.notification.project_has_listeners(project.id):
log.info("Project '{}' is automatically closing due to no client listening".format(project.id))
await project.close()
return ws
@@ -295,23 +305,38 @@ class ProjectHandler:
controller = Controller.instance()
project = await controller.get_loaded_project(request.match_info["project_id"])
if request.query.get("include_images", "no").lower() == "yes":
include_images = True
else:
include_images = False
compression_query = request.query.get("compression", "zip").lower()
if compression_query == "zip":
compression = zipfile.ZIP_DEFLATED
elif compression_query == "none":
compression = zipfile.ZIP_STORED
elif compression_query == "bzip2":
compression = zipfile.ZIP_BZIP2
elif compression_query == "lzma":
compression = zipfile.ZIP_LZMA
try:
begin = time.time()
with tempfile.TemporaryDirectory() as tmp_dir:
stream = await export_project(project,
tmp_dir,
include_images=bool(int(request.query.get("include_images", "0"))))
# We need to do that now because export could failed and raise an HTTP error
# that why response start need to be the later possible
response.content_type = 'application/gns3project'
response.headers['CONTENT-DISPOSITION'] = 'attachment; filename="{}.gns3project"'.format(project.name)
response.enable_chunked_encoding()
await response.prepare(request)
with aiozipstream.ZipFile(compression=compression) as zstream:
await export_project(zstream, project, tmp_dir, include_images=include_images)
for data in stream:
await response.write(data)
# We need to do that now because export could failed and raise an HTTP error
# that why response start need to be the later possible
response.content_type = 'application/gns3project'
response.headers['CONTENT-DISPOSITION'] = 'attachment; filename="{}.gns3project"'.format(project.name)
response.enable_chunked_encoding()
await response.prepare(request)
async for chunk in zstream:
await response.write(chunk)
log.info("Project '{}' exported in {:.4f} seconds".format(project.name, time.time() - begin))
#await response.write_eof() #FIXME: shound't be needed anymore
# Will be raise if you have no space left or permission issue on your temporary directory
# RuntimeError: something was wrong during the zip process
except (ValueError, OSError, RuntimeError) as e:
@@ -343,29 +368,23 @@ class ProjectHandler:
# We write the content to a temporary location and after we extract it all.
# It could be more optimal to stream this but it is not implemented in Python.
# Spooled means the file is temporary kept in memory until max_size is reached
# Cannot use tempfile.SpooledTemporaryFile(max_size=10000) in Python 3.7 due
# to a bug https://bugs.python.org/issue26175
try:
if sys.version_info >= (3, 7) and sys.version_info < (3, 8):
with tempfile.TemporaryFile() as temp:
begin = time.time()
with tempfile.TemporaryDirectory() as tmpdir:
temp_project_path = os.path.join(tmpdir, "project.zip")
async with aiofiles.open(temp_project_path, 'wb') as f:
while True:
chunk = await request.content.read(1024)
chunk = await request.content.read(CHUNK_SIZE)
if not chunk:
break
temp.write(chunk)
project = await import_project(controller, request.match_info["project_id"], temp, location=path, name=name)
else:
with tempfile.SpooledTemporaryFile(max_size=10000) as temp:
while True:
chunk = await request.content.read(1024)
if not chunk:
break
temp.write(chunk)
project = await import_project(controller, request.match_info["project_id"], temp, location=path, name=name)
await f.write(chunk)
with open(temp_project_path, "rb") as f:
project = await import_project(controller, request.match_info["project_id"], f, location=path, name=name)
log.info("Project '{}' imported in {:.4f} seconds".format(project.name, time.time() - begin))
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not import the project: {}".format(e))
response.json(project)
response.set_status(201)
@@ -424,23 +443,7 @@ class ProjectHandler:
raise aiohttp.web.HTTPForbidden()
path = os.path.join(project.path, path)
response.content_type = "application/octet-stream"
response.set_status(200)
response.enable_chunked_encoding()
try:
with open(path, "rb") as f:
await response.prepare(request)
while True:
data = f.read(4096)
if not data:
break
await response.write(data)
except FileNotFoundError:
raise aiohttp.web.HTTPNotFound()
except PermissionError:
raise aiohttp.web.HTTPForbidden()
await response.stream_file(path)
@Route.post(
r"/projects/{project_id}/files/{path:.+}",
@@ -469,15 +472,15 @@ class ProjectHandler:
response.set_status(200)
try:
with open(path, 'wb+') as f:
async with aiofiles.open(path, 'wb+') as f:
while True:
try:
chunk = await request.content.read(1024)
chunk = await request.content.read(CHUNK_SIZE)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when writing to file '{}'".format(path))
if not chunk:
break
f.write(chunk)
await f.write(chunk)
except FileNotFoundError:
raise aiohttp.web.HTTPNotFound()
except PermissionError:
@@ -53,7 +53,7 @@ class SymbolHandler:
controller = Controller.instance()
try:
await response.file(controller.symbols.get_path(request.match_info["symbol_id"]))
await response.stream_file(controller.symbols.get_path(request.match_info["symbol_id"]))
except (KeyError, OSError) as e:
log.warning("Could not get symbol file: {}".format(e))
response.set_status(404)
@@ -85,6 +85,17 @@ class SymbolHandler:
controller.symbols.list()
response.set_status(204)
@Route.get(
r"/default_symbols",
description="List of default symbols",
status_codes={
200: "Default symbols list returned"
})
def list_default_symbols(request, response):
controller = Controller.instance()
response.json(controller.symbols.default_symbols())
# @Route.post(
# r"/symbol_theme",
# description="Create a new symbol theme",
@@ -51,6 +51,8 @@ class TemplateHandler:
controller = Controller.instance()
template = controller.template_manager.add_template(request.json)
# Reset the symbol list
controller.symbols.list()
response.set_status(201)
response.json(template)
@@ -164,8 +166,9 @@ class TemplateHandler:
controller = Controller.instance()
project = controller.get_project(request.match_info["project_id"])
await project.add_node_from_template(request.match_info["template_id"],
x=request.json["x"],
y=request.json["y"],
compute_id=request.json.get("compute_id"))
node = await project.add_node_from_template(request.match_info["template_id"],
x=request.json["x"],
y=request.json["y"],
compute_id=request.json.get("compute_id"))
response.set_status(201)
response.json(node)
+1 -1
View File
@@ -92,7 +92,7 @@ class IndexHandler:
if not os.path.exists(static):
static = get_static_path(os.path.join('web-ui', 'index.html'))
await response.file(static)
await response.stream_file(static)
@Route.get(
r"/v1/version",
+4
View File
@@ -47,6 +47,10 @@ DRAWING_OBJECT_SCHEMA = {
"description": "Z property",
"type": "integer"
},
"locked": {
"description": "Whether the element locked or not",
"type": "boolean"
},
"rotation": {
"description": "Rotation of the element",
"type": "integer",
+3 -3
View File
@@ -180,7 +180,7 @@ VM_CREATE_SCHEMA = {
},
"mac_addr": {
"description": "Base MAC address",
"type": "string",
"type": ["null", "string"],
"minLength": 1,
"pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$"
},
@@ -402,7 +402,7 @@ VM_UPDATE_SCHEMA = {
},
"mac_addr": {
"description": "Base MAC address",
"type": "string",
"type": ["null", "string"],
"minLength": 1,
"pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$"
},
@@ -646,7 +646,7 @@ VM_OBJECT_SCHEMA = {
},
"mac_addr": {
"description": "Base MAC address",
"type": "string",
"type": ["null", "string"]
#"minLength": 1,
#"pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$"
},
+4
View File
@@ -193,6 +193,10 @@ NODE_OBJECT_SCHEMA = {
"description": "Z position of the node",
"type": "integer"
},
"locked": {
"description": "Whether the element locked or not",
"type": "boolean"
},
"port_name_format": {
"description": "Formating for port name {0} will be replace by port number",
"type": "string"
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 1.2 MiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 1.3 MiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 1.3 MiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 256 256"
enable-background="new 0 0 792 612"
xml:space="preserve"
inkscape:version="0.92.2 (30a58d1, 2017-08-07)"
width="100%"
height="100%"
sodipodi:docname="gns3_icon.svg"
inkscape:export-filename="/home/grossmj/Downloads/gns3_icon_128x128.png"
inkscape:export-xdpi="45"
inkscape:export-ydpi="45"><metadata
id="metadata30"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
id="defs28" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1032"
id="namedview26"
showgrid="false"
inkscape:zoom="1.0428039"
inkscape:cx="235.30201"
inkscape:cy="248.57437"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g3" /><g
id="g3"
transform="translate(0,-356)"><linearGradient
id="SVGID_1_"
gradientUnits="userSpaceOnUse"
x1="404.2251"
y1="365.32321"
x2="384.2139"
y2="136.595"
gradientTransform="matrix(1.0930955,0,0,1.0930955,-302.80147,210.56132)"><stop
offset="0"
style="stop-color:#00A5CE"
id="stop6" /><stop
offset="0.1989"
style="stop-color:#1295CB"
id="stop8" /><stop
offset="0.5994"
style="stop-color:#426BC5"
id="stop10" /><stop
offset="1"
style="stop-color:#783CBD"
id="stop12" /></linearGradient><path
d="m 147.99093,422.62227 c 0,6.34 -5.13717,11.47729 -11.47726,11.47729 -6.33986,0 -11.47761,-5.13729 -11.47761,-11.47729 0,-6.34019 5.13775,-11.47804 11.47761,-11.47804 6.34009,0 11.47726,5.13785 11.47726,11.47804 z M 22.394349,480.3376 c -6.886487,37.71152 2.514172,64.92952 17.161614,84.38707 26.124988,36.83736 64.492517,43.28645 64.492517,43.28645 44.81704,8.52584 78.92153,-6.99569 99.36233,-26.23425 25.68784,-24.37577 24.81373,-48.53334 24.92311,-48.86119 0.21851,-11.25931 -3.49797,-19.34782 -10.05651,-24.92306 -11.0404,-9.29128 -24.4855,-5.68414 -24.4855,-5.68414 -23.2827,5.46564 -19.457,26.67165 -19.457,26.67165 1.31155,6.33984 4.59052,10.49362 8.30733,12.89839 7.10497,4.37239 14.64752,3.49799 14.64752,3.49799 11.14958,-1.31129 14.42894,-11.80519 13.77292,-17.05238 -1.09318,-8.30719 -8.63562,-9.83784 -8.63562,-9.83784 -2.84184,-0.54643 -5.02802,-0.10925 -6.66791,0.76517 -2.40425,0.98433 -3.38861,2.62342 -3.38861,2.62342 -1.74868,2.95142 -0.65568,4.59143 0.43749,5.24736 0.65611,0.43714 1.42132,0.43714 1.42132,0.43714 0.87411,0 1.42053,-0.2185 2.07653,-0.54638 1.53057,-0.8745 2.73256,-2.29597 5.02855,-1.42145 3.3886,1.31184 2.514,4.91929 2.514,4.91929 -0.76536,3.93499 -3.06079,5.79343 -5.57488,6.44908 -3.3886,0.8745 -6.88644,-0.21851 -6.88644,-0.21851 -12.02422,-4.9193 -6.66794,-16.943 -6.66794,-16.943 3.71635,-7.54269 12.02432,-8.41719 12.02432,-8.41719 13.00749,-2.07647 21.64316,6.6679 20.22168,21.42461 -1.63939,18.36446 -15.73999,30.49734 -30.82506,38.14961 -56.51289,29.07595 -98.706503,-7.76156 -98.706503,-7.76156 -15.740625,-14.97516 -12.024098,-30.82515 -10.275137,-35.853 0.655867,-1.85856 2.404853,-2.18667 3.279285,-2.18667 12.898528,-1.09269 23.610725,22.73676 24.703825,26.01608 2.2955,5.46552 5.90259,8.52567 9.29119,10.49354 7.65184,4.2631 15.74104,2.18631 15.74104,2.18631 11.14918,-3.82574 7.32358,-12.89835 7.32358,-12.89835 -1.31154,-3.60724 -3.27936,-4.5916 -4.80941,-4.48175 -1.42153,0 -2.51417,1.09316 -2.51417,1.09316 -0.54659,0.65552 -1.20277,1.09252 -1.74915,1.63926 -7.10531,6.01193 -10.6033,-0.32811 -10.6033,-0.32811 -2.51397,-4.04423 1.31224,-6.23061 1.31224,-6.23061 0.76515,-0.43697 1.53004,-0.65583 2.07635,-0.87447 2.84229,-0.874 2.40529,-3.27934 2.40529,-3.27934 -0.43714,-4.4811 -5.46562,-2.62337 -5.46562,-2.62337 -4.9188,1.74936 -6.44941,5.79346 -6.88681,7.54276 -0.10925,0.54665 -0.21864,0.9838 -0.9834,1.09305 -1.42087,0.2185 -1.42087,-2.2956 -1.42087,-2.2956 -0.43766,-17.92675 -5.3563,-25.90628 -7.65197,-28.74817 -0.65595,-0.87508 -1.748963,-1.42143 -2.076703,-1.6399 -5.137765,-2.29543 -10.821818,-6.7772 -10.821818,-6.7772 -10.821645,-7.76093 -6.55856,-14.75663 -6.55856,-14.75663 6.449253,-16.1782 11.696148,-21.75305 11.696148,-21.75305 4.372228,-4.15383 8.963563,-0.43707 8.963563,-0.43707 -3.388589,4.154 -6.012231,8.19861 -6.012231,8.19861 -7.760984,12.35148 -9.072721,17.38012 -9.072721,17.38012 -0.109308,0.43699 -0.109308,1.2022 0.54656,2.0769 7.979631,13.66362 29.513532,15.74023 29.622792,15.74023 5.903,0.87439 10.71217,0.10925 10.71217,0.10925 2.84217,-0.5464 3.27932,1.31202 3.27932,1.31202 0.32816,1.09313 1.53073,4.15392 1.53073,4.15392 2.07675,4.37191 5.24645,2.07639 5.24645,2.07639 1.2029,-0.87441 1.74925,-2.07639 1.74925,-3.06023 0,-0.98427 -0.54635,-1.63992 -0.54635,-1.63992 -1.42154,-2.07704 -1.85853,-4.04413 -1.85853,-4.04413 -0.65564,-2.40529 2.07709,-3.38858 2.07709,-3.38858 5.35574,-2.40531 8.3072,2.18559 8.3072,2.18559 2.84233,3.3886 8.52646,-0.87441 8.52646,-0.87441 4.59089,-3.49785 -1.09323,-8.96303 -1.09323,-8.96303 -11.80522,-10.82192 -25.68768,2.84173 -25.68768,2.84173 -0.76544,0.98401 -1.53071,0.98401 -1.53071,0.98401 -11.47697,0.32817 -22.40799,-8.08903 -22.40799,-8.52636 4.37237,-7.32359 12.78893,-20.11308 12.78893,-20.11308 l 44.48933,17.16181 c 1.74859,0.54637 3.60712,0.10928 3.60712,0.10928 l 69.30196,-13.99174 c 1.53069,-0.21862 1.3122,-2.07653 1.3122,-2.07653 0,-1.53068 -0.10984,-1.74919 -1.42146,-1.96769 -1.42085,-0.21851 -67.44413,-5.35634 -67.44413,-5.35634 -3.6071,-0.21853 -3.6071,-5.13719 0.10925,-5.46498 l 48.53404,-4.04469 c 1.09314,-0.10926 1.31163,-1.20245 1.31163,-1.20245 1.31159,-6.44938 0.87399,-14.86588 0.87399,-14.86588 14.10125,-15.7411 7.32407,-31.26273 7.32407,-31.26273 -8.63521,-21.4247 -27.65565,-21.75286 -27.65565,-21.75286 -31.26208,-34.54162 -72.36236,-30.49735 -72.36236,-30.49735 -1.85878,0 -1.96813,1.53056 -1.96813,1.53056 l -4.69982,29.73201 c -0.21882,1.63996 -1.09349,2.07694 -1.09349,2.07694 l -6.23028,4.91872 c -0.76548,0.76538 -0.98406,0.76538 -2.07708,0.76538 -0.98378,0.10951 -4.04425,0.54685 -4.04425,0.54685 C 32.997429,414.53283 22.394349,480.3376 22.394349,480.3376 z M 220.90065,409.94185 c 2.95154,9.40107 -5.13786,12.35249 -5.13786,12.35249 -1.53006,-7.3239 -6.88645,-17.59894 -6.88645,-17.59894 9.07316,-2.84204 12.02431,5.24645 12.02431,5.24645 z m -45.47307,16.7245 c 0,17.27084 -13.99154,31.37192 -31.3715,31.37192 -17.27088,0 -31.37183,-13.99182 -31.37183,-31.37192 0,-17.37995 14.10095,-31.37147 31.37183,-31.3714Line truncated
id="path14"
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1" /><path
style="stroke-width:0.95895308;fill:#ffffff;fill-opacity:1"
d="m 29.510681,104.95541 c 0,-1.72767 4.629168,-10.485938 8.922862,-16.881827 9.532205,-14.199177 22.127061,-25.580674 36.840478,-33.291355 8.695245,-4.556811 24.61103,-10.609516 25.516669,-9.70388 0.28263,0.282631 0.95772,0.239563 1.50019,-0.09571 0.54248,-0.33527 4.99436,-0.803063 9.89307,-1.039541 4.89871,-0.236478 10.63286,-0.684969 12.74256,-0.996646 l 3.83581,-0.566686 -2.87686,2.434668 c -1.58227,1.339067 -3.14795,2.450186 -3.47929,2.469154 -1.68705,0.09658 -7.68618,10.854586 -9.04972,16.228519 -2.62183,10.333096 0.5702,21.528346 8.42889,29.562183 2.78528,2.847349 3.5502,4.164777 2.58845,4.458047 -0.75085,0.22896 -20.13669,1.997905 -43.079643,3.930994 -22.942952,1.93308 -43.979985,3.76225 -46.748962,4.06482 -3.520186,0.38465 -5.034504,0.21238 -5.034504,-0.57274 z"
id="path3702"
inkscape:connector-curvature="0"
transform="translate(0,356)" /><path
style="stroke-width:0.95895308;fill:#ffffff;fill-opacity:1"
d="m 20.883753,139.13783 c 0.0085,-5.02746 2.975045,-19.84783 5.322913,-26.59244 2.494665,-7.1663 2.58356,-7.27734 5.55674,-6.94098 3.274726,0.37048 78.714134,-5.943709 88.732764,-7.426826 5.48582,-0.8121 6.52647,-0.68727 10.069,1.207818 3.317,1.774438 5.48939,2.113528 13.5404,2.113528 10.7302,0 14.442,-1.34017 21.58487,-7.793315 4.3645,-3.943066 9.57454,-13.722604 9.59579,-18.011849 0.008,-1.629888 0.33172,-3.448849 0.71921,-4.042136 0.42463,-0.650131 10.03894,-1.890095 24.19889,-3.120944 30.29526,-2.633408 27.88976,-2.729233 27.18798,1.083051 -0.8022,4.357824 -3.80236,11.080064 -6.55677,14.691283 -1.92557,2.524555 -2.37472,4.35005 -2.60835,10.601279 l -0.28223,7.551581 -24.80503,2.09802 c -13.64277,1.15391 -25.40194,2.32707 -26.13147,2.60703 -1.54574,0.59315 -1.78171,4.01045 -0.33852,4.90238 0.68162,0.42127 59.31288,5.63597 69.73487,6.20227 0.13185,0.007 0.23973,0.63124 0.23973,1.38683 0,1.51563 -0.90249,1.86399 -8.63057,3.33139 -5.68832,1.08009 -69.10668,6.80224 -75.38896,6.80224 -2.3662,0 -9.42164,-2.17638 -18.63036,-5.74688 -8.15202,-3.16078 -15.04629,-5.74995 -15.32061,-5.75372 -0.27431,-0.004 -2.64772,3.43055 -5.27424,7.63179 -3.47938,5.56542 -5.42616,7.76216 -7.17287,8.09378 -4.64405,0.88171 -17.261159,1.68518 -17.261159,1.09921 0,-1.35251 4.978919,-10.2667 9.080733,-16.25801 2.418926,-3.53321 4.144786,-6.83378 3.835256,-7.33462 -0.30953,-0.50083 -2.009311,-0.91061 -3.777284,-0.91061 -4.301856,0 -7.201623,3.5265 -13.207838,16.06246 -2.590162,5.4061 -5.017616,9.82927 -5.394342,9.82927 -0.376727,0 -12.749895,1.07386 -27.49593,2.38635 -33.117257,2.94765 -31.127147,2.93162 -31.122613,0.25077 z"
id="path3704"
inkscape:connector-curvature="0"
transform="translate(0,356)" /><path
style="stroke-width:0.95895308;fill:#ffffff;fill-opacity:1"
d="m 22.776215,171.26276 c -0.588994,-1.97784 -1.316927,-9.52959 -1.61763,-16.78168 l -0.546734,-13.1856 h 4.41742 c 4.272458,0 45.974498,-3.38285 52.084903,-4.2251 2.729171,-0.37619 3.068349,-0.13433 3.610054,2.57419 0.747297,3.73649 7.88306,10.75622 15.175011,14.92825 4.051489,2.31803 5.884641,4.10484 7.379631,7.19308 1.09401,2.25992 1.9891,4.55758 1.9891,5.10593 0,1.13043 -9.626814,2.21632 -19.738447,2.22647 -3.735922,0.004 -7.090412,0.30464 -7.454422,0.66866 -0.364011,0.36401 -2.701459,0.81187 -5.19433,0.99524 -2.492869,0.18337 -14.079003,1.18013 -25.746961,2.21502 -11.667959,1.03489 -21.680722,1.88162 -22.250585,1.88162 -0.569862,0 -1.518017,-1.61824 -2.10701,-3.59608 z"
id="path3706"
inkscape:connector-curvature="0"
transform="translate(0,356)" /></g></svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>GNS3 Web UI</title>
<!-- It's important to have base here because of the script below //-->
<base href="/static/web-ui/">
<script>
var userAgent = navigator.userAgent.toLowerCase();
//in case we're running in electron because we need it for resources
if (userAgent.indexOf(' electron/') > -1) {
var base = document.getElementsByTagName('base');
if (base.length > 0) {
base[0].href = './';
} else {
document.write('<base href="./" />');
}
}
</script>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="assets/favicon.ico" />
<script type="application/javascript">
// Github Pages redirection
(function() {
var redirect = sessionStorage.redirect;
delete sessionStorage.redirect;
if (redirect && redirect != location.href) {
history.replaceState(null, null, redirect);
}
})();
</script>
<link rel="stylesheet" href="styles.0a22cbd56b6e27a9938f.css"></head>
<!-- <body class="mat-app-background" oncontextmenu="return false;"> -->
<body class="mat-app-background" oncontextmenu="return false;">
<app-root></app-root>
<script type="text/javascript" src="runtime.a5dd35324ddfd942bef1.js"></script><script type="text/javascript" src="polyfills.f4c5bd079c16579a94cf.js"></script><script type="text/javascript" src="main.3afb2dc4036a33c58376.js"></script></body>
</html>
@@ -0,0 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[1],{"+/L5":function(n,e,t){var l=t("t1UP").isCustomProperty,o=t("vd7W").TYPE,r=o.Identifier,i=o.Colon,a=o.ExclamationMark,u=o.Solidus,s=o.Asterisk,d=o.DollarSign,c=o.HyphenMinus,p=o.Semicolon,f=o.PlusSign,h=o.NumberSign;function m(n){return this.Raw(n,a,p,!1,!0)}function v(n){return this.Raw(n,a,p,!1,!1)}function g(){var n=this.scanner.currentToken,e=this.Value();return"Raw"!==e.type&&!1===this.scanner.eof&&this.scanner.tokenType!==p&&this.scanner.tokenType!==a&&!1===this.scanner.isBalanceEdge(n)&&this.scanner.error(),e}n.exports={name:"Declaration",structure:{important:[Boolean,String],property:String,value:["Value","Raw"]},parse:function(){var n,e=this.scanner.tokenStart,t=this.scanner.currentToken,o=(function(){var n=this.scanner.tokenStart,e=0;switch(this.scanner.tokenType){case s:case d:case f:case h:e=1;break;case u:e=this.scanner.lookupType(1)===u?2:1}return this.scanner.lookupType(e)===c&&e++,e&&this.scanner.skip(e),this.scanner.eat(r),this.scanner.substrToCursor(n)}).call(this),b=l(o),y=b?this.parseCustomProperty:this.parseValue,_=b?v:m,w=!1;return this.scanner.skipSC(),this.scanner.eat(i),b||this.scanner.skipSC(),n=y?this.parseWithFallback(g,_):_.call(this,this.scanner.currentToken),this.scanner.tokenType===a&&(w=function(n){n.eat(a),n.skipSC();var e=n.consume(r);return"important"===e||e}(this.scanner),this.scanner.skipSC()),!1===this.scanner.eof&&this.scanner.tokenType!==p&&!1===this.scanner.isBalanceEdge(t)&&this.scanner.error(),{type:"Declaration",loc:this.getLocation(e,this.scanner.tokenStart),important:w,property:o,value:n}},generate:function(n){this.chunk(n.property),this.chunk(":"),this.node(n.value),n.important&&this.chunk(!0===n.important?"!important":"!"+n.important)},walkContext:"declaration"}},"+Kd2":function(n,e,t){var l=t("vd7W").TYPE,o=l.Identifier,r=l.Comma,i=l.Semicolon,a=l.HyphenMinus,u=l.ExclamationMark;n.exports=function(){var n=this.createList();this.scanner.skipSC();var e=this.scanner.tokenStart;return this.scanner.eat(a),this.scanner.source.charCodeAt(this.scanner.tokenStart)!==a&&this.scanner.error("HyphenMinus is expected"),this.scanner.eat(o),n.push({type:"Identifier",loc:this.getLocation(e,this.scanner.tokenStart),name:this.scanner.substrToCursor(e)}),this.scanner.skipSC(),this.scanner.tokenType===r&&(n.push(this.Operator()),n.push(this.parseCustomProperty?this.Value(null):this.Raw(this.scanner.currentToken,u,i,!1,!1))),n}},"+gEr":function(n,e,t){"use strict";t.d(e,"a",function(){return i}),t.d(e,"b",function(){return a});var l=t("mrSG"),o=t("MGBS"),r=t("zotm"),i={leading:!0,trailing:!1};function a(n,e){return void 0===e&&(e=i),function(t){return t.lift(new u(n,e.leading,e.trailing))}}var u=function(){function n(n,e,t){this.durationSelector=n,this.leading=e,this.trailing=t}return n.prototype.call=function(n,e){return e.subscribe(new s(n,this.durationSelector,this.leading,this.trailing))},n}(),s=function(n){function e(e,t,l,o){var r=n.call(this,e)||this;return r.destination=e,r.durationSelector=t,r._leading=l,r._trailing=o,r._hasValue=!1,r}return l.__extends(e,n),e.prototype._next=function(n){this._hasValue=!0,this._sendValue=n,this._throttled||(this._leading?this.send():this.throttle(n))},e.prototype.send=function(){var n=this._sendValue;this._hasValue&&(this.destination.next(n),this.throttle(n)),this._hasValue=!1,this._sendValue=null},e.prototype.throttle=function(n){var e=this.tryDurationSelector(n);e&&this.add(this._throttled=Object(r.a)(this,e))},e.prototype.tryDurationSelector=function(n){try{return this.durationSelector(n)}catch(e){return this.destination.error(e),null}},e.prototype.throttlingDone=function(){var n=this._throttled,e=this._trailing;n&&n.unsubscribe(),this._throttled=null,e&&this.send()},e.prototype.notifyNext=function(n,e,t,l,o){this.throttlingDone()},e.prototype.notifyComplete=function(){this.throttlingDone()},e}(o.a)},"+ryv":function(n,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var l=t("CcnG");e.HotkeyOptions=new l.InjectionToken("HotkeyOptions")},"+tJ4":function(n,e,t){"use strict";t.d(e,"a",function(){return l});var l=function(n){return function(e){for(var t=0,l=n.length;t<l&&!e.closed;t++)e.next(n[t]);e.closed||e.complete()}}},"+umK":function(n,e,t){"use strict";function l(){}t.d(e,"a",function(){return l})},"/+5V":function(n,e){function t(n){function e(n){return null!==n&&("Type"===n.type||"Property"===n.type||"Keyword"===n.type)}var t=null;return null!==this.matched&&function l(o){if(Array.isArray(o.match)){for(var r=0;r<o.match.length;r++)if(l(o.match[r]))return e(o.syntax)&&t.unshift(o.syntax),!0}else if(o.node===n)return t=e(o.syntax)?[o.syntax]:[],!0;return!1}(this.matched),t}function l(n,e,l){var o=t.call(n,e);return null!==o&&o.some(l)}n.exports={getTrace:t,isType:function(n,e){return l(this,n,function(n){return"Type"===n.type&&n.name===e})},isProperty:function(n,e){return l(this,n,function(n){return"Property"===n.type&&n.name===e})},isKeyword:function(n){returLine truncated
@@ -0,0 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[2],{"0TWp":function(e,t,n){!function(){"use strict";!function(e){var t=e.performance;function n(e){t&&t.mark&&t.mark(e)}function r(e,n){t&&t.measure&&t.measure(e,n)}n("Zone");var o=!0===e.__zone_symbol__forceDuplicateZoneCheck;if(e.Zone){if(o||"function"!=typeof e.Zone.__symbol__)throw new Error("Zone already loaded.");return e.Zone}var a,i=function(){function t(e,t){this._parent=e,this._name=t?t.name||"unnamed":"<root>",this._properties=t&&t.properties||{},this._zoneDelegate=new c(this,this._parent&&this._parent._zoneDelegate,t)}return t.assertZonePatched=function(){if(e.Promise!==Z.ZoneAwarePromise)throw new Error("Zone.js has detected that ZoneAwarePromise `(window|global).Promise` has been overwritten.\nMost likely cause is that a Promise polyfill has been loaded after Zone.js (Polyfilling Promise api is not necessary when zone.js is loaded. If you must load one, do so before loading zone.js.)")},Object.defineProperty(t,"root",{get:function(){for(var e=t.current;e.parent;)e=e.parent;return e},enumerable:!0,configurable:!0}),Object.defineProperty(t,"current",{get:function(){return O.zone},enumerable:!0,configurable:!0}),Object.defineProperty(t,"currentTask",{get:function(){return P},enumerable:!0,configurable:!0}),t.__load_patch=function(a,i){if(Z.hasOwnProperty(a)){if(o)throw Error("Already loaded patch: "+a)}else if(!e["__Zone_disable_"+a]){var s="Zone:"+a;n(s),Z[a]=i(e,t,z),r(s,s)}},Object.defineProperty(t.prototype,"parent",{get:function(){return this._parent},enumerable:!0,configurable:!0}),Object.defineProperty(t.prototype,"name",{get:function(){return this._name},enumerable:!0,configurable:!0}),t.prototype.get=function(e){var t=this.getZoneWith(e);if(t)return t._properties[e]},t.prototype.getZoneWith=function(e){for(var t=this;t;){if(t._properties.hasOwnProperty(e))return t;t=t._parent}return null},t.prototype.fork=function(e){if(!e)throw new Error("ZoneSpec required!");return this._zoneDelegate.fork(this,e)},t.prototype.wrap=function(e,t){if("function"!=typeof e)throw new Error("Expecting function got: "+e);var n=this._zoneDelegate.intercept(this,e,t),r=this;return function(){return r.runGuarded(n,this,arguments,t)}},t.prototype.run=function(e,t,n,r){O={parent:O,zone:this};try{return this._zoneDelegate.invoke(this,e,t,n,r)}finally{O=O.parent}},t.prototype.runGuarded=function(e,t,n,r){void 0===t&&(t=null),O={parent:O,zone:this};try{try{return this._zoneDelegate.invoke(this,e,t,n,r)}catch(o){if(this._zoneDelegate.handleError(this,o))throw o}}finally{O=O.parent}},t.prototype.runTask=function(e,t,n){if(e.zone!=this)throw new Error("A task can only be run in the zone of creation! (Creation: "+(e.zone||y).name+"; Execution: "+this.name+")");if(e.state!==m||e.type!==D&&e.type!==S){var r=e.state!=b;r&&e._transitionTo(b,_),e.runCount++;var o=P;P=e,O={parent:O,zone:this};try{e.type==S&&e.data&&!e.data.isPeriodic&&(e.cancelFn=void 0);try{return this._zoneDelegate.invokeTask(this,e,t,n)}catch(a){if(this._zoneDelegate.handleError(this,a))throw a}}finally{e.state!==m&&e.state!==w&&(e.type==D||e.data&&e.data.isPeriodic?r&&e._transitionTo(_,b):(e.runCount=0,this._updateTaskCount(e,-1),r&&e._transitionTo(m,b,m))),O=O.parent,P=o}}},t.prototype.scheduleTask=function(e){if(e.zone&&e.zone!==this)for(var t=this;t;){if(t===e.zone)throw Error("can not reschedule task to "+this.name+" which is descendants of the original zone "+e.zone.name);t=t.parent}e._transitionTo(k,m);var n=[];e._zoneDelegates=n,e._zone=this;try{e=this._zoneDelegate.scheduleTask(this,e)}catch(r){throw e._transitionTo(w,k,m),this._zoneDelegate.handleError(this,r),r}return e._zoneDelegates===n&&this._updateTaskCount(e,1),e.state==k&&e._transitionTo(_,k),e},t.prototype.scheduleMicroTask=function(e,t,n,r){return this.scheduleTask(new l(E,e,t,n,r,void 0))},t.prototype.scheduleMacroTask=function(e,t,n,r,o){return this.scheduleTask(new l(S,e,t,n,r,o))},t.prototype.scheduleEventTask=function(e,t,n,r,o){return this.scheduleTask(new l(D,e,t,n,r,o))},t.prototype.cancelTask=function(e){if(e.zone!=this)throw new Error("A task can only be cancelled in the zone of creation! (Creation: "+(e.zone||y).name+"; Execution: "+this.name+")");e._transitionTo(T,_,b);try{this._zoneDelegate.cancelTask(this,e)}catch(t){throw e._transitionTo(w,T),this._zoneDelegate.handleError(this,t),t}return this._updateTaskCount(e,-1),e._transitionTo(m,T),e.runCount=0,e},t.prototype._updateTaskCount=function(e,t){var n=e._zoneDelegates;-1==t&&(e._zoneDelegates=null);for(var r=0;r<n.length;r++)n[r]._updateTaskCount(e.type,t)},t.__symbol__=I,t}(),s={name:"",onHasTask:function(e,t,n,r){return e.hasTask(n,r)},onScheduleTask:function(e,t,n,r){return e.scheduleTask(n,r)},onInvokeTask:function(e,t,n,r,o,a){return e.invokeTask(n,r,o,a)},onCancelTask:function(e,t,n,r){return e.cancelTask(n,r)}},c=function(){function e(e,t,n){this._taskCounts={microTask:0,macroTask:0,eventTask:0},this.zone=e,this._parentDelegate=t,this._forkZS=n&&(n&&n.onFork?n:t._fLine truncated
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff. Show more