mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-06 15:21:13 +02:00
feat(ubridge): probe iol_bridge's TAP-port support
Whether an IOU port can terminate on a persistent TAP (iol_bridge add_nio_tap) is a capability of its own: the tap module and that command land independently, so a build may have one without the other. Probed on a scratch IOL bridge with a scratch TAP — one add/delete cycle, no iol_bridge start needed (attaching a NIO works on a stopped bridge) — cached by binary identity, with the scratch bridge id above the per-node id space the fabric locks (513..1024) so a probe cannot collide with a running node. Any failure, including an old build's "Unknown command", means unknown: IOU stays on the relay datapath. Reported through /capabilities as ubridge_iol_tap, next to ubridge_tap.
This commit is contained in:
7 files changed
+178
-4
No files matched your search
@@ -25,7 +25,7 @@ from fastapi import APIRouter, Request
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.compute import MODULES
|
||||
from gns3server.compute.ubridge.tc_probe import probe_tc_capabilities, probe_tap_support
|
||||
from gns3server.compute.ubridge.tc_probe import probe_iol_tap_support, probe_tc_capabilities, probe_tap_support
|
||||
from gns3server.utils.path import get_default_project_directory
|
||||
from gns3server.utils.tc_capabilities import usable_ebpf_modes
|
||||
from gns3server.version import __version__
|
||||
@@ -56,6 +56,10 @@ async def get_capabilities(request: Request) -> dict:
|
||||
# datapath. None = unknown (probe failed or uBridge too old).
|
||||
ubridge_tap = await probe_tap_support()
|
||||
|
||||
# IOL-port TAP termination (iol_bridge add_nio_tap): the anchor IOU's
|
||||
# Ethernet ports need for the kernel datapath. None = unknown.
|
||||
ubridge_iol_tap = await probe_iol_tap_support()
|
||||
|
||||
# record the controller hostname or IP address
|
||||
if request.client:
|
||||
request.app.state.controller_host = request.client.host
|
||||
@@ -69,4 +73,5 @@ async def get_capabilities(request: Request) -> dict:
|
||||
"node_types": node_types,
|
||||
"ubridge_tc": ubridge_tc,
|
||||
"ubridge_tap": ubridge_tap,
|
||||
"ubridge_iol_tap": ubridge_iol_tap,
|
||||
}
|
||||
@@ -51,6 +51,8 @@ log = logging.getLogger(__name__)
|
||||
_cache = {}
|
||||
# binary identity -> whether the tap module works (None = unknown)
|
||||
_tap_cache = {}
|
||||
# binary identity -> whether iol_bridge can bind a port to a TAP (None = unknown)
|
||||
_iol_tap_cache = {}
|
||||
_lock = asyncio.Lock()
|
||||
|
||||
|
||||
@@ -183,3 +185,92 @@ async def _probe_tap(path, config, timeout):
|
||||
with contextlib.suppress(Exception):
|
||||
await hypervisor.stop()
|
||||
shutil.rmtree(working_dir, ignore_errors=True)
|
||||
|
||||
|
||||
# Above the per-node IOL bridge ids: a real node's application id is 1..512,
|
||||
# so its bridge locks /tmp/netio<uid>/513..1024. 1050 cannot collide with a
|
||||
# node — only with another probe process, whose failure then answers "unknown"
|
||||
# (the safe answer: IOU stays on the relay datapath).
|
||||
_IOL_PROBE_BRIDGE_ID = 1050
|
||||
|
||||
|
||||
async def probe_iol_tap_support(timeout: float = 15.0):
|
||||
"""
|
||||
Whether this host's uBridge can terminate an IOL port on a persistent
|
||||
TAP (``iol_bridge add_nio_tap``) — the anchor IOU's Ethernet ports need
|
||||
for the kernel datapath. Probed on a scratch IOL bridge with a scratch
|
||||
TAP (both cleaned up), cached by binary identity like the tap probe;
|
||||
any failure (missing binary, old build answering "Unknown command",
|
||||
lock contention with another server process) means unknown, reported as
|
||||
None and cached as such.
|
||||
"""
|
||||
|
||||
config = Config.instance()
|
||||
path = shutil.which(config.settings.Server.ubridge_path)
|
||||
if not path:
|
||||
return None
|
||||
try:
|
||||
stat = os.stat(path)
|
||||
key = (path, stat.st_mtime_ns, stat.st_size)
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
async with _lock:
|
||||
if key in _iol_tap_cache:
|
||||
return _iol_tap_cache[key]
|
||||
supported = await _probe_iol_tap(path, config, timeout)
|
||||
_iol_tap_cache[key] = supported
|
||||
return supported
|
||||
|
||||
|
||||
async def _probe_iol_tap(path, config, timeout):
|
||||
"""
|
||||
Spawn a throwaway uBridge and run one add_nio_tap/delete_nio_tap cycle on
|
||||
a scratch bridge and scratch TAP — attaching works on a stopped bridge,
|
||||
so no iol_bridge start is needed. The scratch resources are cleaned up
|
||||
best-effort even when the probe itself fails.
|
||||
"""
|
||||
|
||||
working_dir = tempfile.mkdtemp(prefix="gns3-iol-probe-")
|
||||
hypervisor = Hypervisor(
|
||||
None,
|
||||
path,
|
||||
working_dir,
|
||||
config.settings.Server.ubridge_control_transport,
|
||||
config.settings.Server.host,
|
||||
str(uuid.uuid4()),
|
||||
)
|
||||
bridge = f"gns3iolprobe{uuid.uuid4().hex[:4]}"
|
||||
tap = f"gns3ita{uuid.uuid4().hex[:6]}"
|
||||
try:
|
||||
|
||||
async def ask():
|
||||
await hypervisor.start()
|
||||
await hypervisor.connect()
|
||||
await hypervisor.send(f"iol_bridge create {bridge} {_IOL_PROBE_BRIDGE_ID}")
|
||||
try:
|
||||
await hypervisor.send(f"tap create {tap}")
|
||||
await hypervisor.send(
|
||||
f"iol_bridge add_nio_tap {bridge} {_IOL_PROBE_BRIDGE_ID - 1} 0 0 {tap}"
|
||||
)
|
||||
await hypervisor.send(f"iol_bridge delete_nio_tap {bridge} 0 0")
|
||||
finally:
|
||||
# iol_bridge delete releases every port NIO (the TAP fd),
|
||||
# so the subsequent tap delete cannot hit EBADFD.
|
||||
with contextlib.suppress(UbridgeError):
|
||||
await hypervisor.send(f"iol_bridge delete {bridge}")
|
||||
with contextlib.suppress(UbridgeError):
|
||||
await hypervisor.send(f"tap delete {tap}")
|
||||
return True
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(ask(), timeout=timeout)
|
||||
except (UbridgeError, OSError, asyncio.TimeoutError, ValueError) as e:
|
||||
# Old build ("Unknown command"), missing module, or no
|
||||
# CAP_NET_ADMIN: unknown.
|
||||
log.debug("uBridge iol-tap probe failed: %s", e)
|
||||
return None
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
await hypervisor.stop()
|
||||
shutil.rmtree(working_dir, ignore_errors=True)
|
||||
@@ -101,6 +101,7 @@ class Compute:
|
||||
"node_types": [],
|
||||
"ubridge_tc": None,
|
||||
"ubridge_tap": None,
|
||||
"ubridge_iol_tap": None,
|
||||
}
|
||||
self.name = name
|
||||
# Cache of interfaces on remote host
|
||||
|
||||
@@ -55,3 +55,9 @@ class Capabilities(BaseModel):
|
||||
description="Whether uBridge can create the persistent TAPs a QEMU adapter anchors on "
|
||||
"(needed to wire QEMU links on the kernel datapath); absent when the probe failed",
|
||||
)
|
||||
ubridge_iol_tap: Optional[bool] = Field(
|
||||
None,
|
||||
description="Whether uBridge can bind an IOL port to a persistent TAP (iol_bridge add_nio_tap, "
|
||||
"the anchor IOU Ethernet ports need to wire IOU links on the kernel datapath); absent when the "
|
||||
"probe failed",
|
||||
)
|
||||
Reference in new issue
Block a user