diff --git a/CHANGELOG b/CHANGELOG
index fc3331f3c..5ddeabb3b 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,11 @@
# Change Log
+## 2.2.58 10/04/2026
+
+* Bundle web-ui v2.2.58
+* Sync appliances
+* Snapshot refactoring
+
## 2.2.57 23/03/2026
* feat(telnet_server): improve error handling and connection management
diff --git a/gns3server/appliances/arista-veos.gns3a b/gns3server/appliances/arista-veos.gns3a
index 835556089..007a4bb7a 100644
--- a/gns3server/appliances/arista-veos.gns3a
+++ b/gns3server/appliances/arista-veos.gns3a
@@ -28,6 +28,13 @@
"options": "-cpu host"
},
"images": [
+ {
+ "filename": "vEOS64-lab-4.35.3F.qcow2",
+ "version": "4.35.3F",
+ "md5sum": "00d11e33dc4288f509441a4ab6319ca0",
+ "filesize": 638910464,
+ "download_url": "https://www.arista.com/en/support/software-download"
+ },
{
"filename": "vEOS64-lab-4.33.2F.qcow2",
"version": "4.33.2F",
@@ -58,6 +65,13 @@
}
],
"versions": [
+ {
+ "name": "4.35.3F",
+ "images": {
+ "hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
+ "hdb_disk_image": "vEOS64-lab-4.35.3F.qcow2"
+ }
+ },
{
"name": "4.33.2F",
"images": {
diff --git a/gns3server/appliances/cisco-alpine-trex.gns3a b/gns3server/appliances/cisco-alpine-trex.gns3a
new file mode 100644
index 000000000..a0e02540a
--- /dev/null
+++ b/gns3server/appliances/cisco-alpine-trex.gns3a
@@ -0,0 +1,43 @@
+{
+ "appliance_id": "924a2ef5-7b91-45ae-a1e4-7220338b76b0",
+ "name": "Cisco Alpine Linux TRex",
+ "category": "guest",
+ "description": "Alpine Linux VM image configured to run Cisco's Trex traffic generator in stateless mode",
+ "vendor_name": "Cisco",
+ "vendor_url": "http://www.cisco.com/",
+ "documentation_url": "https://trex-tgn.cisco.com/",
+ "product_name": "Alpine Linux Virt",
+ "product_url": "https://developer.cisco.com/modeling-labs/",
+ "registry_version": 4,
+ "status": "beta",
+ "maintainer": "GNS3 Team",
+ "maintainer_email": "developers@gns3.net",
+ "usage": "By default, a \"cisco\" account with password \"cisco\" is created.",
+ "port_name_format": "eth{0}",
+ "qemu": {
+ "adapter_type": "virtio-net-pci",
+ "adapters": 3,
+ "ram": 512,
+ "hda_disk_interface": "virtio",
+ "arch": "x86_64",
+ "console_type": "vnc",
+ "kvm": "allow"
+ },
+ "images": [
+ {
+ "filename": "alpine-trex-3-21-3.qcow2",
+ "version": "2.87",
+ "md5sum": "54cf267bbc2ab7a1c71dc53bced6e128",
+ "filesize": 465174528,
+ "download_url": "https://u.cisco.com/my-learning/my-account"
+ }
+ ],
+ "versions": [
+ {
+ "name": "2.87",
+ "images": {
+ "hda_disk_image": "alpine-trex-3-21-3.qcow2"
+ }
+ }
+ ]
+}
diff --git a/gns3server/appliances/cisco-asav.gns3a b/gns3server/appliances/cisco-asav.gns3a
index 35e71c462..dadd94561 100644
--- a/gns3server/appliances/cisco-asav.gns3a
+++ b/gns3server/appliances/cisco-asav.gns3a
@@ -31,21 +31,21 @@
"version": "9.22.1.1 CML",
"md5sum": "250a924cdc2370208eaac9d1dc8dc9e3",
"filesize": 379518976,
- "download_url": "https://learningnetworkstore.cisco.com/cisco-modeling-labs-personal/cisco-modeling-labs-personal/CML-PERSONAL.html"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "asav9-18-2.qcow2",
"version": "9.18.2 CML",
"md5sum": "6f10fe106edfad9163625770a47a6b73",
"filesize": 340262912,
- "download_url": "https://learningnetworkstore.cisco.com/cisco-modeling-labs-personal/cisco-modeling-labs-personal/CML-PERSONAL.html"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "asav9-16-2.qcow2",
"version": "9.16.2 CML",
"md5sum": "1f8db97063a7f738fddc81ac880a906c",
"filesize": 262078976,
- "download_url": "https://learningnetworkstore.cisco.com/cisco-modeling-labs-personal/cisco-modeling-labs-personal/CML-PERSONAL.html"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "asav9-16-2.qcow2",
diff --git a/gns3server/appliances/cisco-cat9k.gns3a b/gns3server/appliances/cisco-cat9k.gns3a
index cd3538128..e5fde082e 100644
--- a/gns3server/appliances/cisco-cat9k.gns3a
+++ b/gns3server/appliances/cisco-cat9k.gns3a
@@ -31,14 +31,14 @@
"version": "17.12(1)",
"md5sum": "e587e92186f42bdf69d7fa27f34425f7",
"filesize": 2739404800,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "cat9kv-prd-17.10.01prd7.qcow2",
"version": "17.10(1)",
"md5sum": "ffdbace33d31deae33e2a920a96b79ef",
"filesize": 2155806720,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
}
],
"versions": [
diff --git a/gns3server/appliances/cisco-csr1000v.gns3a b/gns3server/appliances/cisco-csr1000v.gns3a
index 286431216..7906077e2 100644
--- a/gns3server/appliances/cisco-csr1000v.gns3a
+++ b/gns3server/appliances/cisco-csr1000v.gns3a
@@ -134,21 +134,21 @@
"version": "16.5.1b",
"md5sum": "ac11d33041b8ff6dc3553e324d02cccb",
"filesize": 1209543680,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "csr1000v-universalk9.03.17.00.S.156-1.S-ext.qcow2",
"version": "3.17",
"md5sum": "06cbfcd11f3557391db64fe2a6015a6e",
"filesize": 1346305024,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "csr1000v-universalk9.16.3.1-build2.qcow2",
"version": "16.3.1-build2",
"md5sum": "a770e96de928265515304c9c9d6b46b9",
"filesize": 1280835584,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "csr1000v-universalk9.16.03.01.qcow2",
@@ -162,21 +162,21 @@
"version": "16.3.2",
"md5sum": "2e5803d23cd52cba5d55fa8306be5f13",
"filesize": 1167720448,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "csr1000v-universalk9.16.4.1.qcow2",
"version": "16.4.1",
"md5sum": "3428e0dcf5132a1b11ab7696d8c61b2e",
"filesize": 1261961216,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "csr1000v-universalk9.16.6.1.qcow2",
"version": "16.6.1-VIRL",
"md5sum": "996eb6dbf54781f6393bd689571a818c",
"filesize": 1316301824,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
}
],
"versions": [
diff --git a/gns3server/appliances/cisco-iosv.gns3a b/gns3server/appliances/cisco-iosv.gns3a
index 24b173c48..abde6b8b7 100644
--- a/gns3server/appliances/cisco-iosv.gns3a
+++ b/gns3server/appliances/cisco-iosv.gns3a
@@ -30,77 +30,77 @@
"version": "15.9(3)M9",
"md5sum": "01b707a2e33185d6d33e0255ced45d23",
"filesize": 57323008,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "vios-adventerprisek9-m.spa.159-3.m8.qcow2",
"version": "15.9(3)M8",
"md5sum": "8d93a185c2fa778178a933f20b02150a",
"filesize": 57319424,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "vios-adventerprisek9-m.spa.159-3.m6.qcow2",
"version": "15.9(3)M6",
"md5sum": "49a6977977263b2774bebc56e4e678ff",
"filesize": 57309696,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "vios-adventerprisek9-m.spa.159-3.m4.qcow2",
"version": "15.9(3)M4",
"md5sum": "355b13ab821e64e2939fd7008d6304d7",
"filesize": 57297920,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "vios-adventerprisek9-m.spa.159-3.m3.qcow2",
"version": "15.9(3)M3",
"md5sum": "12893843af18e4c62f13d07266755653",
"filesize": 57296384,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "vios-adventerprisek9-m.spa.159-3.m2.qcow2",
"version": "15.9(3)M2",
"md5sum": "a19e998bc3086825c751d125af722329",
"filesize": 57308672,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "vios-adventerprisek9-m.spa.158-3.m2.qcow2",
"version": "15.8(3)M2",
"md5sum": "40e3d25b5b0cb13d639fcd2cf18e9965",
"filesize": 57129984,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "vios-adventerprisek9-m.vmdk.SPA.157-3.M3",
"version": "15.7(3)M3",
"md5sum": "37c148ffa14a82f418a6e9c2b049fafe",
"filesize": 134021120,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "vios-adventerprisek9-m.vmdk.SPA.156-2.T",
"version": "15.6(2)T",
"md5sum": "83707e3cc93646da58ee6563a68002b5",
"filesize": 128450560,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "vios-adventerprisek9-m.vmdk.SPA.156-1.T",
"version": "15.6(1)T",
"md5sum": "e7cb1bbd0c59280dd946feefa68fa270",
"filesize": 128122880,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "vios-adventerprisek9-m.vmdk.SPA.155-3.M",
"version": "15.5(3)M",
"md5sum": "79f613ac3b179d5a64520730925130b2",
"filesize": 127926272,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "IOSv_startup_config.img",
diff --git a/gns3server/appliances/cisco-iosvl2.gns3a b/gns3server/appliances/cisco-iosvl2.gns3a
index e9b869555..6fa65af5c 100644
--- a/gns3server/appliances/cisco-iosvl2.gns3a
+++ b/gns3server/appliances/cisco-iosvl2.gns3a
@@ -30,35 +30,35 @@
"version": "15.2(20200924:215240)",
"md5sum": "99ecab32de12410c533e6abd4e9710aa",
"filesize": 90409984,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "vios_l2-adventerprisek9-m.ssa.high_iron_20190423.qcow2",
"version": "15.2(6.0.81)E",
"md5sum": "71cacb678f98a106f99e889b97b34686",
"filesize": 44950016,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "vios_l2-adventerprisek9-m.SSA.high_iron_20180619.qcow2",
"version": "15.2.1",
"md5sum": "14b981002e40b660f2d7400401e04c14",
"filesize": 44938752,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "vios_l2-adventerprisek9-m.03.2017.qcow2",
"version": "15.2(20170321:233949)",
"md5sum": "8f14b50083a14688dec2fc791706bb3e",
"filesize": 41157632,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "vios_l2-adventerprisek9-m.vmdk.SSA.152-4.0.55.E",
"version": "15.2(4.0.55)E",
"md5sum": "1a3a21f5697cae64bb930895b986d71e",
"filesize": 96862208,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
}
],
"versions": [
diff --git a/gns3server/appliances/cisco-iosxrv.gns3a b/gns3server/appliances/cisco-iosxrv.gns3a
index e35a5541a..ffe80fd90 100644
--- a/gns3server/appliances/cisco-iosxrv.gns3a
+++ b/gns3server/appliances/cisco-iosxrv.gns3a
@@ -30,14 +30,14 @@
"version": "6.1.3",
"md5sum": "1693b5d22a398587dd0fed2877d8dfac",
"filesize": 428588544,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "iosxrv-k9-demo-6.0.1.qcow2",
"version": "6.0.1",
"md5sum": "0831ecf43628eccb752ebb275de9a62a",
"filesize": 908132352,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
}
],
"versions": [
diff --git a/gns3server/appliances/cisco-iosxrv9k.gns3a b/gns3server/appliances/cisco-iosxrv9k.gns3a
index c856f37f5..ef9e1fbb6 100644
--- a/gns3server/appliances/cisco-iosxrv9k.gns3a
+++ b/gns3server/appliances/cisco-iosxrv9k.gns3a
@@ -31,21 +31,21 @@
"version": "24.3.1",
"md5sum": "bd047c457e29952f265583f299bab845",
"filesize": 1704658432,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "xrv9k-fullk9-x-7.7.1.qcow2",
"version": "7.7.1",
"md5sum": "682fff40d2ff373d8da3342906553b54",
"filesize": 1643905024,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "xrv9k-fullk9-x-7.1.1.qcow2",
"version": "7.1.1",
"md5sum": "dcf241e3f8df0151fec2c7bfac9d96ac",
"filesize": 1443758080,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
}
],
"versions": [
diff --git a/gns3server/appliances/cisco-nxosv.gns3a b/gns3server/appliances/cisco-nxosv.gns3a
index a71b0ad38..f6ca2af02 100644
--- a/gns3server/appliances/cisco-nxosv.gns3a
+++ b/gns3server/appliances/cisco-nxosv.gns3a
@@ -30,14 +30,14 @@
"version": "7.3.0",
"md5sum": "b4cd6edf15ab4c6bce53c3f6c1e3a742",
"filesize": 214368256,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
},
{
"filename": "titanium-d1.7.2.0.D1.1.vmdk",
"version": "7.2.0",
"md5sum": "0ee38c7d717840cb4ca822f4870671d0",
"filesize": 361103360,
- "download_url": "https://learningnetworkstore.cisco.com/myaccount"
+ "download_url": "https://u.cisco.com/my-learning/my-account"
}
],
"versions": [
diff --git a/gns3server/appliances/exos.gns3a b/gns3server/appliances/exos.gns3a
index 2393b3e09..00e3dd5e3 100644
--- a/gns3server/appliances/exos.gns3a
+++ b/gns3server/appliances/exos.gns3a
@@ -7,7 +7,7 @@
"vendor_url": "https://www.extremenetworks.com",
"documentation_url": "https://www.extremenetworks.com/support/documentation",
"product_name": "EXOS VM",
- "registry_version": 4,
+ "registry_version": 5,
"status": "stable",
"maintainer": "Extreme Networks",
"maintainer_email": "GitHubscripting@extremenetworks.com",
@@ -18,18 +18,25 @@
"qemu": {
"adapter_type": "rtl8139",
"adapters": 13,
- "ram": 512,
+ "ram": 1024,
"hda_disk_interface": "ide",
"arch": "x86_64",
"console_type": "telnet",
"boot_priority": "dc",
"kvm": "allow",
- "options": "-cpu core2duo"
+ "options": "-nographic -cpu host -enable-kvm"
},
"images": [
- {
+ {
+ "filename": "EXOS-VM_33.6.1.14.qcow2",
+ "version": "33.6.1.14",
+ "md5sum": "01ea4bea3f321f91910565bb223a2540",
+ "filesize": 278647296,
+ "direct_download_url": "https://akamai-ep.extremenetworks.com/Extreme_P/github-en/Virtual_EXOS/EXOS-VM_33.6.1.14.qcow2"
+ },
+ {
"filename": "EXOS-VM_32.7.2.19.qcow2",
"version": "32.7.2.19",
"md5sum": "eba580a2e18d2a9cc972c9ece8917ea8",
@@ -48,6 +55,12 @@
"versions": [
+ {
+ "name": "33.6.1.14",
+ "images": {
+ "hda_disk_image": "EXOS-VM_33.6.1.14.qcow2"
+ }
+ },
{
"name": "32.7.2.19",
"images": {
diff --git a/gns3server/appliances/infix.gns3a b/gns3server/appliances/infix.gns3a
index f28a3d6a7..736863ba7 100644
--- a/gns3server/appliances/infix.gns3a
+++ b/gns3server/appliances/infix.gns3a
@@ -125,9 +125,23 @@
"md5sum": "0c4da508cfcb702c066f47227644b65f",
"version": "26.02.1",
"direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.02.1/infix-x86_64-v26.02.1.qcow2"
+ },
+ {
+ "filename": "infix-x86_64-v26.03.0.qcow2",
+ "filesize": 320339968,
+ "md5sum": "24cd1006734993dab338e5c75f80b875",
+ "version": "26.03.0",
+ "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.03.0/infix-x86_64-v26.03.0.qcow2"
}
],
"versions": [
+ {
+ "name": "26.03.0",
+ "images": {
+ "bios_image": "OVMF-edk2-stable202305.fd",
+ "hda_disk_image": "infix-x86_64-v26.03.0.qcow2"
+ }
+ },
{
"name": "26.02.1",
"images": {
diff --git a/gns3server/appliances/juniper-vJunos-router.gns3a b/gns3server/appliances/juniper-vJunos-router.gns3a
index 8fecd0681..a9fb999b7 100644
--- a/gns3server/appliances/juniper-vJunos-router.gns3a
+++ b/gns3server/appliances/juniper-vJunos-router.gns3a
@@ -18,7 +18,7 @@
"port_name_format": "ge-0/0/{port0}",
"qemu": {
"adapter_type": "virtio-net-pci",
- "adapters": 10,
+ "adapters": 11,
"ram": 5120,
"cpus": 4,
"hda_disk_interface": "virtio",
diff --git a/gns3server/appliances/mikrotik-chr.gns3a b/gns3server/appliances/mikrotik-chr.gns3a
index 48c1eade8..cda69de36 100644
--- a/gns3server/appliances/mikrotik-chr.gns3a
+++ b/gns3server/appliances/mikrotik-chr.gns3a
@@ -27,6 +27,24 @@
"options": "-nographic"
},
"images": [
+ {
+ "filename": "chr-7.22.1.img",
+ "version": "7.22.1",
+ "md5sum": "a67290634f489071eae2fd99dcadba86",
+ "filesize": 134217728,
+ "download_url": "http://www.mikrotik.com/download",
+ "direct_download_url": "https://download.mikrotik.com/routeros/7.22.1/chr-7.22.1.img.zip",
+ "compression": "zip"
+ },
+ {
+ "filename": "chr-7.20.8.img",
+ "version": "7.20.8",
+ "md5sum": "a432cf89f405115163952eefd82934e4",
+ "filesize": 134217728,
+ "download_url": "http://www.mikrotik.com/download",
+ "direct_download_url": "https://download.mikrotik.com/routeros/7.20.8/chr-7.20.8.img.zip",
+ "compression": "zip"
+ },
{
"filename": "chr-7.16.img",
"version": "7.16",
@@ -74,6 +92,18 @@
}
],
"versions": [
+ {
+ "name": "7.22.1",
+ "images": {
+ "hda_disk_image": "chr-7.22.1.img"
+ }
+ },
+ {
+ "name": "7.20.8",
+ "images": {
+ "hda_disk_image": "chr-7.20.8.img"
+ }
+ },
{
"name": "7.16",
"images": {
diff --git a/gns3server/appliances/vyos.gns3a b/gns3server/appliances/vyos.gns3a
index 10bf9ae4c..f89c82044 100644
--- a/gns3server/appliances/vyos.gns3a
+++ b/gns3server/appliances/vyos.gns3a
@@ -9,27 +9,54 @@
"documentation_url": "https://docs.vyos.io/",
"product_name": "VyOS Universal Router",
"product_url": "https://vyos.io/vyos-universal-router",
- "registry_version": 4,
+ "registry_version": 8,
"status": "stable",
"availability": "service-contract",
"maintainer": "VyOS Inc.",
"maintainer_email": "support@vyos.io",
- "usage": "\nDefault credentials:\nUser: vyos\nPassword: vyos",
+ "installation_instructions": "To install the appliance, you must:\n\n1. Have an active VyOS subscription (for details, visit https://vyos.io/).\n2. Download the QEMU/KVM image for your target version from the support portal: https://support.vyos.io/.\n3. Import the downloaded image for installation in the next step.\n\nAlternatively, you can:\n- Build your own image, or\n- Use any compatible qcow2 images (including rolling or stream releases) by selecting \"Create a new version\" and enabling the \"Allow custom files\" feature.",
+ "usage": "No extra installation steps are required - simply boot the router.\nFor more details, please refer to the documentation: https://docs.vyos.io/",
+ "default_username": "vyos",
+ "default_password": "vyos",
"symbol": "vyos.svg",
- "port_name_format": "eth{0}",
- "qemu": {
- "adapter_type": "virtio-net-pci",
- "adapters": 10,
- "ram": 2048,
- "cpus": 4,
- "hda_disk_interface": "virtio",
- "arch": "x86_64",
- "console_type": "telnet",
- "boot_priority": "c",
- "kvm": "require",
- "on_close": "shutdown_signal"
- },
+ "settings": [
+ {
+ "name": "default x86_64",
+ "default": true,
+ "template_type": "qemu",
+ "template_properties": {
+ "adapter_type": "virtio-net-pci",
+ "adapters": 10,
+ "port_name_format": "eth{0}",
+ "ram": 2048,
+ "cpus": 4,
+ "hda_disk_interface": "virtio",
+ "platform": "x86_64",
+ "console_type": "telnet",
+ "boot_priority": "c",
+ "options": "-cpu host",
+ "uefi": false,
+ "on_close": "shutdown_signal"
+ }
+ },
+ {
+ "name": "1.5 x86_64",
+ "inherit_default_properties": true,
+ "template_type": "qemu",
+ "template_properties": {
+ "ram": 8192,
+ "cpus": 4
+ }
+ }
+ ],
"images": [
+ {
+ "filename": "vyos-1.5.0-kvm-amd64.qcow2",
+ "version": "1.5.0",
+ "md5sum": "f84587bd6c45b139563a1d1d2fe0b2e8",
+ "filesize": 618528768,
+ "download_url": "https://support.vyos.io/"
+ },
{
"filename": "vyos-1.4.4-kvm-amd64.qcow2",
"version": "1.4.4",
@@ -132,11 +159,18 @@
"filename": "vyos-1.2.5-amd64.qcow2",
"version": "1.2.5",
"md5sum": "110c22309ec480600446fd2fb4f27a0d",
- "filesize": 411500544 ,
+ "filesize": 411500544,
"download_url": "https://support.vyos.io/"
}
],
"versions": [
+ {
+ "name": "1.5.0",
+ "settings": "1.5 x86_64",
+ "images": {
+ "hda_disk_image": "vyos-1.5.0-kvm-amd64.qcow2"
+ }
+ },
{
"name": "1.4.4",
"images": {
diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py
index 05b0dfe29..34b5144b9 100644
--- a/gns3server/controller/__init__.py
+++ b/gns3server/controller/__init__.py
@@ -217,7 +217,7 @@ class Controller:
try:
os.makedirs(os.path.dirname(self._config_file), exist_ok=True)
with open(self._config_file, 'w+') as f:
- json.dump(controller_settings, f, indent=4)
+ json.dump(controller_settings, f, indent=4, sort_keys=True)
except OSError as e:
log.error("Cannot write controller configuration file '{}': {}".format(self._config_file, e))
diff --git a/gns3server/controller/export_project.py b/gns3server/controller/export_project.py
index a2d6b5a87..d703a4749 100644
--- a/gns3server/controller/export_project.py
+++ b/gns3server/controller/export_project.py
@@ -32,7 +32,16 @@ log = logging.getLogger(__name__)
CHUNK_SIZE = 1024 * 8 # 8KB
-async def export_project(zstream, project, temporary_dir, include_images=False, include_snapshots=False, keep_compute_ids=False, allow_all_nodes=False, reset_mac_addresses=False):
+async def export_project(
+ zstream,
+ project,
+ temporary_dir,
+ include_images=False,
+ include_snapshots=False,
+ keep_compute_ids=False,
+ allow_all_nodes=False,
+ reset_mac_addresses=False
+):
"""
Export a project to a zip file.
@@ -46,7 +55,7 @@ async def export_project(zstream, project, temporary_dir, include_images=False,
:param include_snapshots: save snapshots to the zip file
:param keep_compute_ids: If false replace all compute IDs y local (standard behavior for .gns3project to make it portable)
:param allow_all_nodes: Allow all nodes type to be included in the zip even if not portable
- :param reset_mac_addresses: Reset MAC addresses for each node.
+ :param reset_mac_addresses: Reset MAC addresses for each node
"""
# To avoid issue with data not saved we disallow the export of a running project
@@ -62,7 +71,16 @@ async def export_project(zstream, project, temporary_dir, include_images=False,
# 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_ids, allow_all_nodes, temporary_dir, reset_mac_addresses)
+ await _patch_project_file(
+ project,
+ os.path.join(project._path, file),
+ zstream,
+ include_images,
+ keep_compute_ids,
+ allow_all_nodes,
+ temporary_dir,
+ reset_mac_addresses
+ )
# Export the local files
for root, dirs, files in os.walk(project._path, topdown=True, followlinks=False):
@@ -87,6 +105,10 @@ async def export_project(zstream, project, temporary_dir, include_images=False,
# save empty directories
for directory in dirs:
path = os.path.join(root, directory)
+ if path == temporary_dir:
+ continue
+ if include_snapshots is False and path.endswith("snapshots"):
+ continue
if not os.listdir(path):
zstream.write(path, os.path.relpath(path, project._path))
except FileNotFoundError as e:
@@ -142,7 +164,7 @@ def _is_exportable(path, include_snapshots=False):
"""
# do not export snapshots by default
- if include_snapshots is False and path.endswith("snapshots"):
+ if include_snapshots is False and os.path.dirname(path).endswith("snapshots"):
return False
# do not export directories of snapshots
@@ -167,7 +189,16 @@ def _is_exportable(path, include_snapshots=False):
return True
-async def _patch_project_file(project, path, zstream, include_images, keep_compute_ids, allow_all_nodes, temporary_dir, reset_mac_addresses):
+async def _patch_project_file(
+ project,
+ path,
+ zstream,
+ include_images,
+ keep_compute_ids,
+ 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
@@ -240,7 +271,7 @@ async def _patch_project_file(project, path, zstream, include_images, keep_compu
for compute_id, image_type, image in remote_images:
await _export_remote_images(project, compute_id, image_type, image, zstream, temporary_dir)
- zstream.writestr("project.gns3", json.dumps(topology).encode())
+ zstream.writestr("project.gns3", json.dumps(topology, indent=4, sort_keys=True).encode())
return images
diff --git a/gns3server/controller/import_project.py b/gns3server/controller/import_project.py
index d7e47980e..6efcc2b70 100644
--- a/gns3server/controller/import_project.py
+++ b/gns3server/controller/import_project.py
@@ -39,8 +39,18 @@ Handle the import of project from a .gns3project
"""
-async def import_project(controller, project_id, stream, location=None, name=None, keep_compute_ids=False,
- auto_start=False, auto_open=False, auto_close=True):
+async def import_project(
+ controller,
+ project_id,
+ stream,
+ location=None,
+ name=None,
+ reset_mac_addresses=False,
+ keep_compute_ids=False,
+ auto_start=False,
+ auto_open=False,
+ auto_close=True,
+):
"""
Import a project contain in a zip file
@@ -51,7 +61,9 @@ async def import_project(controller, project_id, stream, location=None, name=Non
:param stream: A io.BytesIO of the zipfile
:param location: Directory for the project if None put in the default directory
:param name: Wanted project name, generate one from the .gns3 if None
+ :param reset_mac_addresses: Reset MAC addresses for each node
:param keep_compute_ids: keep compute IDs unchanged
+ :param project_name: Original project name when restoring a snapshot
:returns: Project
"""
@@ -72,12 +84,14 @@ async def import_project(controller, project_id, stream, location=None, name=Non
# We import the project on top of an existing project (snapshots)
if topology["project_id"] == project_id:
project_name = topology["name"]
+ restoring_snapshot = True
else:
# If the project name is already used we generate a new one
if name:
project_name = controller.get_free_project_name(name)
else:
project_name = controller.get_free_project_name(topology["name"])
+ restoring_snapshot = False
except (ValueError, KeyError):
raise aiohttp.web.HTTPConflict(text="Cannot import project, the project.gns3 file is corrupted")
@@ -105,27 +119,11 @@ async def import_project(controller, project_id, stream, location=None, name=Non
topology["auto_open"] = auto_open
topology["auto_close"] = auto_close
- # Generate a new node id
- node_old_to_new = {}
- for node in topology["topology"]["nodes"]:
- if "node_id" in node:
- node_old_to_new[node["node_id"]] = str(uuid.uuid4())
- _move_node_file(path, node["node_id"], node_old_to_new[node["node_id"]])
- node["node_id"] = node_old_to_new[node["node_id"]]
- else:
- node["node_id"] = str(uuid.uuid4())
+ if not restoring_snapshot:
+ # Do not re-generate IDs if we are restoring a snapshot because they should be the same in a project
+ regenerate_topology_ids(topology, path, reset_mac_addresses=reset_mac_addresses)
- # Update link to use new id
- for link in topology["topology"]["links"]:
- link["link_id"] = str(uuid.uuid4())
- for node in link["nodes"]:
- node["node_id"] = node_old_to_new[node["node_id"]]
-
- # Generate new drawings id
- for drawing in topology["topology"]["drawings"]:
- drawing["drawing_id"] = str(uuid.uuid4())
-
- # Modify the compute id of the node depending of compute capacity
+ # Modify the compute id of the node depending on compute capacity
if not keep_compute_ids:
# For some VM type we move them to the GNS3 VM if possible
# unless it's a linux host without GNS3 VM
@@ -135,14 +133,6 @@ async def import_project(controller, project_id, stream, location=None, name=Non
node["compute_id"] = "vm"
else:
# Round-robin through available compute resources.
- # computes = []
- # for compute_id in controller.computes:
- # compute = controller.get_compute(compute_id)
- # # only use the local compute or any connected compute
- # if compute_id == "local" or compute.connected:
- # computes.append(compute_id)
- # else:
- # log.warning(compute.name, "is not connected!")
compute_nodes = itertools.cycle(controller.computes)
for node in topology["topology"]["nodes"]:
node["compute_id"] = next(compute_nodes)
@@ -162,7 +152,7 @@ async def import_project(controller, project_id, stream, location=None, name=Non
# We change the project_id to avoid erasing the project
topology["project_id"] = project_id
with open(dot_gns3_path, "w+") as f:
- json.dump(topology, f, indent=4)
+ json.dump(topology, f, indent=4, sort_keys=True)
os.remove(os.path.join(path, "project.gns3"))
images_path = os.path.join(path, "images")
@@ -170,8 +160,8 @@ async def import_project(controller, project_id, stream, location=None, name=Non
await _import_images(controller, images_path)
snapshots_path = os.path.join(path, "snapshots")
- if os.path.exists(snapshots_path):
- await _import_snapshots(snapshots_path, project_name, project_id)
+ if not restoring_snapshot and os.path.exists(snapshots_path):
+ await update_snapshots(snapshots_path, path, project_name, project_id, reset_mac_addresses=reset_mac_addresses)
project = await controller.load_project(dot_gns3_path, load=False)
return project
@@ -195,6 +185,41 @@ def _create_symbolic_links(zip_file, path):
except OSError as e:
raise aiohttp.web.HTTPConflict(text=f"Cannot create symbolic link: {e}")
+def regenerate_topology_ids(topology, new_project_path, reset_mac_addresses=False):
+ """
+ Regenerate IDs in the topology and move the files of the nodes to match the new IDs.
+ This is necessary because IDs must be unique across projects.
+
+ :param topology: topology content
+ :param new_project_path: new project path
+ :param reset_mac_addresses: reset MAC addresses
+ """
+
+ # Generate new node IDs
+ node_old_to_new = {}
+ for node in topology["topology"]["nodes"]:
+ new_node_id = str(uuid.uuid4())
+ if "node_id" in node:
+ node_old_to_new[node["node_id"]] = new_node_id
+ _move_node_file(new_project_path, node["node_id"], new_node_id)
+ node["node_id"] = new_node_id
+ if reset_mac_addresses:
+ if "properties" in node:
+ for prop, value in node["properties"].items():
+ # reset the MAC address
+ if prop in ("mac_addr", "mac_address"):
+ node["properties"][prop] = None
+
+ # Generate new link IDs
+ for link in topology["topology"]["links"]:
+ link["link_id"] = str(uuid.uuid4())
+ for node in link["nodes"]:
+ node["node_id"] = node_old_to_new[node["node_id"]]
+
+ # Generate new drawings IDs
+ for drawing in topology["topology"]["drawings"]:
+ drawing["drawing_id"] = str(uuid.uuid4())
+
def _move_node_file(path, old_id, new_id):
"""
Move a file from a node when changing its id
@@ -261,16 +286,17 @@ async def _import_images(controller, images_path):
await wait_run_in_executor(shutil.move, path, dst)
-async def _import_snapshots(snapshots_path, project_name, project_id):
+async def update_snapshots(snapshots_dir, project_path, project_name, project_id, reset_mac_addresses=True):
"""
- Import the snapshots and update their project name and ID to be the same as the main project.
+ Load the snapshots and update their project name and project ID to be the same as the main project.
+ Regenerate all the node, link and drawing IDs
"""
- for snapshot in os.listdir(snapshots_path):
- if not snapshot.endswith(".gns3project"):
+ for snapshot in os.listdir(snapshots_dir):
+ if not (snapshot.endswith(".gns3snapshot") or snapshot.endswith(".gns3project")):
continue
- snapshot_path = os.path.join(snapshots_path, snapshot)
- with tempfile.TemporaryDirectory(dir=snapshots_path) as tmpdir:
+ snapshot_path = os.path.join(snapshots_dir, snapshot)
+ with tempfile.TemporaryDirectory(dir=snapshots_dir) as tmpdir:
# extract everything to a temporary directory
try:
@@ -288,9 +314,9 @@ async def _import_snapshots(snapshots_path, project_name, project_id):
topology_file_path = os.path.join(tmpdir, "project.gns3")
with open(topology_file_path, encoding="utf-8") as f:
topology = json.load(f)
-
topology["name"] = project_name
topology["project_id"] = project_id
+ regenerate_topology_ids(topology, project_path, reset_mac_addresses)
with open(topology_file_path, "w+", encoding="utf-8") as f:
json.dump(topology, f, indent=4, sort_keys=True)
except OSError as e:
@@ -308,5 +334,6 @@ async def _import_snapshots(snapshots_path, project_name, project_id):
async with aiofiles.open(snapshot_path, 'wb+') as f:
async for chunk in zstream:
await f.write(chunk)
+ log.info("Project '{}': updated and repacked snapshot file '{}'".format(project_name, snapshot))
except OSError as e:
raise aiohttp.web.HTTPConflict(text="Cannot update snapshot '{}': the snapshot cannot be recreated: {}".format(os.path.basename(snapshot), e))
diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py
index eb5bad696..54be7fe02 100644
--- a/gns3server/controller/node.py
+++ b/gns3server/controller/node.py
@@ -91,7 +91,7 @@ class Node:
self._first_port_name = None
self._console_auto_start = False
- # This properties will be recompute
+ # This properties will be recomputed
ignore_properties = ("width", "height", "hover_symbol")
self.properties = kwargs.pop('properties', {})
@@ -382,6 +382,7 @@ class Node:
await self.parse_node_response(response.json)
return True
trial += 1
+ return False
async def update(self, **kwargs):
"""
diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py
index 743758c72..9d633f526 100644
--- a/gns3server/controller/project.py
+++ b/gns3server/controller/project.py
@@ -15,7 +15,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-import sys
import re
import os
import json
@@ -46,7 +45,7 @@ from ..utils.asyncio import locking
from ..utils.asyncio import aiozipstream
from ..utils.asyncio import wait_run_in_executor
from .export_project import export_project
-from .import_project import import_project, _move_node_file
+from .import_project import import_project, update_snapshots, regenerate_topology_ids
import logging
log = logging.getLogger(__name__)
@@ -96,6 +95,7 @@ class Project:
self._show_interface_labels = show_interface_labels
self._variables = variables
self._supplier = supplier
+ self._snapshots_config_file = "snapshots.conf"
self._loading = False
self._closing = False
@@ -189,19 +189,7 @@ class Project:
self._drawings = {}
self._snapshots = {}
self._computes = []
-
- # List the available snapshots
- snapshot_dir = os.path.join(self.path, "snapshots")
- if os.path.exists(snapshot_dir):
- for snap in os.listdir(snapshot_dir):
- if snap.endswith(".gns3project"):
- try:
- snapshot = Snapshot(self, filename=snap)
- except ValueError:
- log.error("Invalid snapshot file: {}".format(snap))
- continue
- self._snapshots[snapshot.id] = snapshot
-
+ self._load_snapshot_config()
# Create the project on demand on the compute node
self._project_created_on_compute = set()
@@ -782,6 +770,60 @@ class Project:
except KeyError:
raise aiohttp.web.HTTPNotFound(text="Snapshot ID {} doesn't exist".format(snapshot_id))
+ def _load_snapshot_config(self):
+
+ snapshot_dir = os.path.join(self.path, "snapshots")
+ self._snapshot_conf_path = os.path.join(snapshot_dir, self._snapshots_config_file)
+ self._snapshot_conf = []
+ if os.path.isfile(self._snapshot_conf_path):
+ try:
+ with open(self._snapshot_conf_path, encoding="utf-8") as f:
+ self._snapshot_conf = json.load(f)
+ except (OSError, UnicodeDecodeError, ValueError):
+ log.exception("Could not read snapshot config %s", self._snapshot_conf_path)
+ raise aiohttp.web.HTTPConflict(text="Could not read snapshot configuration file")
+
+ # Load all legacy snapshots (.gns3project files) to create an initial snapshot config if it doesn't exist
+ if os.path.exists(snapshot_dir) and not self._snapshot_conf:
+ for snap in os.listdir(snapshot_dir):
+ if snap.endswith(".gns3project"):
+ try:
+ snapshot = Snapshot(self, filename=snap)
+ except ValueError:
+ log.error("Invalid snapshot file: {}".format(snap))
+ continue
+ self._snapshots[snapshot.id] = snapshot
+ else:
+ # Create the Snapshot instances from the snapshot config file
+ for snapshot_entry in self._snapshot_conf:
+ try:
+ path = os.path.join(snapshot_dir, snapshot_entry["filename"])
+ if not os.path.isfile(path):
+ log.warning("Snapshot file '{}' does not exist".format(path))
+ continue
+ snapshot_entry.pop("project_id")
+ snapshot = Snapshot(self, **snapshot_entry)
+ self._snapshots[snapshot.id] = snapshot
+ except KeyError:
+ log.error("Invalid entry in snapshot config file: {}".format(snapshot_entry))
+ continue
+
+ self._save_snapshot_config()
+
+ def _save_snapshot_config(self):
+
+ if not self._snapshots:
+ return
+
+ self._snapshot_conf = []
+ for snapshot in self._snapshots.values():
+ self._snapshot_conf.append(snapshot.__json__())
+ try:
+ with open(self._snapshot_conf_path, 'w+') as f:
+ json.dump(self._snapshot_conf, f, indent=4)
+ except OSError as e:
+ log.error("Cannot write snapshot config '{}': {}".format(self._snapshot_conf_path, e))
+
@open_required
async def snapshot(self, name):
"""
@@ -795,12 +837,14 @@ class Project:
snapshot = Snapshot(self, name=name)
await snapshot.create()
self._snapshots[snapshot.id] = snapshot
+ self._save_snapshot_config()
return snapshot
@open_required
async def delete_snapshot(self, snapshot_id):
snapshot = self.get_snapshot(snapshot_id)
del self._snapshots[snapshot.id]
+ self._save_snapshot_config()
os.remove(snapshot.path)
@locking
@@ -891,7 +935,7 @@ class Project:
def _get_default_project_directory(cls):
"""
Return the default location for the project directory
- depending of the operating system
+ depending on the operating system
"""
server_config = Config.instance().get_section_config("Server")
@@ -912,7 +956,7 @@ class Project:
Load topology elements
"""
- if self._closing is True:
+ if self._closing:
raise aiohttp.web.HTTPConflict(text="Project is closing, please try again in a few seconds...")
if self._status == "opened":
@@ -1083,7 +1127,14 @@ class Project:
with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir:
# 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_ids=True, allow_all_nodes=True, reset_mac_addresses=reset_mac_addresses)
+ await export_project(
+ zstream,
+ self,
+ tmpdir,
+ keep_compute_ids=True,
+ include_snapshots=True,
+ allow_all_nodes=True
+ )
# export the project to a temporary location
project_path = os.path.join(tmpdir, "project.gns3p")
@@ -1092,11 +1143,20 @@ class Project:
async for chunk in zstream:
await f.write(chunk)
- # import the temporary project
+ new_project_id = str(uuid.uuid4())
+ # import the duplicated project
with open(project_path, "rb") as f:
- project = await import_project(self._controller, str(uuid.uuid4()), f, location=location, name=name, keep_compute_ids=True)
+ project = await import_project(
+ self._controller,
+ new_project_id,
+ f,
+ location=location,
+ name=name,
+ reset_mac_addresses=reset_mac_addresses,
+ keep_compute_ids=True
+ )
- log.info("Project '{}' duplicated in {:.4f} seconds".format(project.name, time.time() - begin))
+ 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)))
@@ -1105,6 +1165,62 @@ class Project:
return project
+ async def _fast_duplication(self, name=None, location=None, reset_mac_addresses=True):
+ """
+ Fast duplication of a project.
+
+ Copy the project files directly rather than in an import-export fashion.
+
+ :param name: Name of the new project. A new one will be generated in case of conflicts
+ :param location: Parent directory of the new project
+ :param reset_mac_addresses: Reset MAC addresses for the duplicated project
+ """
+
+ # remote replication is not supported with remote computes
+ for compute in self.computes:
+ if compute.id != "local":
+ log.warning("Fast duplication is not supported with remote compute: '{}'".format(compute.id))
+ return None
+ # work dir
+ p_work = pathlib.Path(location or self.path).parent.absolute()
+ t0 = time.time()
+ new_project_id = str(uuid.uuid4())
+ if location:
+ new_project_path = p_work.joinpath(location)
+ else:
+ new_project_path = p_work.joinpath(new_project_id)
+ # copy dir
+ await wait_run_in_executor(shutil.copytree, self.path, new_project_path.as_posix(), symlinks=True, ignore_dangling_symlinks=True)
+ log.info("Project content copied from '{}' to '{}' in {}s".format(self.path, new_project_path, time.time() - t0))
+ topology = json.loads(new_project_path.joinpath('{}.gns3'.format(self.name)).read_bytes())
+ project_name = name or topology["name"]
+ # If the project name is already used we generate a new one
+ project_name = self.controller.get_free_project_name(project_name)
+ topology["name"] = project_name
+ # To avoid unexpected behavior (project start without manual operations just after import)
+ topology["auto_start"] = False
+ topology["auto_open"] = False
+ topology["auto_close"] = False
+
+ # regenerate IDs for the duplicated project
+ regenerate_topology_ids(topology, new_project_path, reset_mac_addresses)
+
+ # dump the updated .gns3 project file
+ dot_gns3_path = new_project_path.joinpath('{}.gns3'.format(project_name))
+ topology["project_id"] = new_project_id
+ with open(dot_gns3_path, "w+") as f:
+ json.dump(topology, f, indent=4, sort_keys=True)
+
+ # update the snapshots with new IDs
+ snapshots_dir = os.path.join(new_project_path, "snapshots")
+ if os.path.isdir(snapshots_dir):
+ await update_snapshots(snapshots_dir, new_project_path, project_name, new_project_id)
+
+ os.remove(new_project_path.joinpath('{}.gns3'.format(self.name)))
+ project = await self.controller.load_project(dot_gns3_path, load=False)
+ log.info("Project '{}': fast duplicated in {:.4f} seconds".format(project.name, time.time() - t0))
+ return project
+
def is_running(self):
"""
If a node is started or paused return True
@@ -1203,11 +1319,13 @@ class Project:
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,
- node.name,
- new_node_uuid,
- node_type=node_type,
- **data)
+ new_node = await self.add_node(
+ node.compute,
+ node.name,
+ new_node_uuid,
+ node_type=node_type,
+ **data
+ )
try:
await node.post("/duplicate", timeout=None, data={
"destination_node_id": new_node_uuid
@@ -1255,72 +1373,4 @@ class Project:
def __repr__(self):
return "".format(self._name, self._id)
- async def _fast_duplication(self, name=None, location=None, reset_mac_addresses=True):
- """
- Fast duplication of a project.
- Copy the project files directly rather than in an import-export fashion.
-
- :param name: Name of the new project. A new one will be generated in case of conflicts
- :param location: Parent directory of the new project
- :param reset_mac_addresses: Reset MAC addresses for the new project
- """
-
- # remote replication is not supported with remote computes
- for compute in self.computes:
- if compute.id != "local":
- log.warning("Fast duplication is not supported with remote compute: '{}'".format(compute.id))
- return None
- # work dir
- p_work = pathlib.Path(location or self.path).parent.absolute()
- t0 = time.time()
- new_project_id = str(uuid.uuid4())
- if location:
- new_project_path = p_work.joinpath(location)
- else:
- new_project_path = p_work.joinpath(new_project_id)
- # copy dir
- await wait_run_in_executor(shutil.copytree, self.path, new_project_path.as_posix(), symlinks=True, ignore_dangling_symlinks=True)
- log.info("Project content copied from '{}' to '{}' in {}s".format(self.path, new_project_path, time.time() - t0))
- topology = json.loads(new_project_path.joinpath('{}.gns3'.format(self.name)).read_bytes())
- project_name = name or topology["name"]
- # If the project name is already used we generate a new one
- project_name = self.controller.get_free_project_name(project_name)
- topology["name"] = project_name
- # To avoid unexpected behavior (project start without manual operations just after import)
- topology["auto_start"] = False
- topology["auto_open"] = False
- topology["auto_close"] = False
- # change node ID
- node_old_to_new = {}
- for node in topology["topology"]["nodes"]:
- new_node_id = str(uuid.uuid4())
- if "node_id" in node:
- node_old_to_new[node["node_id"]] = new_node_id
- _move_node_file(new_project_path, node["node_id"], new_node_id)
- node["node_id"] = new_node_id
- if reset_mac_addresses:
- if "properties" in node:
- for prop, value in node["properties"].items():
- # reset the MAC address
- if prop in ("mac_addr", "mac_address"):
- node["properties"][prop] = None
- # change link ID
- for link in topology["topology"]["links"]:
- link["link_id"] = str(uuid.uuid4())
- for node in link["nodes"]:
- node["node_id"] = node_old_to_new[node["node_id"]]
- # Generate new drawings id
- for drawing in topology["topology"]["drawings"]:
- drawing["drawing_id"] = str(uuid.uuid4())
-
- # And we dump the updated.gns3
- dot_gns3_path = new_project_path.joinpath('{}.gns3'.format(project_name))
- topology["project_id"] = new_project_id
- with open(dot_gns3_path, "w+") as f:
- json.dump(topology, f, indent=4)
-
- os.remove(new_project_path.joinpath('{}.gns3'.format(self.name)))
- project = await self.controller.load_project(dot_gns3_path, load=False)
- log.info("Project '{}' fast duplicated in {:.4f} seconds".format(project.name, time.time() - t0))
- return project
diff --git a/gns3server/controller/snapshot.py b/gns3server/controller/snapshot.py
index 4a45dacab..8bd44e94c 100644
--- a/gns3server/controller/snapshot.py
+++ b/gns3server/controller/snapshot.py
@@ -35,30 +35,47 @@ 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"
+# Used to extract the date and time from the filename
+FILENAME_DATETIME_FORMAT = "%d%m%y_%H%M%S"
+# Used to create a description of the snapshot with a human-readable date and time
+DESCRIPTION_DATETIME_FORMAT = "%Y-%m-%d at %H:%M:%S"
class Snapshot:
"""
A snapshot object
"""
- def __init__(self, project, name=None, filename=None):
+ def __init__(self, project, snapshot_id=None, name=None, filename=None, created_at=None, description=None):
assert filename or name, "You need to pass a name or a filename"
- self._id = str(uuid.uuid4()) # We don't need to keep id between project loading because they are use only as key for operation like delete, update.. but have no impact on disk
+ if snapshot_id:
+ self._id = snapshot_id
+ else:
+ self._id = str(uuid.uuid4())
+
self._project = project
if name:
self._name = name
- self._created_at = datetime.now(timezone.utc).timestamp()
- filename = self._name + "_" + datetime.fromtimestamp(self._created_at, tz=timezone.utc).replace(tzinfo=None).strftime(FILENAME_TIME_FORMAT) + ".gns3project"
+
+ if created_at:
+ self._created_at = created_at
+ else:
+ self._created_at = int(datetime.now(timezone.utc).timestamp())
+ if not filename:
+ filename = self._name + ".gns3snapshot"
else:
self._name = filename.rsplit("_", 2)[0]
datestring = filename.replace(self._name + "_", "").split(".")[0]
- self._created_at = datetime.strptime(datestring, FILENAME_TIME_FORMAT).replace(tzinfo=timezone.utc).timestamp()
+ self._created_at = int(datetime.strptime(datestring, FILENAME_DATETIME_FORMAT).replace(tzinfo=timezone.utc).timestamp())
+ if not description:
+ date = datetime.fromtimestamp(self._created_at, tz=timezone.utc).replace(tzinfo=None).strftime(DESCRIPTION_DATETIME_FORMAT)
+ description = "Snapshot '{}' taken on {}".format(self._name, date)
+
+ self._description = description
+ self._filename = filename
self._path = os.path.join(project.path, "snapshots", filename)
@property
@@ -69,13 +86,17 @@ class Snapshot:
def name(self):
return self._name
+ @property
+ def description(self):
+ return self._description
+
@property
def path(self):
return self._path
@property
def created_at(self):
- return int(self._created_at)
+ return self._created_at
async def create(self):
"""
@@ -114,14 +135,22 @@ class Snapshot:
await self._project.close(ignore_notification=True)
try:
+ begin = time.time()
# delete the current project files
project_files_path = os.path.join(self._project.path, "project-files")
if os.path.exists(project_files_path):
await wait_run_in_executor(shutil.rmtree, project_files_path)
with open(self._path, "rb") as f:
- project = await import_project(self._project.controller, self._project.id, f, location=self._project.path,
- auto_start=self._project.auto_start, auto_open=self._project.auto_open,
- auto_close=self._project.auto_close)
+ project = await import_project(
+ self._project.controller,
+ self._project.id,
+ f,
+ location=self._project.path,
+ auto_start=self._project.auto_start,
+ auto_open=self._project.auto_open,
+ auto_close=self._project.auto_close
+ )
+ log.info("Snapshot '{}' restored in {:.4f} seconds".format(self.name, time.time() - begin))
except (OSError, PermissionError) as e:
raise aiohttp.web.HTTPConflict(text=str(e))
await project.open()
@@ -132,6 +161,8 @@ class Snapshot:
return {
"snapshot_id": self._id,
"name": self._name,
- "created_at": int(self._created_at),
+ "created_at": self._created_at,
+ "description": self._description,
+ "filename": self._filename,
"project_id": self._project.id
}
diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py
index d9953ae27..ab04e412e 100644
--- a/gns3server/crash_report.py
+++ b/gns3server/crash_report.py
@@ -57,7 +57,7 @@ class CrashReport:
Report crash to a third party service
"""
- DSN = "https://b3291ad085cbcc85059160e74ce83ab6@o19455.ingest.us.sentry.io/38482"
+ DSN = "https://74851857a3f05aecd909415e2a4cbd60@o19455.ingest.us.sentry.io/38482"
_instance = None
def __init__(self):
diff --git a/gns3server/schemas/snapshot.py b/gns3server/schemas/snapshot.py
index 1c306cd2e..ddc241171 100644
--- a/gns3server/schemas/snapshot.py
+++ b/gns3server/schemas/snapshot.py
@@ -25,6 +25,10 @@ SNAPSHOT_CREATE_SCHEMA = {
"description": "Snapshot name",
"minLength": 1
},
+ "description": {
+ "description": "Snapshot description",
+ "minLength": 1
+ },
},
"additionalProperties": False,
"required": ["name"]
@@ -50,7 +54,17 @@ SNAPSHOT_OBJECT_SCHEMA = {
"pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
},
"name": {
- "description": "Project name",
+ "description": "Snapshot name",
+ "type": "string",
+ "minLength": 1
+ },
+ "filename": {
+ "description": "Snapshot filename",
+ "type": "string",
+ "minLength": 1
+ },
+ "description": {
+ "description": "Snapshot description",
"type": "string",
"minLength": 1
},
@@ -60,5 +74,5 @@ SNAPSHOT_OBJECT_SCHEMA = {
}
},
"additionalProperties": False,
- "required": ["snapshot_id", "name", "created_at", "project_id"]
+ "required": ["snapshot_id", "name", "filename", "created_at", "description", "project_id"]
}
diff --git a/gns3server/static/web-ui/26.77d4bfd104f37c42e028.js b/gns3server/static/web-ui/26.34a83d9cee7db39cd0de.js
similarity index 86%
rename from gns3server/static/web-ui/26.77d4bfd104f37c42e028.js
rename to gns3server/static/web-ui/26.34a83d9cee7db39cd0de.js
index 0c692c06e..c889a9db3 100644
--- a/gns3server/static/web-ui/26.77d4bfd104f37c42e028.js
+++ b/gns3server/static/web-ui/26.34a83d9cee7db39cd0de.js
@@ -1 +1 @@
-"use strict";(self.webpackChunkgns3_web_ui=self.webpackChunkgns3_web_ui||[]).push([[26],{91026:function(W,g,a){a.r(g),a.d(g,{TopologySummaryComponent:function(){return U}});var t=a(38999),_=a(96852),h=a(14200),f=a(36889),v=a(3941),y=a(15132),p=a(40098),x=a(39095),c=a(88802),S=a(73044),d=a(59412),T=a(93386);function C(i,e){if(1&i){var o=t.EpF();t.TgZ(0,"div",2),t.NdJ("mousemove",function(r){return t.CHM(o),t.oxw().dragWidget(r)},!1,t.evT)("mouseup",function(){return t.CHM(o),t.oxw().toggleDragging(!1)},!1,t.evT),t.qZA()}}function b(i,e){1&i&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",29),t.qZA())}function E(i,e){1&i&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",30),t.qZA())}function Z(i,e){1&i&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",31),t.qZA())}function O(i,e){if(1&i&&(t.TgZ(0,"div"),t._uU(1),t.qZA()),2&i){var o=t.oxw().$implicit;t.xp6(1),t.lnq(" ",o.console_type,"://",o.console_host,":",o.console," ")}}function P(i,e){1&i&&(t.TgZ(0,"div"),t._uU(1," none "),t.qZA())}function M(i,e){if(1&i&&(t.TgZ(0,"div",25),t.TgZ(1,"div"),t.YNc(2,b,2,0,"svg",26),t.YNc(3,E,2,0,"svg",26),t.YNc(4,Z,2,0,"svg",26),t._uU(5),t.qZA(),t.YNc(6,O,2,3,"div",27),t.YNc(7,P,2,0,"div",27),t.qZA()),2&i){var o=e.$implicit;t.xp6(2),t.Q6J("ngIf","started"===o.status),t.xp6(1),t.Q6J("ngIf","suspended"===o.status),t.xp6(1),t.Q6J("ngIf","stopped"===o.status),t.xp6(1),t.hij(" ",o.name," "),t.xp6(1),t.Q6J("ngIf",null!=o.console&&null!=o.console&&"none"!=o.console_type),t.xp6(1),t.Q6J("ngIf",null==o.console||"none"===o.console_type)}}function w(i,e){1&i&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",29),t.qZA())}function A(i,e){1&i&&(t.O4$(),t.TgZ(0,"svg",28),t._UZ(1,"rect",31),t.qZA())}function F(i,e){if(1&i&&(t.TgZ(0,"div",25),t.TgZ(1,"div"),t.YNc(2,w,2,0,"svg",26),t.YNc(3,A,2,0,"svg",26),t._uU(4),t.qZA(),t.TgZ(5,"div"),t._uU(6),t.qZA(),t.TgZ(7,"div"),t._uU(8),t.qZA(),t.qZA()),2&i){var o=e.$implicit,s=t.oxw(2);t.xp6(2),t.Q6J("ngIf",o.connected),t.xp6(1),t.Q6J("ngIf",!o.connected),t.xp6(1),t.hij(" ",o.name," "),t.xp6(2),t.hij(" ",o.host," "),t.xp6(2),t.hij(" ",s.server.location," ")}}var I=function(i){return{lightTheme:i}},D=function(){return{right:!0,left:!0,bottom:!0,top:!0}};function N(i,e){if(1&i){var o=t.EpF();t.TgZ(0,"div",3),t.NdJ("mousedown",function(){return t.CHM(o),t.oxw().toggleDragging(!0)})("resizeStart",function(){return t.CHM(o),t.oxw().toggleDragging(!1)})("resizeEnd",function(n){return t.CHM(o),t.oxw().onResizeEnd(n)}),t.TgZ(1,"div",4),t.TgZ(2,"mat-tab-group"),t.TgZ(3,"mat-tab",5),t.NdJ("click",function(){return t.CHM(o),t.oxw().toggleTopologyVisibility(!0)}),t.TgZ(4,"div",6),t.TgZ(5,"div",7),t.TgZ(6,"mat-select",8),t.TgZ(7,"mat-optgroup",9),t.TgZ(8,"mat-option",10),t.NdJ("onSelectionChange",function(){return t.CHM(o),t.oxw().applyStatusFilter("started")}),t._uU(9,"started"),t.qZA(),t.TgZ(10,"mat-option",11),t.NdJ("onSelectionChange",function(){return t.CHM(o),t.oxw().applyStatusFilter("suspended")}),t._uU(11,"suspended"),t.qZA(),t.TgZ(12,"mat-option",12),t.NdJ("onSelectionChange",function(){return t.CHM(o),t.oxw().applyStatusFilter("stopped")}),t._uU(13,"stopped"),t.qZA(),t.qZA(),t.TgZ(14,"mat-optgroup",13),t.TgZ(15,"mat-option",14),t.NdJ("onSelectionChange",function(){return t.CHM(o),t.oxw().applyCaptureFilter("capture")}),t._uU(16,"active capture(s)"),t.qZA(),t.TgZ(17,"mat-option",15),t.NdJ("onSelectionChange",function(){return t.CHM(o),t.oxw().applyCaptureFilter("packet")}),t._uU(18,"active packet captures"),t.qZA(),t.qZA(),t.qZA(),t.qZA(),t.TgZ(19,"div",16),t.TgZ(20,"mat-select",17),t.NdJ("selectionChange",function(){return t.CHM(o),t.oxw().setSortingOrder()})("valueChange",function(n){return t.CHM(o),t.oxw().sortingOrder=n}),t.TgZ(21,"mat-option",18),t._uU(22,"sort by name ascending"),t.qZA(),t.TgZ(23,"mat-option",19),t._uU(24,"sort by name descending"),t.qZA(),t.qZA(),t.qZA(),t._UZ(25,"mat-divider",20),t.TgZ(26,"div",21),t.YNc(27,M,8,6,"div",22),t.qZA(),t.qZA(),t.qZA(),t.TgZ(28,"mat-tab",23),t.NdJ("click",function(){return t.CHM(o),t.oxw().toggleTopologyVisibility(!1)}),t.TgZ(29,"div",6),t.TgZ(30,"div",24),t.YNc(31,F,9,5,"div",22),t.qZA(),t.qZA(),t.qZA(),t.qZA(),t.qZA(),t.qZA()}if(2&i){var s=t.oxw();t.Q6J("ngStyle",s.style)("ngClass",t.VKq(9,I,s.isLightThemeEnabled))("validateResize",s.validate)("resizeEdges",t.DdM(11,D))("enableGhostResize",!0),t.xp6(20),t.Q6J("value",s.sortingOrder),t.xp6(6),t.Q6J("ngStyle",s.styleInside),t.xp6(1),t.Q6J("ngForOf",s.filteredNodes),t.xp6(4),t.Q6J("ngForOf",s.computes)}}var U=function(){function i(e,o,s,r,n){this.nodesDataSource=e,this.projectService=o,this.computeService=s,this.linksDataSource=r,this.themeService=n,this.closeTopologySummary=new t.vpe,this.style={},this.styleInside={height:"280px"},this.subscriptions=[],this.nodes=[],this.filteredNodes=[],this.sortingOrder="asc",this.startedStatusFilterEnabled=!1,this.suspendedStatusFilterEnabled=!1,this.stoppedStatusFilterEnabled=!1,this.captureFilterEnabled=!1,this.packetFilterEnabled=!1,this.computes=[],this.isTopologyVisible=!0,this.isDraggingEnabled=!1,this.isLightThemeEnabled=!1}return i.prototype.ngOnInit=function(){var e=this;this.isLightThemeEnabled="light"===this.themeService.getActualTheme(),this.subscriptions.push(this.nodesDataSource.changes.subscribe(function(o){e.nodes=o,e.nodes.forEach(function(s){("0.0.0.0"===s.console_host||"0:0:0:0:0:0:0:0"===s.console_host||"::"===s.console_host)&&(s.console_host=e.server.host)}),e.filteredNodes=o.sort("asc"===e.sortingOrder?e.compareAsc:e.compareDesc)})),this.projectService.getStatistics(this.server,this.project.project_id).subscribe(function(o){e.projectsStatistics=o}),this.computeService.getComputes(this.server).subscribe(function(o){e.computes=o}),this.revertPosition()},i.prototype.revertPosition=function(){var e=localStorage.getItem("leftPosition"),o=localStorage.getItem("rightPosition"),s=localStorage.getItem("topPosition"),r=localStorage.getItem("widthOfWidget"),n=localStorage.getItem("heightOfWidget");this.style=s?{position:"fixed",left:+e+"px",right:+o+"px",top:+s+"px",width:+r+"px",height:+n+"px"}:{top:"60px",right:"0px",width:"320px",height:"400px"}},i.prototype.toggleDragging=function(e){this.isDraggingEnabled=e},i.prototype.dragWidget=function(e){var o=Number(e.movementX),s=Number(e.movementY),r=Number(this.style.width.split("px")[0]),n=Number(this.style.height.split("px")[0]),l=Number(this.style.top.split("px")[0])+s;if(this.style.left){var u=Number(this.style.left.split("px")[0])+o;this.style={position:"fixed",left:u+"px",top:l+"px",width:r+"px",height:n+"px"},localStorage.setItem("leftPosition",u.toString()),localStorage.setItem("topPosition",l.toString()),localStorage.setItem("widthOfWidget",r.toString()),localStorage.setItem("heightOfWidget",n.toString())}else{var m=Number(this.style.right.split("px")[0])-o;this.style={position:"fixed",right:m+"px",top:l+"px",width:r+"px",height:n+"px"},localStorage.setItem("rightPosition",m.toString()),localStorage.setItem("topPosition",l.toString()),localStorage.setItem("widthOfWidget",r.toString()),localStorage.setItem("heightOfWidget",n.toString())}},i.prototype.validate=function(e){return!(e.rectangle.width&&e.rectangle.height&&(e.rectangle.width<290||e.rectangle.height<260))},i.prototype.onResizeEnd=function(e){this.style={position:"fixed",left:e.rectangle.left+"px",top:e.rectangle.top+"px",width:e.rectangle.width+"px",height:e.rectangle.height+"px"},this.styleInside={height:e.rectangle.height-120+"px"}},i.prototype.toggleTopologyVisibility=function(e){this.isTopologyVisible=e,this.revertPosition()},i.prototype.compareAsc=function(e,o){return e.name
-
+
@@ -46,6 +46,6 @@
gtag('config', 'G-0BT7QQV1W1');
-
+