diff --git a/gns3server/api/routes/compute/iou_nodes.py b/gns3server/api/routes/compute/iou_nodes.py index 50600a734..661b63bec 100644 --- a/gns3server/api/routes/compute/iou_nodes.py +++ b/gns3server/api/routes/compute/iou_nodes.py @@ -189,15 +189,15 @@ async def reload_iou_node(node: IOUVM = Depends(dep_node)) -> None: @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, - response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], + response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO, schemas.BridgeNIO], dependencies=[Depends(compute_authentication)], ) async def create_iou_node_nio( adapter_number: int, port_number: int, - nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], + nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO, schemas.BridgeNIO], node: IOUVM = Depends(dep_node), -) -> Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]: +) -> Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO, schemas.BridgeNIO]: """ Add a NIO (Network Input/Output) to the node. """ @@ -210,15 +210,15 @@ async def create_iou_node_nio( @router.put( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, - response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], + response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO, schemas.BridgeNIO], dependencies=[Depends(compute_authentication)], ) async def update_iou_node_nio( adapter_number: int, port_number: int, - nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], + nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO, schemas.BridgeNIO], node: IOUVM = Depends(dep_node), -) -> Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]: +) -> Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO, schemas.BridgeNIO]: """ Update a NIO (Network Input/Output) on the node. """ @@ -227,7 +227,8 @@ async def update_iou_node_nio( nio.filters.clear() if isinstance(nio_data, schemas.UDPNIO) and nio_data.filters: nio.filters = nio_data.filters - # NIO type is a Union (Ethernet/TAP/UDP); only UDPNIO carries markers. + # NIO type is a Union (Ethernet/TAP/UDP/bridge); only UDPNIO and + # BridgeNIO carry markers. nio.markers = getattr(nio_data, "markers", None) or {} await node.adapter_update_nio_binding(adapter_number, port_number, nio) return nio.asdict() diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py index 9eca6924a..416bb8050 100644 --- a/gns3server/controller/udp_link.py +++ b/gns3server/controller/udp_link.py @@ -179,7 +179,12 @@ class UDPLink(Link): must be able to anchor: Docker adapters are born as veth pairs, QEMU adapters as persistent TAPs, which need uBridge's tap module — asked of that compute, since an old uBridge leaves QEMU on the relay - datapath and cannot carry a kernel link at all. + datapath and cannot carry a kernel link at all. IOU's Ethernet + bays anchor on persistent TAPs too, bound to its IOL fabric + (``iol_bridge add_nio_tap``) — a separate capability, since it can + be present or absent independently of the tap module. A kernel link + is an Ethernet segment, so non-Ethernet ports (IOU serial) stay on + the relay whatever the node's capabilities. """ if not Config.instance().settings.Server.enable_kernel_datapath: @@ -188,6 +193,10 @@ class UDPLink(Link): return False if _is_unix_socket_docker(node1) or _is_unix_socket_docker(node2): return False + for side in self._nodes: + port = side.get("port") + if port is not None and port.link_type != "ethernet": + return False return self._kernel_endpoint_ready(node1) and self._kernel_endpoint_ready(node2) @staticmethod @@ -198,12 +207,14 @@ class UDPLink(Link): if node.node_type == "docker": return True - if node.node_type != "qemu": - return False capabilities = node.compute.capabilities or {} - # Strict True: an unreported capability (old uBridge, failed probe) - # keeps the link on the relay, where it always works. - return capabilities.get("ubridge_tap") is True + if node.node_type == "qemu": + # Strict True: an unreported capability (old uBridge, failed + # probe) keeps the link on the relay, where it always works. + return capabilities.get("ubridge_tap") is True + if node.node_type == "iou": + return capabilities.get("ubridge_iol_tap") is True + return False async def _prepare(self): """ diff --git a/tests/controller/test_kernel_datapath_link.py b/tests/controller/test_kernel_datapath_link.py index e714994dd..b61d5a644 100644 --- a/tests/controller/test_kernel_datapath_link.py +++ b/tests/controller/test_kernel_datapath_link.py @@ -107,8 +107,8 @@ async def _link(project, node1, node2): @pytest.mark.asyncio async def test_kernel_datapath_not_eligible_for_other_node_types(project): """ - A node type with no anchor of its own (IOU, VPCS, cloud, ...) keeps the - relay however capable the compute is — and a QEMU node on a compute whose + A node type with no anchor of its own (VPCS, cloud, ...) keeps the relay + however capable the compute is — and a QEMU node on a compute whose uBridge cannot create persistent TAPs does too, because it runs on the legacy relay datapath there. """ @@ -120,11 +120,76 @@ async def test_kernel_datapath_not_eligible_for_other_node_types(project): assert link._kernel_datapath_eligible(node1, node2) is False compute = _tap_capable_compute() + node1 = _node(project, compute, "vpcs1", node_type="vpcs") + node2 = _node(project, compute, "docker1") + link = await _link(project, node1, node2) + assert link._kernel_datapath_eligible(node1, node2) is False + + +@pytest.mark.asyncio +async def test_kernel_datapath_eligible_for_iou_with_iol_tap_support(project): + """ + IOU's Ethernet bays anchor on persistent TAPs bound to the IOL fabric + (iol_bridge add_nio_tap) — its own capability, asked per compute like + QEMU's, and mixable with docker/qemu endpoints on the same compute. + """ + + compute = MagicMock() + compute.id = "compute-1" + compute.capabilities = {"ubridge_tap": True, "ubridge_iol_tap": True} + node1 = _node(project, compute, "iou1", node_type="iou") + node2 = _node(project, compute, "iou2", node_type="iou") + link = await _link(project, node1, node2) + assert link._kernel_datapath_eligible(node1, node2) is True + + for peer in (_node(project, compute, "docker1"), _node(project, compute, "qemu1", node_type="qemu")): + link = await _link(project, node1, peer) + assert link._kernel_datapath_eligible(node1, peer) is True + + +@pytest.mark.asyncio +async def test_iou_eligibility_is_its_own_capability(project): + """ + The tap module and the IOL-port TAP command land independently: a + compute reporting only one of them keeps IOU on the relay (an old + uBridge without add_nio_tap), and an unreported/failed probe does too. + """ + + compute = _tap_capable_compute() # ubridge_tap only, no ubridge_iol_tap node1 = _node(project, compute, "iou1", node_type="iou") node2 = _node(project, compute, "docker1") link = await _link(project, node1, node2) assert link._kernel_datapath_eligible(node1, node2) is False + compute = MagicMock() + compute.id = "compute-1" + compute.capabilities = {"ubridge_tap": True, "ubridge_iol_tap": None} + node1 = _node(project, compute, "iou1", node_type="iou") + link = await _link(project, node1, node2) + assert link._kernel_datapath_eligible(node1, node2) is False + + +@pytest.mark.asyncio +async def test_kernel_datapath_not_eligible_for_serial_ports(project): + """ + A kernel link is an Ethernet segment: an IOU serial port stays on the + relay whatever the compute's capabilities — dispatching it to the + kernel path would fail at the compute (no anchor on a serial bay). + """ + + from gns3server.controller.ports.serial_port import SerialPort + + compute = MagicMock() + compute.id = "compute-1" + compute.capabilities = {"ubridge_tap": True, "ubridge_iol_tap": True} + + node1 = _node(project, compute, "iou1", node_type="iou") + node1._ports = [SerialPort("Serial0/0", 0, 0, 0)] + node2 = _node(project, compute, "iou2", node_type="iou") + node2._ports = [SerialPort("Serial0/0", 0, 0, 0)] + link = await _link(project, node1, node2) + assert link._kernel_datapath_eligible(node1, node2) is False + @pytest.mark.asyncio async def test_kernel_datapath_eligible_for_qemu_with_tap_support(project):