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