Merge remote-tracking branch 'origin/3.0' into gh-pages

This commit is contained in:
github-actions 2026-01-28 14:32:06 +00:00
commit 0c6cbe8fbc
38 changed files with 469 additions and 573 deletions

40
.github/workflows/docker-build.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Build and Push Docker Image
on:
workflow_dispatch:
push:
tags:
- v2.*
- v3.*
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v3
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

View File

@ -18,11 +18,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: ["ubuntu-latest"] os: ["ubuntu-latest"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
#include:
# only test with Python 3.10 on Windows
# - os: windows-latest
# python-version: "3.10"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@ -1,5 +1,34 @@
# Change Log # Change Log
## 3.0.6 28/01/2026
* Sync appliances
* Fix creating Qemu disk image. Fixes #2542
* Disable checking for available disk space. Ref #2548
* Set default location of udhcpc in "/etc/network/udhcpc". Fixes #2582
* Support for Python 3.14
* Fix non-ASCII characters in project names
* Increase DB engine pool size and max overflow
* Enable ip cef in IOU L2 and IOU L3 base configs
## 2.2.56 21/01/2026
* Set default location of udhcpc in "/etc/network/udhcpc". Fixes #2582
* Upgrade pywin32 to v311
## 2.2.55 19/11/2025
* Docker API version requirements and process to handle older daemons
* Use docker_name property when creating a container
* Set name of container in Docker
* Support for Python 3.14
* Enable ip cef in IOU L2 and IOU L3 base configs
* Only build and push Docker image when releasing a new version
* Fix pushing Docker image to Docker hub
* Update security issues in README.md. Fixes #2535
* Add missing 'nat' template type in schema. Fixes #2529
* Resolve deprecation warnings of regex library
## 3.0.5 14/05/2025 ## 3.0.5 14/05/2025
* Bundle web-ui v3.0.5 * Bundle web-ui v3.0.5

View File

@ -1,7 +1,7 @@
pytest==8.3.4 pytest==8.4.2 # version 8.4.2 is the last one supporting Python 3.9
flake8==7.1.1 flake8==7.3.0
pytest-timeout==2.3.1 pytest-timeout==2.4.0
pytest-asyncio==0.25.2 pytest-asyncio==0.26.0 # upgrading leads to stuck / failed tests
requests==2.32.3 requests==2.32.5
httpx==0.28.1 httpx==0.28.1
httpx_ws==0.7.1 httpx_ws==0.7.1 # upgrading leads to failures in tests

View File

@ -96,7 +96,13 @@ async def create_qemu_image(
await Qemu.instance().create_disk_image(disk_image_path, options) await Qemu.instance().create_disk_image(disk_image_path, options)
image_info = await read_image_info(disk_image_path, "qemu") image_info = await read_image_info(disk_image_path, "qemu")
return await images_repo.add_image(**image_info)
image = await images_repo.get_image(disk_image_path)
if image:
# the image has already been added to the database
return image
else:
return await images_repo.add_image(**image_info)
@router.get( @router.get(
"", "",

View File

@ -330,6 +330,9 @@ async def export_project(
Required privilege: Project.Audit Required privilege: Project.Audit
""" """
if project.is_running():
raise ControllerError("Project must be stopped in order to export it")
compression_query = compression.lower() compression_query = compression.lower()
if compression_query == "zip": if compression_query == "zip":
compression = zipfile.ZIP_DEFLATED compression = zipfile.ZIP_DEFLATED
@ -380,7 +383,13 @@ async def export_project(
except (ValueError, OSError, RuntimeError) as e: except (ValueError, OSError, RuntimeError) as e:
raise ConnectionError(f"Cannot export project: {e}") raise ConnectionError(f"Cannot export project: {e}")
headers = {"CONTENT-DISPOSITION": f'attachment; filename="{project.name}.gns3project"'} fallback = project.name.encode("ascii", "ignore").decode() or "project"
encoded = urllib.parse.quote(project.name, safe="")
headers = {
"Content-Disposition": (
f'attachment; filename="{fallback}.gns3project"; filename*=UTF-8\'\'{encoded}.gns3project'
)
}
return StreamingResponse(streamer(), media_type="application/gns3project", headers=headers) return StreamingResponse(streamer(), media_type="application/gns3project", headers=headers)

View File

@ -0,0 +1,33 @@
{
"appliance_id": "4aa78f74-c769-43e0-9ef1-5dd2cf1909c6",
"name": "Asterfusion AsterNOS-VPP",
"category": "router",
"description": "AsterNOS-VPP leverages SONiC’s robust routing features and management capability with VPP’s high-performance data forwarding. It supports L3 routing, NAT, PPPoE, and VPN services. Minimum requirements: 4 vCPUs and 4GB RAM.",
"vendor_name": "Asterfusion",
"vendor_url": "https://cloudswit.ch/product/sonic-enterprise-distribution/#vAsterNOS",
"documentation_url": "https://docs.asternos.com/routing",
"product_name": "AsterNOS-VPP",
"registry_version": 6,
"status": "stable",
"maintainer": "Asterfusion Product Team",
"maintainer_email": "bd@cloudswit.ch",
"images": [
{
"filename": "AsterNOS-VPP_V6.1-R0101P02_x86.img.gz",
"version": "V6.1",
"md5sum": "55834c3e8849ab226144d79ef76acf81",
"filesize": 1318201015,
"download_url": "https://asternos.dev/api/file/b2c9fd83-a9d3-46f3-9676-6b49dd481d6c",
"comments": "Registration is required to download the image."
}
],
"qemu": {
"adapter_type": "virtio-net-pci",
"adapters": 4,
"ram": 4096,
"cpus": 4,
"arch": "x86_64",
"console_type": "telnet",
"kvm": "require"
}
}

View File

@ -6,7 +6,7 @@
"vendor_name": "Asterfusion", "vendor_name": "Asterfusion",
"vendor_url": "https://cloudswit.ch/", "vendor_url": "https://cloudswit.ch/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/asterfusion.png", "vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/asterfusion.png",
"documentation_url": "https://help.cloudswit.ch/portal/en/kb/articles/vasternos", "documentation_url": "https://asternos.com/community/space/vasternos/post/vasternos-for-data-center-network",
"product_name": "vAsterNOS", "product_name": "vAsterNOS",
"product_url": "https://cloudswit.ch/product/sonic-enterprise-distribution", "product_url": "https://cloudswit.ch/product/sonic-enterprise-distribution",
"registry_version": 4, "registry_version": 4,
@ -30,18 +30,18 @@
}, },
"images": [ "images": [
{ {
"filename": "vAsterNOS-V3.1.img", "filename": "AsterNOS_V3.1_R0408P01-VS.img.gz",
"version": "V3.1", "version": "V3.1",
"md5sum": "c323c9c3f60e1a93eca2acdc5034b85c", "md5sum": "5c182306af67be2c79a5642551412e48",
"filesize": 2724659200, "filesize": 1425945924,
"download_url": "https://drive.cloudswitch.io/external/8ae2e3932ad8bb2ec30dd25be415d288ff3e4a949c557c6bd48ac6e6265bcfc1" "download_url": "http://pub.asternos.com/AsterNOS_V3.1_R0408P01-VS.img.gz"
} }
], ],
"versions": [ "versions": [
{ {
"name": "V3.1", "name": "V3.1",
"images": { "images": {
"hda_disk_image": "vAsterNOS-V3.1.img" "hda_disk_image": "AsterNOS_V3.1_R0408P01-VS.img.gz"
} }
} }
] ]

View File

@ -50,6 +50,12 @@
"version": "124-25G", "version": "124-25G",
"md5sum": "9c7cc9b3f3b3571411a7f62faaa2c036", "md5sum": "9c7cc9b3f3b3571411a7f62faaa2c036",
"filesize": 71528984 "filesize": 71528984
},
{
"filename": "c7200-itpk9-mz.124-15.SW.image",
"version": "124-15.SW",
"md5sum": "3334a0facdbd26164e83d3c201fff5b5",
"filesize": 46423952
} }
], ],
"versions": [ "versions": [
@ -87,6 +93,13 @@
"images": { "images": {
"image": "c7200-a3jk9s-mz.124-25g.image" "image": "c7200-a3jk9s-mz.124-25g.image"
} }
},
{
"name": "124-15.SW",
"idlepc": "0x60b2bc68",
"images": {
"image": "c7200-itpk9-mz.124-15.SW.image"
}
} }
] ]
} }

View File

@ -28,6 +28,13 @@
"options": "-nographic" "options": "-nographic"
}, },
"images": [ "images": [
{
"filename": "FEGNS3.9.3.0.0.qcow2",
"version": "v9.3.0.0",
"md5sum": "2abfa8d64219aa75522238f87b782726",
"filesize": 490209280,
"direct_download_url": "https://akamai-ep.extremenetworks.com/Extreme_P/github-en/Virtual_VOSS/FEGNS3.9.3.0.0.qcow2"
},
{ {
"filename": "VOSSGNS3.8.10.1.0.qcow2", "filename": "VOSSGNS3.8.10.1.0.qcow2",
"version": "v8.10.1.0", "version": "v8.10.1.0",
@ -93,6 +100,13 @@
} }
], ],
"versions": [ "versions": [
{
"name": "v9.3.0.0",
"images":
{
"hda_disk_image": "FEGNS3.9.3.0.0.qcow2"
}
},
{ {
"name": "v8.10.1.0", "name": "v8.10.1.0",
"images": "images":

View File

@ -25,19 +25,19 @@
}, },
"images": [ "images": [
{ {
"filename": "rtr.qcow2", "filename": "rtr-x86_64.qcow2",
"version": "20.7.1", "version": "25.8.29",
"md5sum": "4707415d15d20ced92423d90e52eedcd", "md5sum": "13d72fdfd772258808a9d4b75fa67083",
"filesize": 254767616, "filesize": 73269248,
"download_url": "http://freerouter.nop.hu/", "download_url": "http://www.freertr.org/",
"direct_download_url": "http://dl.nop.hu/rtr.qcow2" "direct_download_url": "http://dl.nop.hu/rtr-x86_64.qcow2"
} }
], ],
"versions": [ "versions": [
{ {
"name": "20.7.1", "name": "25.8.29",
"images": { "images": {
"hda_disk_image": "rtr.qcow2" "hda_disk_image": "rtr-x86_64.qcow2"
} }
} }
] ]

View File

@ -2,13 +2,13 @@
"appliance_id": "4d351078-c6f5-444c-ab30-0ef20e3d8c53", "appliance_id": "4d351078-c6f5-444c-ab30-0ef20e3d8c53",
"name": "Infix", "name": "Infix",
"category": "router", "category": "router",
"description": "Infix is a Network Operating System based on Linux. It can be set up both as a switch, with offloading using switchdev, and a router with firewalling.", "description": "Infix OS is a free Linux-based operating system with made-easy management using NETCONF, RESTCONF, or the built-in command line interface (CLI). It is suitable for both end-devices with a single Ethernet port and switches/routers with multiple ports. Immutable by design, extensions are possible using Docker containers.",
"vendor_name": "KernelKit", "vendor_name": "KernelKit",
"vendor_url": "https://github.com/kernelkit", "vendor_url": "https://github.com/kernelkit",
"vendor_logo_url": "https://kernelkit.org/assets/img/jack.png", "vendor_logo_url": "https://kernelkit.org/assets/img/jack.png",
"product_name": "Infix", "product_name": "Infix",
"registry_version": 4, "registry_version": 4,
"documentation_url": "https://github.com/kernelkit/infix/tree/main/doc", "documentation_url": "https://kernelkit.org/infix/latest/",
"status": "stable", "status": "stable",
"availability": "free", "availability": "free",
"maintainer": "KernelKit", "maintainer": "KernelKit",
@ -76,9 +76,79 @@
"md5sum": "84bd999513325d0007d0e6587abc6140", "md5sum": "84bd999513325d0007d0e6587abc6140",
"version": "25.04.0", "version": "25.04.0",
"direct_download_url": "https://github.com/kernelkit/infix/releases/download/v25.04.0/infix-x86_64-disk-25.04.0.qcow2" "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v25.04.0/infix-x86_64-disk-25.04.0.qcow2"
},
{
"filename": "infix-x86_64-disk-25.05.0.qcow2",
"filesize": 259958784,
"md5sum": "a6b1b3a6bccc60bb5214f2b864602fde",
"version": "25.05.0",
"direct_download_url": "https://github.com/kernelkit/infix/releases/download/v25.05.0/infix-x86_64-disk-25.05.0.qcow2"
},
{
"filename": "infix-x86_64-disk-25.08.0.qcow2",
"filesize": 284453888,
"md5sum": "5fbf146d2ac73d1bd8fa1c509d6169b1",
"version": "25.08.0",
"direct_download_url": "https://github.com/kernelkit/infix/releases/download/v25.08.0/infix-x86_64-disk-25.08.0.qcow2"
},
{
"filename": "infix-x86_64-disk-25.09.0.qcow2",
"filesize": 285120000,
"md5sum": "9761968406780a5b28311b8527f8629f",
"version": "25.09.0",
"direct_download_url": "https://github.com/kernelkit/infix/releases/download/v25.09.0/infix-x86_64-disk-25.09.0.qcow2"
},
{
"filename": "infix-x86_64-disk-25.10.0.qcow2",
"filesize": 288818688,
"md5sum": "781d0fce9fbf789f6ea6ff10dc104afb",
"version": "25.10.0",
"direct_download_url": "https://github.com/kernelkit/infix/releases/download/v25.10.0/infix-x86_64-disk-25.10.0.qcow2"
},
{
"filename": "infix-x86_64-disk-25.11.0.qcow2",
"filesize": 288930816,
"md5sum": "e2eb1a4fff9d56815f37c2e82a6279fb",
"version": "25.11.0",
"direct_download_url": "https://github.com/kernelkit/infix/releases/download/v25.11.0/infix-x86_64-disk-25.11.0.qcow2"
} }
], ],
"versions": [ "versions": [
{
"name": "25.11.0",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "infix-x86_64-disk-25.11.0.qcow2"
}
},
{
"name": "25.10.0",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "infix-x86_64-disk-25.10.0.qcow2"
}
},
{
"name": "25.09.0",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "infix-x86_64-disk-25.09.0.qcow2"
}
},
{
"name": "25.08.0",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "infix-x86_64-disk-25.08.0.qcow2"
}
},
{
"name": "25.05.0",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "infix-x86_64-disk-25.05.0.qcow2"
}
},
{ {
"name": "25.04.0", "name": "25.04.0",
"images": { "images": {
@ -115,4 +185,4 @@
} }
} }
] ]
} }

View File

@ -11,237 +11,34 @@
"status": "stable", "status": "stable",
"maintainer": "GNS3 Team", "maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net", "maintainer_email": "developers@gns3.net",
"usage": "Default password is toor\nEnable persistence by selecting boot option 'Live USB Persistence'", "usage": "Default username is kali and the default password is kali",
"port_name_format": "eth{0}", "port_name_format": "eth{0}",
"qemu": { "qemu": {
"adapter_type": "e1000", "adapter_type": "e1000",
"adapters": 8, "adapters": 8,
"ram": 1024, "ram": 2048,
"cpus": 2,
"hda_disk_interface": "ide", "hda_disk_interface": "ide",
"arch": "x86_64", "arch": "x86_64",
"console_type": "vnc", "console_type": "vnc",
"boot_priority": "d",
"kvm": "require" "kvm": "require"
}, },
"images": [ "images": [
{ {
"filename": "kali-linux-2021.1-live-amd64.iso", "filename": "kali-linux-2025.4-qemu-amd64.qcow2",
"version": "2021.1", "version": "2025.4",
"md5sum": "3a3716fef866e5c29a1c1ccfc94264b5", "md5sum": "a4be43351b5deb5b2823cb38a73b2095",
"filesize": 3591385088, "filesize": 15719268352,
"download_url": "http://cdimage.kali.org/kali-2021.1/", "download_url": "https://cdimage.kali.org/kali-2025.4/",
"direct_download_url": "http://cdimage.kali.org/kali-2021.1/kali-linux-2021.1-live-amd64.iso" "direct_download_url": "https://cdimage.kali.org/kali-2025.4/kali-linux-2025.4-qemu-amd64.7z",
}, "compression": "7z"
{
"filename": "kali-linux-2019.3-amd64.iso",
"version": "2019.3",
"md5sum": "9c6fb00558f78ed06992d89f745ef975",
"filesize": 3037736960,
"download_url": "http://old.kali.org/kali-images/kali-2019.3",
"direct_download_url": "http://old.kali.org/kali-images/kali-2019.3/kali-linux-2019.3-amd64.iso"
},
{
"filename": "kali-linux-2019.2-amd64.iso",
"version": "2019.2",
"md5sum": "0f89b6225d7ea9c18682f7cc541c1179",
"filesize": 3353227264,
"download_url": "http://old.kali.org/kali-images/kali-2019.2",
"direct_download_url": "http://old.kali.org/kali-images/kali-2019.2/kali-linux-2019.2-amd64.iso"
},
{
"filename": "kali-linux-mate-2019.2-amd64.iso",
"version": "2019.2 (MATE)",
"md5sum": "fec8dd7009f932c51a74323df965a709",
"filesize": 3313217536,
"download_url": "http://old.kali.org/kali-images/kali-2019.2",
"direct_download_url": "http://old.kali.org/kali-images/kali-2019.2/kali-linux-mate-2019.2-amd64.iso"
},
{
"filename": "kali-linux-2019.1a-amd64.iso",
"version": "2019.1a",
"md5sum": "58c6111ed0be1919ea87267e7e65ab0f",
"filesize": 3483873280,
"download_url": "http://old.kali.org/kali-images/kali-2019.1a",
"direct_download_url": "http://old.kali.org/kali-images/kali-2019.1a/kali-linux-2019.1a-amd64.iso"
},
{
"filename": "kali-linux-2018.4-amd64.iso",
"version": "2018.4",
"md5sum": "1b2d598bb8d2003e6207c119c0ba42fe",
"filesize": 3139436544,
"download_url": "http://old.kali.org/kali-images/kali-2018.4",
"direct_download_url": "http://old.kali.org/kali-images/kali-2018.4/kali-linux-2018.4-amd64.iso"
},
{
"filename": "kali-linux-2018.3a-amd64.iso",
"version": "2018.3a",
"md5sum": "2da675d016bd690c05e180e33aa98b94",
"filesize": 3192651776,
"download_url": "http://old.kali.org/kali-images/kali-2018.3a",
"direct_download_url": "http://old.kali.org/kali-images/kali-2018.3a/kali-linux-2018.3a-amd64.iso"
},
{
"filename": "kali-linux-2018.1-amd64.iso",
"version": "2018.1",
"md5sum": "a3feb90df5b71b3c7f4a02bdddf221d7",
"filesize": 3028500480,
"download_url": "http://old.kali.org/kali-images/kali-2018.1",
"direct_download_url": "http://old.kali.org/kali-images/kali-2018.1/kali-linux-2018.1-amd64.iso"
},
{
"filename": "kali-linux-2017.3-amd64.iso",
"version": "2017.3",
"md5sum": "b465580c897e94675ac1daf031fa66b9",
"filesize": 2886402048,
"download_url": "http://old.kali.org/kali-images/kali-2017.3/",
"direct_download_url": "http://old.kali.org/kali-images/kali-2017.3/kali-linux-2017.3-amd64.iso"
},
{
"filename": "kali-linux-2017.2-amd64.iso",
"version": "2017.2",
"md5sum": "541654f8f818450dc0db866a0a0f6eec",
"filesize": 3020619776,
"download_url": "http://old.kali.org/kali-images/kali-2017.2/",
"direct_download_url": "http://old.kali.org/kali-images/kali-2017.2/kali-linux-2017.2-amd64.iso"
},
{
"filename": "kali-linux-2017.1-amd64.iso",
"version": "2017.1",
"md5sum": "c8e742283929d7a12dbe7c58e398ff08",
"filesize": 2794307584,
"download_url": "http://old.kali.org/kali-images/kali-2017.1/",
"direct_download_url": "http://old.kali.org/kali-images/kali-2017.1/kali-linux-2017.1-amd64.iso"
},
{
"filename": "kali-linux-2016.2-amd64.iso",
"version": "2016.2",
"md5sum": "3d163746bc5148e61ad689d94bc263f9",
"filesize": 3076767744,
"download_url": "http://old.kali.org/kali-images/kali-2016.2/",
"direct_download_url": "http://old.kali.org/kali-images/kali-2016.2/kali-linux-2016.2-amd64.iso"
},
{
"filename": "kali-linux-2016.1-amd64.iso",
"version": "2016.1",
"md5sum": "2e1230dc14036935b3279dfe3e49ad39",
"filesize": 2945482752,
"download_url": "http://old.kali.org/kali-images/kali-2016.1/",
"direct_download_url": "http://old.kali.org/kali-images/kali-2016.1/kali-linux-2016.1-amd64.iso"
},
{
"filename": "kali-linux-2.0-amd64.iso",
"version": "2.0",
"md5sum": "ef192433017c5d99a156eaef51fd389d",
"filesize": 3320512512,
"download_url": "https://www.offensive-security.com/kali-linux-vmware-arm-image-download/",
"direct_download_url": "http://images.kali.org/Kali-Linux-2.0.0-vm-amd64.7z"
},
{
"filename": "kali-linux-persistence-1gb.qcow2",
"version": "1.0",
"md5sum": "14e9c92f3ba5a0bd1128c1ea26a129ea",
"filesize": 34734080,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/kali-linux-persistence-1gb.qcow2/download"
} }
], ],
"versions": [ "versions": [
{ {
"name": "2021.1", "name": "2025.4",
"images": { "images": {
"hda_disk_image": "kali-linux-persistence-1gb.qcow2", "hda_disk_image": "kali-linux-2025.4-qemu-amd64.qcow2"
"cdrom_image": "kali-linux-2021.1-live-amd64.iso"
}
},
{
"name": "2019.3",
"images": {
"hda_disk_image": "kali-linux-persistence-1gb.qcow2",
"cdrom_image": "kali-linux-2019.3-amd64.iso"
}
},
{
"name": "2019.2",
"images": {
"hda_disk_image": "kali-linux-persistence-1gb.qcow2",
"cdrom_image": "kali-linux-2019.2-amd64.iso"
}
},
{
"name": "2019.2 (MATE)",
"images": {
"hda_disk_image": "kali-linux-persistence-1gb.qcow2",
"cdrom_image": "kali-linux-mate-2019.2-amd64.iso"
}
},
{
"name": "2019.1a",
"images": {
"hda_disk_image": "kali-linux-persistence-1gb.qcow2",
"cdrom_image": "kali-linux-2019.1a-amd64.iso"
}
},
{
"name": "2018.4",
"images": {
"hda_disk_image": "kali-linux-persistence-1gb.qcow2",
"cdrom_image": "kali-linux-2018.4-amd64.iso"
}
},
{
"name": "2018.3a",
"images": {
"hda_disk_image": "kali-linux-persistence-1gb.qcow2",
"cdrom_image": "kali-linux-2018.3a-amd64.iso"
}
},
{
"name": "2018.1",
"images": {
"hda_disk_image": "kali-linux-persistence-1gb.qcow2",
"cdrom_image": "kali-linux-2018.1-amd64.iso"
}
},
{
"name": "2017.3",
"images": {
"hda_disk_image": "kali-linux-persistence-1gb.qcow2",
"cdrom_image": "kali-linux-2017.3-amd64.iso"
}
},
{
"name": "2017.2",
"images": {
"hda_disk_image": "kali-linux-persistence-1gb.qcow2",
"cdrom_image": "kali-linux-2017.2-amd64.iso"
}
},
{
"name": "2017.1",
"images": {
"hda_disk_image": "kali-linux-persistence-1gb.qcow2",
"cdrom_image": "kali-linux-2017.1-amd64.iso"
}
},
{
"name": "2016.2",
"images": {
"hda_disk_image": "kali-linux-persistence-1gb.qcow2",
"cdrom_image": "kali-linux-2016.2-amd64.iso"
}
},
{
"name": "2016.1",
"images": {
"hda_disk_image": "kali-linux-persistence-1gb.qcow2",
"cdrom_image": "kali-linux-2016.1-amd64.iso"
}
},
{
"name": "2.0",
"images": {
"hda_disk_image": "kali-linux-persistence-1gb.qcow2",
"cdrom_image": "kali-linux-2.0-amd64.iso"
} }
} }
] ]

View File

@ -0,0 +1,22 @@
{
"appliance_id": "1174e46d-4a29-4554-a1e8-71ed90682295",
"name": "MikroTik WinBox 2026",
"category": "guest",
"description": "Mikrotik's WinBox router management software for GNS3 2026",
"vendor_name": "MikroTik",
"vendor_url": "https://mikrotik.com",
"documentation_url": "https://mikrotik.com/winbox",
"product_name": "MikroTik WinBox",
"product_url": "https://mikrotik.com/winbox",
"registry_version": 4,
"status": "stable",
"availability": "free",
"maintainer": "h3ck13r",
"maintainer_email": "pisdushko1886@gmail.com",
"usage": "Connect WinBox to a MikroTik CHR port and start them both. Open WinBox console and wait until MikroTik appear in neighbors tab, provide credentials and connect.",
"docker": {
"adapters": 1,
"image": "snatao808/winbox-mikrotik-2026",
"console_type": "vnc"
}
}

View File

@ -23,6 +23,15 @@
"kvm": "allow" "kvm": "allow"
}, },
"images": [ "images": [
{
"filename": "openwrt-24.10.2-x86-64-generic-ext4-combined.img",
"version": "24.10.2",
"md5sum": "0769916238524161b0a9a639ae261381",
"filesize": 126353408,
"download_url": "https://downloads.openwrt.org/releases/24.10.2/targets/x86/64/",
"direct_download_url": "https://downloads.openwrt.org/releases/24.10.2/targets/x86/64/openwrt-24.10.2-x86-64-generic-ext4-combined.img.gz",
"compression": "gzip"
},
{ {
"filename": "openwrt-23.05.0-x86-64-generic-ext4-combined.img", "filename": "openwrt-23.05.0-x86-64-generic-ext4-combined.img",
"version": "23.05.0", "version": "23.05.0",
@ -223,7 +232,13 @@
} }
], ],
"versions": [ "versions": [
{ {
"name": "24.10.2",
"images": {
"hda_disk_image": "openwrt-24.10.2-x86-64-generic-ext4-combined.img"
}
},
{
"name": "23.05.0", "name": "23.05.0",
"images": { "images": {
"hda_disk_image": "openwrt-23.05.0-x86-64-generic-ext4-combined.img" "hda_disk_image": "openwrt-23.05.0-x86-64-generic-ext4-combined.img"

View File

@ -25,6 +25,20 @@
"kvm": "require" "kvm": "require"
}, },
"images": [ "images": [
{
"filename": "OPNsense-25.7-nano-amd64.img",
"version": "25.7",
"md5sum": "07aa8b44ebc57fa9ca0145f777901a59",
"filesize": 3221225472,
"download_url": "https://opnsense.c0urier.net/releases/25.7/"
},
{
"filename": "OPNsense-25.1-nano-amd64.img",
"version": "25.1",
"md5sum": "9c119e4fed7dd7c1c162d8be4088d983",
"filesize": 3221225472,
"download_url": "https://opnsense.c0urier.net/releases/25.1/"
},
{ {
"filename": "OPNsense-24.7-nano-amd64.img", "filename": "OPNsense-24.7-nano-amd64.img",
"version": "24.7", "version": "24.7",

View File

@ -30,6 +30,27 @@
"on_close": "shutdown_signal" "on_close": "shutdown_signal"
}, },
"images": [ "images": [
{
"filename": "vyos-1.4.4-kvm-amd64.qcow2",
"version": "1.4.4",
"md5sum": "942dae45d63924b58b65d069890765d4",
"filesize": 518782976,
"download_url": "https://support.vyos.io/"
},
{
"filename": "vyos-1.4.3-kvm-amd64.qcow2",
"version": "1.4.3",
"md5sum": "4eec0ba2ec367bbbdf2d2f212f947746",
"filesize": 515047424,
"download_url": "https://support.vyos.io/"
},
{
"filename": "vyos-1.4.2-kvm-amd64.qcow2",
"version": "1.4.2",
"md5sum": "78c5935e05a7bbd94a639fcee18f5799",
"filesize": 514064384,
"download_url": "https://support.vyos.io/"
},
{ {
"filename": "vyos-1.4.1-kvm-amd64.qcow2", "filename": "vyos-1.4.1-kvm-amd64.qcow2",
"version": "1.4.1", "version": "1.4.1",
@ -116,6 +137,24 @@
} }
], ],
"versions": [ "versions": [
{
"name": "1.4.4",
"images": {
"hda_disk_image": "vyos-1.4.4-kvm-amd64.qcow2"
}
},
{
"name": "1.4.3",
"images": {
"hda_disk_image": "vyos-1.4.3-kvm-amd64.qcow2"
}
},
{
"name": "1.4.2",
"images": {
"hda_disk_image": "vyos-1.4.2-kvm-amd64.qcow2"
}
},
{ {
"name": "1.4.1", "name": "1.4.1",
"images": { "images": {

View File

@ -24,6 +24,7 @@ import socket
import shutil import shutil
import re import re
import logging import logging
import inspect
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -196,7 +197,7 @@ class BaseManager:
node_id = str(uuid4()) node_id = str(uuid4())
node = self._NODE_CLASS(name, node_id, project, self, *args, **kwargs) node = self._NODE_CLASS(name, node_id, project, self, *args, **kwargs)
if asyncio.iscoroutinefunction(node.create): if inspect.iscoroutinefunction(node.create):
await node.create() await node.create()
else: else:
node.create() node.create()
@ -245,7 +246,7 @@ class BaseManager:
""" """
node = self.get_node(node_id) node = self.get_node(node_id)
if asyncio.iscoroutinefunction(node.close): if inspect.iscoroutinefunction(node.close):
await node.close() await node.close()
else: else:
node.close() node.close()

View File

@ -38,9 +38,9 @@ log = logging.getLogger(__name__)
# Be careful to keep it consistent # Be careful to keep it consistent
DOCKER_MINIMUM_API_VERSION = "1.25" DOCKER_MINIMUM_API_VERSION = "1.40"
DOCKER_MINIMUM_VERSION = "1.13" DOCKER_MINIMUM_VERSION = "19.03.8"
DOCKER_PREFERRED_API_VERSION = "1.30" DOCKER_PREFERRED_API_VERSION = "1.44"
CHUNK_SIZE = 1024 * 8 # 8KB CHUNK_SIZE = 1024 * 8 # 8KB
@ -125,22 +125,28 @@ class Docker(BaseManager):
if not self._connected: if not self._connected:
try: try:
self._connected = True self._connected = True
version = await self.query("GET", "version") docker_info = await self.query("GET", "version")
except (aiohttp.ClientError, FileNotFoundError): except (aiohttp.ClientError, FileNotFoundError):
self._connected = False self._connected = False
raise DockerError("Can't connect to docker daemon") raise DockerError("Can't connect to Docker daemon")
docker_version = parse_version(version["ApiVersion"]) api_version = parse_version(docker_info['ApiVersion'])
version = docker_info["Version"]
if docker_version < parse_version(DOCKER_MINIMUM_API_VERSION): if api_version < parse_version(DOCKER_MINIMUM_API_VERSION):
raise DockerError( raise DockerError(f"Docker version is {version}. "
f"Docker version is {version['Version']}. " f"GNS3 requires a minimum version of {DOCKER_MINIMUM_VERSION}"
f"GNS3 requires a minimum version of {DOCKER_MINIMUM_VERSION}"
) )
preferred_api_version = parse_version(DOCKER_PREFERRED_API_VERSION) preferred_api_version = parse_version(DOCKER_PREFERRED_API_VERSION)
if docker_version >= preferred_api_version: if api_version >= preferred_api_version:
self._api_version = DOCKER_PREFERRED_API_VERSION self._api_version = DOCKER_PREFERRED_API_VERSION
else:
# use the Min API version supported by the daemon
self._api_version = docker_info['MinAPIVersion']
log.warning("Using Docker client with the minimum API version {}".format(self._api_version))
log.info("Connected to Docker daemon version {} using API version {}".format(version, self._api_version))
def connector(self): def connector(self):
@ -188,7 +194,7 @@ class Docker(BaseManager):
:param method: HTTP method :param method: HTTP method
:param path: Endpoint in API :param path: Endpoint in API
:param data: Dictionnary with the body. Will be transformed to a JSON :param data: Dictionary with the body. Will be transformed to a JSON
:param params: Parameters added as a query arg :param params: Parameters added as a query arg
:param timeout: Timeout :param timeout: Timeout
:returns: HTTP response :returns: HTTP response
@ -199,12 +205,11 @@ class Docker(BaseManager):
timeout = 60 * 60 * 24 * 31 # One month timeout timeout = 60 * 60 * 24 * 31 # One month timeout
if path == 'version': if path == 'version':
url = "http://docker/v1.24/" + path url = "http://docker/" + path
else: else:
url = "http://docker/v" + DOCKER_MINIMUM_API_VERSION + "/" + path await self._check_connection() # version is use by check connection
url = "http://docker/v" + self._api_version + "/" + path
try: try:
if path != "version": # version is use by check connection
await self._check_connection()
if self._session is None or self._session.closed: if self._session is None or self._session.closed:
connector = self.connector() connector = self.connector()
self._session = aiohttp.ClientSession(connector=connector) self._session = aiohttp.ClientSession(connector=connector)

View File

@ -195,6 +195,14 @@ class DockerVM(BaseNode):
def ethernet_adapters(self): def ethernet_adapters(self):
return self._ethernet_adapters return self._ethernet_adapters
@property
def docker_name(self):
"""
Container name in Docker
"""
return "GNS3.{}.{}".format(self.name, self._project.id)
@property @property
def mac_address(self): def mac_address(self):
""" """
@ -539,7 +547,8 @@ class DockerVM(BaseNode):
if extra_hosts: if extra_hosts:
params["Env"].append(f"GNS3_EXTRA_HOSTS={extra_hosts}") params["Env"].append(f"GNS3_EXTRA_HOSTS={extra_hosts}")
result = await self.manager.query("POST", "containers/create", data=params) # Support name in Doker: [a-zA-Z0-9][a-zA-Z0-9_.-]
result = await self.manager.query("POST", f"containers/create?name={self.docker_name}", data=params)
self._cid = result["Id"] self._cid = result["Id"]
log.info(f"Docker container '{self._name}' [{self._id}] created") log.info(f"Docker container '{self._name}' [{self._id}] created")
if self._cpus > 0: if self._cpus > 0:
@ -745,6 +754,7 @@ class DockerVM(BaseNode):
"-rfbport", str(self.console), "-rfbport", str(self.console),
"-AlwaysShared", "-AlwaysShared",
"-SecurityTypes", "None", "-SecurityTypes", "None",
"-desktop", self.name,
":{}".format(self._display), ":{}".format(self._display),
stdout=fd, stderr=subprocess.STDOUT) stdout=fd, stderr=subprocess.STDOUT)

View File

@ -3,7 +3,12 @@
# script for udhcpc # script for udhcpc
# Copyright (c) 2008 Natanael Copa <natanael.copa@gmail.com> # Copyright (c) 2008 Natanael Copa <natanael.copa@gmail.com>
UDHCPC="/gns3/etc/udhcpc" if [ -d /etc/network ]; then
UDHCPC="/etc/network/udhcpc"
else
UDHCPC="/gns3/etc/udhcpc"
fi
UDHCPC_CONF="$UDHCPC/udhcpc.conf" UDHCPC_CONF="$UDHCPC/udhcpc.conf"
RESOLV_CONF="/etc/resolv.conf" RESOLV_CONF="/etc/resolv.conf"

View File

@ -106,7 +106,7 @@ class ProjectManager:
if project_id is not None and project_id in self._projects: if project_id is not None and project_id in self._projects:
return self._projects[project_id] return self._projects[project_id]
project = Project(name=name, project_id=project_id, path=path, variables=variables) project = Project(name=name, project_id=project_id, path=path, variables=variables)
self._check_available_disk_space(project) #self._check_available_disk_space(project) # FIXME: disabled for now
self._projects[project.id] = project self._projects[project.id] = project
return project return project

View File

@ -13,8 +13,7 @@ logging console discriminator EXCESS
! !
no ip icmp rate-limit unreachable no ip icmp rate-limit unreachable
! !
! due to some bugs with IOU, try to change the following line to 'ip cef' if your routing does not work ip cef
no ip cef
no ip domain lookup no ip domain lookup
! !
! !

View File

@ -12,8 +12,7 @@ no ip icmp rate-limit unreachable
! !
! !
! !
! due to some bugs with IOU, try to change the following line to 'ip cef' if your routing does not work ip cef
no ip cef
no ip domain lookup no ip domain lookup
! !
! !

View File

@ -58,7 +58,7 @@ class CrashReport:
Report crash to a third party service Report crash to a third party service
""" """
DSN = "https://61bb46252cabeebd49ee1e09fb8ba72e@o19455.ingest.us.sentry.io/38482" DSN = "https://1c0ae79fd51b77c674718c6942337300@o19455.ingest.us.sentry.io/38482"
_instance = None _instance = None
def __init__(self): def __init__(self):

View File

@ -78,7 +78,7 @@ async def connect_to_db(app: FastAPI) -> None:
db_path = os.path.join(Config.instance().config_dir, "gns3_controller.db") db_path = os.path.join(Config.instance().config_dir, "gns3_controller.db")
db_url = os.environ.get("GNS3_DATABASE_URI", f"sqlite+aiosqlite:///{db_path}") db_url = os.environ.get("GNS3_DATABASE_URI", f"sqlite+aiosqlite:///{db_path}")
engine = create_async_engine(db_url, connect_args={"check_same_thread": False, "timeout": 20}, future=True) engine = create_async_engine(db_url, connect_args={"check_same_thread": False, "timeout": 20}, future=True, pool_size=512, max_overflow=1024)
alembic_cfg = config.Config() alembic_cfg = config.Config()
alembic_cfg.set_main_option("script_location", "gns3server:db_migrations") alembic_cfg.set_main_option("script_location", "gns3server:db_migrations")
#alembic_cfg.set_main_option('sqlalchemy.url', db_url) #alembic_cfg.set_main_option('sqlalchemy.url', db_url)

View File

@ -20,6 +20,7 @@ import asyncio
import sys import sys
import os import os
import threading import threading
import inspect
async def wait_run_in_executor(func, *args, **kwargs): async def wait_run_in_executor(func, *args, **kwargs):
@ -90,9 +91,6 @@ async def wait_for_process_termination(process, timeout=10):
In theory this can be implemented by just: In theory this can be implemented by just:
await asyncio.wait_for(self._iou_process.wait(), timeout=100) await asyncio.wait_for(self._iou_process.wait(), timeout=100)
But it's broken before Python 3.4:
http://bugs.python.org/issue23140
:param process: An asyncio subprocess :param process: An asyncio subprocess
:param timeout: Timeout in seconds :param timeout: Timeout in seconds
""" """
@ -106,7 +104,7 @@ async def wait_for_process_termination(process, timeout=10):
async def _check_process(process, termination_callback): async def _check_process(process, termination_callback):
if not hasattr(sys, "_called_from_test") or not sys._called_from_test: if not hasattr(sys, "_called_from_test") or not sys._called_from_test:
returncode = await process.wait() returncode = await process.wait()
if asyncio.iscoroutinefunction(termination_callback): if inspect.iscoroutinefunction(termination_callback):
await termination_callback(returncode) await termination_callback(returncode)
else: else:
termination_callback(returncode) termination_callback(returncode)

View File

@ -60,7 +60,14 @@ class FileWatcher:
self._hashed[path] = zlib.adler32(open(path, "rb").read()) self._hashed[path] = zlib.adler32(open(path, "rb").read())
except OSError: except OSError:
self._hashed[path] = None self._hashed[path] = None
asyncio.get_event_loop().call_later(self._delay, self._check_config_file_change)
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.call_later(self._delay, self._check_config_file_change)
def __del__(self): def __del__(self):
self._closed = True self._closed = True

View File

@ -22,8 +22,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__ = "3.0.5" __version__ = "3.0.6"
__version_info__ = (3, 0, 5, 0) __version_info__ = (3, 0, 6, 0)
if "dev" in __version__: if "dev" in __version__:
try: try:

View File

@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "gns3-server" name = "gns3-server"
description = "GNS3 graphical interface for the GNS3 server." description = "GNS3 graphical interface for the GNS3 server."
license = {file = "LICENSE"} license-files = ["LICENSE"]
authors = [ authors = [
{ name = "Jeremy Grossmann", email = "developers@gns3.com" } { name = "Jeremy Grossmann", email = "developers@gns3.com" }
] ]

View File

@ -1,26 +1,24 @@
uvicorn==0.34.2 # uvicorn 0.33 is the last version supporting Python 3.8 uvicorn==0.39.0 # version 0.39.0 is the last version supporting Python 3.9
pydantic==2.11.4 pydantic==2.12.5
fastapi==0.115.12 fastapi==0.128.0
python-multipart==0.0.20 python-multipart==0.0.20 # version 0.0.20 is the last to support Python 3.9
websockets==15.0.1 websockets==15.0.1 # version 15.0.1 is the last to support Python 3.9
aiohttp>=3.11.16,<3.12 aiohttp>=3.13.3,<3.14
async-timeout==5.0.1; python_version < '3.11' aiofiles>=25.1.0,<26.0
aiofiles>=24.1.0,<25.0
Jinja2>=3.1.6,<3.2 Jinja2>=3.1.6,<3.2
sentry-sdk>=2.26.1,<2.27 # optional dependency sentry-sdk>=2.50.0,<3 # optional dependency
psutil>=7.0.0 psutil>=7.2.1
async-timeout>=5.0.1,<5.1 async-timeout>=5.0.1,<5.1; python_version < '3.11' # this library has effectively been upstreamed into Python 3.11+
distro>=1.9.0 distro>=1.9.0
py-cpuinfo>=9.0.0,<10.0 py-cpuinfo>=9.0.0,<10.0
greenlet==3.2.0 # necessary to run sqlalchemy on Python 3.13 greenlet==3.3.1; python_version >= '3.13' # necessary to run sqlalchemy on Python >= 3.13
sqlalchemy==2.0.40 sqlalchemy==2.0.43
aiosqlite==0.21.0 aiosqlite==0.21.0
alembic==1.15.2 alembic==1.15.2
bcrypt==4.3.0 bcrypt==5.0.0
joserfc==1.0.4 joserfc==1.6.1
email-validator==2.2.0 email-validator==2.3.0
watchdog==6.0.0 watchdog==6.0.0
zstandard==0.23.0 zstandard==0.25.0
platformdirs>=2.4.0,<3 # platformdirs >=3 conflicts when building Debian packages platformdirs>=2.4.0,<3 # platformdirs >=3 conflicts when building Debian packages
importlib-resources>=1.3; python_version <= '3.9' truststore>=0.10.4; python_version >= '3.10'
truststore>=0.10.1; python_version >= '3.10'

View File

@ -1,238 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
This script connect to the local GNS3 server and will create a random topology
"""
import sys
import json
import math
import aiohttp
import aiohttp.web
import asyncio
import random
import coloredlogs
import logging
coloredlogs.install(fmt=" %(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)
PROJECT_ID = "9e26e37d-4962-4921-8c0e-136d3b04ba9c"
HOST = "192.168.84.151:3080"
# Use for node names uniqueness
node_i = 1
def die(*args):
log.error(*args)
sys.exit(1)
class HTTPError(Exception):
def __init__(self, method, path, response):
self._method = method
self._path = path
self._response = response
@property
def response(self):
return self._response
@property
def path(self):
return self._path
@property
def method(self):
return self._method
class HTTPConflict(HTTPError):
pass
class HTTPNotFound(HTTPError):
pass
async def query(method, path, body=None, **kwargs):
global session
if body:
kwargs["data"] = json.dumps(body)
async with session.request(method, "http://" + HOST + "/v2" + path, **kwargs) as response:
if response.status == 409:
raise HTTPConflict(method, path, response)
elif response.status == 404:
raise HTTPNotFound(method, path, response)
elif response.status >= 300:
raise HTTPError(method, path, response)
log.info("%s %s %d", method, path, response.status)
if response.headers["content-type"] == "application/json":
return await response.json()
else:
return "{}"
async def post(path, **kwargs):
return await query("POST", path, **kwargs)
async def get(path, **kwargs):
return await query("GET", path, **kwargs)
async def delete(path, **kwargs):
return await query("DELETE", path, **kwargs)
async def create_project():
# Delete project if already exists
response = await get("/projects")
project_exists = False
for project in response:
if project["name"] == "random" and project["project_id"] != PROJECT_ID:
await delete("/projects/" + project["project_id"])
elif project["project_id"] == PROJECT_ID:
project_exists = True
tasks = []
for node in await get("/projects/" + PROJECT_ID + "/nodes"):
tasks.append(delete_node(project, node))
await asyncio.gather(*tasks)
if project_exists:
response = await post("/projects/" + PROJECT_ID + "/open")
else:
response = await post("/projects", body={"name": "random", "project_id": PROJECT_ID, "auto_close": False})
return response
async def create_node(project):
global node_i
r = random.randint(0, 1)
if r == 0:
node_type = "ethernet_switch"
symbol = ":/symbols/ethernet_switch.svg"
elif r == 1:
node_type = "vpcs"
symbol = ":/symbols/vpcs_guest.svg"
response = await post("/projects/{}/nodes".format(project["project_id"]), body={
"node_type": node_type,
"compute_id": "local",
"symbol": symbol,
"name": "Node{}".format(node_i),
"x": (math.floor((node_i - 1) % 12.0) * 100) - 500,
"y": (math.ceil((node_i) / 12.0) * 100) - 300
})
node_i += 1
return response
async def delete_node(project, node):
await delete("/projects/{}/nodes/{}".format(project["project_id"], node["node_id"]))
async def create_link(project, nodes):
"""
Create all possible link of a node
"""
node1 = random.choice(list(nodes.values()))
for port in range(0, 8):
node2 = random.choice(list(nodes.values()))
if node1 == node2:
continue
data = {"nodes":
[
{
"adapter_number": 0,
"node_id": node1["node_id"],
"port_number": port
},
{
"adapter_number": 0,
"node_id": node2["node_id"],
"port_number": port
}
]
}
try:
await post("/projects/{}/links".format(project["project_id"]), body=data)
except (HTTPConflict, HTTPNotFound):
pass
async def build_topology():
global node_i
nodes = {}
project = await create_project()
while True:
rand = random.randint(0, 1000)
if rand < 500: # chance to create a new node
if len(nodes.keys()) < 255: # Limit of VPCS:
node = await create_node(project)
nodes[node["node_id"]] = node
elif rand < 600: # start all nodes
await post("/projects/{}/nodes/start".format(project["project_id"]))
elif rand < 700: # stop all nodes
await post("/projects/{}/nodes/stop".format(project["project_id"]))
elif rand < 950: # create a link
if len(nodes.keys()) >= 2:
await create_link(project, nodes)
elif rand < 999: # chance to delete a node
continue
if len(nodes.keys()) > 0:
node = random.choice(list(nodes.values()))
await delete_node(project, node)
del nodes[node["node_id"]]
elif len(nodes.keys()) > 0: # % chance to delete all nodes
continue
node_i = 1
tasks = []
for node in nodes.values():
tasks.append(delete_node(project, node))
await asyncio.gather(*tasks)
nodes = {}
await asyncio.sleep(0.2)
async def main(loop):
global session
async with aiohttp.ClientSession() as session:
try:
await build_topology()
except HTTPError as error:
try:
j = await error.response.json()
die("%s %s invalid status %d:\n%s", error.method, error.path, error.response.status, json.dumps(j, indent=4))
except (ValueError, aiohttp.ServerDisconnectedError):
die("%s %s invalid status %d", error.method, error.path, error.response.status)
loop = asyncio.get_event_loop()
loop.run_until_complete(main(loop))
if session:
session.close()

View File

@ -303,7 +303,7 @@ class TestControllerProjectRoutes:
params={"include_images": "yes"}) params={"include_images": "yes"})
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
assert response.headers['CONTENT-TYPE'] == 'application/gns3project' assert response.headers['CONTENT-TYPE'] == 'application/gns3project'
assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{}.gns3project"'.format(project.name) assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{name}.gns3project"; filename*=UTF-8\'\'{name}.gns3project'.format(name=project.name)
with open(str(tmpdir / 'project.zip'), 'wb+') as f: with open(str(tmpdir / 'project.zip'), 'wb+') as f:
f.write(response.content) f.write(response.content)
@ -346,8 +346,7 @@ class TestControllerProjectRoutes:
params={"include_images": "0"}) params={"include_images": "0"})
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
assert response.headers['CONTENT-TYPE'] == 'application/gns3project' assert response.headers['CONTENT-TYPE'] == 'application/gns3project'
assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{}.gns3project"'.format(project.name) assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{name}.gns3project"; filename*=UTF-8\'\'{name}.gns3project'.format(name=project.name)
with open(str(tmpdir / 'project.zip'), 'wb+') as f: with open(str(tmpdir / 'project.zip'), 'wb+') as f:
f.write(response.content) f.write(response.content)
@ -412,8 +411,7 @@ class TestControllerProjectRoutes:
if response.status_code == status.HTTP_200_OK: if response.status_code == status.HTTP_200_OK:
assert response.headers['CONTENT-TYPE'] == 'application/gns3project' assert response.headers['CONTENT-TYPE'] == 'application/gns3project'
assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{}.gns3project"'.format(project.name) assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{name}.gns3project"; filename*=UTF-8\'\'{name}.gns3project'.format(name=project.name)
with open(str(tmpdir / 'project.zip'), 'wb+') as f: with open(str(tmpdir / 'project.zip'), 'wb+') as f:
f.write(response.content) f.write(response.content)

View File

@ -183,7 +183,7 @@ class TestAuthTokens:
jwt_secret = config.settings.Controller.jwt_secret_key jwt_secret = config.settings.Controller.jwt_secret_key
token = auth_service.create_access_token(test_user.username) token = auth_service.create_access_token(test_user.username)
with pytest.raises(ValueError): with pytest.raises(ValueError):
jwt.decode(token, jwt_secret, algorithms=["ES256"]) jwt.decode(token, jwt_secret, algorithms=["HS256"])
async def test_can_retrieve_username_from_token( async def test_can_retrieve_username_from_token(
self, self,

View File

@ -49,7 +49,7 @@ async def test_query_success(vm):
vm._session.request = AsyncioMagicMock(return_value=response) vm._session.request = AsyncioMagicMock(return_value=response)
data = await vm.query("POST", "test", data={"a": True}, params={"b": 1}) data = await vm.query("POST", "test", data={"a": True}, params={"b": 1})
vm._session.request.assert_called_with('POST', vm._session.request.assert_called_with('POST',
'http://docker/v1.25/test', 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION),
data='{"a": true}', data='{"a": true}',
headers={'content-type': 'application/json'}, headers={'content-type': 'application/json'},
params={'b': 1}, params={'b': 1},
@ -72,7 +72,7 @@ async def test_query_error(vm):
with pytest.raises(DockerError): with pytest.raises(DockerError):
await vm.query("POST", "test", data={"a": True}, params={"b": 1}) await vm.query("POST", "test", data={"a": True}, params={"b": 1})
vm._session.request.assert_called_with('POST', vm._session.request.assert_called_with('POST',
'http://docker/v1.25/test', 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION),
data='{"a": true}', data='{"a": true}',
headers={'content-type': 'application/json'}, headers={'content-type': 'application/json'},
params={'b': 1}, params={'b': 1},
@ -93,7 +93,7 @@ async def test_query_error_json(vm):
with pytest.raises(DockerError): with pytest.raises(DockerError):
await vm.query("POST", "test", data={"a": True}, params={"b": 1}) await vm.query("POST", "test", data={"a": True}, params={"b": 1})
vm._session.request.assert_called_with('POST', vm._session.request.assert_called_with('POST',
'http://docker/v1.25/test', 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION),
data='{"a": true}', data='{"a": true}',
headers={'content-type': 'application/json'}, headers={'content-type': 'application/json'},
params={'b': 1}, params={'b': 1},
@ -188,7 +188,9 @@ async def test_docker_check_connection_docker_minimum_version(vm):
async def test_docker_check_connection_docker_preferred_version_against_newer(vm): async def test_docker_check_connection_docker_preferred_version_against_newer(vm):
response = { response = {
'ApiVersion': '1.31' 'ApiVersion': '1.52',
'Version': '29.0.1',
} }
with patch("gns3server.compute.docker.Docker.connector"), \ with patch("gns3server.compute.docker.Docker.connector"), \
@ -202,7 +204,9 @@ async def test_docker_check_connection_docker_preferred_version_against_newer(vm
async def test_docker_check_connection_docker_preferred_version_against_older(vm): async def test_docker_check_connection_docker_preferred_version_against_older(vm):
response = { response = {
'ApiVersion': '1.27', 'ApiVersion': '1.43',
'Version': '24.0.2',
'MinAPIVersion': '1.40'
} }
with patch("gns3server.compute.docker.Docker.connector"), \ with patch("gns3server.compute.docker.Docker.connector"), \
@ -212,6 +216,21 @@ async def test_docker_check_connection_docker_preferred_version_against_older(vm
assert vm._api_version == DOCKER_MINIMUM_API_VERSION assert vm._api_version == DOCKER_MINIMUM_API_VERSION
@pytest.mark.asyncio
async def test_docker_check_connection_docker_unsupported_version(vm):
response = {
'ApiVersion': '1.25',
'Version': '1.13.1',
}
with patch("gns3server.compute.docker.Docker.connector"), \
asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response):
vm._connected = False
with pytest.raises(DockerError) as e:
await vm._check_connection()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_install_busybox(): async def test_install_busybox():

View File

@ -99,7 +99,7 @@ async def test_create(compute_project, manager):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest") vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest")
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -150,7 +150,7 @@ async def test_create_with_tag(compute_project, manager):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:16.04") vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:16.04")
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -204,7 +204,7 @@ async def test_create_vnc(compute_project, manager):
vm._start_vnc = MagicMock() vm._start_vnc = MagicMock()
vm._display = 42 vm._display = 42
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -356,7 +356,7 @@ async def test_create_start_cmd(compute_project, manager):
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest") vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest")
vm._start_command = "/bin/ls" vm._start_command = "/bin/ls"
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -469,7 +469,7 @@ async def test_create_image_not_available(compute_project, manager):
with asyncio_patch("gns3server.compute.docker.DockerVM.pull_image", return_value=True) as mock_pull: with asyncio_patch("gns3server.compute.docker.DockerVM.pull_image", return_value=True) as mock_pull:
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -524,7 +524,7 @@ async def test_create_with_user(compute_project, manager):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest") vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest")
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -624,7 +624,7 @@ async def test_create_with_extra_volumes_duplicate_1_image(compute_project, mana
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1"]) vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1"])
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -680,7 +680,7 @@ async def test_create_with_extra_volumes_duplicate_2_user(compute_project, manag
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1", "/vol/1"]) vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1", "/vol/1"])
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -736,7 +736,7 @@ async def test_create_with_extra_volumes_duplicate_3_subdir(compute_project, man
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1/", "/vol"]) vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1/", "/vol"])
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -792,7 +792,7 @@ async def test_create_with_extra_volumes_duplicate_4_backslash(compute_project,
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol//", "/vol"]) vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol//", "/vol"])
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -848,7 +848,7 @@ async def test_create_with_extra_volumes_duplicate_5_subdir_issue_1595(compute_p
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/etc"]) vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/etc"])
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -899,7 +899,7 @@ async def test_create_with_extra_volumes_duplicate_6_subdir_issue_1595(compute_p
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/etc/test", "/etc"]) vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/etc/test", "/etc"])
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -956,7 +956,7 @@ async def test_create_with_extra_volumes(compute_project, manager):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/2"]) vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/2"])
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -1243,7 +1243,7 @@ async def test_update(vm):
await vm.update() await vm.update()
mock_query.assert_any_call("DELETE", "containers/e90e34656842", params={"force": 1, "v": 1}) mock_query.assert_any_call("DELETE", "containers/e90e34656842", params={"force": 1, "v": 1})
mock_query.assert_any_call("POST", "containers/create", data={ mock_query.assert_any_call("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -1325,7 +1325,7 @@ async def test_update_running(vm):
await vm.update() await vm.update()
mock_query.assert_any_call("DELETE", "containers/e90e34656842", params={"force": 1, "v": 1}) mock_query.assert_any_call("DELETE", "containers/e90e34656842", params={"force": 1, "v": 1})
mock_query.assert_any_call("POST", "containers/create", data={ mock_query.assert_any_call("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -1685,7 +1685,7 @@ async def test_start_vnc(vm):
with asyncio_patch("asyncio.create_subprocess_exec") as mock_exec: with asyncio_patch("asyncio.create_subprocess_exec") as mock_exec:
await vm._start_vnc() await vm._start_vnc()
assert vm._display is not None assert vm._display is not None
assert mock_exec.call_args[0] == ("/bin/Xtigervnc", "-extension", "MIT-SHM", "-geometry", vm.console_resolution, "-depth", "16", "-interface", "127.0.0.1", "-rfbport", str(vm.console), "-AlwaysShared", "-SecurityTypes", "None", ":{}".format(vm._display)) assert mock_exec.call_args[0] == ("/bin/Xtigervnc", "-extension", "MIT-SHM", "-geometry", vm.console_resolution, "-depth", "16", "-interface", "127.0.0.1", "-rfbport", str(vm.console), "-AlwaysShared", "-SecurityTypes", "None", "-desktop", "test", ":{}".format(vm._display))
mock_wait.assert_called_with("/tmp/.X11-unix/X{}".format(vm._display)) mock_wait.assert_called_with("/tmp/.X11-unix/X{}".format(vm._display))
@ -1791,7 +1791,7 @@ async def test_cpus(compute_project, manager):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", cpus=0.5) vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", cpus=0.5)
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,
@ -1842,7 +1842,7 @@ async def test_memory(compute_project, manager):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", memory=32) vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", memory=32)
await vm.create() await vm.create()
mock.assert_called_with("POST", "containers/create", data={ mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True, "Tty": True,
"OpenStdin": True, "OpenStdin": True,
"StdinOnce": False, "StdinOnce": False,

View File

@ -18,11 +18,9 @@
import asyncio import asyncio
import pytest import pytest
import sys
from unittest.mock import MagicMock
from gns3server.utils.asyncio import wait_run_in_executor, subprocess_check_output, wait_for_process_termination, locking from gns3server.utils.asyncio import wait_run_in_executor, subprocess_check_output, locking
from tests.utils import AsyncioMagicMock
@pytest.mark.asyncio @pytest.mark.asyncio

View File

@ -1,4 +1,4 @@
-r requirements.txt -r requirements.txt
pywin32==306 pywin32==311
wmi==1.5.1 wmi==1.5.1