diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 4713c6da8..e8d78d57d 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -13,11 +13,16 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + os: ["ubuntu-latest"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + #include: + # only test with Python 3.10 on Windows + # - os: windows-latest + # python-version: "3.10" steps: - uses: actions/checkout@v4 @@ -31,6 +36,14 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install .[dev] + + - name: Install Windows specific dependencies + if: runner.os == 'Windows' + run: | + python -m pip install -r win-requirements.txt + curl -O "http://www.win10pcap.org/download/Win10Pcap-v10.2-5002.msi" + msiexec /i "Win10Pcap-v10.2-5002.msi" /qn /norestart + - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/CHANGELOG b/CHANGELOG index c39cc7ba8..b9c0984ef 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,19 @@ # Change Log +## 2.2.50 21/10/2024 + +* Bundle web-ui v2.2.50 +* Symbolic links support for project export/import +* Add comment to indicate sentry-sdk is optional. Ref https://github.com/GNS3/gns3-server/issues/2423 +* Fix issues with recent busybox versions +* Support to reset MAC addresses for Docker nodes and some adjustments for fast duplication. +* Update README.md to change the minimum required Python version. +* Faster project duplication for local projects (no remote compute) +* Improve error message when a project cannot be parsed. +* Fix for running Docker containers with user namespaces enabled +* Support for configuring MAC address in Docker containers +* Upgrade aiohttp to v3.10.3 + ## 3.0.0rc1 11/08/2024 * Bundle web-ui v3.0.0rc1 diff --git a/dev-requirements.txt b/dev-requirements.txt index 0fa15092e..fa50d4fa6 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,4 @@ -pytest==8.3.2 +pytest==8.3.3 flake8==7.1.0 pytest-timeout==2.3.1 pytest-asyncio==0.21.2 diff --git a/gns3server/appliances/almalinux.gns3a b/gns3server/appliances/almalinux.gns3a index f0ad0526d..6e39d7d29 100644 --- a/gns3server/appliances/almalinux.gns3a +++ b/gns3server/appliances/almalinux.gns3a @@ -30,24 +30,24 @@ "version": "9.2", "md5sum": "c5bc76e8c95ac9f810a3482c80a54cc7", "filesize": 563347456, - "download_url": "https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/", - "direct_download_url": "https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/AlmaLinux-9-GenericCloud-9.2-20230513.x86_64.qcow2" + "download_url": "https://vault.almalinux.org/9.2/cloud/x86_64/images/", + "direct_download_url": "https://vault.almalinux.org/9.2/cloud/x86_64/images/AlmaLinux-9-GenericCloud-9.2-20230513.x86_64.qcow2" }, { "filename": "AlmaLinux-8-GenericCloud-8.8-20230524.x86_64.qcow2", "version": "8.8", "md5sum": "3958c5fc25770ef63cf97aa5d93f0a0b", "filesize": 565444608, - "download_url": "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/", - "direct_download_url": "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-8.8-20230524.x86_64.qcow2" + "download_url": "https://vault.almalinux.org/8.8/cloud/x86_64/images/", + "direct_download_url": "https://vault.almalinux.org/8.8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-8.8-20230524.x86_64.qcow2" }, { "filename": "AlmaLinux-8-GenericCloud-8.7-20221111.x86_64.qcow2", "version": "8.7", "md5sum": "b2b8c7fd3b6869362f3f8ed47549c804", "filesize": 566231040, - "download_url": "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/", - "direct_download_url": "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-8.7-20221111.x86_64.qcow2" + "download_url": "https://vault.almalinux.org/8.7/cloud/x86_64/images/", + "direct_download_url": "https://vault.almalinux.org/8.7/cloud/x86_64/images/AlmaLinux-8-GenericCloud-8.7-20221111.x86_64.qcow2" }, { "filename": "almalinux-cloud-init-data.iso", diff --git a/gns3server/appliances/fortigate.gns3a b/gns3server/appliances/fortigate.gns3a index 4fb16e0c0..102bf096b 100644 --- a/gns3server/appliances/fortigate.gns3a +++ b/gns3server/appliances/fortigate.gns3a @@ -29,10 +29,10 @@ }, "images": [ { - "filename": "FGT_VM64_KVM-v7.4.4.F-build2573-FORTINET.out.kvm.qcow2", + "filename": "FGT_VM64_KVM-v7.4.4.F-build2662-FORTINET.out.kvm.qcow2", "version": "7.4.4", "md5sum": "dfe0e78827ec728631539669001bb23f", - "filesize": 100728832, + "filesize": 102170624, "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" }, { @@ -391,7 +391,7 @@ { "name": "7.4.4", "images": { - "hda_disk_image": "FGT_VM64_KVM-v7.4.4.F-build2573-FORTINET.out.kvm.qcow2", + "hda_disk_image": "FGT_VM64_KVM-v7.4.4.F-build2662-FORTINET.out.kvm.qcow2", "hdb_disk_image": "empty30G.qcow2" } }, diff --git a/gns3server/appliances/hbcd-pe.gns3a b/gns3server/appliances/hbcd-pe.gns3a new file mode 100644 index 000000000..224998114 --- /dev/null +++ b/gns3server/appliances/hbcd-pe.gns3a @@ -0,0 +1,62 @@ +{ + "appliance_id": "ac98ab6f-7966-444b-842f-9507c965b8b7", + "name": "HBCD-PE", + "category": "guest", + "description": "Hiren’s BootCD PE (Preinstallation Environment) is a restored edition of Hiren’s BootCD based on Windows 11 PE x64. ", + "vendor_name": "hirensbootcd.org", + "vendor_url": "https://www.hirensbootcd.org/", + "documentation_url": "https://www.hirensbootcd.org/howtos/", + "product_name": "Hiren’s BootCD PE", + "product_url": "https://www.hirensbootcd.org/", + "registry_version": 4, + "status": "stable", + "maintainer": "GNS3 Team", + "maintainer_email": "developers@gns3.net", + "qemu": { + "adapter_type": "e1000", + "adapters": 1, + "ram": 4096, + "hda_disk_interface": "sata", + "arch": "x86_64", + "console_type": "vnc", + "boot_priority": "c", + "kvm": "require" + }, + "images": [ + { + "filename": "HBCD_PE_x64.iso", + "version": "1.0.8", + "md5sum": "45baab64b088431bdf3370292e9a74b0", + "filesize": 3291686912, + "download_url": "https://www.hirensbootcd.org/download/", + "direct_download_url": "https://www.hirensbootcd.org/files/HBCD_PE_x64.iso" + }, + { + "filename": "empty30G.qcow2", + "version": "1.0", + "md5sum": "3411a599e822f2ac6be560a26405821a", + "filesize": 197120, + "download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/", + "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty30G.qcow2/download" + }, + { + "filename": "OVMF-edk2-stable202305.fd", + "version": "stable202305", + "md5sum": "6c4cf1519fec4a4b95525d9ae562963a", + "filesize": 4194304, + "download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/", + "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/OVMF-edk2-stable202305.fd.zip/download", + "compression": "zip" + } + ], + "versions": [ + { + "name": "1.0.8", + "images": { + "bios_image": "OVMF-edk2-stable202305.fd", + "hda_disk_image": "empty30G.qcow2", + "cdrom_image": "HBCD_PE_x64.iso" + } + } + ] +} diff --git a/gns3server/appliances/mikrotik-chr.gns3a b/gns3server/appliances/mikrotik-chr.gns3a index f06c56ad9..48c1eade8 100644 --- a/gns3server/appliances/mikrotik-chr.gns3a +++ b/gns3server/appliances/mikrotik-chr.gns3a @@ -28,149 +28,80 @@ }, "images": [ { - "filename": "chr-7.14.2.img", - "version": "7.14.2", - "md5sum": "531901dac85b67b23011e946a62abc7b", + "filename": "chr-7.16.img", + "version": "7.16", + "md5sum": "a4c2d00a87e73b3129cd66a4e0743c9a", "filesize": 134217728, "download_url": "http://www.mikrotik.com/download", - "direct_download_url": "https://download.mikrotik.com/routeros/7.14.2/chr-7.14.2.img.zip", + "direct_download_url": "https://download.mikrotik.com/routeros/7.16/chr-7.16.img.zip", "compression": "zip" }, { - "filename": "chr-7.11.2.img", - "version": "7.11.2", - "md5sum": "fbffd097d2c5df41fc3335c3977f782c", + "filename": "chr-7.15.3.img", + "version": "7.15.3", + "md5sum": "5af8c748a0de4e8e8b303180738721a9", "filesize": 134217728, "download_url": "http://www.mikrotik.com/download", - "direct_download_url": "https://download.mikrotik.com/routeros/7.11.2/chr-7.11.2.img.zip", + "direct_download_url": "https://download.mikrotik.com/routeros/7.15.3/chr-7.15.3.img.zip", "compression": "zip" }, { - "filename": "chr-7.10.1.img", - "version": "7.10.1", - "md5sum": "917729e79b9992562f4160d461b21cac", + "filename": "chr-7.14.3.img", + "version": "7.14.3", + "md5sum": "73f527efef81b529b267a0683cb87617", "filesize": 134217728, "download_url": "http://www.mikrotik.com/download", - "direct_download_url": "https://download.mikrotik.com/routeros/7.10.1/chr-7.10.1.img.zip", + "direct_download_url": "https://download.mikrotik.com/routeros/7.14.3/chr-7.14.3.img.zip", "compression": "zip" }, { - "filename": "chr-7.7.img", - "version": "7.7", - "md5sum": "efc4fdeb1cc06dc240a14f1215fd59b3", - "filesize": 134217728, - "download_url": "http://www.mikrotik.com/download", - "direct_download_url": "https://download.mikrotik.com/routeros/7.7/chr-7.7.img.zip", - "compression": "zip" - }, - { - "filename": "chr-7.6.img", - "version": "7.6", - "md5sum": "864482f9efaea9d40910c050318f65b9", - "filesize": 134217728, - "download_url": "http://www.mikrotik.com/download", - "direct_download_url": "https://download.mikrotik.com/routeros/7.6/chr-7.6.img.zip", - "compression": "zip" - }, - { - "filename": "chr-7.3.1.img", - "version": "7.3.1", - "md5sum": "99f8ea75f8b745a8bf5ca3cc1bd325e3", - "filesize": 134217728, - "download_url": "http://www.mikrotik.com/download", - "direct_download_url": "https://download.mikrotik.com/routeros/7.3.1/chr-7.3.1.img.zip", - "compression": "zip" - }, - { - "filename": "chr-7.1.5.img", - "version": "7.1.5", - "md5sum": "9c0be05f891df2b1400bdae5e719898e", - "filesize": 134217728, - "download_url": "http://www.mikrotik.com/download", - "direct_download_url": "https://download.mikrotik.com/routeros/7.1.5/chr-7.1.5.img.zip", - "compression": "zip" - }, - { - "filename": "chr-6.49.10.img", - "version": "6.49.10", - "md5sum": "49ae1ecfe310aea1df37b824aa13cf84", + "filename": "chr-6.49.17.img", + "version": "6.49.17", + "md5sum": "ad9f4bd8cd4965a403350deeb5d35b96", "filesize": 67108864, "download_url": "http://www.mikrotik.com/download", - "direct_download_url": "https://download.mikrotik.com/routeros/6.49.10/chr-6.49.10.img.zip", + "direct_download_url": "https://download.mikrotik.com/routeros/6.49.17/chr-6.49.17.img.zip", "compression": "zip" }, { - "filename": "chr-6.49.6.img", - "version": "6.49.6", - "md5sum": "ae27d38acc9c4dcd875e0f97bcae8d97", + "filename": "chr-6.49.13.img", + "version": "6.49.13", + "md5sum": "18349e1c3209495e571bcbee8a7e3259", "filesize": 67108864, "download_url": "http://www.mikrotik.com/download", - "direct_download_url": "https://download.mikrotik.com/routeros/6.49.6/chr-6.49.6.img.zip", - "compression": "zip" - }, - { - "filename": "chr-6.48.6.img", - "version": "6.48.6", - "md5sum": "875574a561570227ff8f395aabe478c6", - "filesize": 67108864, - "download_url": "http://www.mikrotik.com/download", - "direct_download_url": "https://download.mikrotik.com/routeros/6.48.6/chr-6.48.6.img.zip", + "direct_download_url": "https://download.mikrotik.com/routeros/6.49.13/chr-6.49.13.img.zip", "compression": "zip" } ], "versions": [ { - "name": "7.11.2", - "images": { - "hda_disk_image": "chr-7.11.2.img" - } - }, - { - "name": "7.10.1", - "images": { - "hda_disk_image": "chr-7.10.1.img" - } - }, - { - "name": "7.7", - "images": { - "hda_disk_image": "chr-7.7.img" - } - }, - { - "name": "7.6", - "images": { - "hda_disk_image": "chr-7.6.img" - } - }, - { - "name": "7.3.1", + "name": "7.16", "images": { - "hda_disk_image": "chr-7.3.1.img" + "hda_disk_image": "chr-7.16.img" } }, { - "name": "7.1.5", + "name": "7.15.3", "images": { - "hda_disk_image": "chr-7.1.5.img" + "hda_disk_image": "chr-7.15.3.img" } }, { - "name": "6.49.10", + "name": "7.14.3", "images": { - "hda_disk_image": "chr-6.49.10.img" + "hda_disk_image": "chr-7.14.3.img" } }, { - "name": "6.49.6", + "name": "6.49.17", "images": { - "hda_disk_image": "chr-6.49.6.img" + "hda_disk_image": "chr-6.49.17.img" } }, { - "name": "6.48.6", + "name": "6.49.13", "images": { - "hda_disk_image": "chr-6.48.6.img" + "hda_disk_image": "chr-6.49.13.img" } } ] diff --git a/gns3server/appliances/opnsense.gns3a b/gns3server/appliances/opnsense.gns3a index b971f3a45..759701763 100644 --- a/gns3server/appliances/opnsense.gns3a +++ b/gns3server/appliances/opnsense.gns3a @@ -25,6 +25,13 @@ "kvm": "require" }, "images": [ + { + "filename": "OPNsense-24.7-nano-amd64.img", + "version": "24.7", + "md5sum": "4f75dc2c948b907cbf73763b1539677c", + "filesize": 3221225472, + "download_url": "https://opnsense.c0urier.net/releases/24.7/" + }, { "filename": "OPNsense-24.1-nano-amd64.img", "version": "24.1", @@ -69,6 +76,12 @@ } ], "versions": [ + { + "name": "24.7", + "images": { + "hda_disk_image": "OPNsense-24.7-nano-amd64.img" + } + }, { "name": "24.1", "images": { diff --git a/gns3server/appliances/reactos.gns3a b/gns3server/appliances/reactos.gns3a index 1b1c2fd8e..b1d7fdccf 100644 --- a/gns3server/appliances/reactos.gns3a +++ b/gns3server/appliances/reactos.gns3a @@ -24,17 +24,17 @@ }, "images": [ { - "filename": "ReactOS-0.4.14-release-15-gb6088a6.iso", - "version": "Installer-0.4.14-release-15", - "md5sum": "af4be6b27463446905f155f14232d2b4", + "filename": "ReactOS-0.4.14-release-21-g1302c1b.iso", + "version": "Installer-0.4.14-release-21", + "md5sum": "a30bc9143788c76ed584ffd5d25fddfe", "filesize": 140509184, "download_url": "https://reactos.org/download", "direct_download_url": "https://sourceforge.net/projects/reactos/files/ReactOS/0.4.14/ReactOS-0.4.14-release-21-g1302c1b-iso.zip/download" }, { - "filename": "ReactOS-0.4.14-release-15-gb6088a6-Live.iso", - "version": "Live-0.4.14-release-15", - "md5sum": "73c1a0169a9a3b8a4feb91f4d00f5e97", + "filename": "ReactOS-0.4.14-release-21-g1302c1b-Live.iso", + "version": "Live-0.4.14-release-21", + "md5sum": "fc362820069adeea088b3a48dcfa3f9e", "filesize": 267386880, "download_url": "https://reactos.org/download", "direct_download_url": "https://sourceforge.net/projects/reactos/files/ReactOS/0.4.14/ReactOS-0.4.14-release-21-g1302c1b-live.zip/download" @@ -50,17 +50,17 @@ ], "versions": [ { - "name": "Installer-0.4.14-release-15", + "name": "Installer-0.4.14-release-21", "images": { "hda_disk_image": "empty30G.qcow2", - "cdrom_image": "ReactOS-0.4.14-release-15-gb6088a6.iso" + "cdrom_image": "ReactOS-0.4.14-release-21-g1302c1b.iso" } }, { - "name": "Live-0.4.14-release-15", + "name": "Live-0.4.14-release-21", "images": { "hda_disk_image": "empty30G.qcow2", - "cdrom_image": "ReactOS-0.4.14-release-15-gb6088a6-Live.iso" + "cdrom_image": "ReactOS-0.4.14-release-21-g1302c1b-Live.iso" } } ] diff --git a/gns3server/appliances/truenas.gns3a b/gns3server/appliances/truenas.gns3a new file mode 100644 index 000000000..be8e2d03d --- /dev/null +++ b/gns3server/appliances/truenas.gns3a @@ -0,0 +1,104 @@ +{ + "appliance_id": "8c19ccaa-a1d0-4473-94a2-a93b64924d88", + "name": "TrueNAS", + "category": "guest", + "description": "TrueNAS is a family of network-attached storage (NAS) products produced by iXsystems, incorporating both FOSS, as well as commercial offerings. Based on the OpenZFS file system, TrueNAS runs on FreeBSD as well as Linux and is available under the BSD License It is compatible with x86-64 hardware and is also available as turnkey appliances from iXsystems.", + "vendor_name": "iXsystems", + "vendor_url": "https://www.truenas.com/", + "documentation_url": "https://www.truenas.com/docs/", + "product_name": "TrueNAS", + "product_url": "https://www.truenas.com/", + "registry_version": 4, + "status": "stable", + "maintainer": "GNS3 Team", + "maintainer_email": "developers@gns3.net", + "usage": "To install TrueNAS SCALE you may have to select the Legacy BIOS option.", + "port_name_format": "eth{0}", + "qemu": { + "adapter_type": "e1000", + "adapters": 1, + "ram": 8192, + "hda_disk_interface": "ide", + "hdb_disk_interface": "ide", + "arch": "x86_64", + "console_type": "vnc", + "boot_priority": "cd", + "kvm": "require" + }, + "images": [ + { + "filename": "TrueNAS-13.0-U6.2.iso", + "version": "CORE 13.0 U6.2", + "md5sum": "8b2882b53af5e9f3ca905c6acdee1690", + "filesize": 1049112576, + "download_url": "https://www.truenas.com/download-truenas-core/", + "direct_download_url": "https://download-core.sys.truenas.net/13.0/STABLE/U6.2/x64/TrueNAS-13.0-U6.2.iso" + }, + { + "filename": "TrueNAS-13.3-RELEASE.iso", + "version": "CORE 13.3 RELEASE", + "md5sum": "8bb16cfb06f3f1374a27cf6aebb14ed3", + "filesize": 995567616, + "download_url": "https://www.truenas.com/download-truenas-core/", + "direct_download_url": "https://download-core.sys.truenas.net/13.3/STABLE/RELEASE/x64/TrueNAS-13.3-RELEASE.iso" + }, + { + "filename": "TrueNAS-SCALE-24.04.2.2.iso", + "version": "SCALE 24.04.2.2", + "md5sum": "47d9026254a0775800bb2b8ab6d874fd", + "filesize": 1630355456, + "download_url": "https://www.truenas.com/download-truenas-scale/", + "direct_download_url": "https://download.sys.truenas.net/TrueNAS-SCALE-Dragonfish/24.04.2.2/TrueNAS-SCALE-24.04.2.2.iso" + }, + { + "filename": "TrueNAS-SCALE-24.10-BETA.1.iso", + "version": "SCALE 24.10-BETA.1", + "md5sum": "cc3d5758d1db3d55ae9c8716f5d43b88", + "filesize": 1491378176, + "download_url": "https://www.truenas.com/download-truenas-scale/", + "direct_download_url": "https://download.sys.truenas.net/TrueNAS-SCALE-ElectricEel-BETA/24.10-BETA.1/TrueNAS-SCALE-24.10-BETA.1.iso" + }, + { + "filename": "empty30G.qcow2", + "version": "1.0", + "md5sum": "3411a599e822f2ac6be560a26405821a", + "filesize": 197120, + "download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/", + "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty30G.qcow2/download" + } + ], + "versions": [ + { + "name": "CORE 13.0 U6.2", + "images": { + "hda_disk_image": "empty30G.qcow2", + "hdb_disk_image": "empty30G.qcow2", + "cdrom_image": "TrueNAS-13.0-U6.2.iso" + } + }, + { + "name": "CORE 13.3 RELEASE", + "images": { + "hda_disk_image": "empty30G.qcow2", + "hdb_disk_image": "empty30G.qcow2", + "cdrom_image": "TrueNAS-13.3-RELEASE.iso" + } + }, + { + "name": "SCALE 24.04.2.2", + "images": { + "hda_disk_image": "empty30G.qcow2", + "hdb_disk_image": "empty30G.qcow2", + "cdrom_image": "TrueNAS-SCALE-24.04.2.2.iso" + } + }, + { + "name": "SCALE 24.10-BETA.1", + "images": { + "hda_disk_image": "empty30G.qcow2", + "hdb_disk_image": "empty30G.qcow2", + "cdrom_image": "TrueNAS-SCALE-24.10-BETA.1.iso" + } + } + ] +} diff --git a/gns3server/appliances/ubuntu-cloud.gns3a b/gns3server/appliances/ubuntu-cloud.gns3a index cd2eb4571..33643ea96 100644 --- a/gns3server/appliances/ubuntu-cloud.gns3a +++ b/gns3server/appliances/ubuntu-cloud.gns3a @@ -28,36 +28,44 @@ }, "images": [ { - "filename": "ubuntu-23.04-server-cloudimg-amd64.img", - "version": "Ubuntu 23.04 (Lunar Lobster)", - "md5sum": "369e3b1f68416c39245a8014172406dd", - "filesize": 756678656, - "download_url": "https://cloud-images.ubuntu.com/releases/23.04/release/", - "direct_download_url": "https://cloud-images.ubuntu.com/releases/23.04/release/ubuntu-23.04-server-cloudimg-amd64.img" + "filename": "ubuntu-24.10-server-cloudimg-amd64.img", + "version": "Ubuntu 24.10 (Oracular Oriole)", + "md5sum": "f2960f8743efedd0a4968bfcd9451782", + "filesize": 627360256, + "download_url": "https://cloud-images.ubuntu.com/releases/oracular/release-20241009/", + "direct_download_url": "https://cloud-images.ubuntu.com/releases/oracular/release-20241009/ubuntu-24.10-server-cloudimg-amd64.img" + }, + { + "filename": "ubuntu-24.04-server-cloudimg-amd64.img", + "version": "Ubuntu 24.04 LTS (Noble Numbat)", + "md5sum": "a1c8a01953578ad432cbef03db2f3161", + "filesize": 587241984, + "download_url": "https://cloud-images.ubuntu.com/releases/noble/release-20241004/", + "direct_download_url": "https://cloud-images.ubuntu.com/releases/noble/release-20241004/ubuntu-24.04-server-cloudimg-amd64.img" }, { "filename": "ubuntu-22.04-server-cloudimg-amd64.img", "version": "Ubuntu 22.04 LTS (Jammy Jellyfish)", - "md5sum": "3ce0b84f9592482fb645e8253b979827", - "filesize": 686096384, - "download_url": "https://cloud-images.ubuntu.com/releases/jammy/release", - "direct_download_url": "https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-amd64.img" + "md5sum": "8f9a70435dc5b0b86cf5d0d4716b6091", + "filesize": 653668352, + "download_url": "https://cloud-images.ubuntu.com/releases/jammy/release-20241002/", + "direct_download_url": "https://cloud-images.ubuntu.com/releases/jammy/release-20241002/ubuntu-22.04-server-cloudimg-amd64.img" }, { "filename": "ubuntu-20.04-server-cloudimg-amd64.img", "version": "Ubuntu 20.04 LTS (Focal Fossa)", - "md5sum": "044bc979b2238192ee3edb44e2bb6405", - "filesize": 552337408, - "download_url": "https://cloud-images.ubuntu.com/releases/focal/release-20210119.1/", - "direct_download_url": "https://cloud-images.ubuntu.com/releases/focal/release-20210119.1/ubuntu-20.04-server-cloudimg-amd64.img" + "md5sum": "1dff90e16acb0167c27ff82e4ac1813a", + "filesize": 627310592, + "download_url": "https://cloud-images.ubuntu.com/releases/focal/release-20240821/", + "direct_download_url": "https://cloud-images.ubuntu.com/releases/focal/release-20240821/ubuntu-20.04-server-cloudimg-amd64.img" }, { "filename": "ubuntu-18.04-server-cloudimg-amd64.img", "version": "Ubuntu 18.04 LTS (Bionic Beaver)", - "md5sum": "f4134e7fa16d7fa766c7467cbe25c949", - "filesize": 336134144, - "download_url": "https://cloud-images.ubuntu.com/releases/18.04/release-20180426.2/", - "direct_download_url": "https://cloud-images.ubuntu.com/releases/18.04/release-20180426.2/ubuntu-18.04-server-cloudimg-amd64.img" + "md5sum": "62fa110eeb0459c1ff166f897aeb9f78", + "filesize": 405667840, + "download_url": "https://cloud-images.ubuntu.com/releases/bionic/release-20230607/", + "direct_download_url": "https://cloud-images.ubuntu.com/releases/bionic/release-20230607/ubuntu-18.04-server-cloudimg-amd64.img" }, { "filename": "ubuntu-cloud-init-data.iso", @@ -70,9 +78,16 @@ ], "versions": [ { - "name": "Ubuntu 23.04 (Lunar Lobster)", + "name": "Ubuntu 24.10 (Oracular Oriole)", + "images": { + "hda_disk_image": "ubuntu-24.10-server-cloudimg-amd64.img", + "cdrom_image": "ubuntu-cloud-init-data.iso" + } + }, + { + "name": "Ubuntu 24.04 LTS (Noble Numbat)", "images": { - "hda_disk_image": "ubuntu-23.04-server-cloudimg-amd64.img", + "hda_disk_image": "ubuntu-24.04-server-cloudimg-amd64.img", "cdrom_image": "ubuntu-cloud-init-data.iso" } }, diff --git a/gns3server/appliances/ubuntu-gui.gns3a b/gns3server/appliances/ubuntu-gui.gns3a index 9e31f4206..fa8bb59dd 100644 --- a/gns3server/appliances/ubuntu-gui.gns3a +++ b/gns3server/appliances/ubuntu-gui.gns3a @@ -27,6 +27,13 @@ "options": "-vga qxl" }, "images": [ + { + "filename": "Ubuntu 24.04 (64bit).vmdk", + "version": "24.04", + "md5sum": "7709a5a1cf888c7644d245c42d217918", + "filesize": 6432555008, + "download_url": "https://www.osboxes.org/ubuntu/" + }, { "filename": "Ubuntu 22.04 (64bit).vmdk", "version": "22.04", @@ -57,6 +64,12 @@ } ], "versions": [ + { + "name": "24.04", + "images": { + "hda_disk_image": "Ubuntu 24.04 (64bit).vmdk" + } + }, { "name": "22.04", "images": { diff --git a/gns3server/appliances/vyos.gns3a b/gns3server/appliances/vyos.gns3a index 925e39ef5..aade8d863 100644 --- a/gns3server/appliances/vyos.gns3a +++ b/gns3server/appliances/vyos.gns3a @@ -1,181 +1,178 @@ { "appliance_id": "f82b74c4-0f30-456f-a582-63daca528502", - "name": "VyOS", + "name": "VyOS Universal Router", "category": "router", - "description": "VyOS is a community fork of Vyatta, a Linux-based network operating system that provides software-based network routing, firewall, and VPN functionality.", - "vendor_name": "Linux", - "vendor_url": "https://vyos.net/", + "description": "VyOS is an open-source network operating system that provides a comprehensive suite of features for routing, firewalling, and VPN functionality. VyOS offers a robust and flexible solution for both small-scale and large-scale network environments. It is designed to support enterprise-grade networking with the added benefits of community-driven development and continuous updates.\n\nThe VyOS Universal Router, when used in GNS3, brings the power and versatility of VyOS to network simulation and emulation. GNS3 users can deploy the VyOS Universal Router to create and test complex network topologies in a virtual environment. This appliance provides a rich set of features, including dynamic routing protocols, stateful firewall capabilities, various VPNs, as well as high availability configurations.\n\nThe seamless integration with GNS3 allows network engineers and architects to validate network designs, perform testing and troubleshooting, and enhance their skill sets in a controlled, risk-free environment.", + "vendor_name": "VyOS Inc.", + "vendor_url": "https://vyos.io/", + "vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/VyOS.png", "documentation_url": "https://docs.vyos.io/", - "product_name": "VyOS", - "product_url": "https://vyos.net/", + "product_name": "VyOS Universal Router", + "product_url": "https://vyos.io/vyos-universal-router", "registry_version": 4, "status": "stable", - "maintainer": "GNS3 Team", - "maintainer_email": "developers@gns3.net", - "usage": "Default username/password is vyos/vyos.\n\nThe -KVM versions are ready to use, no installation is required.\nThe other images will start the router from the CDROM on initial boot. Login and then type \"install image\" and follow the instructions.", + "availability": "service-contract", + "maintainer": "VyOS Inc.", + "maintainer_email": "support@vyos.io", + "usage": "\nDefault credentials:\nUser: vyos\nPassword: vyos", "symbol": "vyos.svg", "port_name_format": "eth{0}", "qemu": { - "adapter_type": "e1000", - "adapters": 3, - "ram": 512, - "hda_disk_interface": "scsi", + "adapter_type": "virtio-net-pci", + "adapters": 10, + "ram": 2048, + "cpus": 4, + "hda_disk_interface": "virtio", "arch": "x86_64", "console_type": "telnet", - "boot_priority": "cd", - "kvm": "allow" + "boot_priority": "c", + "kvm": "require", + "on_close": "shutdown_signal" }, "images": [ { - "filename": "vyos-1.3.2-amd64.iso", + "filename": "vyos-1.4.0-kvm-amd64.qcow2", + "version": "1.4.0", + "md5sum": "a130e446bc5bf87391981f183ee3694b", + "filesize": 468320256, + "download_url": "https://support.vyos.io/" + }, + { + "filename": "vyos-1.3.7-qemu-amd64.qcow2", + "version": "1.3.7", + "md5sum": "f4663b1e2df115bfa5c7ec17584514d6", + "filesize": 359792640, + "download_url": "https://support.vyos.io/" + }, + { + "filename": "vyos-1.3.2-10G-qemu.qcow2", "version": "1.3.2", - "md5sum": "070743faac800f9e5197058a8b6b3ba1", - "filesize": 334495744, - "download_url": "https://support.vyos.io/en/downloads/files/vyos-1-3-2-generic-iso-image" + "md5sum": "68ad3fb530213189ac9ed496d5fe7897", + "filesize": 326893568, + "download_url": "https://support.vyos.io/" }, { - "filename": "vyos-1.3.1-S1-amd64.iso", + "filename": "vyos-1.3.1-S1-10G-qemu.qcow2", "version": "1.3.1-S1", - "md5sum": "781f345e8a4ab9eb9e075ce5c87c8817", - "filesize": 351272960, - "download_url": "https://support.vyos.io/en/downloads/files/vyos-1-3-1-s1-generic-iso-image" + "md5sum": "d8ed9f82a983295b94b07f8e37c48ed0", + "filesize": 343801856, + "download_url": "https://support.vyos.io/" }, { - "filename": "vyos-1.3.1-amd64.iso", + "filename": "vyos-1.3.1-10G-qemu.qcow2", "version": "1.3.1", - "md5sum": "b6f57bd0cf9b60cdafa337b08ba4f2bc", - "filesize": 350224384, - "download_url": "https://support.vyos.io/en/downloads/files/vyos-1-3-1-generic-iso-image" + "md5sum": "482367c833990fb2b9350e3708d33dc9", + "filesize": 342556672, + "download_url": "https://support.vyos.io/" }, { - "filename": "vyos-1.3.0-amd64.iso", + "filename": "vyos-1.3.0-10G-qemu.qcow2", "version": "1.3.0", - "md5sum": "2019bd9c5efa6194e2761de678d0073f", - "filesize": 338690048, - "download_url": "https://support.vyos.io/en/downloads/files/vyos-1-3-0-generic-iso-image" - }, - { - "filename": "vyos-1.2.9-S1-amd64.iso", - "version": "1.2.9-S1", - "md5sum": "3fece6363f9766f862e26d292d0ed5a3", - "filesize": 430964736, - "download_url": "https://support.vyos.io/en/downloads/files/vyos-1-2-9-s1-generic-iso-image", - "direct_download_url": "https://legacy-lts-images.vyos.io/1.2.9-S1/vyos-1.2.9-S1-amd64.iso" + "md5sum": "086e95e992e9b4d014c5f154cd01a6e6", + "filesize": 330956800, + "download_url": "https://support.vyos.io/" }, { "filename": "vyos-1.2.9-S1-10G-qemu.qcow2", - "version": "1.2.9-S1-KVM", + "version": "1.2.9-S1", "md5sum": "0a70d78b80a3716d42487c02ef44f41f", "filesize": 426967040, - "download_url": "https://support.vyos.io/en/downloads/files/vyos-1-2-9-s1-for-kvm", - "direct_download_url": "https://legacy-lts-images.vyos.io/1.2.9-S1/vyos-1.2.9-S1-10G-qemu.qcow2" - }, - { - "filename": "vyos-1.2.9-amd64.iso", - "version": "1.2.9", - "md5sum": "586be23b6256173e174c82d8f1f699a1", - "filesize": 430964736, - "download_url": "https://support.vyos.io/en/downloads/files/vyos-1-2-9-generic-iso-image", - "direct_download_url": "https://legacy-lts-images.vyos.io/1.2.9/vyos-1.2.9-amd64.iso" + "download_url": "https://support.vyos.io/" }, { - "filename": "vyos-1.2.9-10G-qemu.qcow2", - "version": "1.2.9-KVM", - "md5sum": "76871c7b248c32f75177c419128257ac", - "filesize": 427360256, - "download_url": "https://support.vyos.io/en/downloads/files/vyos-1-2-9-10g-qemu-qcow2", - "direct_download_url": "https://legacy-lts-images.vyos.io/1.2.9/vyos-1.2.9-10G-qemu.qcow2" + "filename": "vyos-1.2.8-10G-qemu.qcow2", + "version": "1.2.8", + "md5sum": "96c76f619d0f8ea11dc8a3a18ed67b98", + "filesize": 425852928, + "download_url": "https://support.vyos.io/" }, { - "filename": "vyos-1.2.8-amd64.iso", - "version": "1.2.8", - "md5sum": "0ad879db903efdbf1c39dc945e165931", - "filesize": 429916160, - "download_url": "https://support.vyos.io/en/downloads/files/vyos-1-2-8-generic-iso-image" + "filename": "vyos-1.2.7-qemu.qcow2", + "version": "1.2.7", + "md5sum": "1be4674c970fcdd65067e504baea5d74", + "filesize": 424607744, + "download_url": "https://support.vyos.io/" }, { - "filename": "vyos-1.1.8-amd64.iso", - "version": "1.1.8", - "md5sum": "95a141d4b592b81c803cdf7e9b11d8ea", - "filesize": 241172480, - "direct_download_url": "https://legacy-lts-images.vyos.io/vyos-1.1.8-amd64.iso" + "filename": "vyos-1.2.6-qemu.qcow2", + "version": "1.2.6", + "md5sum": "d8010d79889ca0ba5cb2634665e548e3", + "filesize": 424607744, + "download_url": "https://support.vyos.io/" }, { - "filename": "empty8G.qcow2", - "version": "1.0", - "md5sum": "f1d2c25b6990f99bd05b433ab603bdb4", - "filesize": 197120, - "download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/", - "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty8G.qcow2/download" + "filename": "vyos-1.2.5-amd64.qcow2", + "version": "1.2.5", + "md5sum": "110c22309ec480600446fd2fb4f27a0d", + "filesize": 411500544 , + "download_url": "https://support.vyos.io/" } ], "versions": [ + { + "name": "1.4.0", + "images": { + "hda_disk_image": "vyos-1.4.0-kvm-amd64.qcow2" + } + }, + { + "name": "1.3.7", + "images": { + "hda_disk_image": "vyos-1.3.7-qemu-amd64.qcow2" + } + }, { "name": "1.3.2", "images": { - "hda_disk_image": "empty8G.qcow2", - "cdrom_image": "vyos-1.3.2-amd64.iso" + "hda_disk_image": "vyos-1.3.2-10G-qemu.qcow2" } }, { "name": "1.3.1-S1", "images": { - "hda_disk_image": "empty8G.qcow2", - "cdrom_image": "vyos-1.3.1-S1-amd64.iso" + "hda_disk_image": "vyos-1.3.1-S1-10G-qemu.qcow2" } }, { "name": "1.3.1", "images": { - "hda_disk_image": "empty8G.qcow2", - "cdrom_image": "vyos-1.3.1-amd64.iso" + "hda_disk_image": "vyos-1.3.1-10G-qemu.qcow2" } }, { "name": "1.3.0", "images": { - "hda_disk_image": "empty8G.qcow2", - "cdrom_image": "vyos-1.3.0-amd64.iso" + "hda_disk_image": "vyos-1.3.0-10G-qemu.qcow2" } }, { "name": "1.2.9-S1", - "images": { - "hda_disk_image": "empty8G.qcow2", - "cdrom_image": "vyos-1.2.9-S1-amd64.iso" - } - }, - { - "name": "1.2.9-S1-KVM", "images": { "hda_disk_image": "vyos-1.2.9-S1-10G-qemu.qcow2" } }, { - "name": "1.2.9", + "name": "1.2.8", "images": { - "hda_disk_image": "empty8G.qcow2", - "cdrom_image": "vyos-1.2.9-amd64.iso" + "hda_disk_image": "vyos-1.2.8-10G-qemu.qcow2" } }, { - "name": "1.2.9-KVM", + "name": "1.2.7", "images": { - "hda_disk_image": "vyos-1.2.9-10G-qemu.qcow2" + "hda_disk_image": "vyos-1.2.7-qemu.qcow2" } }, { - "name": "1.2.8", + "name": "1.2.6", "images": { - "hda_disk_image": "empty8G.qcow2", - "cdrom_image": "vyos-1.2.8-amd64.iso" + "hda_disk_image": "vyos-1.2.6-qemu.qcow2" } }, { - "name": "1.1.8", + "name": "1.2.5", "images": { - "hda_disk_image": "empty8G.qcow2", - "cdrom_image": "vyos-1.1.8-amd64.iso" + "hda_disk_image": "vyos-1.2.5-amd64.qcow2" } } ] diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index 20f437be1..c7a3821c1 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -742,6 +742,7 @@ def ubridge_path(self): path = shutil.which(self._manager.config.settings.Server.ubridge_path) return path + @locking async def _ubridge_send(self, command): """ Sends a command to uBridge hypervisor. diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index ac9b6014b..beb7adde1 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -464,8 +464,8 @@ async def create(self): "Mounts": self._mount_binds(image_infos), "Memory": self._memory * (1024 * 1024), # convert memory to bytes "NanoCpus": int(self._cpus * 1e9), # convert cpus to nano cpus + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "Env": ["container=docker"], # Systemd compliant: https://github.com/GNS3/gns3-server/issues/573 "Cmd": [], diff --git a/gns3server/compute/docker/resources/init.sh b/gns3server/compute/docker/resources/init.sh index 695a79984..05c6ee2e0 100755 --- a/gns3server/compute/docker/resources/init.sh +++ b/gns3server/compute/docker/resources/init.sh @@ -25,7 +25,10 @@ PATH=/gns3/bin:/tmp/gns3/bin:/sbin:$PATH # bootstrap busybox commands if [ ! -d /tmp/gns3/bin ]; then busybox mkdir -p /tmp/gns3/bin - /gns3/bin/busybox --install -s /tmp/gns3/bin + for applet in `busybox --list` + do + ln -s /gns3/bin/busybox "/tmp/gns3/bin/$applet" + done fi # Restore file permission and mount volumes @@ -75,7 +78,7 @@ ip link set dev lo up while true do grep $GNS3_MAX_ETHERNET /proc/net/dev > /dev/null && break - sleep 0.5 + usleep 500000 # wait 0.5 seconds done # activate eth interfaces diff --git a/gns3server/controller/export_project.py b/gns3server/controller/export_project.py index 7c0eb54e3..0c282f1b9 100644 --- a/gns3server/controller/export_project.py +++ b/gns3server/controller/export_project.py @@ -89,14 +89,15 @@ async def export_project( files = [f for f in files if _is_exportable(os.path.join(root, f), include_snapshots)] for file in files: path = os.path.join(root, file) - # check if we can export the file - try: - open(path).close() - except OSError as e: - msg = f"Could not export file {path}: {e}" - log.warning(msg) - project.emit_notification("log.warning", {"message": msg}) - continue + if not os.path.islink(path): + try: + # check if we can export the file + open(path).close() + except OSError as e: + msg = f"Could not export file {path}: {e}" + log.warning(msg) + project.emit_notification("log.warning", {"message": msg}) + continue # ignore the .gns3 file if file.endswith(".gns3"): continue @@ -150,7 +151,10 @@ def _patch_mtime(path): :param path: file path """ - st = os.stat(path) + if sys.platform.startswith("win"): + # only UNIX type platforms + return + st = os.stat(path, follow_symlinks=False) file_date = datetime.fromtimestamp(st.st_mtime) if file_date.year < 1980: new_mtime = file_date.replace(year=1980).timestamp() @@ -166,10 +170,6 @@ def _is_exportable(path, include_snapshots=False): if include_snapshots is False and path.endswith("snapshots"): return False - # do not export symlinks - if os.path.islink(path): - return False - # do not export directories of snapshots if include_snapshots is False and "{sep}snapshots{sep}".format(sep=os.path.sep) in path: return False @@ -228,13 +228,16 @@ async def _patch_project_file( if not keep_compute_ids: node["compute_id"] = "local" # To make project portable all node by default run on local - if "properties" in node and node["node_type"] != "docker": + if "properties" in node: for prop, value in node["properties"].items(): # reset the MAC address if reset_mac_addresses and prop in ("mac_addr", "mac_address"): node["properties"][prop] = None + if node["node_type"] == "docker": + continue + if node["node_type"] == "iou": if not prop == "path": continue diff --git a/gns3server/controller/import_project.py b/gns3server/controller/import_project.py index 49b59f8b9..c03d14f36 100644 --- a/gns3server/controller/import_project.py +++ b/gns3server/controller/import_project.py @@ -17,6 +17,7 @@ import os import sys +import stat import json import uuid import shutil @@ -95,6 +96,7 @@ async def import_project(controller, project_id, stream, location=None, name=Non try: with zipfile_zstd.ZipFile(stream) as zip_file: await wait_run_in_executor(zip_file.extractall, path) + _create_symbolic_links(zip_file, path) except zipfile_zstd.BadZipFile: raise ControllerError("Cannot extract files from GNS3 project (invalid zip)") @@ -184,6 +186,24 @@ async def import_project(controller, project_id, stream, location=None, name=Non project = await controller.load_project(dot_gns3_path, load=False) return project +def _create_symbolic_links(zip_file, path): + """ + Manually create symbolic links (if any) because ZipFile does not support it. + + :param zip_file: ZipFile instance + :param path: project location + """ + + for zip_info in zip_file.infolist(): + if stat.S_ISLNK(zip_info.external_attr >> 16): + symlink_target = zip_file.read(zip_info.filename).decode() + symlink_path = os.path.join(path, zip_info.filename) + try: + # remove the regular file and replace it by a symbolic link + os.remove(symlink_path) + os.symlink(symlink_target, symlink_path) + except OSError as e: + raise aiohttp.web.HTTPConflict(text=f"Cannot create symbolic link: {e}") def _move_node_file(path, old_id, new_id): """ @@ -269,6 +289,7 @@ async def _import_snapshots(snapshots_path, project_name, project_id): with open(snapshot_path, "rb") as f: with zipfile_zstd.ZipFile(f) as zip_file: await wait_run_in_executor(zip_file.extractall, tmpdir) + _create_symbolic_links(zip_file, tmpdir) except OSError as e: raise ControllerError(f"Cannot open snapshot '{os.path.basename(snapshot)}': {e}") except zipfile_zstd.BadZipFile: diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 80b9dd56c..00829912a 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -15,6 +15,7 @@ # 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 @@ -26,6 +27,7 @@ import aiofiles import tempfile import zipfile +import pathlib from uuid import UUID, uuid4 @@ -41,8 +43,9 @@ from ..utils.asyncio.pool import Pool 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 +from .import_project import import_project, _move_node_file from .controller_error import ControllerError, ControllerForbiddenError, ControllerNotFoundError import logging @@ -1062,13 +1065,15 @@ async def duplicate(self, name=None, reset_mac_addresses=True): """ Duplicate a project - It's the save as feature of the 1.X. It's implemented on top of the - export / import features. It will generate a gns3p and reimport it. - It's a little slower but we have only one implementation to maintain. + Implemented on top of the export / import features. It will generate a gns3p and reimport it. + + NEW: fast duplication is used if possible (when there are no remote computes). + If not, the project is exported and reimported as explained above. :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 """ + # If the project was not open we open it temporary previous_status = self._status if self._status == "closed": @@ -1076,6 +1081,18 @@ async def duplicate(self, name=None, reset_mac_addresses=True): self.dump() assert self._status != "closed" + + try: + proj = await self._fast_duplication(name, location, reset_mac_addresses) + if proj: + if previous_status == "closed": + await self.close() + return proj + else: + log.info("Fast duplication failed, fallback to normal duplication") + except Exception as e: + raise aiohttp.web.HTTPConflict(text="Cannot duplicate project: {}".format(str(e))) + try: begin = time.time() @@ -1314,3 +1331,70 @@ def asdict(self): def __repr__(self): return f"" + + 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()) + 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/utils/asyncio/aiozipstream.py b/gns3server/utils/asyncio/aiozipstream.py index c1f74169c..4850eb3b0 100644 --- a/gns3server/utils/asyncio/aiozipstream.py +++ b/gns3server/utils/asyncio/aiozipstream.py @@ -195,14 +195,17 @@ def comment(self, comment): self._comment = comment self._didModify = True - async def data_generator(self, path): - - async with aiofiles.open(path, "rb") as f: - while True: - part = await f.read(self._chunksize) - if not part: - break - yield part + async def data_generator(self, path, islink=False): + + if islink: + yield os.readlink(path).encode() + else: + async with aiofiles.open(path, "rb") as f: + while True: + part = await f.read(self._chunksize) + if not part: + break + yield part return async def _run_in_executor(self, task, *args, **kwargs): @@ -268,12 +271,13 @@ async def _write(self, filename=None, iterable=None, arcname=None, compress_type raise ValueError("either (exclusively) filename or iterable shall be not None") if filename: - st = os.stat(filename) + st = os.stat(filename, follow_symlinks=False) isdir = stat.S_ISDIR(st.st_mode) + islink = stat.S_ISLNK(st.st_mode) mtime = time.localtime(st.st_mtime) date_time = mtime[0:6] else: - st, isdir, date_time = None, False, time.localtime()[0:6] + st, isdir, islink, date_time = None, False, False, time.localtime()[0:6] # Create ZipInfo instance to store file information if arcname is None: arcname = filename @@ -331,7 +335,7 @@ async def _write(self, filename=None, iterable=None, arcname=None, compress_type file_size = 0 if filename: - async for buf in self.data_generator(filename): + async for buf in self.data_generator(filename, islink): file_size = file_size + len(buf) CRC = zipfile.crc32(buf, CRC) & 0xFFFFFFFF if cmpr: diff --git a/requirements.txt b/requirements.txt index 0eac03714..e3edbef98 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,8 +6,8 @@ aiohttp>=3.9.5,<3.10 async-timeout==4.0.3 aiofiles>=24.1.0,<25.0 Jinja2>=3.1.4,<3.2 -sentry-sdk==2.12,<2.13 -psutil==6.0.0 +sentry-sdk>=2.17,<2.18 # optional dependency +psutil>=6.1.0 distro>=1.9.0 py-cpuinfo==9.0.0 sqlalchemy==2.0.31 @@ -20,4 +20,4 @@ watchfiles==0.22.0 zstandard==0.23.0 platformdirs==4.2.2 importlib-resources>=1.3; python_version <= '3.9' -truststore>=0.9.1; python_version >= '3.10' +truststore>=0.10.0; python_version >= '3.10' diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index cb52b64df..debae032c 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -121,9 +121,9 @@ async def test_create(compute_project, manager): ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -172,9 +172,9 @@ async def test_create_with_tag(compute_project, manager): ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -232,9 +232,9 @@ async def test_create_vnc(compute_project, manager): ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -378,9 +378,9 @@ async def test_create_start_cmd(compute_project, manager): ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "Entrypoint": ["/gns3/init.sh"], "Cmd": ["/bin/ls"], @@ -491,9 +491,9 @@ async def information(): ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -547,9 +547,9 @@ async def test_create_with_user(compute_project, manager): ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host", }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -651,9 +651,9 @@ async def test_create_with_extra_volumes_duplicate_1_image(compute_project, mana ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -707,9 +707,9 @@ async def test_create_with_extra_volumes_duplicate_2_user(compute_project, manag ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -763,9 +763,9 @@ async def test_create_with_extra_volumes_duplicate_3_subdir(compute_project, man ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -819,9 +819,9 @@ async def test_create_with_extra_volumes_duplicate_4_backslash(compute_project, ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -870,9 +870,9 @@ async def test_create_with_extra_volumes_duplicate_5_subdir_issue_1595(compute_p ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -921,9 +921,9 @@ async def test_create_with_extra_volumes_duplicate_6_subdir_issue_1595(compute_p ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -988,9 +988,9 @@ async def test_create_with_extra_volumes(compute_project, manager): ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -1265,9 +1265,9 @@ async def test_update(vm): ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -1347,9 +1347,9 @@ async def test_update_running(vm): ], "Privileged": True, "Memory": 0, - "NanoCpus": 0 + "NanoCpus": 0, + "UsernsMode": "host" }, - "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", diff --git a/tests/controller/test_export_project.py b/tests/controller/test_export_project.py index ea3786da1..f39fd9413 100644 --- a/tests/controller/test_export_project.py +++ b/tests/controller/test_export_project.py @@ -21,6 +21,7 @@ import pytest import pytest_asyncio import zipfile +import stat from pathlib import Path from unittest.mock import patch @@ -119,6 +120,8 @@ async def test_export(tmpdir, project): with open(os.path.join(path, "project-files", "snapshots", "test"), 'w+') as f: f.write("WORLD") + os.symlink("/tmp/anywhere", os.path.join(path, "vm-1", "dynamips", "symlink")) + with aiozipstream.ZipFile() as z: with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),): await export_project(z, project, str(tmpdir), include_images=False) @@ -134,9 +137,12 @@ async def test_export(tmpdir, project): assert 'vm-1/dynamips/empty-dir/' in myzip.namelist() assert 'project-files/snapshots/test' not in myzip.namelist() assert 'vm-1/dynamips/test_log.txt' not in myzip.namelist() - assert 'images/IOS/test.image' not in myzip.namelist() + assert 'vm-1/dynamips/symlink' in myzip.namelist() + zip_info = myzip.getinfo('vm-1/dynamips/symlink') + assert stat.S_ISLNK(zip_info.external_attr >> 16) + with myzip.open("project.gns3") as myfile: topo = json.loads(myfile.read().decode())["topology"] assert topo["nodes"][0]["compute_id"] == "local" # All node should have compute_id local after export diff --git a/tests/controller/test_import_project.py b/tests/controller/test_import_project.py index 5c1f0b791..3b478fb5f 100644 --- a/tests/controller/test_import_project.py +++ b/tests/controller/test_import_project.py @@ -23,7 +23,11 @@ from pathlib import Path from tests.utils import asyncio_patch, AsyncioMagicMock +from unittest.mock import patch, MagicMock +from gns3server.utils.asyncio import aiozipstream +from gns3server.controller.project import Project +from gns3server.controller.export_project import export_project from gns3server.controller.import_project import import_project, _move_files_to_compute from gns3server.version import __version__ @@ -111,6 +115,54 @@ async def test_import_project_override(projects_dir, controller): assert project.name == "test" +async def write_file(path, z): + + with open(path, 'wb') as f: + async for chunk in z: + f.write(chunk) + + +async def test_import_project_containing_symlink(tmpdir, controller): + + project = Project(controller=controller, name="test") + project.dump = MagicMock() + path = project.path + + project_id = str(uuid.uuid4()) + topology = { + "project_id": str(uuid.uuid4()), + "name": "test", + "auto_open": True, + "auto_start": True, + "topology": { + }, + "version": "2.0.0" + } + + with open(os.path.join(path, "project.gns3"), 'w+') as f: + json.dump(topology, f) + + os.makedirs(os.path.join(path, "vm1", "dynamips")) + symlink_path = os.path.join(project.path, "vm1", "dynamips", "symlink") + symlink_target = "/tmp/anywhere" + os.symlink(symlink_target, symlink_path) + + zip_path = str(tmpdir / "project.zip") + with aiozipstream.ZipFile() as z: + with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),): + await export_project(z, project, str(tmpdir), include_images=False) + await write_file(zip_path, z) + + with open(zip_path, "rb") as f: + project = await import_project(controller, project_id, f) + + assert project.name == "test" + assert project.id == project_id + symlink_path = os.path.join(project.path, "vm1", "dynamips", "symlink") + assert os.path.islink(symlink_path) + assert os.readlink(symlink_path) == symlink_target + + @pytest.mark.asyncio async def test_import_upgrade(tmpdir, controller): """