From ed0f1bb4deed24e49aeb7142673042ddbadf91b0 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Tue, 11 Aug 2026 00:54:53 +0800 Subject: [PATCH] fix: Dynamips create_nio(node, nio_settings) takes extra arg + add tests Dynamips.create_nio requires the node as first positional argument (unlike every other manager which takes only nio_settings). The batch handler now detects this via parameter-count inspection (3 vs 2) and passes node when needed. Also add Dynamips to _add_nio_binding dispatch: routers use slot_add_nio_binding(slot, port, nio), switches/hubs fall back to add_nio(nio, port_number). Add tests covering Dynamips router dispatch, switch dispatch, and the create_nio signature detection to prevent regression. --- gns3server/api/routes/compute/projects.py | 17 ++++++- tests/api/routes/compute/test_projects.py | 54 +++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/gns3server/api/routes/compute/projects.py b/gns3server/api/routes/compute/projects.py index 5a4cb63fd..2a946c55a 100644 --- a/gns3server/api/routes/compute/projects.py +++ b/gns3server/api/routes/compute/projects.py @@ -21,6 +21,7 @@ API routes for projects. import os import shutil import urllib.parse +import inspect import logging @@ -149,6 +150,13 @@ async def _add_nio_binding(node, adapter_number, port_number, nio): await node.adapter_add_nio_binding(adapter_number, port_number, nio) elif manager_name == "VPCS": await node.port_add_nio_binding(port_number, nio) + elif manager_name == "Dynamips": + # Dynamips routers use slot_add_nio_binding(slot, port, nio); + # Dynamips switches/hubs use add_nio(nio, port_number). + if hasattr(node, "slot_add_nio_binding"): + await node.slot_add_nio_binding(adapter_number, port_number, nio) + else: + await node.add_nio(nio, port_number) elif manager_name == "Builtin": # ethernet_switch / ethernet_hub / cloud / nat: add_nio(nio, port_number) await node.add_nio(nio, port_number) @@ -180,7 +188,14 @@ async def create_batch_nios( added = 0 for entry in batch.nios: node = project.get_node(entry.node_id) - nio = node.manager.create_nio(jsonable_encoder(entry.nio, exclude_unset=True)) + nio_settings = jsonable_encoder(entry.nio, exclude_unset=True) + # Dynamips.create_nio takes an extra positional `node` argument that + # the base signature does not include. Detect it via parameter count. + sig = inspect.signature(node.manager.create_nio) + if len(sig.parameters) == 3: + nio = node.manager.create_nio(node, nio_settings) + else: + nio = node.manager.create_nio(nio_settings) await _add_nio_binding(node, entry.adapter_number, entry.port_number, nio) added += 1 return {"added": added} diff --git a/tests/api/routes/compute/test_projects.py b/tests/api/routes/compute/test_projects.py index 27474044b..28fa35bf4 100644 --- a/tests/api/routes/compute/test_projects.py +++ b/tests/api/routes/compute/test_projects.py @@ -224,3 +224,57 @@ class TestComputeProjectRoutes: project_id=project.id, file_path=file_path), content=b"world") assert response.status_code == status.HTTP_403_FORBIDDEN + + +class TestBatchNIOEdgeCases: + + @pytest.mark.asyncio + async def test_dynamips_router_dispatch_to_slot_add_nio_binding(self): + """_add_nio_binding dispatches Dynamips router to slot_add_nio_binding.""" + from unittest.mock import AsyncMock, MagicMock + from gns3server.api.routes.compute.projects import _add_nio_binding + + node = MagicMock() + type(node.manager).__name__ = "Dynamips" + node.slot_add_nio_binding = AsyncMock() + nio = MagicMock() + + await _add_nio_binding(node, 0, 0, nio) + node.slot_add_nio_binding.assert_called_once_with(0, 0, nio) + + @pytest.mark.asyncio + async def test_dynamips_switch_dispatch_to_add_nio(self): + """_add_nio_binding dispatches Dynamips switch to add_nio.""" + from unittest.mock import AsyncMock, MagicMock + from gns3server.api.routes.compute.projects import _add_nio_binding + + node = MagicMock() + type(node.manager).__name__ = "Dynamips" + del node.slot_add_nio_binding # no slot_add_nio → switch path + node.add_nio = AsyncMock() + nio = MagicMock() + + await _add_nio_binding(node, 0, 0, nio) + node.add_nio.assert_called_once_with(nio, 0) + + @pytest.mark.asyncio + async def test_dynamips_create_nio_passes_node_arg(self): + """ + Dynamips.create_nio(self, node, nio_settings) takes an extra 'node' + positional; the batch handler detects this via parameter count. + """ + import inspect + + # Dynamips-style 3-param signature (self + node + nio_settings) + async def create_nio_with_node(self, node, nio_settings): + pass + + sig = inspect.signature(create_nio_with_node) + assert len(sig.parameters) == 3 + + # Standard base 2-param signature (self + nio_settings) + async def create_nio_base(self, nio_settings): + pass + + sig2 = inspect.signature(create_nio_base) + assert len(sig2.parameters) == 2