From fe2014854ed1549d7b611f77e67bda02ea5b200b Mon Sep 17 00:00:00 2001 From: Patrik Olsson Date: Thu, 5 Feb 2026 15:10:57 +0100 Subject: [PATCH 1/5] Take populated disks into consideration when calculating PCI device ID --- gns3server/compute/qemu/qemu_vm.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 28a00c829..f97e5db31 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -2190,14 +2190,31 @@ class QemuVM(BaseNode): patched_qemu = True # Each 32 PCI device we need to add a PCI bridge with max 9 bridges - pci_devices = 4 + len(self._ethernet_adapters) # 4 PCI devices are use by default by qemu + # Count PCI devices: 4 default + disks (virtio/sata/nvme/scsi) + network adapters + pci_disk_devices = 0 + for drive in ["a", "b", "c", "d"]: + # config disk replaces hdd when enabled + if drive == "d" and self._create_config_disk: + continue + if getattr(self, "_hd{}_disk_image".format(drive)): + interface = getattr(self, "_hd{}_disk_interface".format(drive)) + # virtio, sata, nvme, and scsi each consume 1 PCI slot; ide and none don't + if interface not in ("none", "ide"): + pci_disk_devices += 1 + # config disk (replaces hdd) also consumes a PCI slot if created + if self._create_config_disk and self.config_disk_image: + interface = self.hdd_disk_interface if self.hdd_disk_interface != "none" else self.hda_disk_interface + if interface not in ("none", "ide"): + pci_disk_devices += 1 + + pci_devices = 4 + pci_disk_devices + len(self._ethernet_adapters) pci_bridges = math.floor(pci_devices / 32) pci_bridges_created = 0 if pci_bridges >= 1: if self._qemu_version and parse_version(self._qemu_version) < parse_version("2.4.0"): raise QemuError("Qemu version 2.4 or later is required to run this VM with a large number of network adapters") - pci_device_id = 4 + pci_bridges # Bridge consume PCI ports + pci_device_id = 4 + pci_bridges + pci_disk_devices # Bridges and disks consume PCI ports for adapter_number, adapter in enumerate(self._ethernet_adapters): mac = int_to_macaddress(macaddress_to_int(self._mac_address) + adapter_number) From 3273f7e992eeb2a1761fd56e438ac011b9eec810 Mon Sep 17 00:00:00 2001 From: RishikesavanRamesh <84554651+RishikesavanRamesh@users.noreply.github.com> Date: Fri, 6 Feb 2026 20:34:44 +0530 Subject: [PATCH 2/5] Add dynamips to Dockerfile dependencies --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index afef06893..f40d736a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ RUN apt-get update && apt-get install -y \ RUN locale-gen en_US.UTF-8 # Install uninstall to install dependencies -RUN apt-get install -y vpcs ubridge +RUN apt-get install -y vpcs ubridge dynamips ADD . /server WORKDIR /server From 3dd4184193df244c13f873deaf8db2202f3573a5 Mon Sep 17 00:00:00 2001 From: Dale Arbogast Date: Thu, 19 Feb 2026 16:44:01 -0500 Subject: [PATCH 3/5] fix: busybox static link detection on Alpine/musl On musl-based systems (Alpine), ldd returns exit code 0 for static binaries, unlike glibc which returns 1. This causes install_busybox() to reject all busybox binaries as "dynamically linked" on Alpine. Fix by also accepting binaries whose executable name contains "static" (i.e. busybox-static, busybox.static), which are the first two candidates checked by the function. The generic "busybox" fallback still relies on the ldd return code check. --- gns3server/compute/docker/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gns3server/compute/docker/__init__.py b/gns3server/compute/docker/__init__.py index 696646ade..a441af830 100644 --- a/gns3server/compute/docker/__init__.py +++ b/gns3server/compute/docker/__init__.py @@ -79,8 +79,10 @@ class Docker(BaseManager): stderr=asyncio.subprocess.DEVNULL ) stdout, _ = await proc.communicate() - if proc.returncode == 1: + if proc.returncode == 1 or "static" in busybox_exec: # ldd returns 1 if the file is not a dynamic executable + # on Alpine/musl, ldd returns 0 even for static binaries, + # so also trust binaries named busybox-static or busybox.static log.info(f"Installing busybox from '{busybox_path}' to '{dst_busybox}'") shutil.copy2(busybox_path, dst_busybox, follow_symlinks=True) return From 715b7032bc538e0d86c30aab0bc81f58e0506465 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 20 Feb 2026 15:50:25 +0800 Subject: [PATCH 4/5] Fix tests --- tests/compute/docker/test_docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/compute/docker/test_docker.py b/tests/compute/docker/test_docker.py index 9e411503d..1bc98568d 100644 --- a/tests/compute/docker/test_docker.py +++ b/tests/compute/docker/test_docker.py @@ -251,7 +251,7 @@ async def test_install_busybox_dynamic_linked(): mock_process.communicate = AsyncioMagicMock(return_value=(b"Dynamically linked library", b"")) with patch("os.path.isfile", return_value=False): - with patch("gns3server.compute.docker.shutil.which", return_value="/usr/bin/busybox"): + with patch("gns3server.compute.docker.shutil.which", side_effect=lambda name: "/usr/bin/busybox" if name == "busybox" else None): with asyncio_patch("gns3server.compute.docker.asyncio.create_subprocess_exec", return_value=mock_process): with pytest.raises(DockerError) as e: dst_dir = Docker.resources_path() From 47cc827c1f5789f83146d6afa5299b0a0e2934ee Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 20 Feb 2026 15:56:08 +0800 Subject: [PATCH 5/5] Remove some files not related to PR --- dev-requirements.txt | 3 +-- gns3server/version.py | 4 ++-- requirements.txt | 7 +++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 49eec2b3e..ece129f97 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,6 @@ -rrequirements.txt -pytest==8.4.2; python_version == '3.9' # version 8.4.2 is the last one supporting Python 3.9 -pytest==9.0.2; python_version >= '3.10' +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/gns3server/version.py b/gns3server/version.py index c520f0086..b4b765e96 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.57.dev2" -__version_info__ = (2, 2, 57, 99) +__version__ = "2.2.56.1" +__version_info__ = (2, 2, 56, 0) if "dev" in __version__: try: diff --git a/requirements.txt b/requirements.txt index 4a55c7bfa..9c322ec53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,10 @@ -jsonschema==4.25.1; python_version == '3.9' # version 4.25.1 is the last to support Python 3.9 -jsonschema>=4.26.0,<4.27; python_version >= '3.10' +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.52.0,<3 # optional dependency -psutil>=7.2.2 +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