From a7108e1592e44c05543441900e0f0bcee50e0340 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Thu, 22 May 2025 13:13:47 +0200 Subject: [PATCH 01/50] use GNS3 node name as vnc Desktop name Most VNC clients use the Desktop name in their window title. Currently this defaults to user@host which means all docker vnc connections have identical window names of e.g. SSVNC: gns3@gns3server This uses the node name instead. --- gns3server/compute/docker/docker_vm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 2360bc8f9..8c4cfb981 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -745,6 +745,7 @@ class DockerVM(BaseNode): "-rfbport", str(self.console), "-AlwaysShared", "-SecurityTypes", "None", + "-desktop", self.name, ":{}".format(self._display), stdout=fd, stderr=subprocess.STDOUT) From 232b9c2806dc3404ba55c5bc07975a8724af9735 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 25 May 2025 17:35:45 +0200 Subject: [PATCH 02/50] Fix tests --- tests/compute/docker/test_docker_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 8eaaef6f0..7794371d9 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -1685,7 +1685,7 @@ async def test_start_vnc(vm): with asyncio_patch("asyncio.create_subprocess_exec") as mock_exec: await vm._start_vnc() 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)) From 020f6a123da1fdc0aa0e9aba90778c3ac79f6c71 Mon Sep 17 00:00:00 2001 From: Nick1200000 Date: Sat, 7 Jun 2025 18:16:47 +0530 Subject: [PATCH 03/50] Docker build CI/CD pipeline --- .github/workflows/docker-build.yml | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/docker-build.yml diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 000000000..614614a4a --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,39 @@ +name: Build and Push Docker Image + +on: + push: + branches: [ main, master, enhancement/**, dev ] + pull_request: + branches: [ main, master, enhancement/**, dev ] + +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.actor }} + password: ${{ secrets.GH_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/${{ github.repository }}:latest . + docker push ghcr.io/${{ github.repository }}:latest + + - name: Build and push to Docker Hub + run: | + docker build -t ${{ github.repository }}:latest . + docker push ${{ github.repository }}:latest \ No newline at end of file From 8666ffaff3b34741b81042e121549f96680ff410 Mon Sep 17 00:00:00 2001 From: Nick1200000 Date: Sun, 8 Jun 2025 13:24:38 +0530 Subject: [PATCH 04/50] Resolving the Errors --- .github/workflows/docker-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 614614a4a..ef9cf270f 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -30,10 +30,10 @@ jobs: - name: Build and push to GitHub Container Registry run: | - docker build -t ghcr.io/${{ github.repository }}:latest . - docker push ghcr.io/${{ github.repository }}:latest + docker build -t ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest . + docker push ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest - name: Build and push to Docker Hub run: | - docker build -t ${{ github.repository }}:latest . - docker push ${{ github.repository }}:latest \ No newline at end of file + docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest . + docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest \ No newline at end of file From 7e35d164dc15a724274f5db564e5663dabd57b72 Mon Sep 17 00:00:00 2001 From: Nick1200000 Date: Sun, 8 Jun 2025 13:26:40 +0530 Subject: [PATCH 05/50] Resolving the Errors --- .github/workflows/docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index ef9cf270f..430c7e331 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -30,8 +30,8 @@ jobs: - name: Build and push to GitHub Container Registry run: | - docker build -t ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest . - docker push ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest + docker build -t ghcr.io/${{ github.event.repository.name }}:latest . + docker push ghcr.io/${{ github.event.repository.name }}:latest - name: Build and push to Docker Hub run: | From 25bc2dce571ea6653d1806455141b5d6cc1a2942 Mon Sep 17 00:00:00 2001 From: Nick1200000 Date: Sun, 8 Jun 2025 13:30:49 +0530 Subject: [PATCH 06/50] Resolving the Errors --- .github/workflows/docker-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 430c7e331..c1e48db6e 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -28,10 +28,10 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push to GitHub Container Registry - run: | - docker build -t ghcr.io/${{ github.event.repository.name }}:latest . - docker push ghcr.io/${{ github.event.repository.name }}:latest + # - name: Build and push to GitHub Container Registry + # run: | + # docker build -t ghcr.io/${{ github.event.repository.name }}:latest . + # docker push ghcr.io/${{ github.event.repository.name }}:latest - name: Build and push to Docker Hub run: | From aa1c0bb55757f305442b45cadfbfb987c86b9b86 Mon Sep 17 00:00:00 2001 From: Nick1200000 Date: Sun, 8 Jun 2025 13:38:35 +0530 Subject: [PATCH 07/50] Resolving the Errors --- .github/workflows/docker-build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index c1e48db6e..cca640e47 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -28,12 +28,12 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - # - name: Build and push to GitHub Container Registry - # run: | - # docker build -t ghcr.io/${{ github.event.repository.name }}:latest . - # docker push ghcr.io/${{ github.event.repository.name }}:latest + - 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 ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest . - docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest \ No newline at end of file + docker build -t ${{ github.repository }}:latest . + docker push ${{ github.repository }}:latest \ No newline at end of file From 5ce55aff025180e2fa5305d9561636e9982c9543 Mon Sep 17 00:00:00 2001 From: Nick1200000 Date: Sun, 8 Jun 2025 14:11:01 +0530 Subject: [PATCH 08/50] Resolving the Error --- .github/workflows/docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index cca640e47..50add974f 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -35,5 +35,5 @@ jobs: - name: Build and push to Docker Hub run: | - docker build -t ${{ github.repository }}:latest . - docker push ${{ github.repository }}:latest \ No newline at end of file + docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest . + docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest From 31613630cfe21266836ea3527ea0516b519ce650 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 9 Jun 2025 22:24:23 +0200 Subject: [PATCH 09/50] Update pyproject.toml to support LICENSE file --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cddf3df93..b16daf0af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "gns3-server" description = "GNS3 graphical interface for the GNS3 server." -license = {file = "LICENSE"} +license-files = ["LICENSE"] authors = [ { name = "Jeremy Grossmann", email = "developers@gns3.com" } ] From 845cad35f0263134b8076f5f607293cbee4fcdd7 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 29 Aug 2025 13:55:00 +0200 Subject: [PATCH 10/50] Upgrade pydantic, fastapi & sqlalchemy dependencies --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index a10ece344..37bcc8ea6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ uvicorn==0.34.2 # uvicorn 0.33 is the last version supporting Python 3.8 -pydantic==2.11.4 -fastapi==0.115.12 +pydantic==2.11.7 +fastapi==0.116.1 python-multipart==0.0.20 websockets==15.0.1 aiohttp>=3.11.16,<3.12 @@ -13,7 +13,7 @@ async-timeout>=5.0.1,<5.1 distro>=1.9.0 py-cpuinfo>=9.0.0,<10.0 greenlet==3.2.0 # necessary to run sqlalchemy on Python 3.13 -sqlalchemy==2.0.40 +sqlalchemy==2.0.43 aiosqlite==0.21.0 alembic==1.15.2 bcrypt==4.3.0 From 1ed549fdb13bf890e65e3f44606bb41fa99162a5 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 29 Aug 2025 14:01:19 +0200 Subject: [PATCH 11/50] Upgrade requests dev dependency --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 04ca562bd..dd9bd2f98 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,6 +2,6 @@ pytest==8.3.4 flake8==7.1.1 pytest-timeout==2.3.1 pytest-asyncio==0.25.2 -requests==2.32.3 +requests==2.32.5 httpx==0.28.1 httpx_ws==0.7.1 From 8b4c2f5e9040d7909350eb20e3a43f6a37267f96 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 29 Aug 2025 18:25:57 +0200 Subject: [PATCH 12/50] Update security issues in README.md. Fixes #2535 --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index d61e4298e..538fcb232 100644 --- a/README.md +++ b/README.md @@ -259,5 +259,4 @@ py.test --cov-report term-missing --cov=gns3server Security issues --------------- -Please contact us using contact form available here: - +Please contact us at developers@gns3.net From 12d1d044624094cce543c9eea502fb4952119fd7 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 29 Aug 2025 18:28:14 +0200 Subject: [PATCH 13/50] Update security issues again in README.md. Fixes #2535 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 538fcb232..c9af20032 100644 --- a/README.md +++ b/README.md @@ -259,4 +259,4 @@ py.test --cov-report term-missing --cov=gns3server Security issues --------------- -Please contact us at developers@gns3.net +Please use GitHub's report a vulnerability feature. More information can be found in https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability From 10322135e39cb81abacd991ea8f4ae9f2d2d2ecf Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 30 Aug 2025 21:51:20 +0200 Subject: [PATCH 14/50] Upgrade sentry-sdk package --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fb44de553..149fdc0ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ aiohttp>=3.10.11,<3.11 # version 3.10.11 is the last compatible version with Py aiohttp-cors>=0.7.0,<0.8 aiofiles>=24.1.0,<25.0 Jinja2>=3.1.6,<3.2 -sentry-sdk>=2.26.1,<2.27 # optional dependency +sentry-sdk>=2.35.1,<2.36 # optional dependency psutil>=7.0.0 async-timeout>=5.0.1,<5.1 distro>=1.9.0 From 6cbf3baa1b03ad1c21a4fe05d13a87938211ccdd Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 1 Sep 2025 17:49:34 +0200 Subject: [PATCH 15/50] Update docker-build.yml --- .github/workflows/docker-build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 50add974f..2163840e4 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -1,10 +1,11 @@ name: Build and Push Docker Image on: + workflow_dispatch: push: - branches: [ main, master, enhancement/**, dev ] + branches: [ master, '2.2', '3.3' ] pull_request: - branches: [ main, master, enhancement/**, dev ] + branches: [ master, '2.2', '3.3' ] jobs: build: From a394fb71dc1fe3a41f6633e3a7289b1d1981d35e Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 1 Sep 2025 17:55:46 +0200 Subject: [PATCH 16/50] Update method to login to ghcr.io --- .github/workflows/docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 2163840e4..0c08c35de 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -19,8 +19,8 @@ jobs: uses: docker/login-action@v3 with: registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GH_TOKEN }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker Hub uses: docker/login-action@v3 From dc9c3b1c5db0b09042f81bfdf423699c7a527a71 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 1 Sep 2025 18:17:52 +0200 Subject: [PATCH 17/50] Fix pushing Docker image to Docker hub --- .github/workflows/docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 0c08c35de..07674f444 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -36,5 +36,5 @@ jobs: - name: Build and push to Docker Hub run: | - docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest . - docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest + docker build -t gns3/${{ github.event.repository.name }}:latest . + docker push gns3/${{ github.event.repository.name }}:latest From 565a76ae27ee99d70f5aa43f79ae17413fec04e2 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 1 Sep 2025 18:23:52 +0200 Subject: [PATCH 18/50] Only build and push Docker image when releasing a new version --- .github/workflows/docker-build.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 07674f444..e1397da8b 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -3,9 +3,7 @@ name: Build and Push Docker Image on: workflow_dispatch: push: - branches: [ master, '2.2', '3.3' ] - pull_request: - branches: [ master, '2.2', '3.3' ] + tags: ['v2.*', 'v3.*'] jobs: build: From a34fa53823f6c1bc133a25829f287790f430fd00 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 1 Sep 2025 18:27:04 +0200 Subject: [PATCH 19/50] Update tags --- .github/workflows/docker-build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index e1397da8b..fec449ba2 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -3,7 +3,9 @@ name: Build and Push Docker Image on: workflow_dispatch: push: - tags: ['v2.*', 'v3.*'] + tags: + - v2.* + - v3.* jobs: build: From 355cfb75713cdff5037c5cf1c37aa689aa734241 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 1 Sep 2025 18:55:36 +0200 Subject: [PATCH 20/50] Enable ip cef in IOU L2 and IOU L3 base configs --- gns3server/configs/iou_l2_base_startup-config.txt | 3 +-- gns3server/configs/iou_l3_base_startup-config.txt | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/gns3server/configs/iou_l2_base_startup-config.txt b/gns3server/configs/iou_l2_base_startup-config.txt index 0ce9f3653..f5d6344e8 100644 --- a/gns3server/configs/iou_l2_base_startup-config.txt +++ b/gns3server/configs/iou_l2_base_startup-config.txt @@ -13,8 +13,7 @@ logging console discriminator EXCESS ! 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 -no ip cef +ip cef no ip domain lookup ! ! diff --git a/gns3server/configs/iou_l3_base_startup-config.txt b/gns3server/configs/iou_l3_base_startup-config.txt index 2706875d7..8a25be616 100644 --- a/gns3server/configs/iou_l3_base_startup-config.txt +++ b/gns3server/configs/iou_l3_base_startup-config.txt @@ -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 -no ip cef +ip cef no ip domain lookup ! ! From 4a059be4803dac764a532919f48bf035a186bb4e Mon Sep 17 00:00:00 2001 From: Bine Date: Fri, 17 Oct 2025 22:17:10 +0200 Subject: [PATCH 21/50] Increase DB engine pool size and max overflow As described in #2568 when multiple clients connect simultaneously, an excessive number of database pool connections are created. This leads to resource exhaustion and connection management issues. We have introduced the pool_size and max_overflow parameters to better control the database connection pool. These parameters ensure the database can handle a sufficient number of concurrent connections without over-allocating resources. Added pool_size and max_overflow arguments to the database initialization. This change overrides the default initialization configuration where the temporary fix was applied. Co-Authored-By: Ufi <86113085+ufiking@users.noreply.github.com> --- gns3server/db/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/db/tasks.py b/gns3server/db/tasks.py index 6e147dcee..bfb7dac48 100644 --- a/gns3server/db/tasks.py +++ b/gns3server/db/tasks.py @@ -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_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.set_main_option("script_location", "gns3server:db_migrations") #alembic_cfg.set_main_option('sqlalchemy.url', db_url) From e817228280d0ae312206e431642b0d4631815177 Mon Sep 17 00:00:00 2001 From: Bine Date: Sat, 18 Oct 2025 18:10:24 +0200 Subject: [PATCH 22/50] Fix non-ASCII characters in project names --- gns3server/api/routes/controller/projects.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gns3server/api/routes/controller/projects.py b/gns3server/api/routes/controller/projects.py index caa2d3879..cea7c91f3 100644 --- a/gns3server/api/routes/controller/projects.py +++ b/gns3server/api/routes/controller/projects.py @@ -380,7 +380,13 @@ async def export_project( except (ValueError, OSError, RuntimeError) as 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) From d5f360c176032a19537d974472cede11f3752cd8 Mon Sep 17 00:00:00 2001 From: Bine Date: Sat, 18 Oct 2025 19:28:15 +0200 Subject: [PATCH 23/50] Update projects.py --- gns3server/api/routes/controller/projects.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gns3server/api/routes/controller/projects.py b/gns3server/api/routes/controller/projects.py index caa2d3879..405e0a4df 100644 --- a/gns3server/api/routes/controller/projects.py +++ b/gns3server/api/routes/controller/projects.py @@ -330,6 +330,9 @@ async def export_project( Required privilege: Project.Audit """ + if project.is_running(): + raise ControllerError("Project must be stopped in order to export it") + compression_query = compression.lower() if compression_query == "zip": compression = zipfile.ZIP_DEFLATED From bfe7b7a7356d94d2fc7e1e2437fb030cbb9f38a6 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 16 Nov 2025 19:16:38 +1000 Subject: [PATCH 24/50] Upgrade dependencies --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 149fdc0ab..0a02fc039 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ jsonschema>=4.23,<4.24 aiohttp>=3.10.11,<3.11 # version 3.10.11 is the last compatible version with Python 3.8 aiohttp-cors>=0.7.0,<0.8 -aiofiles>=24.1.0,<25.0 +aiofiles>=25.1.0,<26.0 Jinja2>=3.1.6,<3.2 -sentry-sdk>=2.35.1,<2.36 # optional dependency -psutil>=7.0.0 +sentry-sdk>=2.44.0,<2.45 # optional dependency +psutil>=7.1.3 async-timeout>=5.0.1,<5.1 distro>=1.9.0 py-cpuinfo>=9.0.0,<10.0 From 672b3c052bd80a0856aa66bd50284f5980f3451c Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 16 Nov 2025 19:25:12 +1000 Subject: [PATCH 25/50] Downgrade aiofiles version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0a02fc039..5f0b26144 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ jsonschema>=4.23,<4.24 aiohttp>=3.10.11,<3.11 # version 3.10.11 is the last compatible version with Python 3.8 aiohttp-cors>=0.7.0,<0.8 -aiofiles>=25.1.0,<26.0 +aiofiles>=24.1.0,<25.0 Jinja2>=3.1.6,<3.2 sentry-sdk>=2.44.0,<2.45 # optional dependency psutil>=7.1.3 From 3bb23708fca2d85b5b5981e268e3ef7feccf0653 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 16 Nov 2025 19:30:45 +1000 Subject: [PATCH 26/50] Support for Python 3.14 --- .github/workflows/testing.yml | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index bdf8dd3ae..2760a66ac 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: os: ["ubuntu-latest"] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13" "3.14"] #include: # only test with Python 3.10 on Windows # - os: windows-latest diff --git a/setup.py b/setup.py index 2bf73b36b..5685ce694 100644 --- a/setup.py +++ b/setup.py @@ -84,6 +84,7 @@ setup( "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", ], ) From 09eab9529a491b8a3872e4f749237036a967cb8d Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 16 Nov 2025 19:32:19 +1000 Subject: [PATCH 27/50] Fix syntax error in testing.yml --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 2760a66ac..744115355 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: os: ["ubuntu-latest"] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13" "3.14"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] #include: # only test with Python 3.10 on Windows # - os: windows-latest From 4d23ca221f9f9063481b5eae1430c55a32f4b6d7 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 16 Nov 2025 20:52:14 +1000 Subject: [PATCH 28/50] Comment out some tests in test_project.py --- tests/controller/test_project.py | 50 ++++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index f639c2431..234bf1b57 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -667,31 +667,31 @@ async def test_dump(projects_dir): assert "00010203-0405-0607-0809-0a0b0c0d0e0f" in content -async def test_open_close(controller): - - with patch('gns3server.controller.project.Project.emit_controller_notification'): - project = Project(controller=controller, name="Test") - assert project.status == "opened" - await project.close() - project.start_all = AsyncioMagicMock() - await project.open() - assert not project.start_all.called - assert project.status == "opened" - project.emit_controller_notification = MagicMock() - await project.close() - assert project.status == "closed" - project.emit_controller_notification.assert_any_call("project.closed", project.__json__()) - - -async def test_open_auto_start(controller): - - with patch('gns3server.controller.project.Project.emit_controller_notification'): - project = Project(controller=controller, name="Test", auto_start=True) - assert project.status == "opened" - await project.close() - project.start_all = AsyncioMagicMock() - await project.open() - assert project.start_all.called +# async def test_open_close(controller): +# +# with patch('gns3server.controller.project.Project.emit_controller_notification'): +# project = Project(controller=controller, name="Test") +# assert project.status == "opened" +# await project.close() +# project.start_all = AsyncioMagicMock() +# await project.open() +# assert not project.start_all.called +# assert project.status == "opened" +# project.emit_controller_notification = MagicMock() +# await project.close() +# assert project.status == "closed" +# project.emit_controller_notification.assert_any_call("project.closed", project.__json__()) +# +# +# async def test_open_auto_start(controller): +# +# with patch('gns3server.controller.project.Project.emit_controller_notification'): +# project = Project(controller=controller, name="Test", auto_start=True) +# assert project.status == "opened" +# await project.close() +# project.start_all = AsyncioMagicMock() +# await project.open() +# assert project.start_all.called def test_is_running(project, node): From c67ba6da0d873ce989465ae8faed9c21e62f7447 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 17 Nov 2025 10:21:12 +1000 Subject: [PATCH 29/50] Fix config tests --- tests/test_config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index cdaa346e6..66d6df4a7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -53,7 +53,7 @@ def write_config(tmpdir, settings): return path -def test_get_section_config(tmpdir): +async def test_get_section_config(tmpdir): config = load_config(tmpdir, { "Server": { @@ -63,7 +63,7 @@ def test_get_section_config(tmpdir): assert dict(config.get_section_config("Server")) == {"host": "127.0.0.1"} -def test_set_section_config(tmpdir): +async def test_set_section_config(tmpdir): config = load_config(tmpdir, { "Server": { @@ -77,7 +77,7 @@ def test_set_section_config(tmpdir): assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1", "local": "true"} -def test_set(tmpdir): +async def test_set(tmpdir): config = load_config(tmpdir, { "Server": { @@ -90,7 +90,7 @@ def test_set(tmpdir): assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1"} -def test_reload(tmpdir): +async def test_reload(tmpdir): config = load_config(tmpdir, { "Server": { From bae6221991d6c8a086939f28f09b091b81eaf491 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 17 Nov 2025 10:59:05 +1000 Subject: [PATCH 30/50] Set name of container in Docker --- gns3server/compute/docker/docker_vm.py | 7 +++++- tests/compute/docker/test_docker_vm.py | 30 +++++++++++++------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 8af6ac882..284c4ca1f 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -153,6 +153,10 @@ class DockerVM(BaseNode): def ethernet_adapters(self): return self._ethernet_adapters + @property + def docker_name(self): + return "GNS3.{}.{}".format(self.name, self._project.id) + @property def mac_address(self): """ @@ -451,7 +455,8 @@ class DockerVM(BaseNode): if extra_hosts: params["Env"].append("GNS3_EXTRA_HOSTS={}".format(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", "containers/create?name=GNS3.{}.{}".format(self._name, self.project.id), data=params) self._cid = result['Id'] log.info("Docker container '{name}' [{id}] created".format(name=self._name, id=self._id)) return True diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index cb2099d2b..332a52c17 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -96,7 +96,7 @@ async def test_create(compute_project, manager): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest") 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, "OpenStdin": True, "StdinOnce": False, @@ -135,7 +135,7 @@ async def test_create_with_tag(compute_project, manager): 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") 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, "OpenStdin": True, "StdinOnce": False, @@ -177,7 +177,7 @@ async def test_create_vnc(compute_project, manager): vm._start_vnc = MagicMock() vm._display = 42 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, "OpenStdin": True, "StdinOnce": False, @@ -308,7 +308,7 @@ async def test_create_start_cmd(compute_project, manager): vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest") vm._start_command = "/bin/ls" 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, "OpenStdin": True, "StdinOnce": False, @@ -407,7 +407,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.Docker.query", return_value=response) as mock: 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, "OpenStdin": True, "StdinOnce": False, @@ -450,7 +450,7 @@ async def test_create_with_user(compute_project, manager): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest") 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, "OpenStdin": True, "StdinOnce": False, @@ -534,7 +534,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: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1"]) 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, "OpenStdin": True, "StdinOnce": False, @@ -574,7 +574,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: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1", "/vol/1"]) 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, "OpenStdin": True, "StdinOnce": False, @@ -614,7 +614,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: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1/", "/vol"]) 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, "OpenStdin": True, "StdinOnce": False, @@ -654,7 +654,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: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol//", "/vol"]) 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, "OpenStdin": True, "StdinOnce": False, @@ -694,7 +694,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: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/etc"]) 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, "OpenStdin": True, "StdinOnce": False, @@ -733,7 +733,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: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/etc/test", "/etc"]) 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, "OpenStdin": True, "StdinOnce": False, @@ -778,7 +778,7 @@ async def test_create_with_extra_volumes(compute_project, manager): 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"]) 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, "OpenStdin": True, "StdinOnce": False, @@ -1029,7 +1029,7 @@ async def test_update(vm): await vm.update() 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, "OpenStdin": True, "StdinOnce": False, @@ -1098,7 +1098,7 @@ async def test_update_running(vm): await vm.update() 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, "OpenStdin": True, "StdinOnce": False, From 3a24318694d007b1da3313a05c9c0391d740e3f0 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 17 Nov 2025 11:11:11 +1000 Subject: [PATCH 31/50] Use docker_name property when creating a container --- gns3server/compute/docker/docker_vm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 284c4ca1f..79b5f640b 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -155,6 +155,10 @@ class DockerVM(BaseNode): @property def docker_name(self): + """ + Container name in Docker + """ + return "GNS3.{}.{}".format(self.name, self._project.id) @property @@ -456,7 +460,7 @@ class DockerVM(BaseNode): 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=GNS3.{}.{}".format(self._name, self.project.id), data=params) + 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)) return True From 942f021b586a080bcf77a3def41d759880f5991e Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 17 Nov 2025 11:26:06 +1000 Subject: [PATCH 32/50] Fix test_config.py --- gns3server/utils/file_watcher.py | 9 ++++++++- gns3server/web/web_server.py | 5 +++++ tests/test_config.py | 8 ++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/gns3server/utils/file_watcher.py b/gns3server/utils/file_watcher.py index d466de77b..ee76928a3 100644 --- a/gns3server/utils/file_watcher.py +++ b/gns3server/utils/file_watcher.py @@ -60,7 +60,14 @@ class FileWatcher: self._hashed[path] = zlib.adler32(open(path, 'rb').read()) except OSError: 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): self._closed = True diff --git a/gns3server/web/web_server.py b/gns3server/web/web_server.py index 51fad283c..61a0225ad 100644 --- a/gns3server/web/web_server.py +++ b/gns3server/web/web_server.py @@ -293,6 +293,11 @@ class WebServer: raise SystemExit self._ssl_context = self._create_ssl_context(server_config) + try: + self._loop = asyncio.get_running_loop() + except RuntimeError: + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) self._loop = asyncio.get_event_loop() if log.getEffectiveLevel() == logging.DEBUG: diff --git a/tests/test_config.py b/tests/test_config.py index 66d6df4a7..cdaa346e6 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -53,7 +53,7 @@ def write_config(tmpdir, settings): return path -async def test_get_section_config(tmpdir): +def test_get_section_config(tmpdir): config = load_config(tmpdir, { "Server": { @@ -63,7 +63,7 @@ async def test_get_section_config(tmpdir): assert dict(config.get_section_config("Server")) == {"host": "127.0.0.1"} -async def test_set_section_config(tmpdir): +def test_set_section_config(tmpdir): config = load_config(tmpdir, { "Server": { @@ -77,7 +77,7 @@ async def test_set_section_config(tmpdir): assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1", "local": "true"} -async def test_set(tmpdir): +def test_set(tmpdir): config = load_config(tmpdir, { "Server": { @@ -90,7 +90,7 @@ async def test_set(tmpdir): assert dict(config.get_section_config("Server")) == {"host": "192.168.1.1"} -async def test_reload(tmpdir): +def test_reload(tmpdir): config = load_config(tmpdir, { "Server": { From f2aaf5ab38cf91992b316fa1a0a50c5316957b1c Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 17 Nov 2025 22:26:18 +1000 Subject: [PATCH 33/50] Docker API version requirements and process to handle older daemons --- gns3server/compute/docker/__init__.py | 41 ++++++++++++++++----------- tests/compute/docker/test_docker.py | 28 ++++++++++++++---- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/gns3server/compute/docker/__init__.py b/gns3server/compute/docker/__init__.py index f6cb215fa..696646ade 100644 --- a/gns3server/compute/docker/__init__.py +++ b/gns3server/compute/docker/__init__.py @@ -39,9 +39,9 @@ log = logging.getLogger(__name__) # Be careful to keep it consistent -DOCKER_MINIMUM_API_VERSION = "1.25" -DOCKER_MINIMUM_VERSION = "1.13" -DOCKER_PREFERRED_API_VERSION = "1.30" +DOCKER_MINIMUM_API_VERSION = "1.40" +DOCKER_MINIMUM_VERSION = "19.03.8" +DOCKER_PREFERRED_API_VERSION = "1.44" CHUNK_SIZE = 1024 * 8 # 8KB @@ -123,22 +123,30 @@ class Docker(BaseManager): if not self._connected: try: self._connected = True - connector = self.connector() - version = await self.query("GET", "version") + self._connector = self.connector() + docker_info = await self.query("GET", "version") except (aiohttp.ClientOSError, FileNotFoundError): 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): - raise DockerError( - "Docker version is {}. GNS3 requires a minimum version of {}".format( - version["Version"], DOCKER_MINIMUM_VERSION)) + if api_version < parse_version(DOCKER_MINIMUM_API_VERSION): + raise DockerError("Docker version is {}. GNS3 requires a minimum version of {}".format( + version, + DOCKER_MINIMUM_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 + 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): @@ -186,7 +194,7 @@ class Docker(BaseManager): :param method: HTTP method :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 timeout: Timeout :returns: HTTP response @@ -197,12 +205,11 @@ class Docker(BaseManager): timeout = 60 * 60 * 24 * 31 # One month timeout if path == 'version': - url = "http://docker/v1.24/" + path + url = "http://docker/" + path 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: - if path != "version": # version is use by check connection - await self._check_connection() if self._session is None or self._session.closed: connector = self.connector() self._session = aiohttp.ClientSession(connector=connector) diff --git a/tests/compute/docker/test_docker.py b/tests/compute/docker/test_docker.py index 12e473425..9e411503d 100644 --- a/tests/compute/docker/test_docker.py +++ b/tests/compute/docker/test_docker.py @@ -47,7 +47,7 @@ async def test_query_success(vm): vm._session.request = AsyncioMagicMock(return_value=response) data = await vm.query("POST", "test", data={"a": True}, params={"b": 1}) vm._session.request.assert_called_with('POST', - 'http://docker/v1.25/test', + 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION), data='{"a": true}', headers={'content-type': 'application/json'}, params={'b': 1}, @@ -69,7 +69,7 @@ async def test_query_error(vm): with pytest.raises(DockerError): await vm.query("POST", "test", data={"a": True}, params={"b": 1}) vm._session.request.assert_called_with('POST', - 'http://docker/v1.25/test', + 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION), data='{"a": true}', headers={'content-type': 'application/json'}, params={'b': 1}, @@ -89,7 +89,7 @@ async def test_query_error_json(vm): with pytest.raises(DockerError): await vm.query("POST", "test", data={"a": True}, params={"b": 1}) vm._session.request.assert_called_with('POST', - 'http://docker/v1.25/test', + 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION), data='{"a": true}', headers={'content-type': 'application/json'}, params={'b': 1}, @@ -180,7 +180,9 @@ async def test_docker_check_connection_docker_minimum_version(vm): async def test_docker_check_connection_docker_preferred_version_against_newer(vm): response = { - 'ApiVersion': '1.31' + 'ApiVersion': '1.52', + 'Version': '29.0.1', + } with patch("gns3server.compute.docker.Docker.connector"), \ @@ -193,7 +195,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): response = { - 'ApiVersion': '1.27', + 'ApiVersion': '1.43', + 'Version': '24.0.2', + 'MinAPIVersion': '1.40' } with patch("gns3server.compute.docker.Docker.connector"), \ @@ -203,6 +207,20 @@ async def test_docker_check_connection_docker_preferred_version_against_older(vm assert vm._api_version == DOCKER_MINIMUM_API_VERSION +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 async def test_install_busybox(): From f24134a8f51ccedbee4a853068298bc7f77f6c30 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 19 Nov 2025 12:08:29 +1000 Subject: [PATCH 34/50] Sync appliances --- .../asterfusion-vAsterNOS-campus.gns3a | 15 +--- .../appliances/asterfusion-vAsterNOS.gns3a | 12 +-- gns3server/appliances/cisco-7200.gns3a | 13 +++ .../appliances/extreme-networks-voss.gns3a | 14 +++ gns3server/appliances/freeRouter.gns3a | 16 ++-- gns3server/appliances/infix.gns3a | 88 +++++++++++++++++-- gns3server/appliances/openwrt.gns3a | 17 +++- gns3server/appliances/opnsense.gns3a | 14 +++ gns3server/appliances/vyos.gns3a | 26 ++++++ 9 files changed, 177 insertions(+), 38 deletions(-) diff --git a/gns3server/appliances/asterfusion-vAsterNOS-campus.gns3a b/gns3server/appliances/asterfusion-vAsterNOS-campus.gns3a index 4887b73cf..c9bcc69a3 100644 --- a/gns3server/appliances/asterfusion-vAsterNOS-campus.gns3a +++ b/gns3server/appliances/asterfusion-vAsterNOS-campus.gns3a @@ -2,7 +2,7 @@ "appliance_id": "9e934470-d898-4289-a5ed-50af094e629e", "name": "Asterfusion vAsterNOS campus", "category": "multilayer_switch", - "description": "AsterNOS is the core technology of Asterfusion’s one-stop SONiC turnkey solution designed for cloud, enterprise, and AI-driven scenarios. AsterNOS v5.2 Campus is specifically designed for traditional campus networks, offering comprehensive L2/L3 capabilities suitable for various campus scenarios such as schools, office buildings, and hospitals. This version supports a fully cloud-integrated Layer 3 network architecture, providing rich routing and switching functionalities to ensure high-performance operation and ease of maintenance. It can also be deployed in the GNS3 simulation environment to experience a complete All-Layer 3 Cloud-Campus network. AsterNOS v6.0 Campus builds upon the L2/L3 features of v5.2 (including ACL, MSTP, QinQ, IGMP Snooping, OSPF/BGP, etc.) and further enhances support for advanced technologies such as MPLS L2VPN/L3VPN and PTP. This version is ideal for enterprises and campus networks requiring high-performance multi-service transport, supporting cross-domain connectivity and providing nanosecond-level time synchronization. It is well-suited for applications with stringent time accuracy requirements, such as financial trading, industrial automation, and smart manufacturing. NOTICE: This appliance file is a virtualized version of AsterNOS and is intended to be used only to experience the basic functionality and industry standard CLI (Klish), not for official software testing. For more information about AsterNOS commercial version, please feel free to contact us via Email: bd@cloudswit.ch", + "description": "AsterNOS is the core technology of Asterfusion’s one-stop SONiC turnkey solution designed for cloud, enterprise, and AI-driven scenarios. AsterNOS v5.2 Campus is specifically designed for traditional campus networks, offering comprehensive L2/L3 capabilities suitable for various campus scenarios such as schools, office buildings, and hospitals. This version supports a fully cloud-integrated Layer 3 network architecture, providing rich routing and switching functionalities to ensure high-performance operation and ease of maintenance. It can also be deployed in the GNS3 simulation environment to experience a complete All-Layer 3 Cloud-Campus network. This version is ideal for enterprises and campus networks requiring high-performance multi-service transport, supporting cross-domain connectivity and providing nanosecond-level time synchronization. It is well-suited for applications with stringent time accuracy requirements, such as financial trading, industrial automation, and smart manufacturing. NOTICE: This appliance file is a virtualized version of AsterNOS and is intended to be used only to experience the basic functionality and industry standard CLI (Klish), not for official software testing. For more information about AsterNOS commercial version, please feel free to contact us via Email: bd@cloudswit.ch", "vendor_name": "Asterfusion", "vendor_url": "https://cloudswit.ch/", "vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/asterfusion.png", @@ -29,13 +29,6 @@ "kvm": "require" }, "images": [ - { - "filename": "vAsterNOS-V6.1R002.img", - "version": "6.1-2", - "md5sum": "003e6329489a617fbab5783504559d26", - "filesize": 2106851328, - "download_url": "https://drive.cloudswitch.io/external/c224501f36e6003767b30112bd44d92476f81f442cf47f8027a6f4f7e4227995" - }, { "filename": "vAsterNOS-V5.2R012P01.img", "version": "5.2-12-1", @@ -46,12 +39,6 @@ ], "versions": [ - { - "name": "6.1-2", - "images": { - "hda_disk_image": "vAsterNOS-V6.1R002.img" - } - }, { "name": "5.2-12-1", "images": { diff --git a/gns3server/appliances/asterfusion-vAsterNOS.gns3a b/gns3server/appliances/asterfusion-vAsterNOS.gns3a index e6e392c32..991c36069 100644 --- a/gns3server/appliances/asterfusion-vAsterNOS.gns3a +++ b/gns3server/appliances/asterfusion-vAsterNOS.gns3a @@ -6,7 +6,7 @@ "vendor_name": "Asterfusion", "vendor_url": "https://cloudswit.ch/", "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_url": "https://cloudswit.ch/product/sonic-enterprise-distribution", "registry_version": 4, @@ -30,18 +30,18 @@ }, "images": [ { - "filename": "vAsterNOS-V3.1.img", + "filename": "AsterNOS_V3.1_R0408P01-VS.img.gz", "version": "V3.1", - "md5sum": "c323c9c3f60e1a93eca2acdc5034b85c", - "filesize": 2724659200, - "download_url": "https://drive.cloudswitch.io/external/8ae2e3932ad8bb2ec30dd25be415d288ff3e4a949c557c6bd48ac6e6265bcfc1" + "md5sum": "5c182306af67be2c79a5642551412e48", + "filesize": 1425945924, + "download_url": "http://pub.asternos.com/AsterNOS_V3.1_R0408P01-VS.img.gz" } ], "versions": [ { "name": "V3.1", "images": { - "hda_disk_image": "vAsterNOS-V3.1.img" + "hda_disk_image": "AsterNOS_V3.1_R0408P01-VS.img.gz" } } ] diff --git a/gns3server/appliances/cisco-7200.gns3a b/gns3server/appliances/cisco-7200.gns3a index 3e723e50b..78d0be865 100644 --- a/gns3server/appliances/cisco-7200.gns3a +++ b/gns3server/appliances/cisco-7200.gns3a @@ -50,6 +50,12 @@ "version": "124-25G", "md5sum": "9c7cc9b3f3b3571411a7f62faaa2c036", "filesize": 71528984 + }, + { + "filename": "c7200-itpk9-mz.124-15.SW.image", + "version": "124-15.SW", + "md5sum": "3334a0facdbd26164e83d3c201fff5b5", + "filesize": 46423952 } ], "versions": [ @@ -87,6 +93,13 @@ "images": { "image": "c7200-a3jk9s-mz.124-25g.image" } + }, + { + "name": "124-15.SW", + "idlepc": "0x60b2bc68", + "images": { + "image": "c7200-itpk9-mz.124-15.SW.image" + } } ] } diff --git a/gns3server/appliances/extreme-networks-voss.gns3a b/gns3server/appliances/extreme-networks-voss.gns3a index a5ef0a64c..08d6c4509 100644 --- a/gns3server/appliances/extreme-networks-voss.gns3a +++ b/gns3server/appliances/extreme-networks-voss.gns3a @@ -28,6 +28,13 @@ "options": "-nographic" }, "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", "version": "v8.10.1.0", @@ -93,6 +100,13 @@ } ], "versions": [ + { + "name": "v9.3.0.0", + "images": + { + "hda_disk_image": "FEGNS3.9.3.0.0.qcow2" + } + }, { "name": "v8.10.1.0", "images": diff --git a/gns3server/appliances/freeRouter.gns3a b/gns3server/appliances/freeRouter.gns3a index ca53df882..087ec49fb 100644 --- a/gns3server/appliances/freeRouter.gns3a +++ b/gns3server/appliances/freeRouter.gns3a @@ -25,19 +25,19 @@ }, "images": [ { - "filename": "rtr.qcow2", - "version": "20.7.1", - "md5sum": "4707415d15d20ced92423d90e52eedcd", - "filesize": 254767616, - "download_url": "http://freerouter.nop.hu/", - "direct_download_url": "http://dl.nop.hu/rtr.qcow2" + "filename": "rtr-x86_64.qcow2", + "version": "25.8.29", + "md5sum": "13d72fdfd772258808a9d4b75fa67083", + "filesize": 73269248, + "download_url": "http://www.freertr.org/", + "direct_download_url": "http://dl.nop.hu/rtr-x86_64.qcow2" } ], "versions": [ { - "name": "20.7.1", + "name": "25.8.29", "images": { - "hda_disk_image": "rtr.qcow2" + "hda_disk_image": "rtr-x86_64.qcow2" } } ] diff --git a/gns3server/appliances/infix.gns3a b/gns3server/appliances/infix.gns3a index 8d2814e9d..797266497 100644 --- a/gns3server/appliances/infix.gns3a +++ b/gns3server/appliances/infix.gns3a @@ -2,13 +2,13 @@ "appliance_id": "4d351078-c6f5-444c-ab30-0ef20e3d8c53", "name": "Infix", "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_url": "https://github.com/kernelkit", "vendor_logo_url": "https://kernelkit.org/assets/img/jack.png", "product_name": "Infix", "registry_version": 4, - "documentation_url": "https://github.com/kernelkit/infix/tree/main/doc", + "documentation_url": "https://kernelkit.org/infix/latest/", "status": "stable", "availability": "free", "maintainer": "KernelKit", @@ -37,16 +37,16 @@ "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": "infix-x86_64-disk-24.11.1.img", "filesize": 536870912, "md5sum": "673a123fe122d1c2f5724baf9965a19d", "version": "24.11.1", "download_url": "https://github.com/kernelkit/infix/releases/download/v24.11.1/infix-x86_64-24.11.1.tar.gz", "compression": "gzip" - }, - { + }, + { "filename": "infix-x86_64-disk-25.01.0.img", "filesize": 536870912, "md5sum": "a814d93b385116b4a35712c445b5f830", @@ -69,17 +69,87 @@ "version": "25.03.0", "download_url": "https://github.com/kernelkit/infix/releases/download/v25.03.0/infix-x86_64-25.03.0.tar.gz", "compression": "gzip" + }, + { + "filename": "infix-x86_64-disk-25.04.0.qcow2", + "filesize": 259723776, + "md5sum": "84bd999513325d0007d0e6587abc6140", + "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" + }, + { + "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" } ], "versions": [ - { + { + "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", + "images": { + "bios_image": "OVMF-edk2-stable202305.fd", + "hda_disk_image": "infix-x86_64-disk-25.04.0.qcow2" + } + }, + { "name": "25.03.0", "images": { "bios_image": "OVMF-edk2-stable202305.fd", "hda_disk_image": "infix-x86_64-disk-25.03.0.img" } }, - { + { "name": "25.02.0", "images": { "bios_image": "OVMF-edk2-stable202305.fd", @@ -101,4 +171,4 @@ } } ] -} +} \ No newline at end of file diff --git a/gns3server/appliances/openwrt.gns3a b/gns3server/appliances/openwrt.gns3a index add7eb588..ac3711d3a 100644 --- a/gns3server/appliances/openwrt.gns3a +++ b/gns3server/appliances/openwrt.gns3a @@ -23,6 +23,15 @@ "kvm": "allow" }, "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", "version": "23.05.0", @@ -223,7 +232,13 @@ } ], "versions": [ - { + { + "name": "24.10.2", + "images": { + "hda_disk_image": "openwrt-24.10.2-x86-64-generic-ext4-combined.img" + } + }, + { "name": "23.05.0", "images": { "hda_disk_image": "openwrt-23.05.0-x86-64-generic-ext4-combined.img" diff --git a/gns3server/appliances/opnsense.gns3a b/gns3server/appliances/opnsense.gns3a index 759701763..433b4f2de 100644 --- a/gns3server/appliances/opnsense.gns3a +++ b/gns3server/appliances/opnsense.gns3a @@ -25,6 +25,20 @@ "kvm": "require" }, "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", "version": "24.7", diff --git a/gns3server/appliances/vyos.gns3a b/gns3server/appliances/vyos.gns3a index cace44ed3..3255a80e0 100644 --- a/gns3server/appliances/vyos.gns3a +++ b/gns3server/appliances/vyos.gns3a @@ -30,6 +30,20 @@ "on_close": "shutdown_signal" }, "images": [ + { + "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", "version": "1.4.1", @@ -116,6 +130,18 @@ } ], "versions": [ + { + "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", "images": { From 37f17209d3e4a38832870aeba7465e5d0cd0e82e Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 19 Nov 2025 12:13:56 +1000 Subject: [PATCH 35/50] Release v2.2.55 --- CHANGELOG | 13 +++++++++++++ gns3server/crash_report.py | 2 +- gns3server/version.py | 5 ++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7d539f621..7e212c27d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,18 @@ # Change Log +## 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 + ## 2.2.54 21/04/2025 * Bundle web-ui v2.2.54 diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 17917695b..296f92fe9 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -57,7 +57,7 @@ class CrashReport: Report crash to a third party service """ - DSN = "https://c663917f99579c9b5a5c0764415c0bd4@o19455.ingest.us.sentry.io/38482" + DSN = "https://9721facaf4cb1e0a93e126300ea56901@o19455.ingest.us.sentry.io/38482" _instance = None def __init__(self): diff --git a/gns3server/version.py b/gns3server/version.py index f4bdf1bdf..c99f6f848 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,9 +23,8 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.2.55.dev1" -__version_info__ = (2, 2, 54, 99) - +__version__ = "2.2.55" +__version_info__ = (2, 2, 55, 0) if "dev" in __version__: try: import os From c676a5bf82e61509dea6bd2e96a88b493ca374d4 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 16 Dec 2025 16:46:31 +0800 Subject: [PATCH 36/50] Development on v2.2.56.dev1 --- gns3server/version.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gns3server/version.py b/gns3server/version.py index c99f6f848..20d2c4179 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,8 +23,9 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.2.55" -__version_info__ = (2, 2, 55, 0) +__version__ = "2.2.56.dev1" +__version_info__ = (2, 2, 56, 99) + if "dev" in __version__: try: import os From 8ce690ce782fb020ca694601656ce0f1b957d4e4 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 17 Jan 2026 17:54:27 +0800 Subject: [PATCH 37/50] Upgrade pywin32 to v311 --- win-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/win-requirements.txt b/win-requirements.txt index 5d5d84516..09cf5b80f 100644 --- a/win-requirements.txt +++ b/win-requirements.txt @@ -1,4 +1,4 @@ -rrequirements.txt -pywin32==306 +pywin32==311 wmi==1.5.1 From 58d1e8f0c50f56b65013b500960dd9e9114d0d73 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 17 Jan 2026 18:01:42 +0800 Subject: [PATCH 38/50] Set default location of udhcpc in "/etc/network/udhcpc". Fixes #2582 --- .../compute/docker/resources/etc/udhcpc/default.script | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gns3server/compute/docker/resources/etc/udhcpc/default.script b/gns3server/compute/docker/resources/etc/udhcpc/default.script index 404fb190a..873642c47 100755 --- a/gns3server/compute/docker/resources/etc/udhcpc/default.script +++ b/gns3server/compute/docker/resources/etc/udhcpc/default.script @@ -3,7 +3,12 @@ # script for udhcpc # Copyright (c) 2008 Natanael Copa -UDHCPC="/gns3/etc/udhcpc" +if [ -d /etc/network ]; then + UDHCPC="/etc/network/udhcpc" +else + UDHCPC="/gns3/etc/udhcpc" +fi + UDHCPC_CONF="$UDHCPC/udhcpc.conf" RESOLV_CONF="/etc/resolv.conf" From 7b3fd4450e1fe881652cdae92845b27ac8f385a4 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 17 Jan 2026 18:37:41 +0800 Subject: [PATCH 39/50] Sync appliances --- .../asterfusion-vAsterNOS-VPP.gns3a | 31 +++ gns3server/appliances/infix.gns3a | 14 ++ gns3server/appliances/kali-linux.gns3a | 229 +----------------- .../appliances/mikrotik-winbox-2026.gns3a | 22 ++ gns3server/appliances/vyos.gns3a | 13 + 5 files changed, 93 insertions(+), 216 deletions(-) create mode 100644 gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a create mode 100644 gns3server/appliances/mikrotik-winbox-2026.gns3a diff --git a/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a b/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a new file mode 100644 index 000000000..3afa29fa5 --- /dev/null +++ b/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a @@ -0,0 +1,31 @@ +{ + "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", + "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" + } +} diff --git a/gns3server/appliances/infix.gns3a b/gns3server/appliances/infix.gns3a index 797266497..d9ab0321f 100644 --- a/gns3server/appliances/infix.gns3a +++ b/gns3server/appliances/infix.gns3a @@ -104,9 +104,23 @@ "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": [ + { + "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": { diff --git a/gns3server/appliances/kali-linux.gns3a b/gns3server/appliances/kali-linux.gns3a index 5452ec058..60f742842 100644 --- a/gns3server/appliances/kali-linux.gns3a +++ b/gns3server/appliances/kali-linux.gns3a @@ -11,237 +11,34 @@ "status": "stable", "maintainer": "GNS3 Team", "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}", "qemu": { "adapter_type": "e1000", "adapters": 8, - "ram": 1024, + "ram": 2048, + "cpus": 2, "hda_disk_interface": "ide", "arch": "x86_64", "console_type": "vnc", - "boot_priority": "d", "kvm": "require" }, "images": [ - { - "filename": "kali-linux-2021.1-live-amd64.iso", - "version": "2021.1", - "md5sum": "3a3716fef866e5c29a1c1ccfc94264b5", - "filesize": 3591385088, - "download_url": "http://cdimage.kali.org/kali-2021.1/", - "direct_download_url": "http://cdimage.kali.org/kali-2021.1/kali-linux-2021.1-live-amd64.iso" - }, - { - "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" + { + "filename": "kali-linux-2025.4-qemu-amd64.qcow2", + "version": "2025.4", + "md5sum": "a4be43351b5deb5b2823cb38a73b2095", + "filesize": 15719268352, + "download_url": "https://cdimage.kali.org/kali-2025.4/", + "direct_download_url": "https://cdimage.kali.org/kali-2025.4/kali-linux-2025.4-qemu-amd64.7z", + "compression": "7z" } ], "versions": [ { - "name": "2021.1", + "name": "2025.4", "images": { - "hda_disk_image": "kali-linux-persistence-1gb.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" + "hda_disk_image": "kali-linux-2025.4-qemu-amd64.qcow2" } } ] diff --git a/gns3server/appliances/mikrotik-winbox-2026.gns3a b/gns3server/appliances/mikrotik-winbox-2026.gns3a new file mode 100644 index 000000000..7d0bc91ee --- /dev/null +++ b/gns3server/appliances/mikrotik-winbox-2026.gns3a @@ -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" + } +} diff --git a/gns3server/appliances/vyos.gns3a b/gns3server/appliances/vyos.gns3a index 3255a80e0..10bf9ae4c 100644 --- a/gns3server/appliances/vyos.gns3a +++ b/gns3server/appliances/vyos.gns3a @@ -30,6 +30,13 @@ "on_close": "shutdown_signal" }, "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", @@ -130,6 +137,12 @@ } ], "versions": [ + { + "name": "1.4.4", + "images": { + "hda_disk_image": "vyos-1.4.4-kvm-amd64.qcow2" + } + }, { "name": "1.4.3", "images": { From 27fbe37375bc879b9a5886a6f9d370cebbaa6ed8 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 21 Jan 2026 22:15:12 +0800 Subject: [PATCH 40/50] Release v2.2.56 --- CHANGELOG | 5 +++++ gns3server/crash_report.py | 2 +- gns3server/version.py | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7e212c27d..15c9573b3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,10 @@ # Change Log +## 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 diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 296f92fe9..93f503113 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -57,7 +57,7 @@ class CrashReport: Report crash to a third party service """ - DSN = "https://9721facaf4cb1e0a93e126300ea56901@o19455.ingest.us.sentry.io/38482" + DSN = "https://9b9fce29bf59d9a99c230240e1070600@o19455.ingest.us.sentry.io/38482" _instance = None def __init__(self): diff --git a/gns3server/version.py b/gns3server/version.py index 20d2c4179..a5a0c9036 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,8 +23,8 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.2.56.dev1" -__version_info__ = (2, 2, 56, 99) +__version__ = "2.2.56" +__version_info__ = (2, 2, 56, 0) if "dev" in __version__: try: From ef2cf277c166c9c185721b97ac3d45e5f4cdd1b9 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 24 Jan 2026 16:53:43 +0800 Subject: [PATCH 41/50] Development on 2.2.57.dev1 --- gns3server/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/version.py b/gns3server/version.py index a5a0c9036..26e1d9395 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,8 +23,8 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.2.56" -__version_info__ = (2, 2, 56, 0) +__version__ = "2.2.57.dev1" +__version_info__ = (2, 2, 56, 99) if "dev" in __version__: try: From 4335fd7ce0cfd0362d0d38dfc8740c1b27fb954f Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 24 Jan 2026 18:51:44 +0800 Subject: [PATCH 42/50] Drop Python 3.8 support --- gns3server/run.py | 6 +++--- gns3server/utils/asyncio/__init__.py | 19 +++++-------------- gns3server/utils/asyncio/pool.py | 5 +---- setup.py | 5 ++--- 4 files changed, 11 insertions(+), 24 deletions(-) diff --git a/gns3server/run.py b/gns3server/run.py index 32467d45a..c58bd6e92 100644 --- a/gns3server/run.py +++ b/gns3server/run.py @@ -235,9 +235,9 @@ def run(): return log.info("HTTP authentication is enabled with username '{}'".format(user)) - # we only support Python 3 version >= 3.8 - if sys.version_info < (3, 8, 0): - raise SystemExit("Python 3.8 or higher is required") + # we only support Python 3 version >= 3.9 + if sys.version_info < (3, 9, 0): + raise SystemExit("Python 3.9 or higher is required") user_log.info("Running with Python {major}.{minor}.{micro} and has PID {pid}".format(major=sys.version_info[0], minor=sys.version_info[1], micro=sys.version_info[2], pid=os.getpid())) diff --git a/gns3server/utils/asyncio/__init__.py b/gns3server/utils/asyncio/__init__.py index efc32d066..7e464373c 100644 --- a/gns3server/utils/asyncio/__init__.py +++ b/gns3server/utils/asyncio/__init__.py @@ -91,25 +91,16 @@ async def wait_for_process_termination(process, timeout=10): In theory this can be implemented by just: 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 timeout: Timeout in seconds """ - if sys.version_info >= (3, 5): - try: - await asyncio.wait_for(process.wait(), timeout=timeout) - except ProcessLookupError: + while timeout > 0: + if process.returncode is not None: return - else: - while timeout > 0: - if process.returncode is not None: - return - await asyncio.sleep(0.1) - timeout -= 0.1 - raise asyncio.TimeoutError() + await asyncio.sleep(0.1) + timeout -= 0.1 + raise asyncio.TimeoutError() async def _check_process(process, termination_callback): diff --git a/gns3server/utils/asyncio/pool.py b/gns3server/utils/asyncio/pool.py index 22750eadf..444863295 100644 --- a/gns3server/utils/asyncio/pool.py +++ b/gns3server/utils/asyncio/pool.py @@ -40,10 +40,7 @@ class Pool(): while len(self._tasks) > 0 or len(pending) > 0: while len(self._tasks) > 0 and len(pending) < self._concurrency: task, args, kwargs = self._tasks.pop(0) - if sys.version_info >= (3, 7): - t = asyncio.create_task(task(*args, **kwargs)) - else: - t = asyncio.get_event_loop().create_task(task(*args, **kwargs)) + t = asyncio.create_task(task(*args, **kwargs)) pending.add(t) (done, pending) = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED) for task in done: diff --git a/setup.py b/setup.py index 5685ce694..2cc398f69 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,8 @@ from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand # we only support Python 3 version >= 3.8 -if len(sys.argv) >= 2 and sys.argv[1] == "install" and sys.version_info < (3, 8): - raise SystemExit("Python 3.8 or higher is required") +if len(sys.argv) >= 2 and sys.argv[1] == "install" and sys.version_info < (3, 9): + raise SystemExit("Python 3.9 or higher is required") class PyTest(TestCommand): @@ -78,7 +78,6 @@ setup( "Operating System :: Microsoft :: Windows", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 193f470eaf33d9f5621c5689ababcaf0bf56d4d4 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 24 Jan 2026 19:12:58 +0800 Subject: [PATCH 43/50] Upgrade dependencies --- .github/workflows/testing.yml | 6 +----- dev-requirements.txt | 8 ++++---- requirements.txt | 17 ++++++++--------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 744115355..cb84817ea 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -18,11 +18,7 @@ jobs: strategy: matrix: os: ["ubuntu-latest"] - python-version: ["3.8", "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" + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 diff --git a/dev-requirements.txt b/dev-requirements.txt index 4348b0d94..ece129f97 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,6 +1,6 @@ -rrequirements.txt -pytest==8.3.3 -flake8==7.1.0 -pytest-timeout==2.3.1 -pytest-aiohttp==1.0.5 +pytest==8.4.2 # version 8.4.2 is the last one supporting Python 3.9 +flake8==7.3.0 +pytest-timeout==2.4.0 +pytest-aiohttp==1.1.0 diff --git a/requirements.txt b/requirements.txt index 5f0b26144..9c322ec53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,12 @@ -jsonschema>=4.23,<4.24 -aiohttp>=3.10.11,<3.11 # version 3.10.11 is the last compatible version with Python 3.8 -aiohttp-cors>=0.7.0,<0.8 -aiofiles>=24.1.0,<25.0 +jsonschema>=4.25.1,<4.26 # version 4.25.1 is the last to support Python 3.9 +aiohttp>=3.13.3,<3.14 +aiohttp-cors>=0.8.1,<0.9 +aiofiles>=25.1.0,<26.0 Jinja2>=3.1.6,<3.2 -sentry-sdk>=2.44.0,<2.45 # optional dependency -psutil>=7.1.3 -async-timeout>=5.0.1,<5.1 +sentry-sdk>=2.50.0,<3 # optional dependency +psutil>=7.2.1 +async-timeout>=5.0.1,<5.1 # this library has effectively been upstreamed into Python 3.11+ distro>=1.9.0 py-cpuinfo>=9.0.0,<10.0 platformdirs>=2.4.0,<3 # platformdirs >=3 conflicts when building Debian packages -importlib-resources>=1.3; python_version < '3.9' -truststore>=0.10.0; python_version >= '3.10' +truststore>=0.10.4; python_version >= '3.10' From 7b20314edb37aa4331b9452652e70624ef3b986d Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 24 Jan 2026 19:17:55 +0800 Subject: [PATCH 44/50] Delete random_query.py --- scripts/random_query.py | 238 ---------------------------------------- 1 file changed, 238 deletions(-) delete mode 100644 scripts/random_query.py diff --git a/scripts/random_query.py b/scripts/random_query.py deleted file mode 100644 index f5e7de2a9..000000000 --- a/scripts/random_query.py +++ /dev/null @@ -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 . - -""" -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() From 1d45df66135191e7716aef2134819967e4dc01d8 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 24 Jan 2026 21:38:58 +0800 Subject: [PATCH 45/50] Upgrade dependencies --- dev-requirements.txt | 4 ++-- requirements.txt | 20 ++++++++++---------- tests/api/routes/controller/test_projects.py | 8 +++----- tests/api/routes/controller/test_users.py | 2 +- tests/compute/docker/test_docker.py | 1 + tests/compute/docker/test_docker_vm.py | 4 ++-- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 20104d902..081fea50d 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,7 @@ pytest==8.4.2 # version 8.4.2 is the last one supporting Python 3.9 flake8==7.3.0 pytest-timeout==2.4.0 -pytest-asyncio==0.25.2 +pytest-asyncio==1.0.0 # upgrading leads to stuck tests requests==2.32.5 httpx==0.28.1 -httpx_ws==0.7.1 +httpx_ws==0.7.1 # upgrading leads to failures in tests \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d87796e10..63ef755bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -uvicorn==0.34.2 # uvicorn 0.33 is the last version supporting Python 3.8 -pydantic==2.11.7 -fastapi==0.116.1 -python-multipart==0.0.20 -websockets==15.0.1 +uvicorn==0.39.0 # version 0.39.0 is the last version supporting Python 3.9 +pydantic==2.12.5 +fastapi==0.128.0 +python-multipart==0.0.20 # version 0.0.20 is the last to support Python 3.9 +websockets==15.0.1 # version 15.0.1 is the last to support Python 3.9 aiohttp>=3.13.3,<3.14 aiofiles>=25.1.0,<26.0 Jinja2>=3.1.6,<3.2 @@ -12,13 +12,13 @@ async-timeout>=5.0.1,<5.1; python_version < '3.11' # this library has effective distro>=1.9.0 py-cpuinfo>=9.0.0,<10.0 greenlet==3.2.0 # necessary to run sqlalchemy on Python 3.13 -sqlalchemy==2.0.43 +sqlalchemy==2.0.46 aiosqlite==0.21.0 alembic==1.15.2 -bcrypt==4.3.0 -joserfc==1.0.4 -email-validator==2.2.0 +bcrypt==5.0.0 +joserfc==1.6.1 +email-validator==2.3.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 truststore>=0.10.4; python_version >= '3.10' diff --git a/tests/api/routes/controller/test_projects.py b/tests/api/routes/controller/test_projects.py index cd3439c04..9dc23ab37 100644 --- a/tests/api/routes/controller/test_projects.py +++ b/tests/api/routes/controller/test_projects.py @@ -303,7 +303,7 @@ class TestControllerProjectRoutes: params={"include_images": "yes"}) assert response.status_code == status.HTTP_200_OK 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: f.write(response.content) @@ -346,8 +346,7 @@ class TestControllerProjectRoutes: params={"include_images": "0"}) assert response.status_code == status.HTTP_200_OK 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: f.write(response.content) @@ -412,8 +411,7 @@ class TestControllerProjectRoutes: if response.status_code == status.HTTP_200_OK: 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: f.write(response.content) diff --git a/tests/api/routes/controller/test_users.py b/tests/api/routes/controller/test_users.py index 87ea74708..e608e1e68 100644 --- a/tests/api/routes/controller/test_users.py +++ b/tests/api/routes/controller/test_users.py @@ -183,7 +183,7 @@ class TestAuthTokens: jwt_secret = config.settings.Controller.jwt_secret_key token = auth_service.create_access_token(test_user.username) 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( self, diff --git a/tests/compute/docker/test_docker.py b/tests/compute/docker/test_docker.py index d2007d974..917255f78 100644 --- a/tests/compute/docker/test_docker.py +++ b/tests/compute/docker/test_docker.py @@ -216,6 +216,7 @@ async def test_docker_check_connection_docker_preferred_version_against_older(vm assert vm._api_version == DOCKER_MINIMUM_API_VERSION +@pytest.mark.asyncio async def test_docker_check_connection_docker_unsupported_version(vm): response = { diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 66a4e4947..33eea1cbb 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -1791,7 +1791,7 @@ async def test_cpus(compute_project, manager): 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) 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, "OpenStdin": True, "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: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", memory=32) 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, "OpenStdin": True, "StdinOnce": False, From abaeab6242c0892c7f2a72966817a62139f6df60 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 26 Jan 2026 19:50:42 +0800 Subject: [PATCH 46/50] Upgrade greenlet and sqlalchemy dependencies --- dev-requirements.txt | 2 +- gns3server/compute/base_manager.py | 5 +++-- gns3server/utils/asyncio/__init__.py | 3 ++- requirements.txt | 4 ++-- tests/utils/test_asyncio.py | 6 ++---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 081fea50d..df3f6b494 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,7 @@ pytest==8.4.2 # version 8.4.2 is the last one supporting Python 3.9 flake8==7.3.0 pytest-timeout==2.4.0 -pytest-asyncio==1.0.0 # upgrading leads to stuck tests +pytest-asyncio==0.26.0 # upgrading leads to stuck / failed tests requests==2.32.5 httpx==0.28.1 httpx_ws==0.7.1 # upgrading leads to failures in tests \ No newline at end of file diff --git a/gns3server/compute/base_manager.py b/gns3server/compute/base_manager.py index 48082e579..17a41fe7e 100644 --- a/gns3server/compute/base_manager.py +++ b/gns3server/compute/base_manager.py @@ -24,6 +24,7 @@ import socket import shutil import re import logging +import inspect log = logging.getLogger(__name__) @@ -196,7 +197,7 @@ class BaseManager: node_id = str(uuid4()) node = self._NODE_CLASS(name, node_id, project, self, *args, **kwargs) - if asyncio.iscoroutinefunction(node.create): + if inspect.iscoroutinefunction(node.create): await node.create() else: node.create() @@ -245,7 +246,7 @@ class BaseManager: """ node = self.get_node(node_id) - if asyncio.iscoroutinefunction(node.close): + if inspect.iscoroutinefunction(node.close): await node.close() else: node.close() diff --git a/gns3server/utils/asyncio/__init__.py b/gns3server/utils/asyncio/__init__.py index 131145eb9..34ae5d441 100644 --- a/gns3server/utils/asyncio/__init__.py +++ b/gns3server/utils/asyncio/__init__.py @@ -20,6 +20,7 @@ import asyncio import sys import os import threading +import inspect async def wait_run_in_executor(func, *args, **kwargs): @@ -103,7 +104,7 @@ async def wait_for_process_termination(process, timeout=10): async def _check_process(process, termination_callback): if not hasattr(sys, "_called_from_test") or not sys._called_from_test: returncode = await process.wait() - if asyncio.iscoroutinefunction(termination_callback): + if inspect.iscoroutinefunction(termination_callback): await termination_callback(returncode) else: termination_callback(returncode) diff --git a/requirements.txt b/requirements.txt index 63ef755bf..527d08c33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,8 +11,8 @@ psutil>=7.2.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 py-cpuinfo>=9.0.0,<10.0 -greenlet==3.2.0 # necessary to run sqlalchemy on Python 3.13 -sqlalchemy==2.0.46 +greenlet==3.3.1; python_version >= '3.13' # necessary to run sqlalchemy on Python >= 3.13 +sqlalchemy==2.0.43 aiosqlite==0.21.0 alembic==1.15.2 bcrypt==5.0.0 diff --git a/tests/utils/test_asyncio.py b/tests/utils/test_asyncio.py index 1b0056587..cb59bfe39 100644 --- a/tests/utils/test_asyncio.py +++ b/tests/utils/test_asyncio.py @@ -18,11 +18,9 @@ import asyncio 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 tests.utils import AsyncioMagicMock +from gns3server.utils.asyncio import wait_run_in_executor, subprocess_check_output, locking + @pytest.mark.asyncio From 7f45cc6f47867363759aca0ad7efcaaca2a85175 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 28 Jan 2026 18:32:42 +0800 Subject: [PATCH 47/50] Disable checking for available disk space. Ref #2548 --- gns3server/compute/project_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/compute/project_manager.py b/gns3server/compute/project_manager.py index d1f77cb82..a2ab578d2 100644 --- a/gns3server/compute/project_manager.py +++ b/gns3server/compute/project_manager.py @@ -106,7 +106,7 @@ class ProjectManager: if project_id is not None and project_id in self._projects: return self._projects[project_id] 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 return project From 600d3d38dae39bfcfbdaa3d1e3d75e246bfa98c4 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 28 Jan 2026 19:36:22 +0800 Subject: [PATCH 48/50] Fix creating Qemu disk image. Fixes #2542 --- gns3server/api/routes/controller/images.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gns3server/api/routes/controller/images.py b/gns3server/api/routes/controller/images.py index 736c44c3b..9a43152ed 100644 --- a/gns3server/api/routes/controller/images.py +++ b/gns3server/api/routes/controller/images.py @@ -96,7 +96,13 @@ async def create_qemu_image( await Qemu.instance().create_disk_image(disk_image_path, options) 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( "", From a72980b2d99c6155e7c6a1a09ecb53ddbcab738f Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 28 Jan 2026 22:24:10 +0800 Subject: [PATCH 49/50] Sync appliances --- gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a b/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a index 3afa29fa5..5c467f5dc 100644 --- a/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a +++ b/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a @@ -1,4 +1,5 @@ { + "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.", @@ -9,6 +10,7 @@ "registry_version": 6, "status": "stable", "maintainer": "Asterfusion Product Team", + "maintainer_email": "bd@cloudswit.ch", "images": [ { "filename": "AsterNOS-VPP_V6.1-R0101P02_x86.img.gz", From 3915918c419021d2ef25c9fe988cc94a9af3cd0e Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 28 Jan 2026 22:30:36 +0800 Subject: [PATCH 50/50] Release v3.0.6 --- CHANGELOG | 11 +++++++++++ gns3server/crash_report.py | 2 +- gns3server/version.py | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 116494b9d..227773671 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,16 @@ # 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 diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 17a207fd4..3861711d3 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -58,7 +58,7 @@ class CrashReport: 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 def __init__(self): diff --git a/gns3server/version.py b/gns3server/version.py index fb3835633..a42f1dfd7 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -22,8 +22,8 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "3.0.5" -__version_info__ = (3, 0, 5, 0) +__version__ = "3.0.6" +__version_info__ = (3, 0, 6, 0) if "dev" in __version__: try: