From 27c6535ad0333c8445a658ef4299775f9f99c2d2 Mon Sep 17 00:00:00 2001 From: rafael-33 Date: Fri, 24 Jul 2026 22:51:44 +0200 Subject: [PATCH 01/12] add bug report issue template --- .github/ISSUE_TEMPLATE/bug_report.yml | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..21e1bf803 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,40 @@ +name: Bug report +description: Report a bug so we can fix it. +title: "[Bug]: " +labels: ["bug"] +body: + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear description of the bug. + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + description: How can we reproduce this? Numbered steps if possible. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What did you expect to happen instead? + - type: input + id: version + attributes: + label: Version / commit + description: Which version or commit hash are you on? + - type: textarea + id: environment + attributes: + label: Environment + description: OS, runtime version, anything else that might be relevant. + - type: textarea + id: logs + attributes: + label: Relevant logs + description: Paste any relevant log output. This is automatically rendered as code. + render: shell From 41e877760905211a17b89b3fdba44d6265edb2d1 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Sat, 25 Jul 2026 10:30:16 -0700 Subject: [PATCH 02/12] fix: correct always-true state check in DockerVM.stop() The condition 'state != "stopped" or state != "exited"' is a tautology, so the state check was a no-op and a stop request was sent even for a container that had already exited. _get_container_state() never returns "stopped" (only "running", "paused" or "exited"), so the intended negation of the condition used in _fix_permissions() requires 'and', not 'or' (De Morgan's law). Added a regression test asserting no stop query is issued for an already-exited container. --- gns3server/compute/docker/docker_vm.py | 2 +- tests/compute/docker/test_docker_vm.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index a5822d998..f71ec3776 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -869,7 +869,7 @@ class DockerVM(BaseNode): await self._fix_permissions() state = await self._get_container_state() - if state != "stopped" or state != "exited": + if state != "stopped" and state != "exited": # t=5 number of seconds to wait before killing the container try: await self.manager.query("POST", "containers/{}/stop".format(self._cid), params={"t": 5}) diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 332a52c17..f43fb7460 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -1503,3 +1503,19 @@ async def test_read_console_output_with_binary_mode(vm): with asyncio_patch('gns3server.compute.docker.docker_vm.DockerVM.stop'): await vm._read_console_output(input_stream, output_stream) output_stream.feed_data.assert_called_once_with(b"test") + + +async def test_stop_exited_container_no_stop_query(vm): + + vm._ubridge_hypervisor = None + vm._fix_permissions = MagicMock() + + with asyncio_patch("gns3server.compute.docker.DockerVM._get_container_state", return_value="exited"): + with asyncio_patch("gns3server.compute.docker.Docker.query") as mock_query: + vm._permissions_fixed = False + await vm.stop() + assert not any( + call.args[:2] == ("POST", "containers/e90e34656842/stop") + for call in mock_query.mock_calls + ) + assert vm.status == "stopped" From 00ac2c19bddac57d26aa5090d3de7cc5b2e396cb Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:00:43 -0700 Subject: [PATCH 03/12] Do not start nodes when deleting a project Project.delete() calls open() to rebuild the internal data structures needed for cleanup. open() schedules start_all() when the project has auto_start enabled, so deleting an auto-start project actually launched every node process, allocating ports and consuming resources, only for close() to kill them moments later. open() now takes an auto_start argument (default True, so normal opens are unchanged) and delete() passes auto_start=False. Fixes #2784 --- gns3server/controller/project.py | 9 ++++++--- tests/controller/test_project.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index f26b1ff5f..e35e89038 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -917,7 +917,7 @@ class Project: if self._status != "opened": try: - await self.open() + await self.open(auto_start=False) except aiohttp.web.HTTPConflict as e: # ignore missing images or other conflicts when deleting a project log.warning(f"Conflict while deleting project: {e}") @@ -998,9 +998,12 @@ class Project: return os.path.join(self.path, self._filename) @locking - async def open(self): + async def open(self, auto_start=True): """ Load topology elements + + :param auto_start: whether the nodes may be started when the project + has auto start enabled """ if self._closing: @@ -1114,7 +1117,7 @@ class Project: self._loading = False self.emit_controller_notification("project.opened", self.__json__()) # Should we start the nodes when project is open - if self._auto_start: + if self._auto_start and auto_start: # Start all in the background without waiting for completion # we ignore errors because we want to let the user open # their project and fix it diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index 234bf1b57..5d7d8a52a 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -655,6 +655,19 @@ async def test_delete(project): assert not os.path.exists(project.path) +async def test_delete_does_not_start_nodes(project): + """ + Deleting a project must not start its nodes, even when auto_start is enabled. + """ + + project.auto_start = True + project.dump() + await project.close() + project.start_all = AsyncioMagicMock() + await project.delete() + assert not project.start_all.called + + async def test_dump(projects_dir): directory = projects_dir From 9562c9d828c5804033f6d3e36c6630a32f0d9a99 Mon Sep 17 00:00:00 2001 From: Iskander Nafikov Date: Tue, 28 Jul 2026 00:27:55 +0800 Subject: [PATCH 04/12] fix(qemu): remove trailing space from RNG object argument --- gns3server/compute/qemu/qemu_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index fa7757a62..c2c229ae2 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -2107,7 +2107,7 @@ class QemuVM(BaseNode): options.extend(["-drive", "if=pflash,format=raw,file={}".format(ovmf_vars_node_path)]) # edk2 firmware requires a Random Number Generator (RNG) device in order to turn network adapters on - options.extend(["-object", "rng-random,filename=/dev/urandom,id=rng0 "]) + options.extend(["-object", "rng-random,filename=/dev/urandom,id=rng0"]) options.extend(["-device", "virtio-rng-pci,rng=rng0"]) return options From 37490f2c74d340038b29d7fc206b743cb408d4ff Mon Sep 17 00:00:00 2001 From: my-code-is-grey Date: Mon, 27 Jul 2026 22:27:48 -0400 Subject: [PATCH 05/12] bugfix: docker workflow needs lowercase name --- .github/workflows/docker-build.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 834bad959..3978699ee 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -28,6 +28,13 @@ jobs: echo "stable=false" >> $GITHUB_OUTPUT fi + - name: Set lowercase image name vars + if: steps.ver.outputs.stable == 'true' + id: names + run: | + echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" + echo "repo=$(echo '${{ github.event.repository.name }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" + - name: Set up Docker Buildx if: steps.ver.outputs.stable == 'true' uses: docker/setup-buildx-action@v3 @@ -57,5 +64,5 @@ jobs: tags: | ${{ env.DOCKERHUB_ORG }}/${{ github.event.repository.name }}:${{ steps.ver.outputs.tag }} ${{ env.DOCKERHUB_ORG }}/${{ github.event.repository.name }}:latest - ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ steps.ver.outputs.tag }} - ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest + ghcr.io/${{ steps.names.outputs.owner }}/${{ steps.names.outputs.repo }}:${{ steps.ver.outputs.tag }} + ghcr.io/${{ steps.names.outputs.owner }}/${{ steps.names.outputs.repo }}:latest From e2657febf0d79849fe36e024e23f960f2f456d2b Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 29 Jul 2026 17:23:28 +0200 Subject: [PATCH 06/12] fix(qemu): fix addition of QEMU RNG device causes interface names to change --- gns3server/compute/qemu/qemu_vm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index fa7757a62..e54454628 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -2480,6 +2480,8 @@ class QemuVM(BaseNode): elif sys.platform.startswith("win") or sys.platform.startswith("darwin"): command.extend(["-enable-hax"]) command.extend(["-boot", "order={}".format(self._boot_priority)]) + # network options must be first to have predictable NIC numbering, see https://github.com/GNS3/gns3-server/issues/2838 + command.extend((await self._network_options())) command.extend(self._bios_option()) command.extend(self._cdrom_option()) command.extend((await self._disk_options())) @@ -2497,7 +2499,6 @@ class QemuVM(BaseNode): elif self._console_type != "none": raise QemuError("Console type {} is unknown".format(self._console_type)) command.extend(self._monitor_options()) - command.extend((await self._network_options())) if self.on_close != "save_vm_state": await self._clear_save_vm_stated() else: From d9140d2f8c8dbbadbf0e32bdf1ffeeba75c0d369 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 29 Jul 2026 17:47:03 +0200 Subject: [PATCH 07/12] fix(qemu): move the bios options after the network ones --- gns3server/compute/qemu/qemu_vm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index e54454628..3353048f8 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -2480,9 +2480,6 @@ class QemuVM(BaseNode): elif sys.platform.startswith("win") or sys.platform.startswith("darwin"): command.extend(["-enable-hax"]) command.extend(["-boot", "order={}".format(self._boot_priority)]) - # network options must be first to have predictable NIC numbering, see https://github.com/GNS3/gns3-server/issues/2838 - command.extend((await self._network_options())) - command.extend(self._bios_option()) command.extend(self._cdrom_option()) command.extend((await self._disk_options())) command.extend(self._linux_boot_options()) @@ -2499,6 +2496,9 @@ class QemuVM(BaseNode): elif self._console_type != "none": raise QemuError("Console type {} is unknown".format(self._console_type)) command.extend(self._monitor_options()) + command.extend((await self._network_options())) + # bios options must be last to have predictable NIC numbering, see https://github.com/GNS3/gns3-server/issues/2838 + command.extend(self._bios_option()) if self.on_close != "save_vm_state": await self._clear_save_vm_stated() else: From a225622a6edc5e122b524f4a58255743b62ab72f Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 29 Jul 2026 18:10:52 +0200 Subject: [PATCH 08/12] fix(import): unvalidated symlink creation in import_project --- gns3server/controller/import_project.py | 50 ++++++++++++++++++------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/gns3server/controller/import_project.py b/gns3server/controller/import_project.py index a1912208f..1cd402bad 100644 --- a/gns3server/controller/import_project.py +++ b/gns3server/controller/import_project.py @@ -166,24 +166,48 @@ async def import_project( project = await controller.load_project(dot_gns3_path, load=False) return project + def _create_symbolic_links(zip_file, path): """ - Manually create symbolic links (if any) because ZipFile does not support it. - - :param zip_file: ZipFile instance - :param path: project location + Materialise symlink entries, refusing any target that escapes `path`. """ + path_root = os.path.realpath(path) + os.sep for zip_info in zip_file.infolist(): - if stat.S_ISLNK(zip_info.external_attr >> 16): - symlink_target = zip_file.read(zip_info.filename).decode() - symlink_path = os.path.join(path, zip_info.filename) - try: - # remove the regular file and replace it by a symbolic link - os.remove(symlink_path) - os.symlink(symlink_target, symlink_path) - except OSError as e: - raise aiohttp.web.HTTPConflict(text=f"Cannot create symbolic link: {e}") + if not stat.S_ISLNK(zip_info.external_attr >> 16): + continue + symlink_target = zip_file.read(zip_info.filename).decode() + symlink_path = os.path.join(path, zip_info.filename) + + # 1. Reject absolute targets outright. + if os.path.isabs(symlink_target): + raise aiohttp.web.HTTPConflict( + text=f"Symlink {zip_info.filename!r} has absolute target {symlink_target!r}, refusing" + ) + + # 2. Reject paths where the entry name itself escapes (defence in depth; + # extractall normally would already have caught this). + member_abs = os.path.realpath(symlink_path) + if not (member_abs + os.sep).startswith(path_root) and member_abs + os.sep != path_root: + raise aiohttp.web.HTTPConflict( + text=f"Symlink entry {zip_info.filename!r} escapes project dir, refusing" + ) + + # 3. Resolve the symlink target relative to the entry's own parent + # directory and verify the resolved real path stays inside `path`. + link_dir = os.path.realpath(os.path.dirname(symlink_path)) + resolved_target = os.path.realpath(os.path.join(link_dir, symlink_target)) + if not (resolved_target + os.sep).startswith(path_root) and resolved_target + os.sep != path_root: + raise aiohttp.web.HTTPConflict( + text=f"Symlink {zip_info.filename!r} -> {symlink_target!r} escapes project dir, refusing" + ) + + try: + os.remove(symlink_path) + os.symlink(symlink_target, symlink_path) + except OSError as e: + raise aiohttp.web.HTTPConflict(text=f"Cannot create symbolic link: {e}") + def regenerate_topology_ids(topology, new_project_path, reset_mac_addresses=False): """ From b797981a6512d850a04b8627f1ef03b242898d41 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 29 Jul 2026 18:48:31 +0200 Subject: [PATCH 09/12] test: add tests for unvalidated symlink creation in import_project --- gns3server/controller/import_project.py | 6 +- tests/controller/test_import_project.py | 98 ++++++++++++++++++------- 2 files changed, 76 insertions(+), 28 deletions(-) diff --git a/gns3server/controller/import_project.py b/gns3server/controller/import_project.py index 1cd402bad..c0e272f44 100644 --- a/gns3server/controller/import_project.py +++ b/gns3server/controller/import_project.py @@ -169,7 +169,11 @@ async def import_project( def _create_symbolic_links(zip_file, path): """ - Materialise symlink entries, refusing any target that escapes `path`. + Manually create symbolic links (if any) because ZipFile does not support it. + Refuse any target that escapes `path`. + + :param zip_file: ZipFile instance + :param path: project location """ path_root = os.path.realpath(path) + os.sep diff --git a/tests/controller/test_import_project.py b/tests/controller/test_import_project.py index b70a360ab..affbc9074 100644 --- a/tests/controller/test_import_project.py +++ b/tests/controller/test_import_project.py @@ -19,6 +19,8 @@ import os import uuid import json import zipfile +import pytest +import aiohttp from tests.utils import asyncio_patch, AsyncioMagicMock from unittest.mock import patch, MagicMock @@ -117,36 +119,48 @@ async def write_file(path, z): f.write(chunk) -async def test_import_project_containing_symlink(tmpdir, controller): +@pytest.fixture +def export_project_with_symlink(tmpdir, controller): + async def _export(symlink_target): + project = Project(controller=controller, name="test") + project.dump = MagicMock() - project = Project(controller=controller, name="test") - project.dump = MagicMock() - path = project.path + topology = { + "project_id": str(uuid.uuid4()), + "name": "test", + "auto_open": True, + "auto_start": True, + "topology": { + }, + "version": "2.0.0" + } + + with open(os.path.join(project.path, "project.gns3"), 'w+') as f: + json.dump(topology, f) + + os.makedirs(os.path.join(project.path, "vm1", "dynamips")) + symlink_path = os.path.join(project.path, "vm1", "dynamips", "symlink") + os.symlink(symlink_target, symlink_path) + + zip_path = str(tmpdir / "project.zip") + with aiozipstream.ZipFile() as z: + with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),): + await export_project(z, project, str(tmpdir), include_images=False) + await write_file(zip_path, z) + + return zip_path + + return _export + + +async def test_import_project_containing_symlink(controller, export_project_with_symlink): + """ + Test importing a project containing a valid symlink (target inside the project directory). + """ project_id = str(uuid.uuid4()) - topology = { - "project_id": str(uuid.uuid4()), - "name": "test", - "auto_open": True, - "auto_start": True, - "topology": { - }, - "version": "2.0.0" - } - - with open(os.path.join(path, "project.gns3"), 'w+') as f: - json.dump(topology, f) - - os.makedirs(os.path.join(path, "vm1", "dynamips")) - symlink_path = os.path.join(project.path, "vm1", "dynamips", "symlink") - symlink_target = "/tmp/anywhere" - os.symlink(symlink_target, symlink_path) - - zip_path = str(tmpdir / "project.zip") - with aiozipstream.ZipFile() as z: - with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),): - await export_project(z, project, str(tmpdir), include_images=False) - await write_file(zip_path, z) + symlink_target = "../symlink_target" + zip_path = await export_project_with_symlink(symlink_target) with open(zip_path, "rb") as f: project = await import_project(controller, project_id, f) @@ -158,6 +172,36 @@ async def test_import_project_containing_symlink(tmpdir, controller): assert os.readlink(symlink_path) == symlink_target +async def test_import_project_containing_absolute_symlink(controller, export_project_with_symlink): + """ + Test importing a project containing an absolute symlink. + This should fail because absolute symlinks are not allowed for security reasons. + """ + + project_id = str(uuid.uuid4()) + symlink_target = "/tmp/anywhere" + zip_path = await export_project_with_symlink(symlink_target) + + with pytest.raises(aiohttp.web.HTTPConflict): + with open(zip_path, "rb") as f: + await import_project(controller, project_id, f) + + +async def test_import_project_containing_escaping_symlink(controller, export_project_with_symlink): + """ + Test importing a project containing a symlink that escapes the project directory. + This should fail because symlinks that escape the project directory are not allowed for security reasons. + """ + + project_id = str(uuid.uuid4()) + symlink_target = "../../../../symlink_target" + zip_path = await export_project_with_symlink(symlink_target) + + with pytest.raises(aiohttp.web.HTTPConflict): + with open(zip_path, "rb") as f: + await import_project(controller, project_id, f) + + async def test_import_upgrade(tmpdir, controller): """ Topology made for previous GNS3 version are upgraded during the process From 5485eef3ad5b1590f1a0fd442daf1a496cf8f8c6 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 29 Jul 2026 18:52:23 +0200 Subject: [PATCH 10/12] Sync appliances --- gns3server/appliances/armbian.gns3a | 59 +++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 gns3server/appliances/armbian.gns3a diff --git a/gns3server/appliances/armbian.gns3a b/gns3server/appliances/armbian.gns3a new file mode 100644 index 000000000..094f2bd35 --- /dev/null +++ b/gns3server/appliances/armbian.gns3a @@ -0,0 +1,59 @@ +{ + "appliance_id": "b3b90fde-143a-4129-8031-ccbba73c5e02", + "name": "armbian", + "category": "guest", + "description": "A highly optimized base operating system specialized for single board computers (SBCs) and its extensive build framework.", + "vendor_name": "The Armbian team", + "vendor_url": "https://armbian.com/", + "documentation_url": "https://docs.armbian.com/", + "product_name": "Armbian UEFI x86", + "product_url": "https://armbian.com/boards/uefi-x86", + "registry_version": 4, + "status": "stable", + "maintainer": "GNS3 Team", + "maintainer_email": "developers@gns3.net", + "usage": "By first login you create root password and new sudo user.\n\nBoot disk from UEFI shell, type: FS0:EFI\\BOOT\\BOOTX64 and press ", + "port_name_format": "Ethernet{0}", + "qemu": { + "adapter_type": "virtio-net-pci", + "adapters": 2, + "ram": 256, + "hda_disk_interface": "virtio", + "arch": "x86_64", + "console_type": "spice+agent", + "uefi": false, + "boot_priority": "c", + "kvm": "require", + "options": "-nographic" + }, + "images": [ + { + "filename": "OVMF-edk2-stable202305.fd", + "version": "stable202305", + "md5sum": "6c4cf1519fec4a4b95525d9ae562963a", + "filesize": 4194304, + "download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/", + "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/OVMF-edk2-stable202305.fd.zip/download", + "compression": "zip" + }, + { + "filename": "Armbian_26.5.1_Uefi-x86_trixie_cloud_6.18.32_minimal.img.qcow2", + "version": "Armbian 26.5.1 Minimal (CLI)", + "md5sum": "7f4c915668718d6135406de5a6c4fc30", + "filesize": 877920512, + "download_url": "https://armbian.com/boards/uefi-x86", + "direct_download_url": "https://armbian.atomonetworks.com/dl/uefi-x86/archive/Armbian_26.5.1_Uefi-x86_trixie_cloud_6.18.32_minimal.img.qcow2.xz", + "compression": "xz" + } + + ], + "versions": [ + { + "name": "Armbian 26.5.1 Minimal (CLI)", + "images": { + "bios_image": "OVMF-edk2-stable202305.fd", + "hda_disk_image": "Armbian_26.5.1_Uefi-x86_trixie_cloud_6.18.32_minimal.img.qcow2" + } + } + ] +} From da2942c13978d66f78a874bdc9f3c3b82d179a4f Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 30 Jul 2026 12:53:08 +0200 Subject: [PATCH 11/12] Release v2.2.61 --- CHANGELOG | 9 +++++++++ gns3server/crash_report.py | 2 +- gns3server/version.py | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 02d3d14f9..3469a123a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,14 @@ # Change Log +## 2.2.61 30/07/2026 + +* Sync appliances +* fix(import): unvalidated symlink creation in import_project +* fix(qemu): fix addition of QEMU RNG device causes interface names to change +* fix(qemu): remove trailing space from RNG object argument +* fix: do not start nodes when deleting a project +* fix: correct always-true state check in DockerVM.stop() + ## 2.2.60 15/07/2026 * Sync appliances diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 5b3250a0a..ac2b3bb57 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://deee946d000cb5548acebb1cd7c1a1d3@o19455.ingest.us.sentry.io/38482" + DSN = "https://89ba0bf8d773c2c95518bfb696d6d86f@o19455.ingest.us.sentry.io/38482" _instance = None def __init__(self): diff --git a/gns3server/version.py b/gns3server/version.py index 0df2dccb4..9d8b95878 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.61.dev1" -__version_info__ = (2, 2, 61, 99) +__version__ = "2.2.61" +__version_info__ = (2, 2, 61, 0) if "dev" in __version__: try: From 8c0150cd629f5b2a2e956c5aa5a2d7f42f33d975 Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 30 Jul 2026 19:11:11 +0200 Subject: [PATCH 12/12] Development on 2.2.62.dev1 --- gns3server/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/version.py b/gns3server/version.py index 9d8b95878..7260fcbd8 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.61" -__version_info__ = (2, 2, 61, 0) +__version__ = "2.2.62.dev1" +__version_info__ = (2, 2, 62, 99) if "dev" in __version__: try: