diff --git a/.github/workflows/add-new-issues-to-project.yml b/.github/workflows/add-new-issues-to-project.yml index aa8252552..5296e7a1f 100644 --- a/.github/workflows/add-new-issues-to-project.yml +++ b/.github/workflows/add-new-issues-to-project.yml @@ -10,7 +10,7 @@ jobs: name: Add issue to project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v1.0.1 + - uses: actions/add-to-project@v2 with: project-url: https://github.com/orgs/GNS3/projects/3 github-token: ${{ secrets.ADD_NEW_ISSUES_TO_PROJECT }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 06ab2cc62..7479a13b2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -56,11 +56,11 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -88,6 +88,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index bf668503c..834bad959 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Check for stable release id: ver @@ -34,7 +34,7 @@ jobs: - name: Login to GitHub Container Registry if: steps.ver.outputs.stable == 'true' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -42,7 +42,7 @@ jobs: - name: Login to Docker Hub if: steps.ver.outputs.stable == 'true' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} diff --git a/.github/workflows/publish-api-documentation.yml b/.github/workflows/publish-api-documentation.yml index 4df3f308e..079913aa2 100644 --- a/.github/workflows/publish-api-documentation.yml +++ b/.github/workflows/publish-api-documentation.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 ref: "gh-pages" - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.8 - name: Merge changes from 3.0 branch diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index cb84817ea..f80388526 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -21,9 +21,9 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/CHANGELOG b/CHANGELOG index 42f09ab17..02d3d14f9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,19 @@ # Change Log +## 2.2.60 15/07/2026 + +* Sync appliances +* Only set IOU images to be executable when importing images +* fix(import): project import does not move images symlinks into place +* Search for system UEFI that is compatible with Python < 3.12 +* Add OVMF firmware directory configuration +* Automatically add a Random Number Generator (RNG) device when using uefi option is enabled +* fix(docker): handle container name conflict automatically +* fix: gns3-server crashes on startup if "Open this project in the background" is active but there is a problem with that project +* Handle HTTPNotFound exception when retrieving compute status +* Fix: Check compute connectivity before open() during project deletion +* API endpoints to manage base configuration files for templates + ## 2.2.59 08/05/2026 * Sync appliances diff --git a/conf/gns3_server.conf b/conf/gns3_server.conf index aa54d57c8..7bb2aa6bd 100644 --- a/conf/gns3_server.conf +++ b/conf/gns3_server.conf @@ -103,6 +103,8 @@ enable_hardware_acceleration = True require_hardware_acceleration = False ; Allow unsafe additional command line options allow_unsafe_options = False +; Path to the OVMF firmware directory +ovmf_firmware_dir = "/usr/share/OVMF" [VMware] ; First vmnet interface of the range that can be managed by the GNS3 server diff --git a/docs/api/examples/controller_get_templateidbaseconfig.txt b/docs/api/examples/controller_get_templateidbaseconfig.txt new file mode 100644 index 000000000..9e3d9c25c --- /dev/null +++ b/docs/api/examples/controller_get_templateidbaseconfig.txt @@ -0,0 +1,17 @@ +curl -i -X 'http://localhost:3080/v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt' + +GET /v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt HTTP/1.1 + +HTTP/1.1 200 OK +Connection: close +X-Route: /v2/templates/{template_id}/base-config/{filename} +Server: Python/3.14 GNS3/2.2.60.dev1+713dbb7f +Content-Type: application/json +Content-Length: 144 +Date: Mon, 18 May 2026 18:34:10 GMT + +{ + "content": "# test config\n\ndhcp\n", + "filename": "vpcs_base_config.txt", + "template_id": "19021f99-e36f-394d-b4a1-8aaa902ab9cc" +} \ No newline at end of file diff --git a/docs/api/examples/controller_get_templatesbaseconfigs.txt b/docs/api/examples/controller_get_templatesbaseconfigs.txt new file mode 100644 index 000000000..263cbfa1f --- /dev/null +++ b/docs/api/examples/controller_get_templatesbaseconfigs.txt @@ -0,0 +1,44 @@ +curl -i -X 'http://localhost:3080/v2/templates/base-configs' + +GET /v2/templates/base-configs HTTP/1.1 + +HTTP/1.1 200 OK +Connection: close +X-Route: /v2/templates/base-configs +Server: Python/3.14 GNS3/2.2.60.dev1+713dbb7f +Content-Type: application/json +Content-Length: 576 +Date: Mon, 18 May 2026 18:28:53 GMT + +[ + { + "filename": "config1.txt" + }, + { + "filename": "config2.txt" + }, + { + "filename": "ios_base_startup-config.txt" + }, + { + "filename": "ios_etherswitch_startup-config.txt" + }, + { + "filename": "iou_l2_base_startup-config.txt" + }, + { + "filename": "iou_l3_base_startup-config.txt" + }, + { + "filename": "test_config.txt" + }, + { + "filename": "update_test.txt" + }, + { + "filename": "vpcs_base_config.txt" + }, + { + "filename": "vpcs_base_config2.txt" + } +] \ No newline at end of file diff --git a/docs/api/examples/controller_put_templateidbaseconfig.txt b/docs/api/examples/controller_put_templateidbaseconfig.txt new file mode 100644 index 000000000..c70e315b8 --- /dev/null +++ b/docs/api/examples/controller_put_templateidbaseconfig.txt @@ -0,0 +1,17 @@ +curl -i -X 'http://localhost:3080/v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt' + +PUT /v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt HTTP/1.1 + +HTTP/1.1 200 OK +Connection: close +X-Route: /v2/templates/{template_id}/base-config/{filename} +Server: Python/3.14 GNS3/2.2.60.dev1+713dbb7f +Content-Type: application/json +Content-Length: 144 +Date: Mon, 18 May 2026 18:37:57 GMT + +{ + "content": "# test config\n\ndhcp\n", + "filename": "vpcs_base_config.txt", + "template_id": "19021f99-e36f-394d-b4a1-8aaa902ab9cc" +} \ No newline at end of file diff --git a/docs/api/v2/controller/template/templatesbaseconfigs.rst b/docs/api/v2/controller/template/templatesbaseconfigs.rst new file mode 100644 index 000000000..dc4de3e8c --- /dev/null +++ b/docs/api/v2/controller/template/templatesbaseconfigs.rst @@ -0,0 +1,38 @@ +GET /v2/templates/base-configs +------------------------------------------------------------------------------------------------------------------------------------------ + +.. contents:: + +GET /v2/templates/base-configs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +List all available base configuration files + +Response status codes +********************** +- **200**: List of base configuration files returned + +Output +******* + +.. raw:: html + + + + + + + + + + + + + + +
NameTypeDescription
filenamestringName of the base configuration file
+ +Sample session +*************** + +.. literalinclude:: ../../../examples/controller_get_templatesbaseconfigs.txt \ No newline at end of file diff --git a/docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst b/docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst new file mode 100644 index 000000000..98b356270 --- /dev/null +++ b/docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst @@ -0,0 +1,90 @@ +/v2/templates/{template_id}/base-config/{filename} +------------------------------------------------------------------------------------------------------------------------------------------ + +.. contents:: + +GET /v2/templates/**{template_id}**/base-configs/**{filename}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +List all available base configuration files + +Response status codes +********************** +- **200**: List of base configuration files returned + +Output +******* + +.. raw:: html + + + + + + + + + + + + + + +
NameTypeDescription
filenamestringName of the base configuration file
+ +Sample session +*************** + +.. literalinclude:: ../../../examples/controller_get_templateidbaseconfig.txt + + +PUT /v2/templates/**{template_id}**/base-config/**{filename}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Update base configuration file content + +Response status codes +********************** +- **200**: File updated +- **404**: File not found + +Input +******* + +.. raw:: html + + + + + + + + + + + +
NameMandatoryTypeDescription
contentstringNew file content
+ +Output +******* + +.. raw:: html + + + + + + + + + + + + + +
NameMandatoryTypeDescription
template_idstringTemplate UUID
filenamestringBase configuration filename
contentstringUpdated file content
+ +Sample session +*************** + +.. literalinclude:: ../../../examples/controller_put_templateidbaseconfig.txt \ No newline at end of file diff --git a/gns3server/appliances/bird.gns3a b/gns3server/appliances/bird.gns3a index b18e18e86..9ecf790cc 100644 --- a/gns3server/appliances/bird.gns3a +++ b/gns3server/appliances/bird.gns3a @@ -1,41 +1,56 @@ { - "appliance_id": "088df570-f637-46f5-8a68-85acde538e5e", + "appliance_id": "8fecbf89-5cd1-4aea-b735-5f36cf0efbb7", "name": "BIRD", "category": "router", "description": "The BIRD project aims to develop a fully functional dynamic IP routing daemon primarily targeted on (but not limited to) Linux, FreeBSD and other UNIX-like systems and distributed under the GNU General Public License.", "vendor_name": "CZ.NIC Labs", - "vendor_url": "http://bird.network.cz/", - "documentation_url": "http://bird.network.cz/?get_doc&f=bird.html", + "vendor_url": "https://bird.network.cz", + "documentation_url": "https://bird.network.cz/?get_doc&f=bird.html&v=20", "product_name": "BIRD internet routing daemon", "registry_version": 4, "status": "stable", - "maintainer": "GNS3 Team", - "maintainer_email": "developers@gns3.net", - "usage": "\n*** BIRD v1 is end-of-life ***\nPlease use the BIRD2 appliance.\n\nConfigure interfaces in /opt/bootlocal.sh, BIRD configuration is done in /usr/local/etc/bird", + "maintainer": "Bernhard Ehlers", + "maintainer_email": "dev-ehlers@mailbox.org", + "usage": "Username:\tgns3\nPassword:\tgns3\nTo become root, use \"sudo -s\".\n\nNetwork configuration:\nsudo nano /etc/network/interfaces\nsudo systemctl restart networking\n\nBIRD:\nRestart: sudo systemctl restart bird\nReconfigure: birdc configure", + "port_name_format": "eth{0}", "qemu": { - "adapter_type": "e1000", + "adapter_type": "virtio-net-pci", "adapters": 4, - "ram": 128, - "hda_disk_interface": "ide", + "ram": 512, + "hda_disk_interface": "scsi", "arch": "x86_64", "console_type": "telnet", "kvm": "allow" }, "images": [ { - "filename": "bird-tinycore64-1.5.0.img", - "version": "1.5.0", - "md5sum": "08d50ba2b1b262e2e03e4babf90abf69", - "filesize": 22413312, + "filename": "bird2-debian-2.14.qcow2", + "version": "2.14", + "md5sum": "029cf1756201ee79497c169502b08b88", + "filesize": 303717376, "download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/", - "direct_download_url": "http://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird-tinycore64-1.5.0.img" + "direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.14.qcow2" + }, + { + "filename": "bird2-debian-2.0.12.qcow2", + "version": "2.0.12", + "md5sum": "435218a2e90cba921cc7fde1d64a9419", + "filesize": 287965184, + "download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/", + "direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.0.12.qcow2" } ], "versions": [ { - "name": "1.5.0", + "name": "2.14", "images": { - "hda_disk_image": "bird-tinycore64-1.5.0.img" + "hda_disk_image": "bird2-debian-2.14.qcow2" + } + }, + { + "name": "2.0.12", + "images": { + "hda_disk_image": "bird2-debian-2.0.12.qcow2" } } ] diff --git a/gns3server/appliances/bird2.gns3a b/gns3server/appliances/bird2.gns3a deleted file mode 100644 index 670d56a83..000000000 --- a/gns3server/appliances/bird2.gns3a +++ /dev/null @@ -1,57 +0,0 @@ -{ - "appliance_id": "8fecbf89-5cd1-4aea-b735-5f36cf0efbb7", - "name": "BIRD2", - "category": "router", - "description": "The BIRD project aims to develop a fully functional dynamic IP routing daemon primarily targeted on (but not limited to) Linux, FreeBSD and other UNIX-like systems and distributed under the GNU General Public License.", - "vendor_name": "CZ.NIC Labs", - "vendor_url": "https://bird.network.cz", - "documentation_url": "https://bird.network.cz/?get_doc&f=bird.html&v=20", - "product_name": "BIRD internet routing daemon", - "registry_version": 4, - "status": "stable", - "maintainer": "Bernhard Ehlers", - "maintainer_email": "dev-ehlers@mailbox.org", - "usage": "Username:\tgns3\nPassword:\tgns3\nTo become root, use \"sudo -s\".\n\nNetwork configuration:\nsudo nano /etc/network/interfaces\nsudo systemctl restart networking\n\nBIRD:\nRestart: sudo systemctl restart bird\nReconfigure: birdc configure", - "port_name_format": "eth{0}", - "qemu": { - "adapter_type": "virtio-net-pci", - "adapters": 4, - "ram": 512, - "hda_disk_interface": "scsi", - "arch": "x86_64", - "console_type": "telnet", - "kvm": "allow" - }, - "images": [ - { - "filename": "bird2-debian-2.14.qcow2", - "version": "2.14", - "md5sum": "029cf1756201ee79497c169502b08b88", - "filesize": 303717376, - "download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/", - "direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.14.qcow2" - }, - { - "filename": "bird2-debian-2.0.12.qcow2", - "version": "2.0.12", - "md5sum": "435218a2e90cba921cc7fde1d64a9419", - "filesize": 287965184, - "download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/", - "direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.0.12.qcow2" - } - ], - "versions": [ - { - "name": "2.14", - "images": { - "hda_disk_image": "bird2-debian-2.14.qcow2" - } - }, - { - "name": "2.0.12", - "images": { - "hda_disk_image": "bird2-debian-2.0.12.qcow2" - } - } - ] -} diff --git a/gns3server/appliances/fortimanager.gns3a b/gns3server/appliances/fortimanager.gns3a index b5de0248e..ae51af861 100644 --- a/gns3server/appliances/fortimanager.gns3a +++ b/gns3server/appliances/fortimanager.gns3a @@ -51,250 +51,12 @@ "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" }, { - "filename": "FMG_VM64_KVM-v7.4.3-build2487-FORTINET.out.kvm.qcow2", - "version": "7.4.3", - "md5sum": "b01d9f86aa27c538407d518df1326863", - "filesize": 346107904, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.4.2-build2397-FORTINET.out.kvm.qcow2", - "version": "7.4.2", - "md5sum": "36371fbf06210ded57c00b2ff290f2c5", - "filesize": 322514944, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.4.1-build2308-FORTINET.out.kvm.qcow2", - "version": "7.4.1", - "md5sum": "e542cc8f2d8f46e9c32b783bf31bef39", - "filesize": 309387264, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.2.5-build1574-FORTINET.out.kvm.qcow2", - "version": "7.2.5", - "md5sum": "754326845096afd909ec45d98f8d5a83", - "filesize": 278401024, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.2.4-build1460-FORTINET.out.kvm.qcow2", - "version": "7.2.4", - "md5sum": "98fa9830d9ecb5911a703d03b80026b6", - "filesize": 261992448, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.2.2-build1334-FORTINET.out.kvm.qcow2", - "version": "7.2.2", - "md5sum": "2ff1298257321cd485d2cad91d6ce510", - "filesize": 246083584, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.2.1-build1215-FORTINET.out.kvm.qcow2", - "version": "7.2.1", - "md5sum": "1a3eeff1204fa8f4243773f7521e12b5", - "filesize": 242814976, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.0.12-build0623-FORTINET.out.kvm.qcow2", - "version": "7.0.12", - "md5sum": "5b6f6a2b8bc00e56337aa7023a9025cf", - "filesize": 249520128, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.0.11-build0595-FORTINET.out.kvm.qcow2", - "version": "7.0.11", - "md5sum": "7b166222136e26190159f37cccbaab6e", - "filesize": 249360384, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.0.9-build0489-FORTINET.out.kvm.qcow2", - "version": "7.0.9", - "md5sum": "dbeb6a79b6e421000573dbbbdb50b8b5", - "filesize": 247955456, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.0.6-build0372-FORTINET.out.kvm.qcow2", - "version": "7.0.6", - "md5sum": "dfa4df9e976ed87e73cb9601a8a70323", - "filesize": 239190016, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.0.5-build0365-FORTINET.out.kvm.qcow2", - "version": "7.0.5", - "md5sum": "e8b9c992784cea766b52a427a5fe0279", - "filesize": 237535232, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6.4.14-build2660-FORTINET.out.kvm.qcow2", - "version": "6.4.14", - "md5sum": "0fe56e363b166c07b710bde795e36049", - "filesize": 219430912, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6.4.12-build2610-FORTINET.out.kvm.qcow2", - "version": "6.4.12", - "md5sum": "36c0dc531d921e5f1e1e09b030f7c813", - "filesize": 219455488, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build2288-FORTINET.out.kvm.qcow2", - "version": "6.4.5", - "md5sum": "bd2791984b03f55a6825297e83c6576a", - "filesize": 117014528, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build2253-FORTINET.out.kvm.qcow2", - "version": "6.4.4", - "md5sum": "3554a47fde2dc91d17eec16fd0dc10a3", - "filesize": 116621312, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build1183-FORTINET.out.kvm.qcow2", - "version": "6.2.2", - "md5sum": "f5051a8fe49d916bb554b9bae32a1eb4", - "filesize": 139145216, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build1050-FORTINET.out.kvm.qcow2", - "version": "6.2.0", - "md5sum": "c19d2527f91ad1bbafbde5bf08487867", - "filesize": 126894080, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build0349-FORTINET.out.kvm.qcow2", - "version": "6.0.6", - "md5sum": "d03f024c948ba6e2bb9e66c11ca8f34c", - "filesize": 112553984, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build0255-FORTINET.out.kvm.qcow2", - "version": "6.0.3", - "md5sum": "5f34d52d9289b0be2a4c04943446ea39", - "filesize": 115703808, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build0205-FORTINET.out.kvm.qcow2", - "version": "6.0.2", - "md5sum": "8f748649c537d9b5466b24c5b4e62017", - "filesize": 116981760, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build0092-FORTINET.out.kvm.qcow2", - "version": "6.0.0", - "md5sum": "73bfe1bc70124521a524d857646b9c2e", - "filesize": 119066624, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1631-FORTINET.out.kvm.qcow2", - "version": "5.6.2", - "md5sum": "c81cc247e8eb03249b475fe0e847653e", - "filesize": 106946560, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1619-FORTINET.out.kvm.qcow2", - "version": "5.6.1", - "md5sum": "8cc553842564d232af295d6a0c784c1f", - "filesize": 106831872, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1557-FORTINET.out.kvm.qcow2", - "version": "5.6.0", - "md5sum": "f8bd600796f894f4ca1ea2d6b4066d3d", - "filesize": 108363776, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1225-FORTINET.out.kvm.qcow2", - "version": "5.4.4", - "md5sum": "53bc6e320fe7bde5d2b636bde95a910c", - "filesize": 89911296, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1187-FORTINET.out.kvm.qcow2", - "version": "5.4.3", - "md5sum": "53602c776d215d98e32163a10804fc49", - "filesize": 87425024, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1151-FORTINET.out.kvm.qcow2", - "version": "5.4.2", - "md5sum": "8e131ad40009c740f3efdee6dc3a0ac3", - "filesize": 86437888, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1082-FORTINET.out.kvm.qcow2", - "version": "5.4.1", - "md5sum": "fc1815410f3f0536e2e3a9c1c5c07f41", - "filesize": 83124224, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1019-FORTINET.out.kvm.qcow2", - "version": "5.4.0", - "md5sum": "1cfb22671cb372d8bf3e47b9c3c55ded", - "filesize": 77541376, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build0786-FORTINET.out.kvm.qcow2", - "version": "5.2.10", - "md5sum": "377fe38bf07bc2435608e5b65f780f07", - "filesize": 64962560, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build0780-FORTINET.out.kvm.qcow2", - "version": "5.2.9", - "md5sum": "04268e779d3d5e6c928c6fd638423c52", - "filesize": 65007616, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build0777-FORTINET.out.kvm.qcow2", - "version": "5.2.8", - "md5sum": "6dbf148ace9bf309ad383757afd75fad", - "filesize": 65011712, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build0757-FORTINET.out.kvm.qcow2", - "version": "5.2.7", - "md5sum": "d37dbaa49d7522324681eeba19f7699b", - "filesize": 65056768, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "empty30G.qcow2", + "filename": "empty500G.qcow2", "version": "1.0", - "md5sum": "3411a599e822f2ac6be560a26405821a", - "filesize": 197120, + "md5sum": "658c825441b9b3080ba00f9eec002eaa", + "filesize": 204608, "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" + "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty500G.qcow2/download" } ], "versions": [ @@ -302,259 +64,21 @@ "name": "7.4.6", "images": { "hda_disk_image": "FMG_VM64_KVM-v7.4.6.M-build2588-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" + "hdb_disk_image": "empty500G.qcow2" } }, { "name": "7.4.5", "images": { "hda_disk_image": "FMG_VM64_KVM-v7.4.5.M-build2553-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" + "hdb_disk_image": "empty500G.qcow2" } }, { "name": "7.4.4", "images": { "hda_disk_image": "FMG_VM64_KVM-v7.4.4.F-build2550-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.4.3", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.4.3-build2487-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.4.2", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.4.2-build2397-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.4.1", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.4.1-build2308-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.2.5", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.2.5-build1574-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.2.4", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.2.4-build1460-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.2.2", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.2.2-build1334-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.2.1", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.2.1-build1215-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.0.12", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.0.12-build0623-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.0.11", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.0.11-build0595-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.0.9", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.0.9-build0489-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.0.6", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.0.6-build0372-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.0.5", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.0.5-build0365-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.4.14", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6.4.14-build2660-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.4.12", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6.4.12-build2610-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.4.5", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build2288-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.4.4", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build2253-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.2.2", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build1183-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.2.0", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build1050-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.0.6", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build0349-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.0.3", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build0255-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.0.2", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build0205-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.0.0", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build0092-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.6.2", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1631-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.6.1", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1619-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.6.0", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1557-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.4.4", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1225-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.4.3", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1187-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.4.2", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1151-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.4.1", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1082-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.4.0", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1019-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.2.10", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build0786-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.2.9", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build0780-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.2.8", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build0777-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.2.7", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build0757-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" + "hdb_disk_image": "empty500G.qcow2" } } ] diff --git a/gns3server/appliances/infix.gns3a b/gns3server/appliances/infix.gns3a index 736863ba7..481cba0fd 100644 --- a/gns3server/appliances/infix.gns3a +++ b/gns3server/appliances/infix.gns3a @@ -132,9 +132,37 @@ "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" + }, + { + "filename": "infix-x86_64-v26.05.0.qcow2", + "filesize": 330039296, + "md5sum": "60f7c36c389c33ab108acc021f41ccd5", + "version": "26.05.0", + "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.05.0/infix-x86_64-v26.05.0.qcow2" + }, + { + "filename": "infix-x86_64-v26.06.0.qcow2", + "filesize": 363593728, + "md5sum": "79ca8bd8534bbaa1af0ab874a49d6f4c", + "version": "26.06.0", + "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.06.0/infix-x86_64-v26.06.0.qcow2" } ], "versions": [ + { + "name": "26.06.0", + "images": { + "bios_image": "OVMF-edk2-stable202305.fd", + "hda_disk_image": "infix-x86_64-v26.06.0.qcow2" + } + }, + { + "name": "26.05.0", + "images": { + "bios_image": "OVMF-edk2-stable202305.fd", + "hda_disk_image": "infix-x86_64-v26.05.0.qcow2" + } + }, { "name": "26.03.0", "images": { diff --git a/gns3server/appliances/ubuntu-docker.gns3a b/gns3server/appliances/ubuntu-docker.gns3a index 9cde03e79..c69a1405d 100644 --- a/gns3server/appliances/ubuntu-docker.gns3a +++ b/gns3server/appliances/ubuntu-docker.gns3a @@ -14,7 +14,7 @@ "symbol": "linux_guest.svg", "docker": { "adapters": 1, - "image": "gns3/ubuntu:noble", + "image": "gns3/ubuntu:resolute", "console_type": "telnet" } } diff --git a/gns3server/appliances/westermo-weos.gns3a b/gns3server/appliances/westermo-weos.gns3a index 6ebce85d7..dbcf3a7eb 100644 --- a/gns3server/appliances/westermo-weos.gns3a +++ b/gns3server/appliances/westermo-weos.gns3a @@ -38,6 +38,13 @@ "filesize": 17825792, "direct_download_url": "https://dropzone.westermo.com/file.aspx?id=e6a7676d-85a7-4374-8961-c68aacb74921" }, + { + "filename": "WeOS-zero-5.29.0.disk", + "version": "5.29.0", + "filesize": 81788928, + "md5sum": "f464de7b2b424f4a8ad7c650108fee3d", + "direct_download_url": "https://dropzone.westermo.com/file.aspx?id=52655074-0fab-4119-ba01-b68200a733ab" + }, { "filename": "WeOS-zero-5.28.0.disk", "version": "5.28.0", @@ -54,6 +61,13 @@ } ], "versions": [ + { + "name": "5.29.0", + "images": { + "hda_disk_image": "WeOS-zero-5.29.0.disk", + "hdb_disk_image": "Config-zero-1.0.0.disk" + } + }, { "name": "5.28.0", "images": { diff --git a/gns3server/compute/docker/__init__.py b/gns3server/compute/docker/__init__.py index a441af830..4c12314d5 100644 --- a/gns3server/compute/docker/__init__.py +++ b/gns3server/compute/docker/__init__.py @@ -33,7 +33,7 @@ from gns3server.config import Config from gns3server.utils.asyncio import locking from gns3server.compute.base_manager import BaseManager from gns3server.compute.docker.docker_vm import DockerVM -from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Error, DockerHttp404Error +from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Error, DockerHttp404Error, DockerHttp409Error log = logging.getLogger(__name__) @@ -236,6 +236,8 @@ class Docker(BaseManager): raise DockerHttp304Error("Docker has returned an error: {} {}".format(response.status, body)) elif response.status == 404: raise DockerHttp404Error("Docker has returned an error: {} {}".format(response.status, body)) + elif response.status == 409: + raise DockerHttp409Error("Docker has returned an error: {} {}".format(response.status, body)) else: raise DockerError("Docker has returned an error: {} {}".format(response.status, body)) return response diff --git a/gns3server/compute/docker/docker_error.py b/gns3server/compute/docker/docker_error.py index 5d2b9b1d8..17d45ebbe 100644 --- a/gns3server/compute/docker/docker_error.py +++ b/gns3server/compute/docker/docker_error.py @@ -32,3 +32,7 @@ class DockerHttp304Error(DockerError): class DockerHttp404Error(DockerError): pass + + +class DockerHttp409Error(DockerError): + pass diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 79b5f640b..a5822d998 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -43,7 +43,8 @@ from ..nios.nio_udp import NIOUDP from .docker_error import ( DockerError, DockerHttp304Error, - DockerHttp404Error + DockerHttp404Error, + DockerHttp409Error ) import logging @@ -459,10 +460,31 @@ class DockerVM(BaseNode): if extra_hosts: params["Env"].append("GNS3_EXTRA_HOSTS={}".format(extra_hosts)) - # Support name in Doker: [a-zA-Z0-9][a-zA-Z0-9_.-] - result = await self.manager.query("POST", "containers/create?name={}".format(self.docker_name), data=params) - self._cid = result['Id'] - log.info("Docker container '{name}' [{id}] created".format(name=self._name, id=self._id)) + try: + # Supported names in Docker: [a-zA-Z0-9][a-zA-Z0-9_.-] + result = await self.manager.query("POST", f"containers/create?name={self.docker_name}", data=params) + except DockerHttp409Error: + # Container name already exists. This can happen when the server crashes + # and leaves containers behind. Try to remove the conflicting container. + log.warning(f"Container name '{self.docker_name}' is already in use, attempting to clean up the stale container...") + try: + # Try to get and remove the conflicting container + try: + container_info = await self.manager.query("GET", f"containers/{self.docker_name}/json") + container_id = container_info["Id"] + # Force remove the container + await self.manager.query("DELETE", f"containers/{container_id}", params={"force": 1, "v": 1}) + log.info(f"Removed stale container '{self.docker_name}' ({container_id})") + except DockerHttp404Error: + # Container doesn't exist anymore, race condition - just continue + pass + # Retry creating the container + result = await self.manager.query("POST", f"containers/create?name={self.docker_name}", data=params) + except DockerError as e: + log.error(f"Failed to clean up conflicting container '{self.docker_name}': {e}") + raise + self._cid = result["Id"] + log.info(f"Docker container '{self._name}' [{self._id}] created") return True def _format_env(self, variables, env): diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index f97e5db31..fa7757a62 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -35,6 +35,7 @@ import time import json import psutil +from pathlib import Path from gns3server.utils import parse_version, shlex_quote from gns3server.utils.asyncio import subprocess_check_output, cancellable_wait_run_in_executor from .qemu_error import QemuError @@ -2057,15 +2058,23 @@ class QemuVM(BaseNode): options.extend(["-bios", self._bios_image.replace(",", ",,")]) elif self._uefi: - + ovmf_firmware_dir = self._manager.config.get_section_config("Qemu").get("ovmf_firmware_dir", "/usr/share/OVMF") + system_ovmf_firmware_dir = Path(ovmf_firmware_dir) + log.info("Using OVMF firmware directory: {}".format(system_ovmf_firmware_dir)) old_ovmf_vars_path = os.path.join(self.working_dir, "OVMF_VARS.fd") if os.path.exists(old_ovmf_vars_path): # the node has its own UEFI variables store already, we must also use the old UEFI firmware ovmf_firmware_path = self.manager.get_abs_image_path("OVMF_CODE.fd") else: - system_ovmf_firmware_path = "/usr/share/OVMF/OVMF_CODE_4M.fd" - if os.path.exists(system_ovmf_firmware_path): - ovmf_firmware_path = system_ovmf_firmware_path + # Use a manual case-insensitive search instead + try: + system_ovmf_firmware_path = next((f for f in system_ovmf_firmware_dir.glob("*.fd") + if f.name.lower() == "ovmf_code_4m.fd"), None) + except (FileNotFoundError, StopIteration): + system_ovmf_firmware_path = None + + if system_ovmf_firmware_path: + ovmf_firmware_path = str(system_ovmf_firmware_path) else: # otherwise, get the UEFI firmware from the images directory ovmf_firmware_path = self.manager.get_abs_image_path("OVMF_CODE_4M.fd") @@ -2074,9 +2083,13 @@ class QemuVM(BaseNode): options.extend(["-drive", "if=pflash,format=raw,readonly,file={}".format(ovmf_firmware_path)]) # try to use the UEFI variables store from the system first - system_ovmf_vars_path = "/usr/share/OVMF/OVMF_VARS_4M.fd" - if os.path.exists(system_ovmf_vars_path): - ovmf_vars_path = system_ovmf_vars_path + try: + system_ovmf_vars_path = next((f for f in system_ovmf_firmware_dir.glob("*.fd") + if f.name.lower() == "ovmf_vars_4m.fd"), None) + except (FileNotFoundError, StopIteration): + system_ovmf_vars_path = None + if system_ovmf_vars_path: + ovmf_vars_path = str(system_ovmf_vars_path) else: # otherwise, get the UEFI variables store from the images directory ovmf_vars_path = self.manager.get_abs_image_path("OVMF_VARS_4M.fd") @@ -2092,6 +2105,10 @@ class QemuVM(BaseNode): except OSError as e: raise QemuError("Cannot copy OVMF_VARS_4M.fd file to the node working directory: {}".format(e)) options.extend(["-drive", "if=pflash,format=raw,file={}".format(ovmf_vars_node_path)]) + + # edk2 firmware requires a Random Number Generator (RNG) device in order to turn network adapters on + options.extend(["-object", "rng-random,filename=/dev/urandom,id=rng0 "]) + options.extend(["-device", "virtio-rng-pci,rng=rng0"]) return options def _linux_boot_options(self): diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 34b5144b9..7bd932aa4 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -562,7 +562,15 @@ class Controller: for project in self._projects.values(): if project.auto_open: - await project.open() + try: + await project.open() + except aiohttp.web.HTTPClientError as e: + details = e.text or e.reason or str(e) + log.warning( + "Failed to auto-open project '%s': %s", + project.name, + details, + ) def get_free_project_name(self, base_name): """ diff --git a/gns3server/controller/import_project.py b/gns3server/controller/import_project.py index a0f95bf00..a1912208f 100644 --- a/gns3server/controller/import_project.py +++ b/gns3server/controller/import_project.py @@ -271,7 +271,7 @@ async def _upload_file(compute, project_id, file_path, path): async def _import_images(controller, images_path): """ - Copy images to the images directory or delete them if they already exists. + Copy images to the images directory or delete them if they already exist. """ image_dir = controller.images_path() @@ -279,12 +279,19 @@ async def _import_images(controller, images_path): for (dirpath, dirnames, filenames) in os.walk(root, followlinks=False): for filename in filenames: path = os.path.join(dirpath, filename) - if os.path.islink(path): - continue dst = os.path.join(image_dir, os.path.relpath(path, root)) os.makedirs(os.path.dirname(dst), exist_ok=True) - await wait_run_in_executor(shutil.move, path, dst) - + if not os.path.exists(dst): + await wait_run_in_executor(shutil.move, path, dst) + try: + with open(dst, "rb") as f: + # read the first 7 bytes of the file. + elf_header_start = f.read(7) + # IOU images must start with the ELF magic number, be 32-bit or 64-bit, little endian and have an ELF version of 1 + if elf_header_start == b'\x7fELF\x01\x01\x01' or elf_header_start == b'\x7fELF\x02\x01\x01': + os.chmod(dst, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC) + except OSError as e: + continue async def update_snapshots(snapshots_dir, project_path, project_name, project_id, reset_mac_addresses=True): """ diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 9d633f526..f26b1ff5f 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -905,12 +905,23 @@ class Project: async def delete(self): + # Check compute connectivity before open() to avoid 120s timeout + # when remote computes are unreachable + disconnected = self._get_disconnected_computes() + if disconnected: + compute_names = ", ".join([f"'{c.name}'" for c in disconnected]) + raise aiohttp.web.HTTPForbidden( + f"Cannot delete project '{self.name}': {len(disconnected)} compute(s) are disconnected: {compute_names}. " + f"Please fix the connection or delete the project manually on those computes." + ) + if self._status != "opened": try: await self.open() except aiohttp.web.HTTPConflict as e: # ignore missing images or other conflicts when deleting a project - log.warning("Conflict while deleting project: {}".format(e.text)) + log.warning(f"Conflict while deleting project: {e}") + await self.delete_on_computes() await self.close() try: @@ -922,6 +933,42 @@ class Project: raise aiohttp.web.HTTPConflict(text="Cannot delete project directory {}: {}".format(self.path, str(e))) self.emit_controller_notification("project.deleted", self.__json__()) + def _get_disconnected_computes(self): + """ + Check compute connectivity by reading the topology file directly, + without opening the project (which would try to connect to computes). + Returns a list of disconnected Compute objects. + """ + if self._status == "opened": + # Project is already open, use the already-loaded _computes list + compute_ids = self._computes + else: + # Read compute IDs from topology file without connecting + path = self._topology_file() + if not os.path.exists(path): + return [] + try: + project_data = load_topology(path) + except (ValueError, OSError) as e: + log.warning(f"Could not read topology file for project '{self._name}': {e}") + return [] + topology = project_data.get("topology", {}) + compute_ids = set() + for node in topology.get("nodes", []): + compute_id = node.get("compute_id") + if compute_id: + compute_ids.add(compute_id) + + disconnected = [] + for compute_id in compute_ids: + try: + compute = self._controller.get_compute(compute_id) + if not compute.connected: + disconnected.append(compute) + except aiohttp.web.HTTPNotFound: + log.warning(f"Compute '{compute_id}' not found in controller") + return disconnected + async def delete_on_computes(self): """ Delete the project on computes but not on controller diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 4dce82c80..5b3250a0a 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -57,7 +57,7 @@ class CrashReport: Report crash to a third party service """ - DSN = "https://646d66c1f0b65d59af9c4577a68f3f87@o19455.ingest.us.sentry.io/38482" + DSN = "https://deee946d000cb5548acebb1cd7c1a1d3@o19455.ingest.us.sentry.io/38482" _instance = None def __init__(self): diff --git a/gns3server/handlers/api/controller/template_handler.py b/gns3server/handlers/api/controller/template_handler.py index 277349429..278782276 100644 --- a/gns3server/handlers/api/controller/template_handler.py +++ b/gns3server/handlers/api/controller/template_handler.py @@ -22,6 +22,7 @@ from gns3server.schemas.template import TEMPLATE_USAGE_SCHEMA import hashlib import json +import os from gns3server.schemas.template import ( TEMPLATE_OBJECT_SCHEMA, @@ -173,3 +174,131 @@ class TemplateHandler: compute_id=request.json.get("compute_id")) response.set_status(201) response.json(node) + + @Route.get( + r"/templates/{template_id}/base-config/{filename}", + description="Get base configuration file content", + ) + def get_base_config(request, response): + + controller = Controller.instance() + + template_id = request.match_info["template_id"] + + template = controller.template_manager.get_template(template_id) + + filename = os.path.basename(request.match_info["filename"]) + + path = os.path.join(controller.configs_path(), filename) + + try: + if not os.path.exists(path): + response.set_status(404) + response.json({"message": "File not found"}) + return + + with open(path, encoding="utf-8", errors="ignore") as f: + content = f.read() + + except Exception as e: + response.set_status(500) + response.json({"message": str(e)}) + return + + response.set_status(200) + response.json({ + "template_id": template.id, + "filename": filename, + "content": content + }) + + @Route.put( + r"/templates/{template_id}/base-config/{filename}", + description="Update base configuration file content", + parameters={ + "template_id": "Template UUID", + "filename": "Base config filename" + }, + status_codes={ + 200: "File updated", + 400: "Invalid request", + 404: "Template or file not found" + }, + ) + def update_base_config(request, response): + + controller = Controller.instance() + + template_id = request.match_info["template_id"] + filename = os.path.basename(request.match_info["filename"]) + + try: + template = controller.template_manager.get_template(template_id) + except Exception: + response.set_status(404) + response.json({"message": "Template not found"}) + return + + path = os.path.join(controller.configs_path(), filename) + + body = request.json + + if not body or "content" not in body: + response.set_status(400) + response.json({"message": "Missing 'content' field"}) + return + + content = body["content"] + + try: + os.makedirs(controller.configs_path(), exist_ok=True) + + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + except OSError as e: + response.set_status(500) + response.json({"message": str(e)}) + return + + response.set_status(200) + response.json({ + "template_id": template.id, + "filename": filename, + "content": content + }) + + @Route.get( + r"/templates/base-configs", + description="List all available base configuration files", + status_codes={ + 200: "List of base configuration files returned" + } + ) + def list_base_configs(request, response): + + controller = Controller.instance() + + configs_path = controller.configs_path() + + try: + files = [] + + if os.path.exists(configs_path): + for filename in os.listdir(configs_path): + + path = os.path.join(configs_path, filename) + + if os.path.isfile(path): + files.append({"filename": filename}) + + except OSError as e: + response.set_status(500) + response.json({ + "message": str(e) + }) + return + + files = sorted(files, key=lambda x: x["filename"]) + response.set_status(200) + response.json(files) diff --git a/gns3server/version.py b/gns3server/version.py index 8d4ff1a3f..a384e3124 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,8 +23,8 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.2.60.dev1" -__version_info__ = (2, 2, 60, 99) +__version__ = "2.2.60" +__version_info__ = (2, 2, 60, 0) if "dev" in __version__: try: diff --git a/tests/handlers/api/controller/test_template.py b/tests/handlers/api/controller/test_template.py index 2accbffde..610b7da4b 100644 --- a/tests/handlers/api/controller/test_template.py +++ b/tests/handlers/api/controller/test_template.py @@ -14,8 +14,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - - +import os import uuid from tests.utils import asyncio_patch @@ -955,3 +954,81 @@ async def test_create_node_from_template(controller_api, controller, project): mock.assert_called_with(id, x=42, y=12, name=None, compute_id=None) assert response.route == "/projects/{project_id}/templates/{template_id}" assert response.status == 201 + + +async def test_get_base_config(controller_api, controller): + + template_id = str(uuid.uuid4()) + + controller.template_manager._templates[template_id] = Template(template_id, { + "template_type": "vpcs", + "category": 0, + "name": "test", + "symbol": "guest.svg", + "default_name_format": "{name}-{0}", + "compute_id": "local" + }) + + config_path = os.path.join(controller.configs_path(), "test_config.txt") + + with open(config_path, "w") as f: + f.write("hello config") + + response = await controller_api.get( + f"/templates/{template_id}/base-config/test_config.txt" + ) + + assert response.status == 200 + assert response.json["filename"] == "test_config.txt" + assert response.json["content"] == "hello config" + +async def test_update_base_config(controller_api, controller): + + template_id = str(uuid.uuid4()) + + controller.template_manager._templates[template_id] = Template(template_id, { + "template_type": "vpcs", + "category": 0, + "name": "test", + "symbol": "guest.svg", + "default_name_format": "{name}-{0}", + "compute_id": "local" + }) + + config_path = os.path.join(controller.configs_path(), "update_test.txt") + + with open(config_path, "w") as f: + f.write("old content") + + response = await controller_api.put( + f"/templates/{template_id}/base-config/update_test.txt", + { + "content": "new updated content" + } + ) + + assert response.status == 200 + assert response.json["content"] == "new updated content" + + with open(config_path) as f: + assert f.read() == "new updated content" + +async def test_list_base_configs(controller_api, controller): + + config1 = os.path.join(controller.configs_path(), "config1.txt") + config2 = os.path.join(controller.configs_path(), "config2.txt") + + with open(config1, "w") as f: + f.write("test1") + + with open(config2, "w") as f: + f.write("test2") + + response = await controller_api.get("/templates/base-configs") + + assert response.status == 200 + + filenames = [item["filename"] for item in response.json] + + assert "config1.txt" in filenames + assert "config2.txt" in filenames \ No newline at end of file