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
+
+
+
+ | Name |
+ Type |
+ Description |
+
+
+
+ | filename |
+ string |
+ Name 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
+
+
+
+ | Name |
+ Type |
+ Description |
+
+
+
+ | filename |
+ string |
+ Name 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
+
+
+
+ | Name |
+ Mandatory |
+ Type |
+ Description |
+
+
+ | content | ✔ | string | New file content |
+
+
+
+Output
+*******
+
+.. raw:: html
+
+
+
+ | Name |
+ Mandatory |
+ Type |
+ Description |
+
+
+ | template_id | ✔ | string | Template UUID |
+ | filename | ✔ | string | Base configuration filename |
+ | content | ✔ | string | Updated 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