mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-13 05:55:35 +03:00
feat(iol): anchor IOL runner containers on persistent TAPs
IOL runner containers (IOLDockerVM, GNS3_IOL_RUNNER) had no kernel datapath: their guest leg is the runner's AF_UNIX socket pairs, so every link rode the uBridge relay. Their Ethernet bays now own persistent TAP anchors (gx names, the iol_docker key of the shared naming contract — born at node start like IOU's, before any link, so the switch fast path's deferred join can never miss them), and the per-port bridge's topology leg swaps between UDP (relay) and the anchor (kernel link) through the frozen stop -> delete -> add -> start contract — the unix binding never re-binds across a swap. - iol_docker_vm: KernelDatapathMixin hooks, the anchor lifecycle (_prepare_tap_datapath at _start_ubridge: probe ubridge_bridge_tap + ubridge_tap, sweep leftover, tap create born down), the kernel attach (ensure-then-add — tap create is strictly create-only, IFF_TUN_EXCL, so the ensure checks /sys/class/net first: an unconditional create answers EBUSY on the device the start loop just made), the release (port bridge stop + delete_nio_tap before the shared teardown) and the stop order the fd holders dictate (port bridges first, then tap delete, then the per-link bridges — all before the hypervisor stops). - vendor_docker_vm: the port-bridge creation factored into _ensure_unix_port_bridge (idempotent on the registered name), keeping the NIOBridge rejection for the generic unix-socket containers. - docker_vm: the three _kernel_veths lookups go through the _kernel_host_ifc hook — behavior-identical for DockerVM, and it is what lets the subclass route without overriding the binding methods. - kernel_anchor: the iol_docker key (gx) and kernel_anchor_type, the controller-side mapping for naming an IOL runner's anchor on the switch fast path (the marker check stays inline: this module sits below everything and must not import the controller package).
This commit is contained in:
parent
c20125c180
commit
4b4a0fc35f
@ -1807,16 +1807,17 @@ class DockerVM(DockerKernelDatapathMixin, BaseNode):
|
||||
|
||||
async def _connect_nio(self, adapter_number, nio, port_number=0):
|
||||
|
||||
host_ifc = self._kernel_veths.get((adapter_number, port_number))
|
||||
host_ifc = self._kernel_host_ifc(adapter_number, port_number)
|
||||
|
||||
if isinstance(nio, NIOBridge):
|
||||
if host_ifc is None:
|
||||
# Only reachable for containers started before the
|
||||
# unified-veth change (or unix-socket vendor containers,
|
||||
# rejected earlier): the kernel path needs the veth host end.
|
||||
# rejected earlier): the kernel path needs an anchor — the
|
||||
# veth host end, or the persistent TAP of an IOL runner.
|
||||
raise DockerError(
|
||||
"Adapter {adapter_number} port {port_number} of container '{name}' has no veth "
|
||||
"interface (it was started before the unified-veth datapath); "
|
||||
"Adapter {adapter_number} port {port_number} of container '{name}' has no host-side "
|
||||
"anchor interface (it was started before the unified-veth datapath); "
|
||||
"restart the node to attach a kernel link to it".format(
|
||||
adapter_number=adapter_number, port_number=port_number, name=self.name
|
||||
)
|
||||
@ -1894,7 +1895,7 @@ class DockerVM(DockerKernelDatapathMixin, BaseNode):
|
||||
if self.ubridge:
|
||||
if isinstance(nio, NIOBridge):
|
||||
if self.status == "started":
|
||||
host_ifc = self._kernel_veths.get((adapter_number, port_number))
|
||||
host_ifc = self._kernel_host_ifc(adapter_number, port_number)
|
||||
if host_ifc is not None:
|
||||
# Incremental marker + filter reconcile on the anchor
|
||||
# — the relay-datapath equivalent of the branch below.
|
||||
@ -2008,9 +2009,11 @@ class DockerVM(DockerKernelDatapathMixin, BaseNode):
|
||||
nio = self._ethernet_adapters[adapter_number].get_nio(port_number)
|
||||
if isinstance(nio, NIOBridge):
|
||||
# Kernel datapath: no relay bridge exists — capture via uBridge's
|
||||
# AF_PACKET module bound to the veth host end. Single capture per
|
||||
# uBridge process: a concurrent second one returns EALREADY.
|
||||
host_ifc = self._kernel_veths.get((adapter_number, port_number))
|
||||
# AF_PACKET module bound to the anchor (a veth host end for the
|
||||
# standard container, a persistent TAP for an IOL runner).
|
||||
# Single capture per uBridge process: a concurrent second one
|
||||
# returns EALREADY.
|
||||
host_ifc = self._kernel_host_ifc(adapter_number, port_number)
|
||||
await self._ubridge_send(f'capture start_kernel {host_ifc} "{output_file}"')
|
||||
return
|
||||
# Relay datapath (even though the adapter interface is a veth, the
|
||||
|
||||
@ -35,6 +35,17 @@ linked nodes get distinct MACs; starting a node without an allocation is an
|
||||
error, not a fallback — an uncoordinated id could collide with the pool and
|
||||
blackhole traffic as a MAC loop.
|
||||
|
||||
Kernel datapath: the unix-socket pair is this container's only physical
|
||||
layer, so one userspace hop on its leg is irreducible — but the *link
|
||||
segment* need not ride the relay. Every Ethernet bay/unit owns a persistent
|
||||
TAP anchor (``gx`` names, created at node start like IOU's), and the
|
||||
per-port uBridge bridge swaps its topology leg between UDP (relay) and the
|
||||
anchor (kernel link) through ``bridge add_nio_tap`` / ``bridge
|
||||
delete_nio_tap`` — stop → delete → add → start, the c-socket binding
|
||||
surviving the swap. Eligibility needs the compute's uBridge to report both
|
||||
``ubridge_tap`` (the tap module) and ``ubridge_bridge_tap`` (the swappable
|
||||
leg); anything less keeps the node relay-only, exactly as before.
|
||||
|
||||
This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker.
|
||||
"""
|
||||
|
||||
@ -51,6 +62,11 @@ from gns3server.compute.docker.docker_error import DockerError, DockerHttp404Err
|
||||
from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM
|
||||
from gns3server.compute.iou.utils.iou_export import nvram_export
|
||||
from gns3server.compute.iou.utils.iou_import import nvram_import
|
||||
from gns3server.compute.kernel_datapath import KernelDatapathMixin
|
||||
from gns3server.compute.nios.nio_bridge import NIOBridge
|
||||
from gns3server.compute.ubridge.tc_probe import probe_bridge_tap_support, probe_tap_support
|
||||
from gns3server.compute.ubridge.ubridge_error import UbridgeError
|
||||
from gns3server.utils.kernel_anchor import kernel_anchor_name
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -95,6 +111,15 @@ class IOLDockerVM(VendorDockerVM):
|
||||
_startup_config_content: str | None = None
|
||||
_startup_config_dirty = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
# Kernel datapath (see KernelDatapathMixin): one persistent TAP per
|
||||
# Ethernet bay/unit, held by uBridge's port bridge while a kernel
|
||||
# link binds the port (bridge add_nio_tap).
|
||||
self._kernel_taps = {}
|
||||
self._tap_datapath = False
|
||||
|
||||
def _parse_vendor_environment(self):
|
||||
|
||||
super()._parse_vendor_environment()
|
||||
@ -383,3 +408,268 @@ class IOLDockerVM(VendorDockerVM):
|
||||
"""
|
||||
|
||||
self._permissions_fixed = True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Kernel datapath (see KernelDatapathMixin)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _kernel_host_ifc(self, adapter_number, port_number=0):
|
||||
"""
|
||||
The persistent TAP this Ethernet bay/unit owns (one 4-port unit per
|
||||
adapter, the IOU model), or None before the node started — or on a
|
||||
uBridge that cannot serve the anchor lifecycle, in which case the
|
||||
node runs relay-only.
|
||||
"""
|
||||
|
||||
return self._kernel_taps.get((adapter_number, port_number))
|
||||
|
||||
def _kernel_anchors(self):
|
||||
"""
|
||||
Every anchor TAP this node currently owns.
|
||||
"""
|
||||
|
||||
return set(self._kernel_taps.values())
|
||||
|
||||
def _kernel_error(self, message):
|
||||
return DockerError(message)
|
||||
|
||||
def _tap_name(self, adapter_number, port_number):
|
||||
"""
|
||||
Deterministic anchor TAP name for an Ethernet bay/unit — the shared
|
||||
utils.kernel_anchor naming contract under the ``iol_docker`` key, so
|
||||
the controller names a peer's anchor with the same function when an
|
||||
Ethernet switch absorbs it.
|
||||
"""
|
||||
|
||||
return kernel_anchor_name("iol_docker", self._id, adapter_number, port_number)
|
||||
|
||||
async def _start_ubridge(self, require_privileged_access=False):
|
||||
"""
|
||||
Override: with the control channel up, give every Ethernet bay/unit
|
||||
its persistent TAP anchor — before the first link can attach (the
|
||||
start loop calls _add_ubridge_connection right after this). The
|
||||
anchors exist whether or not any link ever uses them: an anchor born
|
||||
with the link would leave the switch fast path's deferred join
|
||||
waiting forever (a link to a stopped node must not fail — and one to
|
||||
a just-started node must not silently miss its anchor either).
|
||||
"""
|
||||
|
||||
await super()._start_ubridge(require_privileged_access=require_privileged_access)
|
||||
await self._prepare_tap_datapath()
|
||||
|
||||
async def _prepare_tap_datapath(self):
|
||||
"""
|
||||
Probe uBridge for the commands the anchor lifecycle needs — the tap
|
||||
module (``tap create``/``tap delete``, the ``ubridge_tap``
|
||||
capability) and the bridge module's swappable TAP leg
|
||||
(``bridge delete_nio_tap``, ``ubridge_bridge_tap``) — and when both
|
||||
answer, create the persistent TAP every Ethernet bay/unit owns.
|
||||
A uBridge missing either keeps this node on the relay datapath:
|
||||
every link rides unix ↔ UDP through the port bridges, exactly as
|
||||
before the kernel datapath existed.
|
||||
"""
|
||||
|
||||
self._tap_datapath = False
|
||||
self._kernel_taps.clear()
|
||||
|
||||
if await probe_bridge_tap_support() is not True:
|
||||
log.info(
|
||||
"IOL container '%s': this compute's uBridge cannot swap a bridge's TAP leg "
|
||||
"(bridge delete_nio_tap); the node runs on the relay datapath and cannot "
|
||||
"carry kernel links",
|
||||
self._name,
|
||||
)
|
||||
return
|
||||
if await probe_tap_support() is not True:
|
||||
log.info(
|
||||
"IOL container '%s': this compute's uBridge has no persistent-TAP module; "
|
||||
"the node runs on the relay datapath and cannot carry kernel links",
|
||||
self._name,
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
async def _ensure_anchor(self, adapter_number, port_number):
|
||||
"""
|
||||
The ensure half of the frozen ensure-then-add contract: ``bridge
|
||||
add_nio_tap`` deliberately opens a *transient* TAP when the name is
|
||||
absent (cloud's bridge-interface path depends on it), so a persistent
|
||||
anchor swept away between the node's start and the link's attach
|
||||
would silently become a device that dies with the fd. ``tap create``
|
||||
is strictly create-only (``IFF_TUN_EXCL`` — re-creating an existing
|
||||
persistent device answers EBUSY), so the existence check is local:
|
||||
uBridge runs on this host, and /sys/class/net names every device.
|
||||
The repair create starts DOWN like every anchor at birth, so the
|
||||
carrier pass in the attach flow is what brings it up.
|
||||
"""
|
||||
|
||||
tap = self._tap_name(adapter_number, port_number)
|
||||
if os.path.exists(os.path.join("/sys/class/net", tap)):
|
||||
return
|
||||
await self._ubridge_send(f'tap create "{tap}"')
|
||||
|
||||
async def _add_ubridge_connection(self, nio, adapter_number, port_number=0):
|
||||
"""
|
||||
Override: the vendor rejection of kernel-datapath NIOs does not
|
||||
apply here — IOL runner adapters own persistent TAP anchors, so a
|
||||
NIOBridge wires the port bridge's topology leg to the anchor instead
|
||||
of raising (_connect_nio routes it; the port bridge and its unix NIO
|
||||
are ensured inside).
|
||||
"""
|
||||
|
||||
if isinstance(nio, NIOBridge):
|
||||
await self._connect_nio(adapter_number, nio, port_number)
|
||||
return
|
||||
await super()._add_ubridge_connection(nio, adapter_number, port_number)
|
||||
|
||||
async def _connect_nio(self, adapter_number, nio, port_number=0):
|
||||
"""
|
||||
Override: route the NIO onto the port bridge's topology leg. A
|
||||
kernel link (NIOBridge) swaps that leg to the port's anchor TAP and
|
||||
runs the shared mixin flow on it; a relay NIO keeps the unix ↔ UDP
|
||||
shape of the base class (the port bridge and its unix NIO ensured
|
||||
first, so the base path only adds the UDP half).
|
||||
"""
|
||||
|
||||
if isinstance(nio, NIOBridge):
|
||||
await self._attach_kernel_link(adapter_number, port_number, nio)
|
||||
return
|
||||
|
||||
await self._ensure_unix_port_bridge(adapter_number, port_number)
|
||||
await super()._connect_nio(adapter_number, nio, port_number)
|
||||
|
||||
async def _attach_kernel_link(self, adapter_number, port_number, nio):
|
||||
"""
|
||||
Wire a kernel link's NIO (NIOBridge) on this port: the port bridge's
|
||||
topology leg becomes the anchor TAP (``bridge add_nio_tap`` — uBridge
|
||||
opens and holds the fd, relaying the unix-socket guest leg onto the
|
||||
device), then the anchor is enslaved into the per-link kernel bridge
|
||||
— the shared mixin flow, with the TAP playing the role Docker's veth
|
||||
and QEMU's TAP play. The relay starts only once both NIOs are in
|
||||
(uBridge requires two), which is also what carries the guest frames
|
||||
onto the anchor.
|
||||
"""
|
||||
|
||||
anchor = self._kernel_host_ifc(adapter_number, port_number)
|
||||
if anchor is None:
|
||||
raise DockerError(
|
||||
"Bay {bay}/{unit} of IOL container '{name}' has no TAP anchor to carry a kernel "
|
||||
"link (this compute's uBridge lacks the tap module or bridge delete_nio_tap, "
|
||||
"or the node was started before the kernel-datapath support); restart the "
|
||||
"node after upgrading uBridge".format(bay=adapter_number, unit=port_number, name=self._name)
|
||||
)
|
||||
await self._ensure_unix_port_bridge(adapter_number, port_number)
|
||||
await self._ensure_anchor(adapter_number, port_number)
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
await self._ubridge_send(f'bridge add_nio_tap {bridge_name} "{anchor}"')
|
||||
# Per-link kernel bridge (brctl), capture, markers and impairment
|
||||
# filters on the anchor — everything keyed on the interface name.
|
||||
await self._kernel_attach(anchor, nio)
|
||||
# Two NIOs now: start the [unix ↔ tap] relay.
|
||||
await self._ubridge_send(f"bridge start {bridge_name}")
|
||||
# The carrier pass refines the anchor's admin state (a suspended NIO
|
||||
# sets it back down) — it is also what brings a born-down anchor up.
|
||||
await self._set_adapter_carrier(adapter_number, not nio.suspend, port_number)
|
||||
|
||||
async def _release_port_tap(self, adapter_number, port_number):
|
||||
"""
|
||||
The teardown half of the swap contract: stop the port bridge (its
|
||||
relay threads hold the NIO pointers for their whole life — freeing a
|
||||
NIO under them is a use-after-free, which is why delete_nio_tap
|
||||
refuses while running), then release the TAP NIO by name. The anchor
|
||||
device itself survives (the node owns it, not the link); the c-socket
|
||||
binding survives too (stop keeps every NIO), so the container's
|
||||
egress frames queue on the socket until a leg is swapped in again.
|
||||
"""
|
||||
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
with contextlib.suppress(UbridgeError):
|
||||
await self._ubridge_send(f"bridge stop {bridge_name}")
|
||||
tap = self._kernel_taps.get((adapter_number, port_number))
|
||||
if tap is not None:
|
||||
with contextlib.suppress(UbridgeError):
|
||||
await self._ubridge_send(f'bridge delete_nio_tap {bridge_name} "{tap}"')
|
||||
|
||||
async def adapter_remove_nio_binding(self, adapter_number, port_number=0):
|
||||
"""
|
||||
Override: a kernel link leaves through the port-leg swap (stop +
|
||||
delete_nio_tap) before the shared anchor-side teardown — markers,
|
||||
tc reset, brctl delif and the per-link bridge deletion — runs in the
|
||||
base path.
|
||||
"""
|
||||
|
||||
if self.ubridge:
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
except IndexError:
|
||||
adapter = None
|
||||
if adapter is not None and isinstance(adapter.get_nio(port_number), NIOBridge):
|
||||
await self._release_port_tap(adapter_number, port_number)
|
||||
await super().adapter_remove_nio_binding(adapter_number, port_number)
|
||||
|
||||
async def _stop_ubridge(self):
|
||||
"""
|
||||
Override: release the kernel datapath's host state while the control
|
||||
channel is still up, in the order the fd holders dictate — the port
|
||||
bridges hold the anchor TAP fds (``bridge add_nio_tap``), so they go
|
||||
first (``bridge delete`` stops their relay threads and frees the
|
||||
NIOs), then the anchors themselves (``tap delete`` answers EBADFD on
|
||||
a device another fd still holds), then the per-link kernel bridges.
|
||||
The port bridges die with the process anyway, but the anchors are
|
||||
persistent: deleting them here is what keeps a stopped node from
|
||||
littering the host with ``gx`` devices. The next start sweeps and
|
||||
re-creates everything.
|
||||
"""
|
||||
|
||||
if self.ubridge:
|
||||
# stop() clears _bridges before this point, so derive the port
|
||||
# bridge names from the adapters (the historical naming is a
|
||||
# pure function of adapter/port).
|
||||
for adapter_number in range(0, len(self._ethernet_adapters)):
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
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()
|
||||
self._tap_datapath = False
|
||||
self._ubridge_tc_caps = None
|
||||
await super()._stop_ubridge()
|
||||
|
||||
async def _set_adapter_carrier(self, adapter_number, connected, port_number=0):
|
||||
"""
|
||||
Override: refine the vendor no-op. A port with an anchor (the kernel
|
||||
datapath) toggles the anchor's admin state — the port bridge's TAP
|
||||
NIO writes answer EIO and its reads fall silent while it is down,
|
||||
which is the suspend semantics of every other anchored node type. A
|
||||
port without an anchor (relay datapath on a uBridge without the
|
||||
swappable leg) keeps the no-op: the unix-socket pair has no carrier,
|
||||
and the port bridge carries no TAP NIO for the base class to toggle.
|
||||
"""
|
||||
|
||||
if self._kernel_host_ifc(adapter_number, port_number) is not None:
|
||||
await KernelDatapathMixin._set_adapter_carrier(self, adapter_number, connected, port_number)
|
||||
|
||||
|
||||
@ -395,7 +395,28 @@ class VendorDockerVM(DockerVM):
|
||||
Override: with GNS3_UNIX_SOCKET_NIO, bridge the adapter port through
|
||||
the image's AF_UNIX datagram socket pair (raw Ethernet frames)
|
||||
instead of a TAP interface moved into the container's network
|
||||
namespace.
|
||||
namespace — see ``_ensure_unix_port_bridge`` for the wiring.
|
||||
"""
|
||||
|
||||
if not self._unix_socket_nio:
|
||||
return await super()._add_ubridge_connection(nio, adapter_number, port_number)
|
||||
|
||||
if isinstance(nio, NIOBridge):
|
||||
raise DockerError(
|
||||
f"Container '{self._name}' bridges its adapters through AF_UNIX sockets; "
|
||||
"kernel-datapath links are not supported for this container"
|
||||
)
|
||||
|
||||
await self._ensure_unix_port_bridge(adapter_number, port_number)
|
||||
|
||||
if nio:
|
||||
await self._connect_nio(adapter_number, nio, port_number)
|
||||
|
||||
async def _ensure_unix_port_bridge(self, adapter_number, port_number):
|
||||
"""
|
||||
The adapter port's uBridge bridge with its unix-socket NIO — the
|
||||
guest leg every link topology rides on. Idempotent: an already
|
||||
created bridge (this start wired it) is left alone.
|
||||
|
||||
Ports are addressed flat across adapters: interface index = adapter
|
||||
number × ports-per-adapter + port number (single-port adapters
|
||||
@ -413,15 +434,6 @@ class VendorDockerVM(DockerVM):
|
||||
uses.
|
||||
"""
|
||||
|
||||
if not self._unix_socket_nio:
|
||||
return await super()._add_ubridge_connection(nio, adapter_number, port_number)
|
||||
|
||||
if isinstance(nio, NIOBridge):
|
||||
raise DockerError(
|
||||
f"Container '{self._name}' bridges its adapters through AF_UNIX sockets; "
|
||||
"kernel-datapath links are not supported for this container"
|
||||
)
|
||||
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
except IndexError:
|
||||
@ -429,6 +441,8 @@ class VendorDockerVM(DockerVM):
|
||||
|
||||
interface_number = adapter_number * adapter.interfaces + port_number
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
if bridge_name in self._bridges:
|
||||
return
|
||||
try:
|
||||
await self._ubridge_send(f"bridge create {bridge_name}")
|
||||
self._bridges.add(bridge_name)
|
||||
@ -470,9 +484,6 @@ class VendorDockerVM(DockerVM):
|
||||
remote_sock,
|
||||
)
|
||||
|
||||
if nio:
|
||||
await self._connect_nio(adapter_number, nio, port_number)
|
||||
|
||||
async def _set_adapter_carrier(self, adapter_number, connected, port_number=0):
|
||||
"""
|
||||
Override: with GNS3_UNIX_SOCKET_NIO the bridges carry only unix and
|
||||
|
||||
@ -20,10 +20,12 @@ The kernel-datapath anchor naming contract, in one place.
|
||||
Every node type whose adapters own a host-side interface names that anchor
|
||||
deterministically: ``g<type>{node_id[:8]}e{adapter}p{port}`` — ``gv`` for a
|
||||
Docker veth host end, ``gq`` for a QEMU TAP, ``gi`` for an IOU port TAP,
|
||||
``gd`` for a Dynamips port TAP. The prefixes keep anchors out of the
|
||||
``gns3`` bridge/TAP name space and name-collision-free on a host; the 8
|
||||
hex chars of the node id plus adapter/port keep every name unique and
|
||||
within IFNAMSIZ (15).
|
||||
``gd`` for a Dynamips port TAP, ``gx`` for an IOL runner container's port
|
||||
TAP (a Docker node whose guest leg is unix sockets, so its anchor is a TAP
|
||||
like IOU's, not a veth). The prefixes keep anchors out of the ``gns3``
|
||||
bridge/TAP name space and name-collision-free on a host; the 8 hex chars of
|
||||
the node id plus adapter/port keep every name unique and within IFNAMSIZ
|
||||
(15).
|
||||
|
||||
The compute classes create their anchors with these helpers (single source
|
||||
of truth), and so does the controller when a switch link has to name the
|
||||
@ -40,12 +42,32 @@ log = logging.getLogger(__name__)
|
||||
# Docker veth guest end) is container-local and deliberately absent.
|
||||
ANCHOR_PREFIX = {
|
||||
"docker": "gv",
|
||||
"iol_docker": "gx",
|
||||
"qemu": "gq",
|
||||
"iou": "gi",
|
||||
"dynamips": "gd",
|
||||
}
|
||||
|
||||
|
||||
def kernel_anchor_type(node_type, environment=None):
|
||||
"""
|
||||
The anchor-type key a node of this type and environment names its
|
||||
anchors under. IOL runner containers are Docker nodes (node_type
|
||||
"docker") whose anchors are persistent TAPs, not veth host ends — their
|
||||
own key keeps the ``gv`` veth namespace free of TAPs (stale-interface
|
||||
sweeps and human eyes both key on the prefix). Every other node type is
|
||||
its own key unchanged.
|
||||
"""
|
||||
|
||||
# The marker check mirrors utils.application_id.is_iol_runner_environment
|
||||
# inline: this module sits below every node module (compute and
|
||||
# controller alike import it during their own import), so it must not
|
||||
# depend on anything that pulls the controller package in.
|
||||
if node_type == "docker" and "GNS3_IOL_RUNNER=" in (environment or ""):
|
||||
return "iol_docker"
|
||||
return node_type
|
||||
|
||||
|
||||
def anchor_suffix(node_id, adapter_number, port_number=0):
|
||||
"""
|
||||
The shared ``{node_id[:8]}e{adapter}p{port}`` name stem (8 hex chars of
|
||||
@ -59,7 +81,8 @@ def kernel_anchor_name(node_type, node_id, adapter_number, port_number=0):
|
||||
"""
|
||||
The host-side kernel-datapath anchor of one adapter port, or None for a
|
||||
node type without one (the anchor-less types never anchor a kernel
|
||||
link).
|
||||
link). Accepts either a raw node type or an anchor-type key from
|
||||
:func:`kernel_anchor_type`.
|
||||
"""
|
||||
|
||||
prefix = ANCHOR_PREFIX.get(node_type)
|
||||
|
||||
395
tests/compute/docker/test_iol_docker_kernel_datapath.py
Normal file
395
tests/compute/docker/test_iol_docker_kernel_datapath.py
Normal file
@ -0,0 +1,395 @@
|
||||
#
|
||||
# Copyright (C) 2026 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software and is distributed under the same terms as gns3-server.
|
||||
# See the LICENSE file for licensing information.
|
||||
#
|
||||
|
||||
"""
|
||||
Kernel-datapath tests for IOLDockerVM: the persistent TAP anchors (one per
|
||||
Ethernet bay/unit), the per-port bridge's swappable topology leg (unix ↔ UDP
|
||||
relay vs unix ↔ TAP anchor) and the teardown order the fd holders dictate.
|
||||
|
||||
Image-free: everything is asserted against the uBridge command stream and
|
||||
the anchor naming contract.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from tests.utils import AsyncioMagicMock
|
||||
|
||||
from gns3server.compute.docker import Docker
|
||||
from gns3server.compute.docker.docker_error import DockerError
|
||||
from gns3server.compute.docker.iol_docker_vm import IOLDockerVM
|
||||
|
||||
|
||||
NODE_ID = "00010203-0405-0607-0809-0a0b0c0d0e0f"
|
||||
TAP00 = "gx00010203e0p0" # the vm fixture's node id, bay 0 unit 0
|
||||
TAP01 = "gx00010203e0p1"
|
||||
TAP10 = "gx00010203e1p0"
|
||||
PORT_BRIDGE = "bridge0" # adapter 0 port 0 (single-port naming)
|
||||
BRIDGE = "gns3abc123def45" # the per-link kernel bridge carried by the NIO
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def manager(port_manager):
|
||||
|
||||
m = Docker.instance()
|
||||
m.port_manager = port_manager
|
||||
return m
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def vm(compute_project, manager):
|
||||
|
||||
vm = IOLDockerVM(
|
||||
"iol-xe-1",
|
||||
NODE_ID,
|
||||
compute_project,
|
||||
manager,
|
||||
"iol-xe/iol-xe:17-18-02",
|
||||
environment="GNS3_IOL_RUNNER=1",
|
||||
adapters=2,
|
||||
)
|
||||
vm._cid = "e90e34656842"
|
||||
vm.application_id = 700
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._ubridge_hypervisor.is_running.return_value = True
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
return vm
|
||||
|
||||
|
||||
def _capable(vm, monkeypatch):
|
||||
"""Make both anchor-lifecycle probes answer True."""
|
||||
|
||||
async def probe(timeout=15.0):
|
||||
return True
|
||||
|
||||
monkeypatch.setattr("gns3server.compute.docker.iol_docker_vm.probe_bridge_tap_support", probe)
|
||||
monkeypatch.setattr("gns3server.compute.docker.iol_docker_vm.probe_tap_support", probe)
|
||||
return vm
|
||||
|
||||
|
||||
def _commands(vm):
|
||||
return [str(c.args[0]) for c in vm._ubridge_send.call_args_list]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Anchor lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_tap_datapath_creates_one_tap_per_bay_unit(vm, monkeypatch):
|
||||
"""
|
||||
On a capable uBridge every Ethernet bay/unit gets its persistent TAP —
|
||||
four units per adapter (the IOU model), swept of leftovers first and
|
||||
born down. No set_owner: uBridge itself holds these fds.
|
||||
"""
|
||||
|
||||
_capable(vm, monkeypatch)
|
||||
await vm._prepare_tap_datapath()
|
||||
|
||||
assert vm._tap_datapath is True
|
||||
assert set(vm._kernel_taps) == {(bay, unit) for bay in range(2) for unit in range(4)}
|
||||
assert vm._kernel_taps[(0, 0)] == TAP00
|
||||
commands = _commands(vm)
|
||||
assert f'tap delete "{TAP00}"' in commands # the sweep precedes every create
|
||||
assert commands.index(f'tap delete "{TAP00}"') < commands.index(f'tap create "{TAP00}"')
|
||||
assert f'link set "{TAP00}" down' in commands
|
||||
assert not any("set_owner" in c for c in commands)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_tap_datapath_falls_back_without_either_probe(vm, monkeypatch):
|
||||
"""
|
||||
A uBridge without the swappable TAP leg (or without the tap module at
|
||||
all) keeps the node on the relay datapath — it still starts, it just
|
||||
cannot carry kernel links.
|
||||
"""
|
||||
|
||||
async def probe(timeout=15.0):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("gns3server.compute.docker.iol_docker_vm.probe_bridge_tap_support", probe)
|
||||
monkeypatch.setattr("gns3server.compute.docker.iol_docker_vm.probe_tap_support", probe)
|
||||
await vm._prepare_tap_datapath()
|
||||
|
||||
assert vm._tap_datapath is False
|
||||
assert vm._kernel_taps == {}
|
||||
assert not any("tap create" in c for c in _commands(vm))
|
||||
|
||||
|
||||
def test_tap_name_fits_ifnamsiz(vm):
|
||||
"""
|
||||
The anchor name has to fit IFNAMSIZ (15) even for a late bay/unit.
|
||||
"""
|
||||
|
||||
assert len(vm._tap_name(7, 3)) <= 15
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Link attach / detach (the port bridge's swappable topology leg)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_attach_kernel_link_swaps_the_port_leg_to_the_anchor(vm):
|
||||
"""
|
||||
A kernel link ensures the anchor (the frozen ensure-then-add contract —
|
||||
add_nio_tap on an absent name would open a transient TAP), swaps the
|
||||
port bridge's topology leg to it, runs the shared mixin flow (per-link
|
||||
kernel bridge, capture/markers/filters) and starts the relay — the unix
|
||||
binding untouched throughout.
|
||||
"""
|
||||
|
||||
vm._kernel_taps[(0, 0)] = TAP00
|
||||
vm._bridges.add(PORT_BRIDGE) # the start flow registered the port bridge
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
|
||||
await vm._attach_kernel_link(0, 0, nio)
|
||||
|
||||
commands = _commands(vm)
|
||||
assert f'tap create "{TAP00}"' in commands # ensure-then-add
|
||||
assert commands.index(f'tap create "{TAP00}"') < commands.index(f'bridge add_nio_tap {PORT_BRIDGE} "{TAP00}"')
|
||||
assert f'brctl create "{BRIDGE}"' in commands
|
||||
assert f'brctl addif "{BRIDGE}" "{TAP00}"' in commands
|
||||
assert commands.index(f'brctl addif "{BRIDGE}" "{TAP00}"') < commands.index(f"bridge start {PORT_BRIDGE}")
|
||||
assert f'link set "{TAP00}" up' in commands # the carrier pass brings the anchor up
|
||||
assert not any("add_nio_udp" in c for c in commands)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ensure_anchor_creates_only_when_missing(vm, monkeypatch):
|
||||
"""
|
||||
tap create is strictly create-only (IFF_TUN_EXCL — an existing device
|
||||
answers EBUSY), so the ensure checks existence first: a present anchor
|
||||
costs no command, a swept one is recreated (the repair half of the
|
||||
ensure-then-add contract).
|
||||
"""
|
||||
|
||||
vm._kernel_taps[(0, 0)] = TAP00
|
||||
|
||||
with monkeypatch.context() as missing:
|
||||
missing.setattr("os.path.exists", lambda p: False)
|
||||
await vm._ensure_anchor(0, 0)
|
||||
assert f'tap create "{TAP00}"' in _commands(vm)
|
||||
|
||||
vm._ubridge_send.reset_mock()
|
||||
with monkeypatch.context() as present:
|
||||
present.setattr("os.path.exists", lambda p: True)
|
||||
await vm._ensure_anchor(0, 0)
|
||||
vm._ubridge_send.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_attach_kernel_link_without_anchor_raises(vm):
|
||||
"""
|
||||
A kernel link on a node that never anchored (relay-only uBridge, or a
|
||||
start that predates the kernel datapath) is an error naming the relay
|
||||
datapath, not a silent fallback.
|
||||
"""
|
||||
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
|
||||
with pytest.raises(DockerError, match="no TAP anchor"):
|
||||
await vm._attach_kernel_link(0, 0, nio)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_ubridge_connection_routes_a_kernel_nio_to_the_anchor(vm):
|
||||
"""
|
||||
The start-flow entry (also the restart-with-attached-link path) accepts
|
||||
a kernel NIO where the vendor class rejects it: the port bridge is
|
||||
ensured, then the same attach flow runs.
|
||||
"""
|
||||
|
||||
vm._kernel_taps[(0, 0)] = TAP00
|
||||
vm._bridges.add(PORT_BRIDGE)
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
|
||||
await vm._add_ubridge_connection(nio, 0, 0)
|
||||
|
||||
assert f'bridge add_nio_tap {PORT_BRIDGE} "{TAP00}"' in _commands(vm)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_nio_binding_relay_keeps_the_unix_udp_shape(vm):
|
||||
"""
|
||||
A relay link on a running node wires only the UDP half — the port bridge
|
||||
and its unix NIO already exist from the start flow.
|
||||
"""
|
||||
|
||||
vm.status = "started"
|
||||
vm._bridges.add(PORT_BRIDGE)
|
||||
nio = vm.manager.create_nio({"type": "nio_udp", "lport": 20000, "rhost": "127.0.0.1", "rport": 20001})
|
||||
|
||||
await vm.adapter_add_nio_binding(0, nio, 0)
|
||||
|
||||
commands = _commands(vm)
|
||||
assert "bridge add_nio_udp bridge0 20000 127.0.0.1 20001" in commands
|
||||
assert "bridge start bridge0" in commands
|
||||
assert not any("add_nio_tap" in c for c in commands)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nio_binding_kernel_reapplies_on_the_anchor(vm):
|
||||
"""
|
||||
A filter update on an attached kernel link reconciles on the anchor (tc)
|
||||
without re-swapping the port leg or re-enslaving the TAP.
|
||||
"""
|
||||
|
||||
vm.status = "started"
|
||||
vm._kernel_taps[(0, 0)] = TAP00
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||
nio.filters = {"delay": [10]}
|
||||
|
||||
await vm.adapter_update_nio_binding(0, nio, 0)
|
||||
|
||||
commands = _commands(vm)
|
||||
assert f'tc reset "{TAP00}"' in commands
|
||||
assert f'tc netem set "{TAP00}" delay 10' in commands
|
||||
assert not any("add_nio_tap" in c for c in commands)
|
||||
assert not any("brctl addif" in c for c in commands)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nio_binding_kernel_suspend_flaps_the_anchor(vm):
|
||||
"""
|
||||
Suspending a kernel link admin-downs the anchor: the port bridge's TAP
|
||||
writes fail EIO (dropped) and nothing comes back — a genuinely dead
|
||||
link, where the relay datapath used the synthetic filter instead.
|
||||
"""
|
||||
|
||||
vm.status = "started"
|
||||
vm._kernel_taps[(0, 0)] = TAP00
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||
nio.suspend = True
|
||||
|
||||
await vm.adapter_update_nio_binding(0, nio, 0)
|
||||
|
||||
assert f'link set "{TAP00}" down' in _commands(vm)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_nio_binding_kernel_releases_the_port_leg_first(vm):
|
||||
"""
|
||||
Removing a kernel link stops the port bridge (its relay threads hold the
|
||||
NIO pointers for their whole life — delete_nio_tap refuses while
|
||||
running), releases the TAP NIO by name, then the shared anchor-side
|
||||
teardown runs: qdisc reset, brctl delif, per-link bridge deletion.
|
||||
"""
|
||||
|
||||
vm._kernel_taps[(0, 0)] = TAP00
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||
|
||||
await vm.adapter_remove_nio_binding(0, 0)
|
||||
|
||||
commands = _commands(vm)
|
||||
assert f"bridge stop {PORT_BRIDGE}" in commands
|
||||
assert f'bridge delete_nio_tap {PORT_BRIDGE} "{TAP00}"' in commands
|
||||
assert commands.index(f"bridge stop {PORT_BRIDGE}") < commands.index(f'bridge delete_nio_tap {PORT_BRIDGE} "{TAP00}"')
|
||||
assert f'tc reset "{TAP00}"' in commands
|
||||
assert f'brctl delif "{BRIDGE}" "{TAP00}"' in commands
|
||||
assert f'brctl delete "{BRIDGE}"' in commands
|
||||
assert commands.index(f'bridge delete_nio_tap {PORT_BRIDGE} "{TAP00}"') < commands.index(f'tc reset "{TAP00}"')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_nio_binding_relay_keeps_the_udp_teardown(vm):
|
||||
"""
|
||||
Removing a relay link keeps the historical teardown: stop + UDP NIO
|
||||
removal, the unix NIO and its binding untouched.
|
||||
"""
|
||||
|
||||
vm.status = "started"
|
||||
vm._bridges.add(PORT_BRIDGE)
|
||||
nio = vm.manager.create_nio({"type": "nio_udp", "lport": 20000, "rhost": "127.0.0.1", "rport": 20001})
|
||||
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||
|
||||
await vm.adapter_remove_nio_binding(0, 0)
|
||||
|
||||
commands = _commands(vm)
|
||||
assert f"bridge stop {PORT_BRIDGE}" in commands
|
||||
assert "bridge remove_nio_udp bridge0 20000 127.0.0.1 20001" in commands
|
||||
assert not any("delete_nio_tap" in c for c in commands)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stop order (the port bridges hold the anchor fds)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_ubridge_releases_fds_before_deleting_taps(vm):
|
||||
"""
|
||||
The port bridges hold the anchor TAP fds (bridge add_nio_tap), so they
|
||||
are deleted first — closing the fds — before the taps are unpersisted
|
||||
(tap delete answers EBADFD on a held device), and the per-link kernel
|
||||
bridges still need the live control channel. All before the hypervisor
|
||||
itself stops.
|
||||
"""
|
||||
|
||||
vm._kernel_taps[(0, 0)] = TAP00
|
||||
vm._kernel_taps[(1, 0)] = TAP10
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||
|
||||
with patch("gns3server.compute.docker.docker_vm.DockerVM._stop_ubridge", new=AsyncioMagicMock()) as stop:
|
||||
await vm._stop_ubridge()
|
||||
|
||||
stop.assert_called_once()
|
||||
commands = _commands(vm)
|
||||
assert f"bridge delete {PORT_BRIDGE}" in commands
|
||||
assert "bridge delete bridge1" in commands # bay 1 unit 0 (port 0 keeps the historical name)
|
||||
assert commands.index(f"bridge delete {PORT_BRIDGE}") < commands.index(f'tap delete "{TAP00}"')
|
||||
assert commands.index("bridge delete bridge1") < commands.index(f'tap delete "{TAP10}"')
|
||||
assert commands.index(f'tap delete "{TAP00}"') < commands.index(f'brctl delete "{BRIDGE}"')
|
||||
assert vm._kernel_taps == {}
|
||||
assert vm._tap_datapath is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_ubridge_prepares_the_datapath_before_any_link(vm):
|
||||
"""
|
||||
The anchors are born with the node (uBridge control channel up, before
|
||||
the start loop attaches links): an anchor that appears only with its
|
||||
link would leave the switch fast path's deferred join waiting forever.
|
||||
"""
|
||||
|
||||
with (
|
||||
patch("gns3server.compute.base_node.BaseNode._start_ubridge", new=AsyncioMagicMock()) as start,
|
||||
patch.object(IOLDockerVM, "_prepare_tap_datapath", new=AsyncioMagicMock()) as prepare,
|
||||
):
|
||||
await vm._start_ubridge(require_privileged_access=True)
|
||||
|
||||
start.assert_called_once()
|
||||
prepare.assert_called_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Anchor naming (the shared contract)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_anchor_names_follow_the_iol_docker_key():
|
||||
"""
|
||||
The controller names an IOL runner's anchor for switch absorption with
|
||||
the same function the compute creates it under — the iol_docker key,
|
||||
never the docker veth namespace.
|
||||
"""
|
||||
|
||||
from gns3server.utils.kernel_anchor import kernel_anchor_name, kernel_anchor_type
|
||||
|
||||
environment = "GNS3_IOL_RUNNER=1\nGNS3_IOL_STARTUP_CONFIG=cfg.txt"
|
||||
assert kernel_anchor_type("docker", environment) == "iol_docker"
|
||||
assert kernel_anchor_type("docker", "GNS3_UNIX_SOCKET_NIO=1") == "docker"
|
||||
assert kernel_anchor_name(kernel_anchor_type("docker", environment), NODE_ID, 0, 1) == TAP01
|
||||
@ -650,8 +650,8 @@ async def test_generic_unix_socket_dir_honored_in_wiring(compute_project, manage
|
||||
async def test_link_operations_never_send_tap_carrier(compute_project, manager):
|
||||
"""
|
||||
uBridge rejects set_nio_tap_carrier on a bridge without a TAP NIO
|
||||
("bridge has no TAP NIO"), and unix-socket NIO bridges never have one:
|
||||
every link create/update/delete on a running node would fail. The
|
||||
("bridge has no TAP NIO"), and unix-socket NIO relay bridges never have
|
||||
one: every link create/update/delete on a running node would fail. The
|
||||
vendor override must keep the carrier command out of all three paths.
|
||||
"""
|
||||
|
||||
@ -659,6 +659,10 @@ async def test_link_operations_never_send_tap_carrier(compute_project, manager):
|
||||
vm._ubridge_hypervisor = MagicMock() # truthy ubridge, like a started node
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
vm.status = "started"
|
||||
# A started node's port bridge exists and is registered by the start
|
||||
# flow — record it, or the relay attach would run the ensure path
|
||||
# (bridge create + socket wait) this fake never set up.
|
||||
vm._bridges.add(vm._bridge_name(0, 0))
|
||||
nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"})
|
||||
|
||||
await vm.adapter_add_nio_binding(0, nio, 0)
|
||||
|
||||
@ -21,7 +21,7 @@ Ethernet-switch link), plus the cascade veth naming for switch-to-switch
|
||||
links.
|
||||
"""
|
||||
|
||||
from gns3server.utils.kernel_anchor import anchor_suffix, kernel_anchor_name, kernel_cascade_names
|
||||
from gns3server.utils.kernel_anchor import anchor_suffix, kernel_anchor_name, kernel_anchor_type, kernel_cascade_names
|
||||
|
||||
NODE_ID = "00010203-0405-0607-0809-0a0b0c0d0e0f"
|
||||
LINK_ID = "1a2b3c4d-5e6f-4a5b-8c9d-0e1f2a3b4c5d"
|
||||
@ -32,6 +32,22 @@ def test_every_node_type_has_its_own_prefix():
|
||||
assert kernel_anchor_name("qemu", NODE_ID, 1, 2) == "gq00010203e1p2"
|
||||
assert kernel_anchor_name("iou", NODE_ID, 0, 3) == "gi00010203e0p3"
|
||||
assert kernel_anchor_name("dynamips", NODE_ID, 6, 48) == "gd00010203e6p48"
|
||||
assert kernel_anchor_name("iol_docker", NODE_ID, 0, 0) == "gx00010203e0p0"
|
||||
|
||||
|
||||
def test_anchor_type_keys_iol_runner_containers_apart():
|
||||
"""
|
||||
IOL runner containers are Docker nodes whose anchors are persistent TAPs,
|
||||
not veth host ends — their own key keeps the gv namespace free of TAPs.
|
||||
The marker check must ignore a mere mention of the knob and survive a
|
||||
multi-line environment.
|
||||
"""
|
||||
|
||||
assert kernel_anchor_type("docker", "GNS3_IOL_RUNNER=1") == "iol_docker"
|
||||
assert kernel_anchor_type("docker", "GNS3_IOL_STARTUP_CONFIG=cfg.txt\nGNS3_IOL_RUNNER=1") == "iol_docker"
|
||||
assert kernel_anchor_type("docker") == "docker"
|
||||
assert kernel_anchor_type("docker", "GNS3_UNIX_SOCKET_NIO=1") == "docker"
|
||||
assert kernel_anchor_type("qemu") == "qemu"
|
||||
|
||||
|
||||
def test_unknown_node_types_have_no_anchor():
|
||||
@ -62,7 +78,7 @@ def test_cascade_names_fit_ifnamsiz():
|
||||
|
||||
|
||||
def test_cascade_names_stay_out_of_the_node_anchor_namespace():
|
||||
# "gs" cannot collide with any node anchor prefix (gv/gq/gi/gd), so a
|
||||
# "gs" cannot collide with any node anchor prefix (gv/gq/gi/gd/gx), so a
|
||||
# cascade end is never mistaken for another node's anchor
|
||||
for end in kernel_cascade_names(LINK_ID):
|
||||
assert not any(end.startswith(prefix) for prefix in ("gv", "gq", "gi", "gd"))
|
||||
assert not any(end.startswith(prefix) for prefix in ("gv", "gq", "gi", "gd", "gx"))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user