From f23c3bb6e602cd526335ca1191619f809b6c8503 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Thu, 1 Oct 2026 00:59:53 +0800 Subject: [PATCH] fix(iou): carry suspend from the NIO update body The IOU NIO update route never copied the body's suspend flag onto the NIO; the Docker and QEMU routes always did. Harmless on the relay, where suspend is emulated by the synthetic frequency_drop filter the controller injects, but fatal on a kernel link, whose suspend *is* the anchor's admin state: a suspended link kept forwarding traffic, and the resume looked like it worked because nothing had ever stopped. --- gns3server/api/routes/compute/iou_nodes.py | 5 +++- tests/api/routes/compute/test_iou_nodes.py | 34 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/gns3server/api/routes/compute/iou_nodes.py b/gns3server/api/routes/compute/iou_nodes.py index 661b63bec..4ae44a215 100644 --- a/gns3server/api/routes/compute/iou_nodes.py +++ b/gns3server/api/routes/compute/iou_nodes.py @@ -228,8 +228,11 @@ async def update_iou_node_nio( if isinstance(nio_data, schemas.UDPNIO) and nio_data.filters: nio.filters = nio_data.filters # NIO type is a Union (Ethernet/TAP/UDP/bridge); only UDPNIO and - # BridgeNIO carry markers. + # BridgeNIO carry markers and suspend. Suspend is what the compute turns + # into an admin-down anchor on a kernel link (native carrier), so it must + # reach the NIO like it does on the Docker/QEMU routes. nio.markers = getattr(nio_data, "markers", None) or {} + nio.suspend = getattr(nio_data, "suspend", None) or False await node.adapter_update_nio_binding(adapter_number, port_number, nio) return nio.asdict() diff --git a/tests/api/routes/compute/test_iou_nodes.py b/tests/api/routes/compute/test_iou_nodes.py index feebe2b87..1b815ded1 100644 --- a/tests/api/routes/compute/test_iou_nodes.py +++ b/tests/api/routes/compute/test_iou_nodes.py @@ -297,6 +297,40 @@ class TestIOUNodesRoutes: assert response.json()["type"] == "nio_udp" assert response.json()["filters"] == {} + async def test_iou_nio_update_carries_suspend_and_bridge( + self, app: FastAPI, compute_client: AsyncClient, vm: dict + ) -> None: + """ + A kernel-datapath NIO (nio_bridge) is accepted on the IOU routes, and + the update body's suspend flag reaches the NIO — the compute turns it + into an admin-down anchor, so a dropped suspend would leave a + "suspended" link forwarding traffic. + """ + + url = app.url_path_for( + "compute:create_iou_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="1", + port_number="0", + ) + params = {"type": "nio_bridge", "bridge": "gns3a1b2c3d4e5f"} + response = await compute_client.post(url, json=params) + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["type"] == "nio_bridge" + + url = app.url_path_for( + "compute:update_iou_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="1", + port_number="0", + ) + params["suspend"] = True + response = await compute_client.put(url, json=params) + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["suspend"] is True + async def test_iou_nio_create_ethernet( self, app: FastAPI, compute_client: AsyncClient, vm: dict, ethernet_device: str ) -> None: