Merge pull request #2819 from GNS3/release/v2.2.60

Release v2.2.60
This commit is contained in:
Jeremy Grossmann 2026-07-15 20:44:04 +02:00 committed by GitHub
commit c1e845cb22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 652 additions and 593 deletions

View File

@ -10,7 +10,7 @@ jobs:
name: Add issue to project name: Add issue to project
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/add-to-project@v1.0.1 - uses: actions/add-to-project@v2
with: with:
project-url: https://github.com/orgs/GNS3/projects/3 project-url: https://github.com/orgs/GNS3/projects/3
github-token: ${{ secrets.ADD_NEW_ISSUES_TO_PROJECT }} github-token: ${{ secrets.ADD_NEW_ISSUES_TO_PROJECT }}

View File

@ -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 # 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: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v4
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@ -88,6 +88,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v4
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@ -15,7 +15,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Check for stable release - name: Check for stable release
id: ver id: ver
@ -34,7 +34,7 @@ jobs:
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: steps.ver.outputs.stable == 'true' if: steps.ver.outputs.stable == 'true'
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -42,7 +42,7 @@ jobs:
- name: Login to Docker Hub - name: Login to Docker Hub
if: steps.ver.outputs.stable == 'true' if: steps.ver.outputs.stable == 'true'
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}

View File

@ -12,11 +12,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
ref: "gh-pages" ref: "gh-pages"
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: 3.8 python-version: 3.8
- name: Merge changes from 3.0 branch - name: Merge changes from 3.0 branch

View File

@ -21,9 +21,9 @@ jobs:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies

View File

@ -1,5 +1,19 @@
# Change Log # 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 ## 2.2.59 08/05/2026
* Sync appliances * Sync appliances

View File

@ -103,6 +103,8 @@ enable_hardware_acceleration = True
require_hardware_acceleration = False require_hardware_acceleration = False
; Allow unsafe additional command line options ; Allow unsafe additional command line options
allow_unsafe_options = False allow_unsafe_options = False
; Path to the OVMF firmware directory
ovmf_firmware_dir = "/usr/share/OVMF"
[VMware] [VMware]
; First vmnet interface of the range that can be managed by the GNS3 server ; First vmnet interface of the range that can be managed by the GNS3 server

View File

@ -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"
}

View File

@ -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"
}
]

View File

@ -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"
}

View File

@ -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
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td>filename</td>
<td>string</td>
<td>Name of the base configuration file</td>
</tr>
</table>
Sample session
***************
.. literalinclude:: ../../../examples/controller_get_templatesbaseconfigs.txt

View File

@ -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
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td>filename</td>
<td>string</td>
<td>Name of the base configuration file</td>
</tr>
</table>
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
<table>
<tr>
<th>Name</th>
<th>Mandatory</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr><td>content</td><td>&#10004;</td><td>string</td><td>New file content</td></tr>
</table>
Output
*******
.. raw:: html
<table>
<tr>
<th>Name</th>
<th>Mandatory</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr><td>template_id</td><td>&#10004;</td><td>string</td><td>Template UUID</td></tr>
<tr><td>filename</td><td>&#10004;</td><td>string</td><td>Base configuration filename</td></tr>
<tr><td>content</td><td>&#10004;</td><td>string</td><td>Updated file content</td></tr>
</table>
Sample session
***************
.. literalinclude:: ../../../examples/controller_put_templateidbaseconfig.txt

View File

@ -1,41 +1,56 @@
{ {
"appliance_id": "088df570-f637-46f5-8a68-85acde538e5e", "appliance_id": "8fecbf89-5cd1-4aea-b735-5f36cf0efbb7",
"name": "BIRD", "name": "BIRD",
"category": "router", "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.", "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_name": "CZ.NIC Labs",
"vendor_url": "http://bird.network.cz/", "vendor_url": "https://bird.network.cz",
"documentation_url": "http://bird.network.cz/?get_doc&f=bird.html", "documentation_url": "https://bird.network.cz/?get_doc&f=bird.html&v=20",
"product_name": "BIRD internet routing daemon", "product_name": "BIRD internet routing daemon",
"registry_version": 4, "registry_version": 4,
"status": "stable", "status": "stable",
"maintainer": "GNS3 Team", "maintainer": "Bernhard Ehlers",
"maintainer_email": "developers@gns3.net", "maintainer_email": "dev-ehlers@mailbox.org",
"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", "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": { "qemu": {
"adapter_type": "e1000", "adapter_type": "virtio-net-pci",
"adapters": 4, "adapters": 4,
"ram": 128, "ram": 512,
"hda_disk_interface": "ide", "hda_disk_interface": "scsi",
"arch": "x86_64", "arch": "x86_64",
"console_type": "telnet", "console_type": "telnet",
"kvm": "allow" "kvm": "allow"
}, },
"images": [ "images": [
{ {
"filename": "bird-tinycore64-1.5.0.img", "filename": "bird2-debian-2.14.qcow2",
"version": "1.5.0", "version": "2.14",
"md5sum": "08d50ba2b1b262e2e03e4babf90abf69", "md5sum": "029cf1756201ee79497c169502b08b88",
"filesize": 22413312, "filesize": 303717376,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/", "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": [ "versions": [
{ {
"name": "1.5.0", "name": "2.14",
"images": { "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"
} }
} }
] ]

View File

@ -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"
}
}
]
}

View File

@ -51,250 +51,12 @@
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
}, },
{ {
"filename": "FMG_VM64_KVM-v7.4.3-build2487-FORTINET.out.kvm.qcow2", "filename": "empty500G.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",
"version": "1.0", "version": "1.0",
"md5sum": "3411a599e822f2ac6be560a26405821a", "md5sum": "658c825441b9b3080ba00f9eec002eaa",
"filesize": 197120, "filesize": 204608,
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/", "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": [ "versions": [
@ -302,259 +64,21 @@
"name": "7.4.6", "name": "7.4.6",
"images": { "images": {
"hda_disk_image": "FMG_VM64_KVM-v7.4.6.M-build2588-FORTINET.out.kvm.qcow2", "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", "name": "7.4.5",
"images": { "images": {
"hda_disk_image": "FMG_VM64_KVM-v7.4.5.M-build2553-FORTINET.out.kvm.qcow2", "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", "name": "7.4.4",
"images": { "images": {
"hda_disk_image": "FMG_VM64_KVM-v7.4.4.F-build2550-FORTINET.out.kvm.qcow2", "hda_disk_image": "FMG_VM64_KVM-v7.4.4.F-build2550-FORTINET.out.kvm.qcow2",
"hdb_disk_image": "empty30G.qcow2" "hdb_disk_image": "empty500G.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"
} }
} }
] ]

View File

@ -132,9 +132,37 @@
"md5sum": "24cd1006734993dab338e5c75f80b875", "md5sum": "24cd1006734993dab338e5c75f80b875",
"version": "26.03.0", "version": "26.03.0",
"direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.03.0/infix-x86_64-v26.03.0.qcow2" "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": [ "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", "name": "26.03.0",
"images": { "images": {

View File

@ -14,7 +14,7 @@
"symbol": "linux_guest.svg", "symbol": "linux_guest.svg",
"docker": { "docker": {
"adapters": 1, "adapters": 1,
"image": "gns3/ubuntu:noble", "image": "gns3/ubuntu:resolute",
"console_type": "telnet" "console_type": "telnet"
} }
} }

View File

@ -38,6 +38,13 @@
"filesize": 17825792, "filesize": 17825792,
"direct_download_url": "https://dropzone.westermo.com/file.aspx?id=e6a7676d-85a7-4374-8961-c68aacb74921" "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", "filename": "WeOS-zero-5.28.0.disk",
"version": "5.28.0", "version": "5.28.0",
@ -54,6 +61,13 @@
} }
], ],
"versions": [ "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", "name": "5.28.0",
"images": { "images": {

View File

@ -33,7 +33,7 @@ from gns3server.config import Config
from gns3server.utils.asyncio import locking from gns3server.utils.asyncio import locking
from gns3server.compute.base_manager import BaseManager from gns3server.compute.base_manager import BaseManager
from gns3server.compute.docker.docker_vm import DockerVM 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__) log = logging.getLogger(__name__)
@ -236,6 +236,8 @@ class Docker(BaseManager):
raise DockerHttp304Error("Docker has returned an error: {} {}".format(response.status, body)) raise DockerHttp304Error("Docker has returned an error: {} {}".format(response.status, body))
elif response.status == 404: elif response.status == 404:
raise DockerHttp404Error("Docker has returned an error: {} {}".format(response.status, body)) 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: else:
raise DockerError("Docker has returned an error: {} {}".format(response.status, body)) raise DockerError("Docker has returned an error: {} {}".format(response.status, body))
return response return response

View File

@ -32,3 +32,7 @@ class DockerHttp304Error(DockerError):
class DockerHttp404Error(DockerError): class DockerHttp404Error(DockerError):
pass pass
class DockerHttp409Error(DockerError):
pass

View File

@ -43,7 +43,8 @@ from ..nios.nio_udp import NIOUDP
from .docker_error import ( from .docker_error import (
DockerError, DockerError,
DockerHttp304Error, DockerHttp304Error,
DockerHttp404Error DockerHttp404Error,
DockerHttp409Error
) )
import logging import logging
@ -459,10 +460,31 @@ class DockerVM(BaseNode):
if extra_hosts: if extra_hosts:
params["Env"].append("GNS3_EXTRA_HOSTS={}".format(extra_hosts)) params["Env"].append("GNS3_EXTRA_HOSTS={}".format(extra_hosts))
# Support name in Doker: [a-zA-Z0-9][a-zA-Z0-9_.-] try:
result = await self.manager.query("POST", "containers/create?name={}".format(self.docker_name), data=params) # Supported names in Docker: [a-zA-Z0-9][a-zA-Z0-9_.-]
self._cid = result['Id'] result = await self.manager.query("POST", f"containers/create?name={self.docker_name}", data=params)
log.info("Docker container '{name}' [{id}] created".format(name=self._name, id=self._id)) 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 return True
def _format_env(self, variables, env): def _format_env(self, variables, env):

View File

@ -35,6 +35,7 @@ import time
import json import json
import psutil import psutil
from pathlib import Path
from gns3server.utils import parse_version, shlex_quote from gns3server.utils import parse_version, shlex_quote
from gns3server.utils.asyncio import subprocess_check_output, cancellable_wait_run_in_executor from gns3server.utils.asyncio import subprocess_check_output, cancellable_wait_run_in_executor
from .qemu_error import QemuError from .qemu_error import QemuError
@ -2057,15 +2058,23 @@ class QemuVM(BaseNode):
options.extend(["-bios", self._bios_image.replace(",", ",,")]) options.extend(["-bios", self._bios_image.replace(",", ",,")])
elif self._uefi: 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") old_ovmf_vars_path = os.path.join(self.working_dir, "OVMF_VARS.fd")
if os.path.exists(old_ovmf_vars_path): 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 # 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") ovmf_firmware_path = self.manager.get_abs_image_path("OVMF_CODE.fd")
else: else:
system_ovmf_firmware_path = "/usr/share/OVMF/OVMF_CODE_4M.fd" # Use a manual case-insensitive search instead
if os.path.exists(system_ovmf_firmware_path): try:
ovmf_firmware_path = system_ovmf_firmware_path 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: else:
# otherwise, get the UEFI firmware from the images directory # otherwise, get the UEFI firmware from the images directory
ovmf_firmware_path = self.manager.get_abs_image_path("OVMF_CODE_4M.fd") 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)]) options.extend(["-drive", "if=pflash,format=raw,readonly,file={}".format(ovmf_firmware_path)])
# try to use the UEFI variables store from the system first # try to use the UEFI variables store from the system first
system_ovmf_vars_path = "/usr/share/OVMF/OVMF_VARS_4M.fd" try:
if os.path.exists(system_ovmf_vars_path): system_ovmf_vars_path = next((f for f in system_ovmf_firmware_dir.glob("*.fd")
ovmf_vars_path = system_ovmf_vars_path 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: else:
# otherwise, get the UEFI variables store from the images directory # otherwise, get the UEFI variables store from the images directory
ovmf_vars_path = self.manager.get_abs_image_path("OVMF_VARS_4M.fd") ovmf_vars_path = self.manager.get_abs_image_path("OVMF_VARS_4M.fd")
@ -2092,6 +2105,10 @@ class QemuVM(BaseNode):
except OSError as e: except OSError as e:
raise QemuError("Cannot copy OVMF_VARS_4M.fd file to the node working directory: {}".format(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)]) 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 return options
def _linux_boot_options(self): def _linux_boot_options(self):

View File

@ -562,7 +562,15 @@ class Controller:
for project in self._projects.values(): for project in self._projects.values():
if project.auto_open: 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): def get_free_project_name(self, base_name):
""" """

View File

@ -271,7 +271,7 @@ async def _upload_file(compute, project_id, file_path, path):
async def _import_images(controller, images_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() 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 (dirpath, dirnames, filenames) in os.walk(root, followlinks=False):
for filename in filenames: for filename in filenames:
path = os.path.join(dirpath, filename) path = os.path.join(dirpath, filename)
if os.path.islink(path):
continue
dst = os.path.join(image_dir, os.path.relpath(path, root)) dst = os.path.join(image_dir, os.path.relpath(path, root))
os.makedirs(os.path.dirname(dst), exist_ok=True) 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): async def update_snapshots(snapshots_dir, project_path, project_name, project_id, reset_mac_addresses=True):
""" """

View File

@ -905,12 +905,23 @@ class Project:
async def delete(self): 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": if self._status != "opened":
try: try:
await self.open() await self.open()
except aiohttp.web.HTTPConflict as e: except aiohttp.web.HTTPConflict as e:
# ignore missing images or other conflicts when deleting a project # 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.delete_on_computes()
await self.close() await self.close()
try: try:
@ -922,6 +933,42 @@ class Project:
raise aiohttp.web.HTTPConflict(text="Cannot delete project directory {}: {}".format(self.path, str(e))) raise aiohttp.web.HTTPConflict(text="Cannot delete project directory {}: {}".format(self.path, str(e)))
self.emit_controller_notification("project.deleted", self.__json__()) 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): async def delete_on_computes(self):
""" """
Delete the project on computes but not on controller Delete the project on computes but not on controller

View File

@ -57,7 +57,7 @@ class CrashReport:
Report crash to a third party service 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 _instance = None
def __init__(self): def __init__(self):

View File

@ -22,6 +22,7 @@ from gns3server.schemas.template import TEMPLATE_USAGE_SCHEMA
import hashlib import hashlib
import json import json
import os
from gns3server.schemas.template import ( from gns3server.schemas.template import (
TEMPLATE_OBJECT_SCHEMA, TEMPLATE_OBJECT_SCHEMA,
@ -173,3 +174,131 @@ class TemplateHandler:
compute_id=request.json.get("compute_id")) compute_id=request.json.get("compute_id"))
response.set_status(201) response.set_status(201)
response.json(node) 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)

View File

@ -23,8 +23,8 @@
# or negative for a release candidate or beta (after the base version # or negative for a release candidate or beta (after the base version
# number has been incremented) # number has been incremented)
__version__ = "2.2.60.dev1" __version__ = "2.2.60"
__version_info__ = (2, 2, 60, 99) __version_info__ = (2, 2, 60, 0)
if "dev" in __version__: if "dev" in __version__:
try: try:

View File

@ -14,8 +14,7 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import uuid import uuid
from tests.utils import asyncio_patch 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) 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.route == "/projects/{project_id}/templates/{template_id}"
assert response.status == 201 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