mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-29 05:20:14 +03:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3e85b7041 | ||
|
|
80da7da515 | ||
|
|
d2f73cae28 | ||
|
|
da2942c139 | ||
|
|
5485eef3ad | ||
|
|
ccf4b3f9db | ||
|
|
b797981a65 | ||
|
|
a225622a6e | ||
|
|
3f19ede8d2 | ||
|
|
668169d1cc | ||
|
|
66b39f0616 | ||
|
|
7efef28d4f | ||
|
|
d9140d2f8c | ||
|
|
e2657febf0 | ||
|
|
d161f1997d | ||
|
|
37490f2c74 | ||
|
|
9b1d7d44bf | ||
|
|
9562c9d828 | ||
|
|
6c2a980afc | ||
|
|
00ac2c19bd | ||
|
|
6742d09a70 | ||
|
|
41e8777609 | ||
|
|
20868aa233 | ||
|
|
27c6535ad0 | ||
|
|
c75e1ba0be | ||
|
|
c1e845cb22 | ||
|
|
803cc17410 | ||
|
|
f088c55cfe | ||
|
|
8a8864c90b | ||
|
|
1986bce538 | ||
|
|
c528d2cd95 | ||
|
|
7529ee4168 | ||
|
|
61c7f4469d | ||
|
|
896b4fc56a | ||
|
|
a58fa8602b | ||
|
|
d8f8b6ec70 | ||
|
|
b256d5a5f6 | ||
|
|
ea09cc2724 | ||
|
|
e3e0a511b6 | ||
|
|
0ecaab4da0 | ||
|
|
40644f33e6 | ||
|
|
809d4cc609 | ||
|
|
89e4e836c9 | ||
|
|
69ddcf553f | ||
|
|
1e1ec5aae8 | ||
|
|
e30e6435ca | ||
|
|
77700f361c | ||
|
|
ad4aa1d49b | ||
|
|
8499ed3d65 | ||
|
|
db605120fa | ||
|
|
9106ac4522 | ||
|
|
713dbb7f45 | ||
|
|
c3d23d7cae | ||
|
|
a1edf74fcf | ||
|
|
d59709071a |
57
.dockerignore
Normal file
57
.dockerignore
Normal file
@ -0,0 +1,57 @@
|
||||
# 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
|
||||
*~
|
||||
40
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
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
|
||||
@ -10,7 +10,7 @@ jobs:
|
||||
name: Add issue to project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v1.0.1
|
||||
- uses: actions/add-to-project@v2
|
||||
with:
|
||||
project-url: https://github.com/orgs/GNS3/projects/3
|
||||
github-token: ${{ secrets.ADD_NEW_ISSUES_TO_PROJECT }}
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -56,11 +56,11 @@ jobs:
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@ -88,6 +88,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
54
.github/workflows/docker-build.yml
vendored
54
.github/workflows/docker-build.yml
vendored
@ -10,31 +10,59 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
DOCKERHUB_ORG: ${{ vars.DOCKERHUB_ORG || 'gns3' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- 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
|
||||
uses: docker/login-action@v3
|
||||
if: steps.ver.outputs.stable == 'true'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
if: steps.ver.outputs.stable == 'true'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push to GitHub Container Registry
|
||||
run: |
|
||||
docker build -t ghcr.io/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]'):latest .
|
||||
docker push ghcr.io/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]'):latest
|
||||
|
||||
- name: Build and push to Docker Hub
|
||||
run: |
|
||||
docker build -t gns3/${{ github.event.repository.name }}:latest .
|
||||
docker push gns3/${{ github.event.repository.name }}:latest
|
||||
- name: Build and push
|
||||
if: steps.ver.outputs.stable == 'true'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKERHUB_ORG }}/${{ github.event.repository.name }}:${{ steps.ver.outputs.tag }}
|
||||
${{ env.DOCKERHUB_ORG }}/${{ 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
|
||||
|
||||
@ -12,11 +12,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: "gh-pages"
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Merge changes from 3.0 branch
|
||||
|
||||
4
.github/workflows/testing.yml
vendored
4
.github/workflows/testing.yml
vendored
@ -21,9 +21,9 @@ jobs:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
|
||||
23
CHANGELOG
23
CHANGELOG
@ -1,5 +1,28 @@
|
||||
# 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
|
||||
|
||||
* Sync appliances
|
||||
|
||||
@ -103,6 +103,8 @@ enable_hardware_acceleration = True
|
||||
require_hardware_acceleration = False
|
||||
; Allow unsafe additional command line options
|
||||
allow_unsafe_options = False
|
||||
; Path to the OVMF firmware directory
|
||||
ovmf_firmware_dir = "/usr/share/OVMF"
|
||||
|
||||
[VMware]
|
||||
; First vmnet interface of the range that can be managed by the GNS3 server
|
||||
|
||||
17
docs/api/examples/controller_get_templateidbaseconfig.txt
Normal file
17
docs/api/examples/controller_get_templateidbaseconfig.txt
Normal 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"
|
||||
}
|
||||
44
docs/api/examples/controller_get_templatesbaseconfigs.txt
Normal file
44
docs/api/examples/controller_get_templatesbaseconfigs.txt
Normal 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"
|
||||
}
|
||||
]
|
||||
17
docs/api/examples/controller_put_templateidbaseconfig.txt
Normal file
17
docs/api/examples/controller_put_templateidbaseconfig.txt
Normal 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"
|
||||
}
|
||||
38
docs/api/v2/controller/template/templatesbaseconfigs.rst
Normal file
38
docs/api/v2/controller/template/templatesbaseconfigs.rst
Normal 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
|
||||
@ -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>✔</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>✔</td><td>string</td><td>Template UUID</td></tr>
|
||||
<tr><td>filename</td><td>✔</td><td>string</td><td>Base configuration filename</td></tr>
|
||||
<tr><td>content</td><td>✔</td><td>string</td><td>Updated file content</td></tr>
|
||||
|
||||
</table>
|
||||
|
||||
Sample session
|
||||
***************
|
||||
|
||||
.. literalinclude:: ../../../examples/controller_put_templateidbaseconfig.txt
|
||||
59
gns3server/appliances/armbian.gns3a
Normal file
59
gns3server/appliances/armbian.gns3a
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,41 +1,56 @@
|
||||
{
|
||||
"appliance_id": "088df570-f637-46f5-8a68-85acde538e5e",
|
||||
"appliance_id": "8fecbf89-5cd1-4aea-b735-5f36cf0efbb7",
|
||||
"name": "BIRD",
|
||||
"category": "router",
|
||||
"description": "The BIRD project aims to develop a fully functional dynamic IP routing daemon primarily targeted on (but not limited to) Linux, FreeBSD and other UNIX-like systems and distributed under the GNU General Public License.",
|
||||
"vendor_name": "CZ.NIC Labs",
|
||||
"vendor_url": "http://bird.network.cz/",
|
||||
"documentation_url": "http://bird.network.cz/?get_doc&f=bird.html",
|
||||
"vendor_url": "https://bird.network.cz",
|
||||
"documentation_url": "https://bird.network.cz/?get_doc&f=bird.html&v=20",
|
||||
"product_name": "BIRD internet routing daemon",
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
"usage": "\n*** BIRD v1 is end-of-life ***\nPlease use the BIRD2 appliance.\n\nConfigure interfaces in /opt/bootlocal.sh, BIRD configuration is done in /usr/local/etc/bird",
|
||||
"maintainer": "Bernhard Ehlers",
|
||||
"maintainer_email": "dev-ehlers@mailbox.org",
|
||||
"usage": "Username:\tgns3\nPassword:\tgns3\nTo become root, use \"sudo -s\".\n\nNetwork configuration:\nsudo nano /etc/network/interfaces\nsudo systemctl restart networking\n\nBIRD:\nRestart: sudo systemctl restart bird\nReconfigure: birdc configure",
|
||||
"port_name_format": "eth{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
"adapter_type": "virtio-net-pci",
|
||||
"adapters": 4,
|
||||
"ram": 128,
|
||||
"hda_disk_interface": "ide",
|
||||
"ram": 512,
|
||||
"hda_disk_interface": "scsi",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"kvm": "allow"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "bird-tinycore64-1.5.0.img",
|
||||
"version": "1.5.0",
|
||||
"md5sum": "08d50ba2b1b262e2e03e4babf90abf69",
|
||||
"filesize": 22413312,
|
||||
"filename": "bird2-debian-2.14.qcow2",
|
||||
"version": "2.14",
|
||||
"md5sum": "029cf1756201ee79497c169502b08b88",
|
||||
"filesize": 303717376,
|
||||
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
|
||||
"direct_download_url": "http://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird-tinycore64-1.5.0.img"
|
||||
"direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.14.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "bird2-debian-2.0.12.qcow2",
|
||||
"version": "2.0.12",
|
||||
"md5sum": "435218a2e90cba921cc7fde1d64a9419",
|
||||
"filesize": 287965184,
|
||||
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
|
||||
"direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.0.12.qcow2"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "1.5.0",
|
||||
"name": "2.14",
|
||||
"images": {
|
||||
"hda_disk_image": "bird-tinycore64-1.5.0.img"
|
||||
"hda_disk_image": "bird2-debian-2.14.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2.0.12",
|
||||
"images": {
|
||||
"hda_disk_image": "bird2-debian-2.0.12.qcow2"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -51,250 +51,12 @@
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v7.4.3-build2487-FORTINET.out.kvm.qcow2",
|
||||
"version": "7.4.3",
|
||||
"md5sum": "b01d9f86aa27c538407d518df1326863",
|
||||
"filesize": 346107904,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v7.4.2-build2397-FORTINET.out.kvm.qcow2",
|
||||
"version": "7.4.2",
|
||||
"md5sum": "36371fbf06210ded57c00b2ff290f2c5",
|
||||
"filesize": 322514944,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v7.4.1-build2308-FORTINET.out.kvm.qcow2",
|
||||
"version": "7.4.1",
|
||||
"md5sum": "e542cc8f2d8f46e9c32b783bf31bef39",
|
||||
"filesize": 309387264,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v7.2.5-build1574-FORTINET.out.kvm.qcow2",
|
||||
"version": "7.2.5",
|
||||
"md5sum": "754326845096afd909ec45d98f8d5a83",
|
||||
"filesize": 278401024,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v7.2.4-build1460-FORTINET.out.kvm.qcow2",
|
||||
"version": "7.2.4",
|
||||
"md5sum": "98fa9830d9ecb5911a703d03b80026b6",
|
||||
"filesize": 261992448,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v7.2.2-build1334-FORTINET.out.kvm.qcow2",
|
||||
"version": "7.2.2",
|
||||
"md5sum": "2ff1298257321cd485d2cad91d6ce510",
|
||||
"filesize": 246083584,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v7.2.1-build1215-FORTINET.out.kvm.qcow2",
|
||||
"version": "7.2.1",
|
||||
"md5sum": "1a3eeff1204fa8f4243773f7521e12b5",
|
||||
"filesize": 242814976,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v7.0.12-build0623-FORTINET.out.kvm.qcow2",
|
||||
"version": "7.0.12",
|
||||
"md5sum": "5b6f6a2b8bc00e56337aa7023a9025cf",
|
||||
"filesize": 249520128,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v7.0.11-build0595-FORTINET.out.kvm.qcow2",
|
||||
"version": "7.0.11",
|
||||
"md5sum": "7b166222136e26190159f37cccbaab6e",
|
||||
"filesize": 249360384,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v7.0.9-build0489-FORTINET.out.kvm.qcow2",
|
||||
"version": "7.0.9",
|
||||
"md5sum": "dbeb6a79b6e421000573dbbbdb50b8b5",
|
||||
"filesize": 247955456,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v7.0.6-build0372-FORTINET.out.kvm.qcow2",
|
||||
"version": "7.0.6",
|
||||
"md5sum": "dfa4df9e976ed87e73cb9601a8a70323",
|
||||
"filesize": 239190016,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v7.0.5-build0365-FORTINET.out.kvm.qcow2",
|
||||
"version": "7.0.5",
|
||||
"md5sum": "e8b9c992784cea766b52a427a5fe0279",
|
||||
"filesize": 237535232,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v6.4.14-build2660-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.4.14",
|
||||
"md5sum": "0fe56e363b166c07b710bde795e36049",
|
||||
"filesize": 219430912,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v6.4.12-build2610-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.4.12",
|
||||
"md5sum": "36c0dc531d921e5f1e1e09b030f7c813",
|
||||
"filesize": 219455488,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v6-build2288-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.4.5",
|
||||
"md5sum": "bd2791984b03f55a6825297e83c6576a",
|
||||
"filesize": 117014528,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v6-build2253-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.4.4",
|
||||
"md5sum": "3554a47fde2dc91d17eec16fd0dc10a3",
|
||||
"filesize": 116621312,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v6-build1183-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.2.2",
|
||||
"md5sum": "f5051a8fe49d916bb554b9bae32a1eb4",
|
||||
"filesize": 139145216,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v6-build1050-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.2.0",
|
||||
"md5sum": "c19d2527f91ad1bbafbde5bf08487867",
|
||||
"filesize": 126894080,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v6-build0349-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.0.6",
|
||||
"md5sum": "d03f024c948ba6e2bb9e66c11ca8f34c",
|
||||
"filesize": 112553984,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v6-build0255-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.0.3",
|
||||
"md5sum": "5f34d52d9289b0be2a4c04943446ea39",
|
||||
"filesize": 115703808,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v6-build0205-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.0.2",
|
||||
"md5sum": "8f748649c537d9b5466b24c5b4e62017",
|
||||
"filesize": 116981760,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v6-build0092-FORTINET.out.kvm.qcow2",
|
||||
"version": "6.0.0",
|
||||
"md5sum": "73bfe1bc70124521a524d857646b9c2e",
|
||||
"filesize": 119066624,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v5-build1631-FORTINET.out.kvm.qcow2",
|
||||
"version": "5.6.2",
|
||||
"md5sum": "c81cc247e8eb03249b475fe0e847653e",
|
||||
"filesize": 106946560,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v5-build1619-FORTINET.out.kvm.qcow2",
|
||||
"version": "5.6.1",
|
||||
"md5sum": "8cc553842564d232af295d6a0c784c1f",
|
||||
"filesize": 106831872,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v5-build1557-FORTINET.out.kvm.qcow2",
|
||||
"version": "5.6.0",
|
||||
"md5sum": "f8bd600796f894f4ca1ea2d6b4066d3d",
|
||||
"filesize": 108363776,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v5-build1225-FORTINET.out.kvm.qcow2",
|
||||
"version": "5.4.4",
|
||||
"md5sum": "53bc6e320fe7bde5d2b636bde95a910c",
|
||||
"filesize": 89911296,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v5-build1187-FORTINET.out.kvm.qcow2",
|
||||
"version": "5.4.3",
|
||||
"md5sum": "53602c776d215d98e32163a10804fc49",
|
||||
"filesize": 87425024,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v5-build1151-FORTINET.out.kvm.qcow2",
|
||||
"version": "5.4.2",
|
||||
"md5sum": "8e131ad40009c740f3efdee6dc3a0ac3",
|
||||
"filesize": 86437888,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v5-build1082-FORTINET.out.kvm.qcow2",
|
||||
"version": "5.4.1",
|
||||
"md5sum": "fc1815410f3f0536e2e3a9c1c5c07f41",
|
||||
"filesize": 83124224,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v5-build1019-FORTINET.out.kvm.qcow2",
|
||||
"version": "5.4.0",
|
||||
"md5sum": "1cfb22671cb372d8bf3e47b9c3c55ded",
|
||||
"filesize": 77541376,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v5-build0786-FORTINET.out.kvm.qcow2",
|
||||
"version": "5.2.10",
|
||||
"md5sum": "377fe38bf07bc2435608e5b65f780f07",
|
||||
"filesize": 64962560,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v5-build0780-FORTINET.out.kvm.qcow2",
|
||||
"version": "5.2.9",
|
||||
"md5sum": "04268e779d3d5e6c928c6fd638423c52",
|
||||
"filesize": 65007616,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v5-build0777-FORTINET.out.kvm.qcow2",
|
||||
"version": "5.2.8",
|
||||
"md5sum": "6dbf148ace9bf309ad383757afd75fad",
|
||||
"filesize": 65011712,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "FMG_VM64_KVM-v5-build0757-FORTINET.out.kvm.qcow2",
|
||||
"version": "5.2.7",
|
||||
"md5sum": "d37dbaa49d7522324681eeba19f7699b",
|
||||
"filesize": 65056768,
|
||||
"download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx"
|
||||
},
|
||||
{
|
||||
"filename": "empty30G.qcow2",
|
||||
"filename": "empty500G.qcow2",
|
||||
"version": "1.0",
|
||||
"md5sum": "3411a599e822f2ac6be560a26405821a",
|
||||
"filesize": 197120,
|
||||
"md5sum": "658c825441b9b3080ba00f9eec002eaa",
|
||||
"filesize": 204608,
|
||||
"download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/",
|
||||
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty30G.qcow2/download"
|
||||
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty500G.qcow2/download"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
@ -302,259 +64,21 @@
|
||||
"name": "7.4.6",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.4.6.M-build2588-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
"hdb_disk_image": "empty500G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.4.5",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.4.5.M-build2553-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
"hdb_disk_image": "empty500G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.4.4",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.4.4.F-build2550-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.4.3",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.4.3-build2487-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.4.2",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.4.2-build2397-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.4.1",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.4.1-build2308-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.2.5",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.2.5-build1574-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.2.4",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.2.4-build1460-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.2.2",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.2.2-build1334-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.2.1",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.2.1-build1215-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.12",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.0.12-build0623-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.11",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.0.11-build0595-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.9",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.0.9-build0489-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.6",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.0.6-build0372-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.5",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v7.0.5-build0365-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.4.14",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v6.4.14-build2660-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.4.12",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v6.4.12-build2610-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.4.5",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v6-build2288-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.4.4",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v6-build2253-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.2.2",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v6-build1183-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.2.0",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v6-build1050-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.0.6",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v6-build0349-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.0.3",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v6-build0255-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.0.2",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v6-build0205-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.0.0",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v6-build0092-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.6.2",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v5-build1631-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.6.1",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v5-build1619-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.6.0",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v5-build1557-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.4.4",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v5-build1225-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.4.3",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v5-build1187-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.4.2",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v5-build1151-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.4.1",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v5-build1082-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.4.0",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v5-build1019-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.2.10",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v5-build0786-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.2.9",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v5-build0780-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.2.8",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v5-build0777-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.2.7",
|
||||
"images": {
|
||||
"hda_disk_image": "FMG_VM64_KVM-v5-build0757-FORTINET.out.kvm.qcow2",
|
||||
"hdb_disk_image": "empty30G.qcow2"
|
||||
"hdb_disk_image": "empty500G.qcow2"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@ -132,9 +132,37 @@
|
||||
"md5sum": "24cd1006734993dab338e5c75f80b875",
|
||||
"version": "26.03.0",
|
||||
"direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.03.0/infix-x86_64-v26.03.0.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "infix-x86_64-v26.05.0.qcow2",
|
||||
"filesize": 330039296,
|
||||
"md5sum": "60f7c36c389c33ab108acc021f41ccd5",
|
||||
"version": "26.05.0",
|
||||
"direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.05.0/infix-x86_64-v26.05.0.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "infix-x86_64-v26.06.0.qcow2",
|
||||
"filesize": 363593728,
|
||||
"md5sum": "79ca8bd8534bbaa1af0ab874a49d6f4c",
|
||||
"version": "26.06.0",
|
||||
"direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.06.0/infix-x86_64-v26.06.0.qcow2"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "26.06.0",
|
||||
"images": {
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "infix-x86_64-v26.06.0.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "26.05.0",
|
||||
"images": {
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "infix-x86_64-v26.05.0.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "26.03.0",
|
||||
"images": {
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
"symbol": "linux_guest.svg",
|
||||
"docker": {
|
||||
"adapters": 1,
|
||||
"image": "gns3/ubuntu:noble",
|
||||
"image": "gns3/ubuntu:resolute",
|
||||
"console_type": "telnet"
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +38,13 @@
|
||||
"filesize": 17825792,
|
||||
"direct_download_url": "https://dropzone.westermo.com/file.aspx?id=e6a7676d-85a7-4374-8961-c68aacb74921"
|
||||
},
|
||||
{
|
||||
"filename": "WeOS-zero-5.29.0.disk",
|
||||
"version": "5.29.0",
|
||||
"filesize": 81788928,
|
||||
"md5sum": "f464de7b2b424f4a8ad7c650108fee3d",
|
||||
"direct_download_url": "https://dropzone.westermo.com/file.aspx?id=52655074-0fab-4119-ba01-b68200a733ab"
|
||||
},
|
||||
{
|
||||
"filename": "WeOS-zero-5.28.0.disk",
|
||||
"version": "5.28.0",
|
||||
@ -54,6 +61,13 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "5.29.0",
|
||||
"images": {
|
||||
"hda_disk_image": "WeOS-zero-5.29.0.disk",
|
||||
"hdb_disk_image": "Config-zero-1.0.0.disk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.28.0",
|
||||
"images": {
|
||||
|
||||
@ -311,7 +311,7 @@ class Cloud(BaseNode):
|
||||
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))
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
if sys.platform.startswith("linux") or sys.platform.startswith("openbsd"):
|
||||
await self._add_linux_ethernet(port_info, bridge_name)
|
||||
elif sys.platform.startswith("darwin"):
|
||||
await self._add_osx_ethernet(port_info, bridge_name)
|
||||
|
||||
@ -33,7 +33,7 @@ from gns3server.config import Config
|
||||
from gns3server.utils.asyncio import locking
|
||||
from gns3server.compute.base_manager import BaseManager
|
||||
from gns3server.compute.docker.docker_vm import DockerVM
|
||||
from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Error, DockerHttp404Error
|
||||
from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Error, DockerHttp404Error, DockerHttp409Error
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -236,6 +236,8 @@ class Docker(BaseManager):
|
||||
raise DockerHttp304Error("Docker has returned an error: {} {}".format(response.status, body))
|
||||
elif response.status == 404:
|
||||
raise DockerHttp404Error("Docker has returned an error: {} {}".format(response.status, body))
|
||||
elif response.status == 409:
|
||||
raise DockerHttp409Error("Docker has returned an error: {} {}".format(response.status, body))
|
||||
else:
|
||||
raise DockerError("Docker has returned an error: {} {}".format(response.status, body))
|
||||
return response
|
||||
|
||||
@ -32,3 +32,7 @@ class DockerHttp304Error(DockerError):
|
||||
|
||||
class DockerHttp404Error(DockerError):
|
||||
pass
|
||||
|
||||
|
||||
class DockerHttp409Error(DockerError):
|
||||
pass
|
||||
|
||||
@ -43,7 +43,8 @@ from ..nios.nio_udp import NIOUDP
|
||||
from .docker_error import (
|
||||
DockerError,
|
||||
DockerHttp304Error,
|
||||
DockerHttp404Error
|
||||
DockerHttp404Error,
|
||||
DockerHttp409Error
|
||||
)
|
||||
|
||||
import logging
|
||||
@ -459,10 +460,31 @@ class DockerVM(BaseNode):
|
||||
if extra_hosts:
|
||||
params["Env"].append("GNS3_EXTRA_HOSTS={}".format(extra_hosts))
|
||||
|
||||
# Support name in Doker: [a-zA-Z0-9][a-zA-Z0-9_.-]
|
||||
result = await self.manager.query("POST", "containers/create?name={}".format(self.docker_name), data=params)
|
||||
self._cid = result['Id']
|
||||
log.info("Docker container '{name}' [{id}] created".format(name=self._name, id=self._id))
|
||||
try:
|
||||
# Supported names in Docker: [a-zA-Z0-9][a-zA-Z0-9_.-]
|
||||
result = await self.manager.query("POST", f"containers/create?name={self.docker_name}", data=params)
|
||||
except DockerHttp409Error:
|
||||
# Container name already exists. This can happen when the server crashes
|
||||
# and leaves containers behind. Try to remove the conflicting container.
|
||||
log.warning(f"Container name '{self.docker_name}' is already in use, attempting to clean up the stale container...")
|
||||
try:
|
||||
# Try to get and remove the conflicting container
|
||||
try:
|
||||
container_info = await self.manager.query("GET", f"containers/{self.docker_name}/json")
|
||||
container_id = container_info["Id"]
|
||||
# Force remove the container
|
||||
await self.manager.query("DELETE", f"containers/{container_id}", params={"force": 1, "v": 1})
|
||||
log.info(f"Removed stale container '{self.docker_name}' ({container_id})")
|
||||
except DockerHttp404Error:
|
||||
# Container doesn't exist anymore, race condition - just continue
|
||||
pass
|
||||
# Retry creating the container
|
||||
result = await self.manager.query("POST", f"containers/create?name={self.docker_name}", data=params)
|
||||
except DockerError as e:
|
||||
log.error(f"Failed to clean up conflicting container '{self.docker_name}': {e}")
|
||||
raise
|
||||
self._cid = result["Id"]
|
||||
log.info(f"Docker container '{self._name}' [{self._id}] created")
|
||||
return True
|
||||
|
||||
def _format_env(self, variables, env):
|
||||
@ -847,7 +869,7 @@ class DockerVM(BaseNode):
|
||||
await self._fix_permissions()
|
||||
|
||||
state = await self._get_container_state()
|
||||
if state != "stopped" or state != "exited":
|
||||
if state != "stopped" and state != "exited":
|
||||
# t=5 number of seconds to wait before killing the container
|
||||
try:
|
||||
await self.manager.query("POST", "containers/{}/stop".format(self._cid), params={"t": 5})
|
||||
|
||||
@ -35,6 +35,7 @@ import time
|
||||
import json
|
||||
import psutil
|
||||
|
||||
from pathlib import Path
|
||||
from gns3server.utils import parse_version, shlex_quote
|
||||
from gns3server.utils.asyncio import subprocess_check_output, cancellable_wait_run_in_executor
|
||||
from .qemu_error import QemuError
|
||||
@ -2057,15 +2058,23 @@ class QemuVM(BaseNode):
|
||||
options.extend(["-bios", self._bios_image.replace(",", ",,")])
|
||||
|
||||
elif self._uefi:
|
||||
|
||||
ovmf_firmware_dir = self._manager.config.get_section_config("Qemu").get("ovmf_firmware_dir", "/usr/share/OVMF")
|
||||
system_ovmf_firmware_dir = Path(ovmf_firmware_dir)
|
||||
log.info("Using OVMF firmware directory: {}".format(system_ovmf_firmware_dir))
|
||||
old_ovmf_vars_path = os.path.join(self.working_dir, "OVMF_VARS.fd")
|
||||
if os.path.exists(old_ovmf_vars_path):
|
||||
# the node has its own UEFI variables store already, we must also use the old UEFI firmware
|
||||
ovmf_firmware_path = self.manager.get_abs_image_path("OVMF_CODE.fd")
|
||||
else:
|
||||
system_ovmf_firmware_path = "/usr/share/OVMF/OVMF_CODE_4M.fd"
|
||||
if os.path.exists(system_ovmf_firmware_path):
|
||||
ovmf_firmware_path = system_ovmf_firmware_path
|
||||
# Use a manual case-insensitive search instead
|
||||
try:
|
||||
system_ovmf_firmware_path = next((f for f in system_ovmf_firmware_dir.glob("*.fd")
|
||||
if f.name.lower() == "ovmf_code_4m.fd"), None)
|
||||
except (FileNotFoundError, StopIteration):
|
||||
system_ovmf_firmware_path = None
|
||||
|
||||
if system_ovmf_firmware_path:
|
||||
ovmf_firmware_path = str(system_ovmf_firmware_path)
|
||||
else:
|
||||
# otherwise, get the UEFI firmware from the images directory
|
||||
ovmf_firmware_path = self.manager.get_abs_image_path("OVMF_CODE_4M.fd")
|
||||
@ -2074,9 +2083,13 @@ class QemuVM(BaseNode):
|
||||
options.extend(["-drive", "if=pflash,format=raw,readonly,file={}".format(ovmf_firmware_path)])
|
||||
|
||||
# try to use the UEFI variables store from the system first
|
||||
system_ovmf_vars_path = "/usr/share/OVMF/OVMF_VARS_4M.fd"
|
||||
if os.path.exists(system_ovmf_vars_path):
|
||||
ovmf_vars_path = system_ovmf_vars_path
|
||||
try:
|
||||
system_ovmf_vars_path = next((f for f in system_ovmf_firmware_dir.glob("*.fd")
|
||||
if f.name.lower() == "ovmf_vars_4m.fd"), None)
|
||||
except (FileNotFoundError, StopIteration):
|
||||
system_ovmf_vars_path = None
|
||||
if system_ovmf_vars_path:
|
||||
ovmf_vars_path = str(system_ovmf_vars_path)
|
||||
else:
|
||||
# otherwise, get the UEFI variables store from the images directory
|
||||
ovmf_vars_path = self.manager.get_abs_image_path("OVMF_VARS_4M.fd")
|
||||
@ -2092,6 +2105,10 @@ class QemuVM(BaseNode):
|
||||
except OSError as e:
|
||||
raise QemuError("Cannot copy OVMF_VARS_4M.fd file to the node working directory: {}".format(e))
|
||||
options.extend(["-drive", "if=pflash,format=raw,file={}".format(ovmf_vars_node_path)])
|
||||
|
||||
# edk2 firmware requires a Random Number Generator (RNG) device in order to turn network adapters on
|
||||
options.extend(["-object", "rng-random,filename=/dev/urandom,id=rng0"])
|
||||
options.extend(["-device", "virtio-rng-pci,rng=rng0"])
|
||||
return options
|
||||
|
||||
def _linux_boot_options(self):
|
||||
@ -2463,7 +2480,6 @@ class QemuVM(BaseNode):
|
||||
elif sys.platform.startswith("win") or sys.platform.startswith("darwin"):
|
||||
command.extend(["-enable-hax"])
|
||||
command.extend(["-boot", "order={}".format(self._boot_priority)])
|
||||
command.extend(self._bios_option())
|
||||
command.extend(self._cdrom_option())
|
||||
command.extend((await self._disk_options()))
|
||||
command.extend(self._linux_boot_options())
|
||||
@ -2481,6 +2497,8 @@ class QemuVM(BaseNode):
|
||||
raise QemuError("Console type {} is unknown".format(self._console_type))
|
||||
command.extend(self._monitor_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":
|
||||
await self._clear_save_vm_stated()
|
||||
else:
|
||||
|
||||
@ -562,7 +562,15 @@ class Controller:
|
||||
|
||||
for project in self._projects.values():
|
||||
if project.auto_open:
|
||||
await project.open()
|
||||
try:
|
||||
await project.open()
|
||||
except aiohttp.web.HTTPClientError as e:
|
||||
details = e.text or e.reason or str(e)
|
||||
log.warning(
|
||||
"Failed to auto-open project '%s': %s",
|
||||
project.name,
|
||||
details,
|
||||
)
|
||||
|
||||
def get_free_project_name(self, base_name):
|
||||
"""
|
||||
|
||||
@ -166,24 +166,52 @@ async def import_project(
|
||||
project = await controller.load_project(dot_gns3_path, load=False)
|
||||
return project
|
||||
|
||||
|
||||
def _create_symbolic_links(zip_file, path):
|
||||
"""
|
||||
Manually create symbolic links (if any) because ZipFile does not support it.
|
||||
Refuse any target that escapes `path`.
|
||||
|
||||
:param zip_file: ZipFile instance
|
||||
:param path: project location
|
||||
"""
|
||||
|
||||
path_root = os.path.realpath(path) + os.sep
|
||||
for zip_info in zip_file.infolist():
|
||||
if stat.S_ISLNK(zip_info.external_attr >> 16):
|
||||
symlink_target = zip_file.read(zip_info.filename).decode()
|
||||
symlink_path = os.path.join(path, zip_info.filename)
|
||||
try:
|
||||
# remove the regular file and replace it by a symbolic link
|
||||
os.remove(symlink_path)
|
||||
os.symlink(symlink_target, symlink_path)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text=f"Cannot create symbolic link: {e}")
|
||||
if not stat.S_ISLNK(zip_info.external_attr >> 16):
|
||||
continue
|
||||
symlink_target = zip_file.read(zip_info.filename).decode()
|
||||
symlink_path = os.path.join(path, zip_info.filename)
|
||||
|
||||
# 1. Reject absolute targets outright.
|
||||
if os.path.isabs(symlink_target):
|
||||
raise aiohttp.web.HTTPConflict(
|
||||
text=f"Symlink {zip_info.filename!r} has absolute target {symlink_target!r}, refusing"
|
||||
)
|
||||
|
||||
# 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):
|
||||
"""
|
||||
@ -271,7 +299,7 @@ async def _upload_file(compute, project_id, file_path, path):
|
||||
|
||||
async def _import_images(controller, images_path):
|
||||
"""
|
||||
Copy images to the images directory or delete them if they already exists.
|
||||
Copy images to the images directory or delete them if they already exist.
|
||||
"""
|
||||
|
||||
image_dir = controller.images_path()
|
||||
@ -279,12 +307,19 @@ async def _import_images(controller, images_path):
|
||||
for (dirpath, dirnames, filenames) in os.walk(root, followlinks=False):
|
||||
for filename in filenames:
|
||||
path = os.path.join(dirpath, filename)
|
||||
if os.path.islink(path):
|
||||
continue
|
||||
dst = os.path.join(image_dir, os.path.relpath(path, root))
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
await wait_run_in_executor(shutil.move, path, dst)
|
||||
|
||||
if not os.path.exists(dst):
|
||||
await wait_run_in_executor(shutil.move, path, dst)
|
||||
try:
|
||||
with open(dst, "rb") as f:
|
||||
# read the first 7 bytes of the file.
|
||||
elf_header_start = f.read(7)
|
||||
# IOU images must start with the ELF magic number, be 32-bit or 64-bit, little endian and have an ELF version of 1
|
||||
if elf_header_start == b'\x7fELF\x01\x01\x01' or elf_header_start == b'\x7fELF\x02\x01\x01':
|
||||
os.chmod(dst, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
|
||||
except OSError as e:
|
||||
continue
|
||||
|
||||
async def update_snapshots(snapshots_dir, project_path, project_name, project_id, reset_mac_addresses=True):
|
||||
"""
|
||||
|
||||
@ -905,12 +905,23 @@ class Project:
|
||||
|
||||
async def delete(self):
|
||||
|
||||
# Check compute connectivity before open() to avoid 120s timeout
|
||||
# when remote computes are unreachable
|
||||
disconnected = self._get_disconnected_computes()
|
||||
if disconnected:
|
||||
compute_names = ", ".join([f"'{c.name}'" for c in disconnected])
|
||||
raise aiohttp.web.HTTPForbidden(
|
||||
f"Cannot delete project '{self.name}': {len(disconnected)} compute(s) are disconnected: {compute_names}. "
|
||||
f"Please fix the connection or delete the project manually on those computes."
|
||||
)
|
||||
|
||||
if self._status != "opened":
|
||||
try:
|
||||
await self.open()
|
||||
await self.open(auto_start=False)
|
||||
except aiohttp.web.HTTPConflict as e:
|
||||
# ignore missing images or other conflicts when deleting a project
|
||||
log.warning("Conflict while deleting project: {}".format(e.text))
|
||||
log.warning(f"Conflict while deleting project: {e}")
|
||||
|
||||
await self.delete_on_computes()
|
||||
await self.close()
|
||||
try:
|
||||
@ -922,6 +933,42 @@ class Project:
|
||||
raise aiohttp.web.HTTPConflict(text="Cannot delete project directory {}: {}".format(self.path, str(e)))
|
||||
self.emit_controller_notification("project.deleted", self.__json__())
|
||||
|
||||
def _get_disconnected_computes(self):
|
||||
"""
|
||||
Check compute connectivity by reading the topology file directly,
|
||||
without opening the project (which would try to connect to computes).
|
||||
Returns a list of disconnected Compute objects.
|
||||
"""
|
||||
if self._status == "opened":
|
||||
# Project is already open, use the already-loaded _computes list
|
||||
compute_ids = self._computes
|
||||
else:
|
||||
# Read compute IDs from topology file without connecting
|
||||
path = self._topology_file()
|
||||
if not os.path.exists(path):
|
||||
return []
|
||||
try:
|
||||
project_data = load_topology(path)
|
||||
except (ValueError, OSError) as e:
|
||||
log.warning(f"Could not read topology file for project '{self._name}': {e}")
|
||||
return []
|
||||
topology = project_data.get("topology", {})
|
||||
compute_ids = set()
|
||||
for node in topology.get("nodes", []):
|
||||
compute_id = node.get("compute_id")
|
||||
if compute_id:
|
||||
compute_ids.add(compute_id)
|
||||
|
||||
disconnected = []
|
||||
for compute_id in compute_ids:
|
||||
try:
|
||||
compute = self._controller.get_compute(compute_id)
|
||||
if not compute.connected:
|
||||
disconnected.append(compute)
|
||||
except aiohttp.web.HTTPNotFound:
|
||||
log.warning(f"Compute '{compute_id}' not found in controller")
|
||||
return disconnected
|
||||
|
||||
async def delete_on_computes(self):
|
||||
"""
|
||||
Delete the project on computes but not on controller
|
||||
@ -951,9 +998,12 @@ class Project:
|
||||
return os.path.join(self.path, self._filename)
|
||||
|
||||
@locking
|
||||
async def open(self):
|
||||
async def open(self, auto_start=True):
|
||||
"""
|
||||
Load topology elements
|
||||
|
||||
:param auto_start: whether the nodes may be started when the project
|
||||
has auto start enabled
|
||||
"""
|
||||
|
||||
if self._closing:
|
||||
@ -1067,7 +1117,7 @@ class Project:
|
||||
self._loading = False
|
||||
self.emit_controller_notification("project.opened", self.__json__())
|
||||
# Should we start the nodes when project is open
|
||||
if self._auto_start:
|
||||
if self._auto_start and auto_start:
|
||||
# Start all in the background without waiting for completion
|
||||
# we ignore errors because we want to let the user open
|
||||
# their project and fix it
|
||||
|
||||
@ -57,7 +57,7 @@ class CrashReport:
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "https://646d66c1f0b65d59af9c4577a68f3f87@o19455.ingest.us.sentry.io/38482"
|
||||
DSN = "https://89ba0bf8d773c2c95518bfb696d6d86f@o19455.ingest.us.sentry.io/38482"
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@ -22,6 +22,7 @@ from gns3server.schemas.template import TEMPLATE_USAGE_SCHEMA
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
||||
from gns3server.schemas.template import (
|
||||
TEMPLATE_OBJECT_SCHEMA,
|
||||
@ -173,3 +174,131 @@ class TemplateHandler:
|
||||
compute_id=request.json.get("compute_id"))
|
||||
response.set_status(201)
|
||||
response.json(node)
|
||||
|
||||
@Route.get(
|
||||
r"/templates/{template_id}/base-config/{filename}",
|
||||
description="Get base configuration file content",
|
||||
)
|
||||
def get_base_config(request, response):
|
||||
|
||||
controller = Controller.instance()
|
||||
|
||||
template_id = request.match_info["template_id"]
|
||||
|
||||
template = controller.template_manager.get_template(template_id)
|
||||
|
||||
filename = os.path.basename(request.match_info["filename"])
|
||||
|
||||
path = os.path.join(controller.configs_path(), filename)
|
||||
|
||||
try:
|
||||
if not os.path.exists(path):
|
||||
response.set_status(404)
|
||||
response.json({"message": "File not found"})
|
||||
return
|
||||
|
||||
with open(path, encoding="utf-8", errors="ignore") as f:
|
||||
content = f.read()
|
||||
|
||||
except Exception as e:
|
||||
response.set_status(500)
|
||||
response.json({"message": str(e)})
|
||||
return
|
||||
|
||||
response.set_status(200)
|
||||
response.json({
|
||||
"template_id": template.id,
|
||||
"filename": filename,
|
||||
"content": content
|
||||
})
|
||||
|
||||
@Route.put(
|
||||
r"/templates/{template_id}/base-config/{filename}",
|
||||
description="Update base configuration file content",
|
||||
parameters={
|
||||
"template_id": "Template UUID",
|
||||
"filename": "Base config filename"
|
||||
},
|
||||
status_codes={
|
||||
200: "File updated",
|
||||
400: "Invalid request",
|
||||
404: "Template or file not found"
|
||||
},
|
||||
)
|
||||
def update_base_config(request, response):
|
||||
|
||||
controller = Controller.instance()
|
||||
|
||||
template_id = request.match_info["template_id"]
|
||||
filename = os.path.basename(request.match_info["filename"])
|
||||
|
||||
try:
|
||||
template = controller.template_manager.get_template(template_id)
|
||||
except Exception:
|
||||
response.set_status(404)
|
||||
response.json({"message": "Template not found"})
|
||||
return
|
||||
|
||||
path = os.path.join(controller.configs_path(), filename)
|
||||
|
||||
body = request.json
|
||||
|
||||
if not body or "content" not in body:
|
||||
response.set_status(400)
|
||||
response.json({"message": "Missing 'content' field"})
|
||||
return
|
||||
|
||||
content = body["content"]
|
||||
|
||||
try:
|
||||
os.makedirs(controller.configs_path(), exist_ok=True)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
except OSError as e:
|
||||
response.set_status(500)
|
||||
response.json({"message": str(e)})
|
||||
return
|
||||
|
||||
response.set_status(200)
|
||||
response.json({
|
||||
"template_id": template.id,
|
||||
"filename": filename,
|
||||
"content": content
|
||||
})
|
||||
|
||||
@Route.get(
|
||||
r"/templates/base-configs",
|
||||
description="List all available base configuration files",
|
||||
status_codes={
|
||||
200: "List of base configuration files returned"
|
||||
}
|
||||
)
|
||||
def list_base_configs(request, response):
|
||||
|
||||
controller = Controller.instance()
|
||||
|
||||
configs_path = controller.configs_path()
|
||||
|
||||
try:
|
||||
files = []
|
||||
|
||||
if os.path.exists(configs_path):
|
||||
for filename in os.listdir(configs_path):
|
||||
|
||||
path = os.path.join(configs_path, filename)
|
||||
|
||||
if os.path.isfile(path):
|
||||
files.append({"filename": filename})
|
||||
|
||||
except OSError as e:
|
||||
response.set_status(500)
|
||||
response.json({
|
||||
"message": str(e)
|
||||
})
|
||||
return
|
||||
|
||||
files = sorted(files, key=lambda x: x["filename"])
|
||||
response.set_status(200)
|
||||
response.json(files)
|
||||
|
||||
@ -147,7 +147,7 @@ def is_interface_up(interface):
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
if sys.platform.startswith("linux") or sys.platform.startswith("openbsd"):
|
||||
|
||||
if interface not in psutil.net_if_addrs():
|
||||
return False
|
||||
@ -234,7 +234,7 @@ def interfaces():
|
||||
result["special"] = False
|
||||
for special_interface in ("lo", "vmnet", "vboxnet", "docker", "lxcbr",
|
||||
"virbr", "ovs-system", "veth", "fw", "p2p",
|
||||
"bridge", "vmware", "virtualbox", "gns3"):
|
||||
"bridge", "vmware", "virtualbox", "gns3","veb"):
|
||||
if result["name"].lower().startswith(special_interface):
|
||||
result["special"] = True
|
||||
for special_interface in ("-nic"):
|
||||
|
||||
@ -23,8 +23,8 @@
|
||||
# or negative for a release candidate or beta (after the base version
|
||||
# number has been incremented)
|
||||
|
||||
__version__ = "2.2.59"
|
||||
__version_info__ = (2, 2, 59, 0)
|
||||
__version__ = "2.2.61"
|
||||
__version_info__ = (2, 2, 61, 0)
|
||||
|
||||
if "dev" in __version__:
|
||||
try:
|
||||
|
||||
@ -1503,3 +1503,19 @@ async def test_read_console_output_with_binary_mode(vm):
|
||||
with asyncio_patch('gns3server.compute.docker.docker_vm.DockerVM.stop'):
|
||||
await vm._read_console_output(input_stream, output_stream)
|
||||
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"
|
||||
|
||||
@ -19,6 +19,8 @@ import os
|
||||
import uuid
|
||||
import json
|
||||
import zipfile
|
||||
import pytest
|
||||
import aiohttp
|
||||
|
||||
from tests.utils import asyncio_patch, AsyncioMagicMock
|
||||
from unittest.mock import patch, MagicMock
|
||||
@ -117,36 +119,48 @@ async def write_file(path, z):
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
async def test_import_project_containing_symlink(tmpdir, controller):
|
||||
@pytest.fixture
|
||||
def export_project_with_symlink(tmpdir, controller):
|
||||
async def _export(symlink_target):
|
||||
project = Project(controller=controller, name="test")
|
||||
project.dump = MagicMock()
|
||||
|
||||
project = Project(controller=controller, name="test")
|
||||
project.dump = MagicMock()
|
||||
path = project.path
|
||||
topology = {
|
||||
"project_id": str(uuid.uuid4()),
|
||||
"name": "test",
|
||||
"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())
|
||||
topology = {
|
||||
"project_id": str(uuid.uuid4()),
|
||||
"name": "test",
|
||||
"auto_open": True,
|
||||
"auto_start": True,
|
||||
"topology": {
|
||||
},
|
||||
"version": "2.0.0"
|
||||
}
|
||||
|
||||
with open(os.path.join(path, "project.gns3"), 'w+') as f:
|
||||
json.dump(topology, f)
|
||||
|
||||
os.makedirs(os.path.join(path, "vm1", "dynamips"))
|
||||
symlink_path = os.path.join(project.path, "vm1", "dynamips", "symlink")
|
||||
symlink_target = "/tmp/anywhere"
|
||||
os.symlink(symlink_target, symlink_path)
|
||||
|
||||
zip_path = str(tmpdir / "project.zip")
|
||||
with aiozipstream.ZipFile() as z:
|
||||
with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),):
|
||||
await export_project(z, project, str(tmpdir), include_images=False)
|
||||
await write_file(zip_path, z)
|
||||
symlink_target = "../symlink_target"
|
||||
zip_path = await export_project_with_symlink(symlink_target)
|
||||
|
||||
with open(zip_path, "rb") as f:
|
||||
project = await import_project(controller, project_id, f)
|
||||
@ -158,6 +172,36 @@ async def test_import_project_containing_symlink(tmpdir, controller):
|
||||
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):
|
||||
"""
|
||||
Topology made for previous GNS3 version are upgraded during the process
|
||||
|
||||
@ -655,6 +655,19 @@ async def test_delete(project):
|
||||
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):
|
||||
|
||||
directory = projects_dir
|
||||
|
||||
@ -14,8 +14,7 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from tests.utils import asyncio_patch
|
||||
@ -955,3 +954,81 @@ async def test_create_node_from_template(controller_api, controller, project):
|
||||
mock.assert_called_with(id, x=42, y=12, name=None, compute_id=None)
|
||||
assert response.route == "/projects/{project_id}/templates/{template_id}"
|
||||
assert response.status == 201
|
||||
|
||||
|
||||
async def test_get_base_config(controller_api, controller):
|
||||
|
||||
template_id = str(uuid.uuid4())
|
||||
|
||||
controller.template_manager._templates[template_id] = Template(template_id, {
|
||||
"template_type": "vpcs",
|
||||
"category": 0,
|
||||
"name": "test",
|
||||
"symbol": "guest.svg",
|
||||
"default_name_format": "{name}-{0}",
|
||||
"compute_id": "local"
|
||||
})
|
||||
|
||||
config_path = os.path.join(controller.configs_path(), "test_config.txt")
|
||||
|
||||
with open(config_path, "w") as f:
|
||||
f.write("hello config")
|
||||
|
||||
response = await controller_api.get(
|
||||
f"/templates/{template_id}/base-config/test_config.txt"
|
||||
)
|
||||
|
||||
assert response.status == 200
|
||||
assert response.json["filename"] == "test_config.txt"
|
||||
assert response.json["content"] == "hello config"
|
||||
|
||||
async def test_update_base_config(controller_api, controller):
|
||||
|
||||
template_id = str(uuid.uuid4())
|
||||
|
||||
controller.template_manager._templates[template_id] = Template(template_id, {
|
||||
"template_type": "vpcs",
|
||||
"category": 0,
|
||||
"name": "test",
|
||||
"symbol": "guest.svg",
|
||||
"default_name_format": "{name}-{0}",
|
||||
"compute_id": "local"
|
||||
})
|
||||
|
||||
config_path = os.path.join(controller.configs_path(), "update_test.txt")
|
||||
|
||||
with open(config_path, "w") as f:
|
||||
f.write("old content")
|
||||
|
||||
response = await controller_api.put(
|
||||
f"/templates/{template_id}/base-config/update_test.txt",
|
||||
{
|
||||
"content": "new updated content"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status == 200
|
||||
assert response.json["content"] == "new updated content"
|
||||
|
||||
with open(config_path) as f:
|
||||
assert f.read() == "new updated content"
|
||||
|
||||
async def test_list_base_configs(controller_api, controller):
|
||||
|
||||
config1 = os.path.join(controller.configs_path(), "config1.txt")
|
||||
config2 = os.path.join(controller.configs_path(), "config2.txt")
|
||||
|
||||
with open(config1, "w") as f:
|
||||
f.write("test1")
|
||||
|
||||
with open(config2, "w") as f:
|
||||
f.write("test2")
|
||||
|
||||
response = await controller_api.get("/templates/base-configs")
|
||||
|
||||
assert response.status == 200
|
||||
|
||||
filenames = [item["filename"] for item in response.json]
|
||||
|
||||
assert "config1.txt" in filenames
|
||||
assert "config2.txt" in filenames
|
||||
Loading…
x
Reference in New Issue
Block a user