Compare commits

..

No commits in common. "master" and "v2.2.59" have entirely different histories.

37 changed files with 647 additions and 1004 deletions

View File

@ -1,57 +0,0 @@
# Version control
.git
.gitignore
.gitattributes
# CI / GitHub / Docker
.github
.whitesource
.dockerignore
# Editor / IDE
.idea
.vscode
.settings
.project
.pydevproject
.mr.developer.cfg
# Claude
.claude
# Python build artifacts
__pycache__
*.py[cod]
*.so
*.egg
*.egg-info
build/
dist/
eggs/
parts/
var/
sdist/
develop-eggs/
.installed.cfg
lib/
lib64/
.ropeproject
# Test & coverage
tests/
pytest.ini
.coveragerc
.coverage
.coverage*
.tox
.cache
.pytest_cache
nosetests.xml
# Virtualenv
env/
venv/
.venv/
# Editor backup files
*~

View File

@ -1,40 +0,0 @@
name: Bug report
description: Report a bug so we can fix it.
title: "[Bug]: "
labels: ["bug"]
body:
- type: textarea
id: what-happened
attributes:
label: What happened?
description: A clear description of the bug.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: How can we reproduce this? Numbered steps if possible.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What did you expect to happen instead?
- type: input
id: version
attributes:
label: Version / commit
description: Which version or commit hash are you on?
- type: textarea
id: environment
attributes:
label: Environment
description: OS, runtime version, anything else that might be relevant.
- type: textarea
id: logs
attributes:
label: Relevant logs
description: Paste any relevant log output. This is automatically rendered as code.
render: shell

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@v2 - uses: actions/add-to-project@v1.0.1
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@v7 uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v4 uses: github/codeql-action/init@v3
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@v4 uses: github/codeql-action/analyze@v3
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@ -10,59 +10,31 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
DOCKERHUB_ORG: ${{ vars.DOCKERHUB_ORG || 'gns3' }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v7 uses: actions/checkout@v4
- name: Check for stable release
id: ver
run: |
TAG="${GITHUB_REF_NAME#v}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
if echo "$TAG" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "stable=true" >> $GITHUB_OUTPUT
else
echo "stable=false" >> $GITHUB_OUTPUT
fi
- name: Set lowercase image name vars
if: steps.ver.outputs.stable == 'true'
id: names
run: |
echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
echo "repo=$(echo '${{ github.event.repository.name }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
if: steps.ver.outputs.stable == 'true'
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
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 }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub - name: Login to Docker Hub
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 }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push to GitHub Container Registry
if: steps.ver.outputs.stable == 'true' run: |
uses: docker/build-push-action@v6 docker build -t ghcr.io/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]'):latest .
with: docker push ghcr.io/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]'):latest
context: .
push: true - name: Build and push to Docker Hub
tags: | run: |
${{ env.DOCKERHUB_ORG }}/${{ github.event.repository.name }}:${{ steps.ver.outputs.tag }} docker build -t gns3/${{ github.event.repository.name }}:latest .
${{ env.DOCKERHUB_ORG }}/${{ github.event.repository.name }}:latest docker push gns3/${{ github.event.repository.name }}:latest
ghcr.io/${{ steps.names.outputs.owner }}/${{ steps.names.outputs.repo }}:${{ steps.ver.outputs.tag }}
ghcr.io/${{ steps.names.outputs.owner }}/${{ steps.names.outputs.repo }}:latest

View File

@ -12,11 +12,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
ref: "gh-pages" ref: "gh-pages"
- uses: actions/setup-python@v6 - uses: actions/setup-python@v5
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@v7 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies

View File

@ -1,28 +1,5 @@
# Change Log # Change Log
## 2.2.61 30/07/2026
* Sync appliances
* fix(import): unvalidated symlink creation in import_project
* fix(qemu): fix addition of QEMU RNG device causes interface names to change
* fix(qemu): remove trailing space from RNG object argument
* fix: do not start nodes when deleting a project
* fix: correct always-true state check in DockerVM.stop()
## 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,8 +103,6 @@ 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

@ -1,17 +0,0 @@
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

@ -1,44 +0,0 @@
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

@ -1,17 +0,0 @@
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

@ -1,38 +0,0 @@
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

@ -1,90 +0,0 @@
/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,59 +0,0 @@
{
"appliance_id": "b3b90fde-143a-4129-8031-ccbba73c5e02",
"name": "armbian",
"category": "guest",
"description": "A highly optimized base operating system specialized for single board computers (SBCs) and its extensive build framework.",
"vendor_name": "The Armbian team",
"vendor_url": "https://armbian.com/",
"documentation_url": "https://docs.armbian.com/",
"product_name": "Armbian UEFI x86",
"product_url": "https://armbian.com/boards/uefi-x86",
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"usage": "By first login you create root password and new sudo user.\n\nBoot disk from UEFI shell, type: FS0:EFI\\BOOT\\BOOTX64 and press <Enter>",
"port_name_format": "Ethernet{0}",
"qemu": {
"adapter_type": "virtio-net-pci",
"adapters": 2,
"ram": 256,
"hda_disk_interface": "virtio",
"arch": "x86_64",
"console_type": "spice+agent",
"uefi": false,
"boot_priority": "c",
"kvm": "require",
"options": "-nographic"
},
"images": [
{
"filename": "OVMF-edk2-stable202305.fd",
"version": "stable202305",
"md5sum": "6c4cf1519fec4a4b95525d9ae562963a",
"filesize": 4194304,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/OVMF-edk2-stable202305.fd.zip/download",
"compression": "zip"
},
{
"filename": "Armbian_26.5.1_Uefi-x86_trixie_cloud_6.18.32_minimal.img.qcow2",
"version": "Armbian 26.5.1 Minimal (CLI)",
"md5sum": "7f4c915668718d6135406de5a6c4fc30",
"filesize": 877920512,
"download_url": "https://armbian.com/boards/uefi-x86",
"direct_download_url": "https://armbian.atomonetworks.com/dl/uefi-x86/archive/Armbian_26.5.1_Uefi-x86_trixie_cloud_6.18.32_minimal.img.qcow2.xz",
"compression": "xz"
}
],
"versions": [
{
"name": "Armbian 26.5.1 Minimal (CLI)",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "Armbian_26.5.1_Uefi-x86_trixie_cloud_6.18.32_minimal.img.qcow2"
}
}
]
}

View File

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

View File

@ -0,0 +1,57 @@
{
"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,12 +51,250 @@
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
}, },
{ {
"filename": "empty500G.qcow2", "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",
"version": "1.0", "version": "1.0",
"md5sum": "658c825441b9b3080ba00f9eec002eaa", "md5sum": "3411a599e822f2ac6be560a26405821a",
"filesize": 204608, "filesize": 197120,
"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/empty500G.qcow2/download" "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty30G.qcow2/download"
} }
], ],
"versions": [ "versions": [
@ -64,21 +302,259 @@
"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": "empty500G.qcow2" "hdb_disk_image": "empty30G.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": "empty500G.qcow2" "hdb_disk_image": "empty30G.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": "empty500G.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"
} }
} }
] ]

View File

@ -132,37 +132,9 @@
"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:resolute", "image": "gns3/ubuntu:noble",
"console_type": "telnet" "console_type": "telnet"
} }
} }

View File

@ -38,13 +38,6 @@
"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",
@ -61,13 +54,6 @@
} }
], ],
"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

@ -311,7 +311,7 @@ class Cloud(BaseNode):
if not port_info["interface"] in network_interfaces: if not port_info["interface"] in network_interfaces:
raise NodeError("Interface '{}' could not be found on this system, please update '{}'".format(port_info["interface"], self.name)) raise NodeError("Interface '{}' could not be found on this system, please update '{}'".format(port_info["interface"], self.name))
if sys.platform.startswith("linux") or sys.platform.startswith("openbsd"): if sys.platform.startswith("linux"):
await self._add_linux_ethernet(port_info, bridge_name) await self._add_linux_ethernet(port_info, bridge_name)
elif sys.platform.startswith("darwin"): elif sys.platform.startswith("darwin"):
await self._add_osx_ethernet(port_info, bridge_name) await self._add_osx_ethernet(port_info, bridge_name)

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, DockerHttp409Error from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Error, DockerHttp404Error
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -236,8 +236,6 @@ 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,7 +32,3 @@ class DockerHttp304Error(DockerError):
class DockerHttp404Error(DockerError): class DockerHttp404Error(DockerError):
pass pass
class DockerHttp409Error(DockerError):
pass

View File

@ -43,8 +43,7 @@ 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
@ -460,31 +459,10 @@ 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))
try: # Support name in Doker: [a-zA-Z0-9][a-zA-Z0-9_.-]
# Supported names in Docker: [a-zA-Z0-9][a-zA-Z0-9_.-] result = await self.manager.query("POST", "containers/create?name={}".format(self.docker_name), data=params)
result = await self.manager.query("POST", f"containers/create?name={self.docker_name}", data=params) self._cid = result['Id']
except DockerHttp409Error: log.info("Docker container '{name}' [{id}] created".format(name=self._name, id=self._id))
# 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):
@ -869,7 +847,7 @@ class DockerVM(BaseNode):
await self._fix_permissions() await self._fix_permissions()
state = await self._get_container_state() state = await self._get_container_state()
if state != "stopped" and state != "exited": if state != "stopped" or state != "exited":
# t=5 number of seconds to wait before killing the container # t=5 number of seconds to wait before killing the container
try: try:
await self.manager.query("POST", "containers/{}/stop".format(self._cid), params={"t": 5}) await self.manager.query("POST", "containers/{}/stop".format(self._cid), params={"t": 5})

View File

@ -35,7 +35,6 @@ 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
@ -2058,23 +2057,15 @@ 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:
# Use a manual case-insensitive search instead system_ovmf_firmware_path = "/usr/share/OVMF/OVMF_CODE_4M.fd"
try: if os.path.exists(system_ovmf_firmware_path):
system_ovmf_firmware_path = next((f for f in system_ovmf_firmware_dir.glob("*.fd") ovmf_firmware_path = system_ovmf_firmware_path
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")
@ -2083,13 +2074,9 @@ 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
try: system_ovmf_vars_path = "/usr/share/OVMF/OVMF_VARS_4M.fd"
system_ovmf_vars_path = next((f for f in system_ovmf_firmware_dir.glob("*.fd") if os.path.exists(system_ovmf_vars_path):
if f.name.lower() == "ovmf_vars_4m.fd"), None) ovmf_vars_path = system_ovmf_vars_path
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")
@ -2105,10 +2092,6 @@ 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):
@ -2480,6 +2463,7 @@ class QemuVM(BaseNode):
elif sys.platform.startswith("win") or sys.platform.startswith("darwin"): elif sys.platform.startswith("win") or sys.platform.startswith("darwin"):
command.extend(["-enable-hax"]) command.extend(["-enable-hax"])
command.extend(["-boot", "order={}".format(self._boot_priority)]) command.extend(["-boot", "order={}".format(self._boot_priority)])
command.extend(self._bios_option())
command.extend(self._cdrom_option()) command.extend(self._cdrom_option())
command.extend((await self._disk_options())) command.extend((await self._disk_options()))
command.extend(self._linux_boot_options()) command.extend(self._linux_boot_options())
@ -2497,8 +2481,6 @@ class QemuVM(BaseNode):
raise QemuError("Console type {} is unknown".format(self._console_type)) raise QemuError("Console type {} is unknown".format(self._console_type))
command.extend(self._monitor_options()) command.extend(self._monitor_options())
command.extend((await self._network_options())) command.extend((await self._network_options()))
# bios options must be last to have predictable NIC numbering, see https://github.com/GNS3/gns3-server/issues/2838
command.extend(self._bios_option())
if self.on_close != "save_vm_state": if self.on_close != "save_vm_state":
await self._clear_save_vm_stated() await self._clear_save_vm_stated()
else: else:

View File

@ -562,15 +562,7 @@ class Controller:
for project in self._projects.values(): for project in self._projects.values():
if project.auto_open: if project.auto_open:
try: await project.open()
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

@ -166,52 +166,24 @@ async def import_project(
project = await controller.load_project(dot_gns3_path, load=False) project = await controller.load_project(dot_gns3_path, load=False)
return project return project
def _create_symbolic_links(zip_file, path): def _create_symbolic_links(zip_file, path):
""" """
Manually create symbolic links (if any) because ZipFile does not support it. Manually create symbolic links (if any) because ZipFile does not support it.
Refuse any target that escapes `path`.
:param zip_file: ZipFile instance :param zip_file: ZipFile instance
:param path: project location :param path: project location
""" """
path_root = os.path.realpath(path) + os.sep
for zip_info in zip_file.infolist(): for zip_info in zip_file.infolist():
if not stat.S_ISLNK(zip_info.external_attr >> 16): if stat.S_ISLNK(zip_info.external_attr >> 16):
continue symlink_target = zip_file.read(zip_info.filename).decode()
symlink_target = zip_file.read(zip_info.filename).decode() symlink_path = os.path.join(path, zip_info.filename)
symlink_path = os.path.join(path, zip_info.filename) try:
# remove the regular file and replace it by a symbolic link
# 1. Reject absolute targets outright. os.remove(symlink_path)
if os.path.isabs(symlink_target): os.symlink(symlink_target, symlink_path)
raise aiohttp.web.HTTPConflict( except OSError as e:
text=f"Symlink {zip_info.filename!r} has absolute target {symlink_target!r}, refusing" raise aiohttp.web.HTTPConflict(text=f"Cannot create symbolic link: {e}")
)
# 2. Reject paths where the entry name itself escapes (defence in depth;
# extractall normally would already have caught this).
member_abs = os.path.realpath(symlink_path)
if not (member_abs + os.sep).startswith(path_root) and member_abs + os.sep != path_root:
raise aiohttp.web.HTTPConflict(
text=f"Symlink entry {zip_info.filename!r} escapes project dir, refusing"
)
# 3. Resolve the symlink target relative to the entry's own parent
# directory and verify the resolved real path stays inside `path`.
link_dir = os.path.realpath(os.path.dirname(symlink_path))
resolved_target = os.path.realpath(os.path.join(link_dir, symlink_target))
if not (resolved_target + os.sep).startswith(path_root) and resolved_target + os.sep != path_root:
raise aiohttp.web.HTTPConflict(
text=f"Symlink {zip_info.filename!r} -> {symlink_target!r} escapes project dir, refusing"
)
try:
os.remove(symlink_path)
os.symlink(symlink_target, symlink_path)
except OSError as e:
raise aiohttp.web.HTTPConflict(text=f"Cannot create symbolic link: {e}")
def regenerate_topology_ids(topology, new_project_path, reset_mac_addresses=False): def regenerate_topology_ids(topology, new_project_path, reset_mac_addresses=False):
""" """
@ -299,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 exist. Copy images to the images directory or delete them if they already exists.
""" """
image_dir = controller.images_path() image_dir = controller.images_path()
@ -307,19 +279,12 @@ 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)
if not os.path.exists(dst): await wait_run_in_executor(shutil.move, path, 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,23 +905,12 @@ 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(auto_start=False) 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(f"Conflict while deleting project: {e}") log.warning("Conflict while deleting project: {}".format(e.text))
await self.delete_on_computes() await self.delete_on_computes()
await self.close() await self.close()
try: try:
@ -933,42 +922,6 @@ 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
@ -998,12 +951,9 @@ class Project:
return os.path.join(self.path, self._filename) return os.path.join(self.path, self._filename)
@locking @locking
async def open(self, auto_start=True): async def open(self):
""" """
Load topology elements Load topology elements
:param auto_start: whether the nodes may be started when the project
has auto start enabled
""" """
if self._closing: if self._closing:
@ -1117,7 +1067,7 @@ class Project:
self._loading = False self._loading = False
self.emit_controller_notification("project.opened", self.__json__()) self.emit_controller_notification("project.opened", self.__json__())
# Should we start the nodes when project is open # Should we start the nodes when project is open
if self._auto_start and auto_start: if self._auto_start:
# Start all in the background without waiting for completion # Start all in the background without waiting for completion
# we ignore errors because we want to let the user open # we ignore errors because we want to let the user open
# their project and fix it # their project and fix it

View File

@ -57,7 +57,7 @@ class CrashReport:
Report crash to a third party service Report crash to a third party service
""" """
DSN = "https://89ba0bf8d773c2c95518bfb696d6d86f@o19455.ingest.us.sentry.io/38482" DSN = "https://646d66c1f0b65d59af9c4577a68f3f87@o19455.ingest.us.sentry.io/38482"
_instance = None _instance = None
def __init__(self): def __init__(self):

View File

@ -22,7 +22,6 @@ 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,
@ -174,131 +173,3 @@ 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

@ -147,7 +147,7 @@ def is_interface_up(interface):
:returns: boolean :returns: boolean
""" """
if sys.platform.startswith("linux") or sys.platform.startswith("openbsd"): if sys.platform.startswith("linux"):
if interface not in psutil.net_if_addrs(): if interface not in psutil.net_if_addrs():
return False return False
@ -234,7 +234,7 @@ def interfaces():
result["special"] = False result["special"] = False
for special_interface in ("lo", "vmnet", "vboxnet", "docker", "lxcbr", for special_interface in ("lo", "vmnet", "vboxnet", "docker", "lxcbr",
"virbr", "ovs-system", "veth", "fw", "p2p", "virbr", "ovs-system", "veth", "fw", "p2p",
"bridge", "vmware", "virtualbox", "gns3","veb"): "bridge", "vmware", "virtualbox", "gns3"):
if result["name"].lower().startswith(special_interface): if result["name"].lower().startswith(special_interface):
result["special"] = True result["special"] = True
for special_interface in ("-nic"): for special_interface in ("-nic"):

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.61" __version__ = "2.2.59"
__version_info__ = (2, 2, 61, 0) __version_info__ = (2, 2, 59, 0)
if "dev" in __version__: if "dev" in __version__:
try: try:

View File

@ -1503,19 +1503,3 @@ async def test_read_console_output_with_binary_mode(vm):
with asyncio_patch('gns3server.compute.docker.docker_vm.DockerVM.stop'): with asyncio_patch('gns3server.compute.docker.docker_vm.DockerVM.stop'):
await vm._read_console_output(input_stream, output_stream) await vm._read_console_output(input_stream, output_stream)
output_stream.feed_data.assert_called_once_with(b"test") output_stream.feed_data.assert_called_once_with(b"test")
async def test_stop_exited_container_no_stop_query(vm):
vm._ubridge_hypervisor = None
vm._fix_permissions = MagicMock()
with asyncio_patch("gns3server.compute.docker.DockerVM._get_container_state", return_value="exited"):
with asyncio_patch("gns3server.compute.docker.Docker.query") as mock_query:
vm._permissions_fixed = False
await vm.stop()
assert not any(
call.args[:2] == ("POST", "containers/e90e34656842/stop")
for call in mock_query.mock_calls
)
assert vm.status == "stopped"

View File

@ -19,8 +19,6 @@ import os
import uuid import uuid
import json import json
import zipfile import zipfile
import pytest
import aiohttp
from tests.utils import asyncio_patch, AsyncioMagicMock from tests.utils import asyncio_patch, AsyncioMagicMock
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
@ -119,48 +117,36 @@ async def write_file(path, z):
f.write(chunk) f.write(chunk)
@pytest.fixture async def test_import_project_containing_symlink(tmpdir, controller):
def export_project_with_symlink(tmpdir, controller):
async def _export(symlink_target):
project = Project(controller=controller, name="test")
project.dump = MagicMock()
topology = { project = Project(controller=controller, name="test")
"project_id": str(uuid.uuid4()), project.dump = MagicMock()
"name": "test", path = project.path
"auto_open": True,
"auto_start": True,
"topology": {
},
"version": "2.0.0"
}
with open(os.path.join(project.path, "project.gns3"), 'w+') as f:
json.dump(topology, f)
os.makedirs(os.path.join(project.path, "vm1", "dynamips"))
symlink_path = os.path.join(project.path, "vm1", "dynamips", "symlink")
os.symlink(symlink_target, symlink_path)
zip_path = str(tmpdir / "project.zip")
with aiozipstream.ZipFile() as z:
with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),):
await export_project(z, project, str(tmpdir), include_images=False)
await write_file(zip_path, z)
return zip_path
return _export
async def test_import_project_containing_symlink(controller, export_project_with_symlink):
"""
Test importing a project containing a valid symlink (target inside the project directory).
"""
project_id = str(uuid.uuid4()) project_id = str(uuid.uuid4())
symlink_target = "../symlink_target" topology = {
zip_path = await export_project_with_symlink(symlink_target) "project_id": str(uuid.uuid4()),
"name": "test",
"auto_open": True,
"auto_start": True,
"topology": {
},
"version": "2.0.0"
}
with open(os.path.join(path, "project.gns3"), 'w+') as f:
json.dump(topology, f)
os.makedirs(os.path.join(path, "vm1", "dynamips"))
symlink_path = os.path.join(project.path, "vm1", "dynamips", "symlink")
symlink_target = "/tmp/anywhere"
os.symlink(symlink_target, symlink_path)
zip_path = str(tmpdir / "project.zip")
with aiozipstream.ZipFile() as z:
with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),):
await export_project(z, project, str(tmpdir), include_images=False)
await write_file(zip_path, z)
with open(zip_path, "rb") as f: with open(zip_path, "rb") as f:
project = await import_project(controller, project_id, f) project = await import_project(controller, project_id, f)
@ -172,36 +158,6 @@ async def test_import_project_containing_symlink(controller, export_project_with
assert os.readlink(symlink_path) == symlink_target assert os.readlink(symlink_path) == symlink_target
async def test_import_project_containing_absolute_symlink(controller, export_project_with_symlink):
"""
Test importing a project containing an absolute symlink.
This should fail because absolute symlinks are not allowed for security reasons.
"""
project_id = str(uuid.uuid4())
symlink_target = "/tmp/anywhere"
zip_path = await export_project_with_symlink(symlink_target)
with pytest.raises(aiohttp.web.HTTPConflict):
with open(zip_path, "rb") as f:
await import_project(controller, project_id, f)
async def test_import_project_containing_escaping_symlink(controller, export_project_with_symlink):
"""
Test importing a project containing a symlink that escapes the project directory.
This should fail because symlinks that escape the project directory are not allowed for security reasons.
"""
project_id = str(uuid.uuid4())
symlink_target = "../../../../symlink_target"
zip_path = await export_project_with_symlink(symlink_target)
with pytest.raises(aiohttp.web.HTTPConflict):
with open(zip_path, "rb") as f:
await import_project(controller, project_id, f)
async def test_import_upgrade(tmpdir, controller): async def test_import_upgrade(tmpdir, controller):
""" """
Topology made for previous GNS3 version are upgraded during the process Topology made for previous GNS3 version are upgraded during the process

View File

@ -655,19 +655,6 @@ async def test_delete(project):
assert not os.path.exists(project.path) assert not os.path.exists(project.path)
async def test_delete_does_not_start_nodes(project):
"""
Deleting a project must not start its nodes, even when auto_start is enabled.
"""
project.auto_start = True
project.dump()
await project.close()
project.start_all = AsyncioMagicMock()
await project.delete()
assert not project.start_all.called
async def test_dump(projects_dir): async def test_dump(projects_dir):
directory = projects_dir directory = projects_dir

View File

@ -14,7 +14,8 @@
# #
# 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
@ -954,81 +955,3 @@ 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