refactor: share the anchor-TAP lifecycle in the kernel-datapath mixin

The persistent-TAP lifecycle — probe, sweep, create, register, delete —
was implemented four times, once per node type, and the copies had drifted
(probe mechanism, _tap_datapath reset, keep-vs-recreate policy). The
mechanical half moves into KernelDatapathMixin:

- _probe_persistent_taps(probe_name): the scratch-TAP probe, returning the
  refusal for the caller's own warning (QEMU, Dynamips);
- _create_anchor_taps(ports, set_owner, keep_existing): sweep + create +
  optional ownership handover + carrier down + registry, with the ports
  enumerated by the node type and named through the new _tap_name hook;
- _delete_anchor_taps(): the suppressed tap sweep + per-link bridge
  cleanup + registry clear.

Each node type keeps what its fd holder dictates: the release step before
the tap sweep (the QEMU process, the IOL bridge, the hypervisor's NIO
bindings, the container port bridges), its probe policy, and its registry
lifetime. QEMU's now-unconditional registry clear aligns it with the other
three; everything else is behavior-preserving.
This commit is contained in:
YueGuobin 2026-10-06 21:17:50 +08:00
parent 35cf964882
commit 1542a222f4
No known key found for this signature in database
5 changed files with 133 additions and 140 deletions

View File

@ -489,25 +489,13 @@ class IOLDockerVM(VendorDockerVM):
return
self._tap_datapath = True
for adapter_number in range(0, len(self._ethernet_adapters)):
for port_number in range(0, self._ethernet_adapters[adapter_number].interfaces):
tap = self._tap_name(adapter_number, port_number)
# A persistent TAP outlives a crash: sweep any leftover of a
# previous run before recreating it (a surviving device would
# also keep its stale tc qdisc).
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f'tap delete "{tap}"')
await self._ubridge_send(f'tap create "{tap}"')
# Carrier off until a link attaches (the anchor contract).
await self._ubridge_send(f'link set "{tap}" down')
self._kernel_taps[(adapter_number, port_number)] = tap
log.debug(
"IOL container '%s': anchor TAP %s created for bay %d unit %d",
self._name,
tap,
adapter_number,
port_number,
)
# Every bay/unit anchors. No set_owner: the container port bridges
# hold these fds, and only while a kernel link is attached.
await self._create_anchor_taps(
(adapter_number, port_number)
for adapter_number in range(0, len(self._ethernet_adapters))
for port_number in range(0, self._ethernet_adapters[adapter_number].interfaces)
)
async def _ensure_anchor(self, adapter_number, port_number):
"""
@ -650,11 +638,7 @@ class IOLDockerVM(VendorDockerVM):
for port_number in range(0, adapter.interfaces):
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f"bridge delete {self._bridge_name(adapter_number, port_number)}")
for tap in self._kernel_taps.values():
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f'tap delete "{tap}"')
await self._remove_kernel_bridges()
self._kernel_taps.clear()
await self._delete_anchor_taps()
self._tap_datapath = False
self._ubridge_tc_caps = None
await super()._stop_ubridge()

View File

@ -42,7 +42,6 @@ from ...base_node import BaseNode
from ...error import NodeError
from ...kernel_datapath import KernelDatapathMixin
from ...nios.nio_bridge import NIOBridge
from ...ubridge.ubridge_error import UbridgeError
from ..adapters.adapter import ETHERNET_ADAPTERS, ETHERNET_WICS
from ..dynamips_error import DynamipsError
from ..nios.nio_tap import NIOTAP
@ -1648,19 +1647,15 @@ class Router(KernelDatapathMixin, BaseNode):
# uBridge (re)started: the tc capabilities must be probed again.
self._ubridge_tc_caps = None
if not self._tap_datapath:
probe = f"gd{self._id.replace('-', '')[:8]}prob"
try:
await self._ubridge_send(f'tap create "{probe}"')
except UbridgeError as e:
probe_error = await self._probe_persistent_taps(f"gd{self._id.replace('-', '')[:8]}prob")
if probe_error is not None:
message = (
f"Router '{self._name}': uBridge cannot create persistent TAPs ({e}); "
f"Router '{self._name}': uBridge cannot create persistent TAPs ({probe_error}); "
"this node runs on the relay datapath and cannot carry kernel links"
)
log.warning(message)
self.project.emit("log.warning", {"message": message})
return
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f'tap delete "{probe}"')
self._tap_datapath = True
await self._create_taps()
@ -1675,31 +1670,15 @@ class Router(KernelDatapathMixin, BaseNode):
async def _create_taps(self):
"""
Create the persistent TAP every Ethernet slot/port owns. Each TAP
starts DOWN (carrier off until a kernel link attaches); uBridge
creates the device and hands ownership to this user, so the
unprivileged Dynamips hypervisor can open it. A port whose anchor
already exists (a restart, or an adapter hot-added to a running
router) keeps it — an anchor is never recreated under a live link.
A persistent TAP outlives its creator, so a leftover from a previous
run (crash, kill) is swept first.
Create the persistent TAP every Ethernet slot/port owns (WIC ports
included), handed to this user so the unprivileged Dynamips
hypervisor can open the device. Unlike QEMU's, these anchors outlive
a node stop — the hypervisor keeps its NIO bindings — so a port that
already holds one keeps it: an anchor is never recreated under a
live link (a restart, or an adapter hot-added to a running router).
"""
for slot_number, port_number in self._ethernet_slot_ports():
if (slot_number, port_number) in self._kernel_taps:
continue
tap = self._tap_name(slot_number, port_number)
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f'tap delete "{tap}"')
await self._ubridge_send(f'tap create "{tap}"')
try:
await self._ubridge_send(f"tap set_owner {tap} {os.getuid()}")
except UbridgeError as e:
# Only root could open the TAP now: the hypervisor would fail
# to bind its NIO, which is worth surfacing at that point.
log.warning("Router '%s': could not hand TAP %s to uid %s: %s", self._name, tap, os.getuid(), e)
await self._ubridge_send(f'link set "{tap}" down')
self._kernel_taps[(slot_number, port_number)] = tap
await self._create_anchor_taps(self._ethernet_slot_ports(), set_owner=True, keep_existing=True)
async def _attach_kernel_nio(self, slot_number, port_number, nio):
"""
@ -1792,11 +1771,7 @@ class Router(KernelDatapathMixin, BaseNode):
with contextlib.suppress(DynamipsError, OSError):
await tap_nio.delete()
self._tap_nios.clear()
for tap in self._kernel_taps.values():
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f'tap delete "{tap}"')
await self._remove_kernel_bridges()
self._kernel_taps.clear()
await self._delete_anchor_taps()
self._ubridge_tc_caps = None
await super()._stop_ubridge()

View File

@ -929,27 +929,15 @@ class IOUVM(KernelDatapathMixin, BaseNode):
return
self._tap_datapath = True
await self._create_taps()
async def _create_taps(self):
"""
Create the persistent TAP every Ethernet bay/unit owns (four units
per bay, the IOU adapter model). uBridge holds a device's fd only
while a kernel link binds the port to it (``iol_bridge
add_nio_tap``); until then the TAP sits DOWN with no holder, and it
survives link churn untouched — links attach to it, never recreate
it. A persistent TAP outlives its creator, so a leftover from a
previous run (crash, kill) is swept first.
"""
for adapter_number, adapter in enumerate(self._ethernet_adapters):
for port_number in adapter.ports.keys():
tap = self._tap_name(adapter_number, port_number)
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f'tap delete "{tap}"')
await self._ubridge_send(f'tap create "{tap}"')
await self._ubridge_send(f'link set "{tap}" down')
self._kernel_taps[(adapter_number, port_number)] = tap
# Every Ethernet bay/unit anchors (four units per bay, the IOU
# adapter model; serial bays never do). No set_owner: uBridge holds
# these fds itself, and only while a kernel link binds the port —
# until then, and after, the TAP survives link churn untouched.
await self._create_anchor_taps(
(adapter_number, port_number)
for adapter_number, adapter in enumerate(self._ethernet_adapters)
for port_number in adapter.ports.keys()
)
async def _attach_kernel_nio(self, adapter_number, port_number, nio):
"""
@ -988,11 +976,7 @@ class IOUVM(KernelDatapathMixin, BaseNode):
if self.ubridge:
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f"iol_bridge delete {self._iol_bridge_name()}")
for tap in self._kernel_taps.values():
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f'tap delete "{tap}"')
await self._remove_kernel_bridges()
self._kernel_taps.clear()
await self._delete_anchor_taps()
self._ubridge_tc_caps = None
await super()._stop_ubridge()

View File

@ -34,6 +34,9 @@ A node type using this mixin provides:
or None when the adapter has none (a vendor container without a veth);
* ``_kernel_anchors()`` — every anchor it currently owns, used to recognise
the polymorphic marker anchor;
* ``_tap_name(adapter_number, port_number)`` — the deterministic anchor TAP
name of a port (the shared utils.kernel_anchor contract), for the node
types whose anchor is a persistent TAP;
* optionally ``_relay_carrier_bridge(adapter_number, port_number)`` — the
uBridge bridge carrying the adapter's TAP relay, for node types whose relay
datapath still rides a uBridge-owned TAP instead of an anchor;
@ -91,6 +94,15 @@ class KernelDatapathMixin:
return None
def _tap_name(self, adapter_number, port_number=0):
"""
Returns the deterministic anchor TAP name of a port (the shared
utils.kernel_anchor naming contract — the controller names a peer's
anchor with the same function when an Ethernet switch absorbs it).
"""
raise NotImplementedError
def _kernel_error(self, message):
"""
Returns the node module's error for a kernel-datapath failure
@ -130,6 +142,86 @@ class KernelDatapathMixin:
state = "on" if connected else "off"
await self._ubridge_send(f"bridge set_nio_tap_carrier {bridge_name} {state}")
# ------------------------------------------------------------------
# Anchor TAP lifecycle
# ------------------------------------------------------------------
async def _probe_persistent_taps(self, probe_name):
"""
Probe the node's uBridge for the persistent-TAP module with a
scratch TAP that is deleted again. Returns None when the module
works, or the UbridgeError that refused it — an old build answers
"Unknown command"/"Unknown module", a build without CAP_NET_ADMIN
fails outright. The caller composes its own warning and fallback:
the legacy relay datapath keeps working either way.
"""
try:
await self._ubridge_send(f'tap create "{probe_name}"')
except UbridgeError as e:
return e
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f'tap delete "{probe_name}"')
return None
async def _create_anchor_taps(self, ports, set_owner=False, keep_existing=False):
"""
Create the persistent anchor TAP of every port, named through
``_tap_name`` and registered in ``_kernel_taps``.
*ports* is an iterable of ``(adapter_number, port_number)`` keys — a
node type enumerates whichever of its ports anchor (QEMU's whole
adapters, IOU's bay/units, Dynamips' Ethernet slot ports, the IOL
container's bay/units). A persistent TAP outlives its creator, so a
leftover of a previous run (crash, kill) is swept first; each TAP is
created and starts DOWN (carrier off until a link attaches).
``set_owner`` hands the device to this user for the node types whose
unprivileged emulator opens it itself (QEMU, Dynamips);
``keep_existing`` leaves ports that already hold an anchor alone —
for a node whose anchors outlive its stop (Dynamips) or an adapter
hot-added to a running node, where an anchor must never be recreated
under a live link.
"""
for key in ports:
if keep_existing and key in self._kernel_taps:
continue
tap = self._tap_name(*key)
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f'tap delete "{tap}"')
await self._ubridge_send(f'tap create "{tap}"')
if set_owner:
try:
await self._ubridge_send(f"tap set_owner {tap} {os.getuid()}")
except UbridgeError as e:
# Only root could open the TAP then: the emulator would
# fail to attach its netdev, which is worth surfacing
# here rather than as a launch failure.
log.warning(
"Node '%s' [%s]: could not hand TAP %s to uid %s: %s", self._name, self._id, tap, os.getuid(), e
)
await self._ubridge_send(f'link set "{tap}" down')
self._kernel_taps[key] = tap
log.debug("Node '%s' [%s]: anchor TAP %s created for port %s", self._name, self._id, tap, key)
async def _delete_anchor_taps(self):
"""
Delete every registered anchor TAP and the per-link kernel bridges
this node still holds, then drop the registry. The caller releases
whatever holds each TAP's fd first (the QEMU process, the IOL
bridge, the hypervisor's NIO bindings, the container port bridges):
a held device answers EBADFD and the best-effort delete below would
leak it. The sweep is suppressed per TAP — an anchor already gone
is not an error — and the registry is cleared either way.
"""
if self.ubridge:
for tap in self._kernel_taps.values():
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f'tap delete "{tap}"')
await self._remove_kernel_bridges()
self._kernel_taps.clear()
# ------------------------------------------------------------------
# Link attach / detach
# ------------------------------------------------------------------

View File

@ -20,7 +20,6 @@ order to run a QEMU VM.
"""
import asyncio
import contextlib
import json
import logging
import math
@ -54,7 +53,6 @@ from ..kernel_datapath import KernelDatapathMixin
from ..nios.nio_bridge import NIOBridge
from ..nios.nio_tap import NIOTAP
from ..nios.nio_udp import NIOUDP
from ..ubridge.ubridge_error import UbridgeError
from .qemu_error import QemuError
from .utils.qcow2 import Qcow2, Qcow2Error
from .utils.ziputils import pack_zip, unpack_zip
@ -1565,65 +1563,26 @@ class QemuVM(KernelDatapathMixin, BaseNode):
if not self.ubridge:
return
probe = f"gq{self._id.replace('-', '')[:8]}prob"
try:
await self._ubridge_send(f'tap create "{probe}"')
except UbridgeError as e:
log.warning("QEMU VM '%s': uBridge cannot create persistent TAPs (%s)", self._name, e)
probe_error = await self._probe_persistent_taps(f"gq{self._id.replace('-', '')[:8]}prob")
if probe_error is not None:
log.warning("QEMU VM '%s': uBridge cannot create persistent TAPs (%s)", self._name, probe_error)
self.project.emit(
"log.warning",
{
"message": f"QEMU VM '{self._name}': uBridge does not support persistent TAPs ({e}); "
"message": f"QEMU VM '{self._name}': uBridge does not support persistent TAPs ({probe_error}); "
"this node runs on the legacy relay datapath and cannot carry kernel links"
},
)
return
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f'tap delete "{probe}"')
self._tap_datapath = True
await self._create_taps()
async def _create_taps(self):
"""
Create the persistent TAP every adapter owns. Each adapter is born as
a TAP whatever its NIO type — the anchor role Docker's veth host end
plays — so a link attaches to (or detaches from) a running VM without
touching its interfaces. uBridge creates the device and hands
ownership to this user, so the unprivileged QEMU process can open it;
the TAP starts DOWN (carrier off until a link attaches). A persistent
TAP outlives its creator, so a leftover from a previous run (crash,
kill) is swept first.
"""
for adapter_number, adapter in enumerate(self._ethernet_adapters):
tap = self._tap_name(adapter_number)
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f'tap delete "{tap}"')
await self._ubridge_send(f'tap create "{tap}"')
try:
await self._ubridge_send(f"tap set_owner {tap} {os.getuid()}")
except UbridgeError as e:
# Only root could open the TAP now: the VM would fail to
# attach its netdev, which is worth surfacing here rather
# than as a launch failure.
log.warning("QEMU VM '%s': could not hand TAP %s to uid %s: %s", self._name, tap, os.getuid(), e)
await self._ubridge_send(f'link set "{tap}" down')
self._kernel_taps[(adapter_number, 0)] = tap
async def _remove_taps(self):
"""
Delete the adapter TAPs together with the per-link kernel bridges
this VM still holds: a stopped VM has no links attached, so those
bridges are orphans whose ports just disappeared.
"""
if self.ubridge:
for tap in self._kernel_taps.values():
with contextlib.suppress(UbridgeError):
await self._ubridge_send(f'tap delete "{tap}"')
await self._remove_kernel_bridges()
self._kernel_taps.clear()
# Every adapter anchors (port 0 only — QEMU adapters are
# single-port). The shared lifecycle sweeps leftovers first, creates
# each TAP and hands it to this user (unprivileged QEMU opens it
# itself), leaving it down until a link attaches.
await self._create_anchor_taps(
((adapter_number, 0) for adapter_number, _adapter in enumerate(self._ethernet_adapters)), set_owner=True
)
async def _stop_ubridge(self):
"""
@ -1635,8 +1594,7 @@ class QemuVM(KernelDatapathMixin, BaseNode):
probed again.
"""
if self.ubridge:
await self._remove_taps()
await self._delete_anchor_taps()
self._ubridge_tc_caps = None
await super()._stop_ubridge()