mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
Merge branch '2.2' into 3.0
# Conflicts: # gns3server/controller/__init__.py # gns3server/controller/export_project.py # gns3server/controller/project.py # gns3server/controller/snapshot.py # gns3server/crash_report.py # gns3server/schemas/snapshot.py # gns3server/static/web-ui/26.34a83d9cee7db39cd0de.js # gns3server/static/web-ui/index.html # gns3server/static/web-ui/main.d74d38241f0602d1b3e0.js # gns3server/version.py # gns3server/web/route.py # tests/handlers/api/controller/test_project.py # tests/handlers/api/controller/test_snapshot.py
This commit is contained in:
23 files changed
+432
-204
No files matched your search
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -288,7 +288,7 @@ class Controller:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(self._vars_file), exist_ok=True)
|
||||
with open(self._vars_file, 'w+') as f:
|
||||
json.dump(controller_vars, f, indent=4)
|
||||
json.dump(controller_vars, f, indent=4, sort_keys=True)
|
||||
except OSError as e:
|
||||
log.error(f"Cannot write controller vars file '{self._vars_file}': {e}")
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ async def export_project(
|
||||
keep_compute_ids,
|
||||
allow_all_nodes,
|
||||
temporary_dir,
|
||||
reset_mac_addresses,
|
||||
reset_mac_addresses
|
||||
)
|
||||
|
||||
# Export the local files
|
||||
@@ -107,6 +107,10 @@ async def export_project(
|
||||
# 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:
|
||||
@@ -168,7 +172,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
|
||||
@@ -194,7 +198,14 @@ def _is_exportable(path, include_snapshots=False):
|
||||
|
||||
|
||||
async def _patch_project_file(
|
||||
project, path, zstream, include_images, keep_compute_ids, allow_all_nodes, temporary_dir, reset_mac_addresses
|
||||
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.
|
||||
@@ -268,7 +279,7 @@ async def _patch_project_file(
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -40,8 +40,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
|
||||
|
||||
@@ -52,7 +62,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
|
||||
"""
|
||||
@@ -73,12 +85,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 ControllerError("Cannot import project, the project.gns3 file is corrupted")
|
||||
|
||||
@@ -106,27 +120,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
|
||||
@@ -136,14 +134,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)
|
||||
@@ -171,7 +161,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")
|
||||
@@ -179,8 +169,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
|
||||
@@ -204,6 +194,41 @@ def _create_symbolic_links(zip_file, path):
|
||||
except OSError as e:
|
||||
raise ControllerError(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
|
||||
@@ -272,16 +297,17 @@ async def _import_images(controller, images_path):
|
||||
os.chmod(dst, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
|
||||
|
||||
|
||||
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:
|
||||
@@ -301,9 +327,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:
|
||||
@@ -325,6 +351,7 @@ 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 ControllerError(
|
||||
f"Cannot update snapshot '{os.path.basename(snapshot)}': the snapshot cannot be recreated: {e}"
|
||||
|
||||
@@ -113,7 +113,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", {})
|
||||
|
||||
@@ -426,6 +426,7 @@ class Node:
|
||||
await self.parse_node_response(response.json)
|
||||
return True
|
||||
trial += 1
|
||||
return False
|
||||
|
||||
async def update(self, **kwargs):
|
||||
"""
|
||||
|
||||
@@ -45,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
|
||||
from .controller_error import ControllerError, ControllerForbiddenError, ControllerNotFoundError
|
||||
|
||||
import logging
|
||||
@@ -119,6 +119,7 @@ class Project:
|
||||
self._variables = variables
|
||||
self._supplier = supplier
|
||||
self._created_by = created_by
|
||||
self._snapshots_config_file = "snapshots.conf"
|
||||
|
||||
self._loading = False
|
||||
self._closing = False
|
||||
@@ -209,19 +210,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()
|
||||
|
||||
@@ -808,6 +797,59 @@ class Project:
|
||||
except KeyError:
|
||||
raise ControllerNotFoundError(f"Snapshot ID {snapshot_id} doesn't exist")
|
||||
|
||||
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) as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Could not read snapshot config {}: {}".format(self._snapshot_conf_path, str(e)))
|
||||
|
||||
# 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):
|
||||
"""
|
||||
@@ -821,12 +863,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
|
||||
@@ -943,7 +987,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().settings.Server
|
||||
@@ -1150,8 +1194,8 @@ class Project:
|
||||
self,
|
||||
tmpdir,
|
||||
keep_compute_ids=True,
|
||||
allow_all_nodes=True,
|
||||
reset_mac_addresses=reset_mac_addresses,
|
||||
include_snapshots=True,
|
||||
allow_all_nodes=True
|
||||
)
|
||||
|
||||
# export the project to a temporary location
|
||||
@@ -1161,13 +1205,15 @@ 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()),
|
||||
new_project_id,
|
||||
f,
|
||||
name=name,
|
||||
reset_mac_addresses=reset_mac_addresses,
|
||||
keep_compute_ids=True
|
||||
)
|
||||
|
||||
@@ -1180,6 +1226,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
|
||||
@@ -1324,7 +1426,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})
|
||||
except ControllerNotFoundError:
|
||||
@@ -1371,68 +1479,3 @@ class Project:
|
||||
def __repr__(self):
|
||||
return f"<gns3server.controller.Project {self._name} {self._id}>"
|
||||
|
||||
async def _fast_duplication(self, name=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 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(self.path).parent.absolute()
|
||||
t0 = time.time()
|
||||
new_project_id = str(uuid.uuid4())
|
||||
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
|
||||
@@ -36,32 +36,47 @@ import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 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("%d%m%y_%H%M%S")
|
||||
+ ".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, "%d%m%y_%H%M%S").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
|
||||
@@ -72,13 +87,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):
|
||||
"""
|
||||
@@ -117,14 +136,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 ControllerError(str(e))
|
||||
await project.open()
|
||||
@@ -135,6 +162,8 @@ class Snapshot:
|
||||
return {
|
||||
"snapshot_id": self._id,
|
||||
"name": self._name,
|
||||
"created_at": int(self._created_at),
|
||||
"project_id": self._project.id,
|
||||
"created_at": self._created_at,
|
||||
"description": self._description,
|
||||
"filename": self._filename,
|
||||
"project_id": self._project.id
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
!function(){"use strict";var e,v={},g={};function n(e){var u=g[e];if(void 0!==u)return u.exports;var t=g[e]={id:e,loaded:!1,exports:{}};return v[e].call(t.exports,t,t.exports,n),t.loaded=!0,t.exports}n.m=v,e=[],n.O=function(u,t,a,o){if(!t){var r=1/0;for(i=0;i<e.length;i++){t=e[i][0],a=e[i][1],o=e[i][2];for(var l=!0,f=0;f<t.length;f++)(!1&o||r>=o)&&Object.keys(n.O).every(function(b){return n.O[b](t[f])})?t.splice(f--,1):(l=!1,o<r&&(r=o));if(l){e.splice(i--,1);var s=a();void 0!==s&&(u=s)}}return u}o=o||0;for(var i=e.length;i>0&&e[i-1][2]>o;i--)e[i]=e[i-1];e[i]=[t,a,o]},n.n=function(e){var u=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(u,{a:u}),u},n.d=function(e,u){for(var t in u)n.o(u,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:u[t]})},n.f={},n.e=function(e){return Promise.all(Object.keys(n.f).reduce(function(u,t){return n.f[t](e,u),u},[]))},n.u=function(e){return e+".34a83d9cee7db39cd0de.js"},n.miniCssF=function(e){return"styles.b001e5acc6978e8e4067.css"},n.hmd=function(e){return(e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e},n.o=function(e,u){return Object.prototype.hasOwnProperty.call(e,u)},function(){var e={},u="gns3-web-ui:";n.l=function(t,a,o,i){if(e[t])e[t].push(a);else{var r,l;if(void 0!==o)for(var f=document.getElementsByTagName("script"),s=0;s<f.length;s++){var c=f[s];if(c.getAttribute("src")==t||c.getAttribute("data-webpack")==u+o){r=c;break}}r||(l=!0,(r=document.createElement("script")).charset="utf-8",r.timeout=120,n.nc&&r.setAttribute("nonce",n.nc),r.setAttribute("data-webpack",u+o),r.src=n.tu(t)),e[t]=[a];var d=function(h,b){r.onerror=r.onload=null,clearTimeout(p);var _=e[t];if(delete e[t],r.parentNode&&r.parentNode.removeChild(r),_&&_.forEach(function(m){return m(b)}),h)return h(b)},p=setTimeout(d.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=d.bind(null,r.onerror),r.onload=d.bind(null,r.onload),l&&document.head.appendChild(r)}}}(),n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},function(){var e;n.tu=function(u){return void 0===e&&(e={createScriptURL:function(t){return t}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e.createScriptURL(u)}}(),n.p="",function(){var e={666:0};n.f.j=function(a,o){var i=n.o(e,a)?e[a]:void 0;if(0!==i)if(i)o.push(i[2]);else if(666!=a){var r=new Promise(function(c,d){i=e[a]=[c,d]});o.push(i[2]=r);var l=n.p+n.u(a),f=new Error;n.l(l,function(c){if(n.o(e,a)&&(0!==(i=e[a])&&(e[a]=void 0),i)){var d=c&&("load"===c.type?"missing":c.type),p=c&&c.target&&c.target.src;f.message="Loading chunk "+a+" failed.\n("+d+": "+p+")",f.name="ChunkLoadError",f.type=d,f.request=p,i[1](f)}},"chunk-"+a,a)}else e[a]=0},n.O.j=function(a){return 0===e[a]};var u=function(a,o){var f,s,i=o[0],r=o[1],l=o[2],c=0;for(f in r)n.o(r,f)&&(n.m[f]=r[f]);if(l)var d=l(n);for(a&&a(o);c<i.length;c++)n.o(e,s=i[c])&&e[s]&&e[s][0](),e[i[c]]=0;return n.O(d)},t=self.webpackChunkgns3_web_ui=self.webpackChunkgns3_web_ui||[];t.forEach(u.bind(null,0)),t.push=u.bind(null,t.push.bind(t))}()}();
|
||||
@@ -0,0 +1,9 @@
|
||||
@charset "UTF-8";
|
||||
/*!
|
||||
* Bootstrap v5.1.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg,hsla(0,0%,100%,.15),hsla(0,0%,100%,0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,:after,:before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Liberation Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-family:var(--bs-body-font-family);font-size:1rem;font-size:var(--bs-body-font-size);font-weight:400;font-weight:var(--bs-body-font-weight);line-height:1.5;line-height:var(--bs-body-line-height);color:#212529;color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:#fff;background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{text-decoration:underline dotted;cursor:help;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border:0 solid;border-color:inherit}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:Line truncated
|
||||
/*!*/}@keyframes cdk-text-field-autofill-end{
|
||||
/*!*/}.cdk-text-field-autofill-monitored:-webkit-autofill{animation:cdk-text-field-autofill-start 0s 1ms}.cdk-text-field-autofill-monitored:not(:-webkit-autofill){animation:cdk-text-field-autofill-end 0s 1ms}.mat-focus-indicator,.mat-mdc-focus-indicator{position:relative}.dark-theme{color:#fff!important}.dark-theme .mat-badge{position:relative}.dark-theme .mat-badge-hidden .mat-badge-content{display:none}.dark-theme .mat-badge-content{position:absolute;text-align:center;display:inline-block;border-radius:50%;transition:transform .2s ease-in-out;transform:scale(.6);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;pointer-events:none}.dark-theme .mat-badge-content._mat-animation-noopable,.dark-theme .ng-animate-disabled .mat-badge-content{transition:none}.dark-theme .mat-badge-content.mat-badge-active{transform:none}.dark-theme .mat-badge-small .mat-badge-content{width:16px;height:16px;line-height:16px}.dark-theme .mat-badge-small.mat-badge-above .mat-badge-content{top:-8px}.dark-theme .mat-badge-small.mat-badge-below .mat-badge-content{bottom:-8px}.dark-theme .mat-badge-small.mat-badge-before .mat-badge-content{left:-16px}[dir=rtl] .dark-theme .mat-badge-small.mat-badge-before .mat-badge-content{left:auto;right:-16px}.dark-theme .mat-badge-small.mat-badge-after .mat-badge-content{right:-16px}[dir=rtl] .dark-theme .mat-badge-small.mat-badge-after .mat-badge-content{right:auto;left:-16px}.dark-theme .mat-badge-small.mat-badge-overlap.mat-badge-before .mat-badge-content{left:-8px}[dir=rtl] .dark-theme .mat-badge-small.mat-badge-overlap.mat-badge-before .mat-badge-content{left:auto;right:-8px}.dark-theme .mat-badge-small.mat-badge-overlap.mat-badge-after .mat-badge-content{right:-8px}[dir=rtl] .dark-theme .mat-badge-small.mat-badge-overlap.mat-badge-after .mat-badge-content{right:auto;left:-8px}.dark-theme .mat-badge-medium .mat-badge-content{width:22px;height:22px;line-height:22px}.dark-theme .mat-badge-medium.mat-badge-above .mat-badge-content{top:-11px}.dark-theme .mat-badge-medium.mat-badge-below .mat-badge-content{bottom:-11px}.dark-theme .mat-badge-medium.mat-badge-before .mat-badge-content{left:-22px}[dir=rtl] .dark-theme .mat-badge-medium.mat-badge-before .mat-badge-content{left:auto;right:-22px}.dark-theme .mat-badge-medium.mat-badge-after .mat-badge-content{right:-22px}[dir=rtl] .dark-theme .mat-badge-medium.mat-badge-after .mat-badge-content{right:auto;left:-22px}.dark-theme .mat-badge-medium.mat-badge-overlap.mat-badge-before .mat-badge-content{left:-11px}[dir=rtl] .dark-theme .mat-badge-medium.mat-badge-overlap.mat-badge-before .mat-badge-content{left:auto;right:-11px}.dark-theme .mat-badge-medium.mat-badge-overlap.mat-badge-after .mat-badge-content{right:-11px}[dir=rtl] .dark-theme .mat-badge-medium.mat-badge-overlap.mat-badge-after .mat-badge-content{right:auto;left:-11px}.dark-theme .mat-badge-large .mat-badge-content{width:28px;height:28px;line-height:28px}.dark-theme .mat-badge-large.mat-badge-above .mat-badge-content{top:-14px}.dark-theme .mat-badge-large.mat-badge-below .mat-badge-content{bottom:-14px}.dark-theme .mat-badge-large.mat-badge-before .mat-badge-content{left:-28px}[dir=rtl] .dark-theme .mat-badge-large.mat-badge-before .mat-badge-content{left:auto;right:-28px}.dark-theme .mat-badge-large.mat-badge-after .mat-badge-content{right:-28px}[dir=rtl] .dark-theme .mat-badge-large.mat-badge-after .mat-badge-content{right:auto;left:-28px}.dark-theme .mat-badge-large.mat-badge-overlap.mat-badge-before .mat-badge-content{left:-14px}[dir=rtl] .dark-theme .mat-badge-large.mat-badge-overlap.mat-badge-before .mat-badge-content{left:auto;right:-14px}.dark-theme .mat-badge-large.mat-badge-overlap.mat-badge-after .mat-badge-content{right:-14px}[dir=rtl] .dark-theme .mat-badge-large.mat-badge-overlap.mat-badge-after .mat-badge-content{right:auto;left:-14px}.dark-theme .mat-badge-content{color:#fff;background:#0097a7}.cdk-high-contrast-active .dark-theme .mat-badge-content{outline:1px solid;border-radius:0}.dark-theme .mat-badge-accent .mat-badge-content{background:#b0bec5;color:rgba(0,0,0,.87)}.dark-theme .mat-badge-warn .mat-badge-content{color:#fff;background:#f44336}.dark-theme .mat-badge-disabled .mat-badge-content{background:#677074;color:hsla(0,0%,100%,.5)}.dark-theme .mat-bottom-sheet-container{box-shadow:0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12);background:#263238;color:#fff}.mat-button-toggle-appearance-standard .mat-button-toggle-label-content{line-height:48px}.dark-theme .mat-divider{border-top-color:hsla(0,0%,100%,.12)}.dark-theme .mat-divider-vertical{border-right-color:hsla(0,0%,100%,.12)}.mat-expansion-panel-header{height:48px}.mat-expansion-panel-header.mat-expanded{height:64px}.mat-paginator-container{min-height:56px}.mat-horizontal-stepper-header{height:72px}.mat-stepper-label-position-bottom .mat-horizontal-stepper-header,.mat-vertical-stepper-header{padding:24px}.mat-stepper-vertical-line:before{top:-16px;bottom:-16pxLine truncated
|
||||
Reference in new issue
Block a user