feat(controller): offer IOU the kernel datapath

Eligibility gains an IOU branch gated on the compute's ubridge_iol_tap
(strict True: an unreported capability keeps the link on the relay), and
a port-level rule: a kernel link is an Ethernet segment, so any endpoint
whose port is not Ethernet (IOU serial) disqualifies the link before it
is dispatched. Without that, a serial link between two capable IOU nodes
would be sent as nio_bridge and fail at the compute.

The IOU NIO routes accept nio_bridge alongside the UDP/TAP/Ethernet NIOs
— without the widening the controller's POST is 422'd before the node
ever sees it, the same trap the QEMU routes hit.
This commit is contained in:
YueGuobin committed 2026-10-06 00:21:09 +08:00
1 parent 200c946eef
commit 806afec0d8
3 files changed
+92 -15

No files matched your search

+8 -7
View File
@@ -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()
+17 -6
View File
@@ -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):
"""