Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ad632976f | ||
|
|
b8bf2bf465 | ||
|
|
55be74f93c | ||
|
|
7d591b0228 | ||
|
|
a059e6b943 | ||
|
|
c6990dfeee | ||
|
|
aff67b324f | ||
|
|
f7e90a7d7f | ||
|
|
ed6b1d909d | ||
|
|
010d65114d | ||
|
|
3dcb7799e3 | ||
|
|
aaaeb410d5 | ||
|
|
d506962bdb | ||
|
|
4d9728b6f5 | ||
|
|
ff9048b5a8 | ||
|
|
6c96be2acc | ||
|
|
21afaf31f9 | ||
|
|
8e5ac228d5 | ||
|
|
94f63f54ba | ||
|
|
cf91e904f7 | ||
|
|
767bb770ca | ||
|
|
bae3fb84b9 | ||
|
|
889d29ec55 | ||
|
|
d3ec2a7275 | ||
|
|
2d01ef489b | ||
|
|
d64d7de541 | ||
|
|
372017f467 | ||
|
|
826f6b2d57 | ||
|
|
0ae66a2608 | ||
|
|
380224dca1 | ||
|
|
af80b0bb6e | ||
|
|
b0df7ecabf | ||
|
|
4c83fc1243 | ||
|
|
69ba0ccf77 | ||
|
|
0681c296a7 | ||
|
|
3f679bd106 | ||
|
|
724eda1f35 | ||
|
|
3f7c4c0474 | ||
|
|
5b6011a248 | ||
|
|
80eb06f67c | ||
|
|
9132ba8d05 | ||
|
|
85ea1cf7a9 | ||
|
|
8cc8269842 | ||
|
|
52bfa636c1 | ||
|
|
4eca82174d | ||
|
|
32f5dbb645 | ||
|
|
7aae682f0a | ||
|
|
879e32eb9a | ||
|
|
01481cde1f | ||
|
|
b5deb9d177 | ||
|
|
a8990c9e89 | ||
|
|
e1aa13fa73 | ||
|
|
135d56371d | ||
|
|
7fe8f7e716 | ||
|
|
10702f87bc | ||
|
|
2fb5e47c8d | ||
|
|
4487e578bc | ||
|
|
5754747a90 | ||
|
|
62c51edbae | ||
|
|
a13d063aa1 | ||
|
|
1ef1872f8e | ||
|
|
657698a961 | ||
|
|
081ba31b50 | ||
|
|
aea4ae808f | ||
|
|
3e601a8a24 | ||
|
|
ae3515434c | ||
|
|
8e2d8f3962 | ||
|
|
589c9754e8 | ||
|
|
4ecd3b2015 | ||
|
|
84ee3263ba | ||
|
|
174624121d | ||
|
|
d9a9abf845 | ||
|
|
0b07299472 | ||
|
|
f7d0f54b6d | ||
|
|
7fb192699b | ||
|
|
bccdfc97d1 | ||
|
|
792c4310eb | ||
|
|
3e21f96bf9 | ||
|
|
5f932fee9f |
No files matched your search
@@ -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,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": {
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}$"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
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 |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 734 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
@@ -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
|
||||