mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-12 21:45:41 +03:00
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:
parent
200c946eef
commit
806afec0d8
@ -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()
|
||||
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user