mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-12 13:35:31 +03:00
feat(dynamips): anchor every Ethernet port on a persistent TAP
Dynamips links can now ride the kernel datapath like Docker, QEMU and IOU:
each Ethernet slot/port gets a persistent TAP anchor (gd{id}e{slot}p{port}),
created through uBridge's tap module, and the Dynamips hypervisor opens it
with nio create_tap — the hypervisor holds the fd, exactly QEMU's shape.
Links attach the anchor to their per-link kernel bridge; the legacy UDP
tunnel remains the relay datapath for every other port.
Anchor lifecycle: born at node start (tap module probed with one
create/delete cycle, sweep of stale devices, ownership handed to the server
user, born down); a kernel link opens the anchor in the hypervisor, binds
the slot/port and enslaves it; removing the link releases the fd again
(nio delete) and detaches. A link bound while the router is stopped is
wired by the next start (deferred wiring). Node close follows QEMU's stop
order — the hypervisor's TAP NIOs go before tap delete, since it is
Dynamips, not uBridge, that holds the fds.
Stop is not close: vm stop only halts the emulated router, so the
hypervisor, the TAP fds, the bindings and the per-link bridges all survive
a stop/start untouched (attach skips ports that still hold their TAP NIO).
Port selection follows the adapter registry: ETHERNET_ADAPTERS and
ETHERNET_WICS name the models whose ports carry Ethernet frames (WIC-1ENET
included, at its Dynamips port number); serial, ATM and POS ports never
anchor, and a kernel link there errors with the relay-datapath message
instead of half-wiring. Adapters hot-added to a running router (OIR) get
their anchors at once.
The compute NIO routes accept nio_bridge alongside nio_udp (the same
widening QEMU needed), carry the endpoint's suspend flag on update (it is
the anchor's admin state on a kernel link), skip the hypervisor delete for
a kernel NIO (its TAP NIO was already released by the slot removal), and
the close path leaves NIOBridge instances to _stop_ubridge.
This commit is contained in:
parent
6633304e97
commit
7201a870da
@ -29,6 +29,7 @@ from fastapi.responses import StreamingResponse
|
||||
from gns3server import schemas
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.dynamips.nodes.router import Router
|
||||
from gns3server.compute.nios.nio_bridge import NIOBridge
|
||||
|
||||
from .dependencies.authentication import compute_authentication, ws_compute_authentication
|
||||
|
||||
@ -173,12 +174,15 @@ async def reload_router(node: Router = Depends(dep_node)) -> None:
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
response_model=Union[schemas.UDPNIO, schemas.BridgeNIO],
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
)
|
||||
async def create_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: Router = Depends(dep_node)
|
||||
) -> schemas.UDPNIO:
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.UDPNIO, schemas.BridgeNIO],
|
||||
node: Router = Depends(dep_node),
|
||||
) -> Union[schemas.UDPNIO, schemas.BridgeNIO]:
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
"""
|
||||
@ -191,12 +195,15 @@ async def create_nio(
|
||||
@router.put(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
response_model=Union[schemas.UDPNIO, schemas.BridgeNIO],
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
)
|
||||
async def update_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: Router = Depends(dep_node)
|
||||
) -> schemas.UDPNIO:
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
nio_data: Union[schemas.UDPNIO, schemas.BridgeNIO],
|
||||
node: Router = Depends(dep_node),
|
||||
) -> Union[schemas.UDPNIO, schemas.BridgeNIO]:
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
"""
|
||||
@ -206,6 +213,10 @@ async def update_nio(
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
nio.markers = nio_data.markers or {}
|
||||
# Suspend is what the compute turns into an admin-down anchor on a
|
||||
# kernel link (native carrier), so it must reach the NIO like it does
|
||||
# on the Docker/QEMU/IOU routes.
|
||||
nio.suspend = getattr(nio_data, "suspend", None) or False
|
||||
await node.slot_update_nio_binding(adapter_number, port_number, nio)
|
||||
return nio.asdict()
|
||||
|
||||
@ -221,7 +232,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: Router = Depen
|
||||
"""
|
||||
|
||||
nio = await node.slot_remove_nio_binding(adapter_number, port_number)
|
||||
await nio.delete()
|
||||
if not isinstance(nio, NIOBridge):
|
||||
# A kernel NIO has no hypervisor side left: its TAP NIO was deleted
|
||||
# by the slot binding removal itself.
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post(
|
||||
|
||||
@ -39,6 +39,7 @@ from gns3server.utils.asyncio import subprocess_check_output, wait_run_in_execut
|
||||
from gns3server.utils.interfaces import is_interface_up
|
||||
|
||||
from ..base_manager import BaseManager
|
||||
from ..nios.nio_bridge import NIOBridge
|
||||
from ..port_manager import PortManager
|
||||
|
||||
# Adapters
|
||||
@ -364,7 +365,16 @@ class Dynamips(BaseManager):
|
||||
"""
|
||||
|
||||
nio = None
|
||||
if nio_settings["type"] == "nio_udp":
|
||||
if nio_settings["type"] == "nio_bridge":
|
||||
# Kernel-datapath NIO: the router binds the port's anchor TAP to
|
||||
# a hypervisor nio_tap and enslaves it into this per-link bridge
|
||||
# (Router._attach_kernel_nio). No hypervisor NIO is created here.
|
||||
nio = NIOBridge(nio_settings["bridge"])
|
||||
nio.filters = nio_settings.get("filters", {})
|
||||
nio.markers = nio_settings.get("markers", {})
|
||||
nio.suspend = nio_settings.get("suspend", False)
|
||||
return nio
|
||||
elif nio_settings["type"] == "nio_udp":
|
||||
lport = nio_settings["lport"]
|
||||
rhost = nio_settings["rhost"]
|
||||
rport = nio_settings["rport"]
|
||||
|
||||
@ -15,6 +15,37 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# Adapter and WIC models whose ports carry Ethernet frames (by __str__
|
||||
# name, matching the controller's port matrix). Only these ports get a
|
||||
# kernel-datapath anchor TAP: a TAP cannot carry the serial/ATM/POS
|
||||
# encapsulations the other models speak.
|
||||
ETHERNET_ADAPTERS = frozenset(
|
||||
{
|
||||
"C1700-MB-1FE",
|
||||
"C2600-MB-1E",
|
||||
"C2600-MB-1FE",
|
||||
"C2600-MB-2E",
|
||||
"C2600-MB-2FE",
|
||||
"C7200-IO-2FE",
|
||||
"C7200-IO-FE",
|
||||
"C7200-IO-GE-E",
|
||||
"GT96100-FE",
|
||||
"Leopard-2FE",
|
||||
"NM-16ESW",
|
||||
"NM-1E",
|
||||
"NM-1FE-TX",
|
||||
"NM-4E",
|
||||
"PA-2FE-TX",
|
||||
"PA-4E",
|
||||
"PA-8E",
|
||||
"PA-FE-TX",
|
||||
"PA-GE",
|
||||
}
|
||||
)
|
||||
|
||||
ETHERNET_WICS = frozenset({"WIC-1ENET"})
|
||||
|
||||
|
||||
class Adapter:
|
||||
"""
|
||||
Base class for adapters.
|
||||
|
||||
@ -22,6 +22,7 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L77
|
||||
import asyncio
|
||||
import base64
|
||||
import binascii
|
||||
import contextlib
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
@ -37,10 +38,16 @@ from gns3server.utils.hostname import is_ios_hostname_valid
|
||||
from gns3server.utils.images import md5sum
|
||||
|
||||
from ...base_node import BaseNode
|
||||
from ...error import NodeError
|
||||
from ...kernel_datapath import KernelDatapathMixin
|
||||
from ...nios.nio_bridge import NIOBridge
|
||||
from ...nios.nio_tap import NIOTAP
|
||||
from ...ubridge.ubridge_error import UbridgeError
|
||||
from ..adapters.adapter import ETHERNET_ADAPTERS, ETHERNET_WICS
|
||||
from ..dynamips_error import DynamipsError
|
||||
|
||||
|
||||
class Router(BaseNode):
|
||||
class Router(KernelDatapathMixin, BaseNode):
|
||||
"""
|
||||
Dynamips router implementation.
|
||||
|
||||
@ -123,6 +130,13 @@ class Router(BaseNode):
|
||||
self._slots = []
|
||||
self._ghost_flag = ghost_flag
|
||||
self._memory_watcher = None
|
||||
# Kernel datapath: the persistent anchor TAP per Ethernet slot/port
|
||||
# (in _kernel_taps) and the hypervisor TAP NIO holding each anchor
|
||||
# open while a kernel link binds the port (in _tap_nios).
|
||||
self._kernel_taps = {}
|
||||
self._tap_nios = {}
|
||||
self._tap_datapath = False
|
||||
self._ubridge_tc_caps = None
|
||||
|
||||
if not ghost_flag:
|
||||
if not dynamips_id:
|
||||
@ -320,7 +334,16 @@ class Router(BaseNode):
|
||||
# an empty private-config can prevent a router to boot.
|
||||
private_config_path = ""
|
||||
|
||||
await self._hypervisor.send(f'vm set_config "{self._name}" "{startup_config_path}" "{private_config_path}"')
|
||||
# uBridge owns the kernel datapath's anchor TAPs; start it and
|
||||
# create them before the VM boots, so kernel links bound while
|
||||
# the node was stopped wire themselves as IOS comes up.
|
||||
await self._prepare_tap_datapath()
|
||||
|
||||
await self._hypervisor.send(
|
||||
'vm set_config "{name}" "{startup}" "{private}"'.format(
|
||||
name=self._name, startup=startup_config_path, private=private_config_path
|
||||
)
|
||||
)
|
||||
await self._hypervisor.send(f'vm start "{self._name}"')
|
||||
self.status = "started"
|
||||
log.debug(f'router "{self._name}" [{self._id}] has been started')
|
||||
@ -421,7 +444,9 @@ class Router(BaseNode):
|
||||
for adapter in self._slots:
|
||||
if adapter is not None:
|
||||
for nio in adapter.ports.values():
|
||||
if nio:
|
||||
# NIOBridge has no hypervisor side to close here — its
|
||||
# release runs in _stop_ubridge below.
|
||||
if nio and not isinstance(nio, NIOBridge):
|
||||
await nio.close()
|
||||
|
||||
await self._stop_ubridge()
|
||||
@ -1143,6 +1168,11 @@ class Router(BaseNode):
|
||||
|
||||
log.debug(f'Router "{self._name}" [{self._id}]: OIR start event sent to slot {slot_number}')
|
||||
|
||||
if self._tap_datapath:
|
||||
# The new adapter's Ethernet ports need their anchor TAPs now
|
||||
# (not at next start) for kernel links to attach at once.
|
||||
await self._create_taps()
|
||||
|
||||
async def slot_remove_binding(self, slot_number):
|
||||
"""
|
||||
Removes a slot binding (a module from a slot).
|
||||
@ -1281,6 +1311,19 @@ class Router(BaseNode):
|
||||
if not adapter.port_exists(port_number):
|
||||
raise DynamipsError(f"Port {port_number} does not exist on adapter {adapter}")
|
||||
|
||||
if isinstance(nio, NIOBridge):
|
||||
# Kernel datapath: the port's anchor TAP is opened in the
|
||||
# hypervisor and enslaved into the NIO's per-link bridge (the
|
||||
# wiring is deferred to the node's start when no anchors exist).
|
||||
adapter.add_nio(port_number, nio)
|
||||
await self._attach_kernel_nio(slot_number, port_number, nio)
|
||||
log.debug(
|
||||
'Router "{name}" [{id}]: {nio} bound to port {slot_number}/{port_number}'.format(
|
||||
name=self._name, id=self._id, nio=nio, slot_number=slot_number, port_number=port_number
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
await self._hypervisor.send(f'vm slot_add_nio_binding "{self._name}" {slot_number} {port_number} {nio}')
|
||||
except DynamipsError:
|
||||
@ -1302,6 +1345,15 @@ class Router(BaseNode):
|
||||
:param nio: NIO instance to add to the slot/port
|
||||
"""
|
||||
|
||||
if isinstance(nio, NIOBridge):
|
||||
# Filters, markers or suspend changed on a kernel link: re-apply
|
||||
# on the anchor, no re-binding and no re-enslaving. A NIO bound
|
||||
# while the node was stopped carries its state; start applies it.
|
||||
anchor = self._kernel_host_ifc(slot_number, port_number)
|
||||
if anchor is not None and self.ubridge:
|
||||
await self._kernel_update(anchor, nio)
|
||||
await self._set_adapter_carrier(slot_number, not nio.suspend, port_number)
|
||||
return
|
||||
await nio.update()
|
||||
|
||||
async def slot_remove_nio_binding(self, slot_number, port_number):
|
||||
@ -1326,12 +1378,27 @@ class Router(BaseNode):
|
||||
raise DynamipsError(f"Port {port_number} does not exist on adapter {adapter}")
|
||||
|
||||
await self.stop_capture(slot_number, port_number)
|
||||
await self.slot_disable_nio(slot_number, port_number)
|
||||
await self._hypervisor.send(f'vm slot_remove_nio_binding "{self._name}" {slot_number} {port_number}')
|
||||
|
||||
nio = adapter.get_nio(port_number)
|
||||
if nio is None:
|
||||
return
|
||||
|
||||
if isinstance(nio, NIOBridge):
|
||||
await self._remove_kernel_nio_binding(slot_number, port_number, nio)
|
||||
adapter.remove_nio(port_number)
|
||||
log.debug(
|
||||
'Router "{name}" [{id}]: {nio} removed from port {slot_number}/{port_number}'.format(
|
||||
name=self._name, id=self._id, nio=nio, slot_number=slot_number, port_number=port_number
|
||||
)
|
||||
)
|
||||
return nio
|
||||
|
||||
await self.slot_disable_nio(slot_number, port_number)
|
||||
await self._hypervisor.send(
|
||||
'vm slot_remove_nio_binding "{name}" {slot_number} {port_number}'.format(
|
||||
name=self._name, slot_number=slot_number, port_number=port_number
|
||||
)
|
||||
)
|
||||
await nio.close()
|
||||
adapter.remove_nio(port_number)
|
||||
|
||||
@ -1421,6 +1488,21 @@ class Router(BaseNode):
|
||||
if not nio:
|
||||
raise DynamipsError(f"Port {slot_number}/{port_number} is not connected")
|
||||
|
||||
if isinstance(nio, NIOBridge):
|
||||
# Kernel link: capture on the port's TAP anchor (AF_PACKET),
|
||||
# not in the Dynamips NIO (this link has no Dynamips NIO).
|
||||
nio.start_packet_capture(output_file, data_link_type)
|
||||
if self.ubridge:
|
||||
anchor = self._kernel_host_ifc(slot_number, port_number)
|
||||
if anchor is not None:
|
||||
await self._ubridge_send(f'capture start_kernel {anchor} "{output_file}"')
|
||||
log.debug(
|
||||
'Router "{name}" [{id}]: starting packet capture on port {slot_number}/{port_number}'.format(
|
||||
name=self._name, id=self._id, slot_number=slot_number, port_number=port_number
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
|
||||
raise DynamipsError(f"Port {port_number} has already a filter applied on {adapter}")
|
||||
await nio.start_packet_capture(output_file, data_link_type)
|
||||
@ -1448,10 +1530,265 @@ class Router(BaseNode):
|
||||
|
||||
if not nio.capturing:
|
||||
return
|
||||
|
||||
if isinstance(nio, NIOBridge):
|
||||
nio.stop_packet_capture()
|
||||
if self.ubridge and self._kernel_host_ifc(slot_number, port_number) is not None:
|
||||
await self._ubridge_send("capture stop_kernel")
|
||||
return
|
||||
await nio.stop_packet_capture()
|
||||
|
||||
log.debug(f'Router "{self._name}" [{self._id}]: stopping packet capture on port {slot_number}/{port_number}')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Kernel datapath: the port anchor is a persistent TAP held by the
|
||||
# Dynamips hypervisor (nio create_tap) — QEMU's shape, not IOU's
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _tap_name(self, slot_number, port_number):
|
||||
"""
|
||||
Deterministic anchor TAP name for a slot/port. The ``gd`` prefix
|
||||
keeps it out of the ``gns3`` bridge/TAP name space (and apart from
|
||||
Docker's ``gv``/``gc``, QEMU's ``gq`` and IOU's ``gi``); 8 hex chars
|
||||
of the node id plus slot/port keep it unique and within IFNAMSIZ
|
||||
(15), even for WIC port numbers (16, 32, 48).
|
||||
"""
|
||||
|
||||
return f"gd{self._id.replace('-', '')[:8]}e{slot_number}p{port_number}"
|
||||
|
||||
def _kernel_host_ifc(self, slot_number, port_number=0):
|
||||
"""
|
||||
The persistent TAP this Ethernet slot/port owns, or None for serial,
|
||||
ATM or POS ports (never anchored) and before the router started.
|
||||
"""
|
||||
|
||||
return self._kernel_taps.get((slot_number, port_number))
|
||||
|
||||
def _kernel_anchors(self):
|
||||
"""
|
||||
Every anchor TAP this router currently owns.
|
||||
"""
|
||||
|
||||
return set(self._kernel_taps.values())
|
||||
|
||||
def _kernel_error(self, message):
|
||||
return DynamipsError(message)
|
||||
|
||||
def _ethernet_slot_ports(self):
|
||||
"""
|
||||
Every (slot, port) that can carry Ethernet frames: slot adapters
|
||||
whose model is Ethernet, plus WIC-1ENET ports (numbered from 16 per
|
||||
WIC slot, the Dynamips convention) in any motherboard.
|
||||
"""
|
||||
|
||||
ports = []
|
||||
for slot_number, adapter in enumerate(self._slots):
|
||||
if adapter is None:
|
||||
continue
|
||||
if str(adapter) in ETHERNET_ADAPTERS:
|
||||
ports.extend((slot_number, port_number) for port_number in adapter.ports)
|
||||
for wic_slot_number, wic in enumerate(adapter.wics or []):
|
||||
if wic is not None and str(wic) in ETHERNET_WICS:
|
||||
ports.append((slot_number, 16 * (wic_slot_number + 1)))
|
||||
return ports
|
||||
|
||||
@property
|
||||
def _ethernet_adapters(self):
|
||||
"""
|
||||
Slot adapters owning at least one anchor — the mixin's per-link
|
||||
bridge sweep walks them. Dynamips ports live in slot adapters (WIC
|
||||
ports included), so this derives from the anchor map rather than
|
||||
the slot list.
|
||||
"""
|
||||
|
||||
adapters = []
|
||||
for slot_number, _port_number in self._kernel_taps:
|
||||
if slot_number < len(self._slots):
|
||||
adapter = self._slots[slot_number]
|
||||
if adapter is not None and adapter not in adapters:
|
||||
adapters.append(adapter)
|
||||
return adapters
|
||||
|
||||
async def _prepare_tap_datapath(self):
|
||||
"""
|
||||
Start uBridge and create the persistent TAP every Ethernet slot/port
|
||||
owns — the anchor role QEMU's TAPs and Docker's veth host ends play.
|
||||
The Dynamips hypervisor opens the device (ownership handed to this
|
||||
user by _create_taps), so it needs no privileges of its own to hold
|
||||
the fd. A uBridge without the tap module keeps this router on the
|
||||
relay datapath: every link rides the NIOUDP tunnel, exactly as
|
||||
before.
|
||||
|
||||
Nothing here is undone at node stop — the hypervisor, its NIO
|
||||
bindings and the per-link kernel bridges all outlive ``vm stop``,
|
||||
which is what makes a stopped router's links self-heal on the next
|
||||
start. The anchors are only retired when the node closes.
|
||||
"""
|
||||
|
||||
if self._ghost_flag:
|
||||
# Ghost IOS images share RAM with a real router and never link.
|
||||
return
|
||||
|
||||
try:
|
||||
await self._start_ubridge()
|
||||
except NodeError as e:
|
||||
# A Dynamips router without links never needed uBridge; keep the
|
||||
# node bootable and let the relay raise at link time (as before).
|
||||
log.warning("Router '%s': %s", self._name, e)
|
||||
return
|
||||
|
||||
# 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:
|
||||
message = (
|
||||
f"Router '{self._name}': uBridge cannot create persistent TAPs ({e}); "
|
||||
"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()
|
||||
# Kernel links bound while the router was stopped could not wire (no
|
||||
# anchors existed); wire them now, before IOS comes up.
|
||||
for slot_number, adapter in enumerate(self._slots):
|
||||
if adapter is None:
|
||||
continue
|
||||
for port_number, nio in list(adapter.ports.items()):
|
||||
if isinstance(nio, NIOBridge):
|
||||
await self._attach_kernel_nio(slot_number, port_number, nio)
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
async def _attach_kernel_nio(self, slot_number, port_number, nio):
|
||||
"""
|
||||
Wire a kernel link's NIO (NIOBridge) on a port: open the port's
|
||||
anchor TAP in the Dynamips hypervisor (``nio create_tap`` — the
|
||||
hypervisor, not uBridge, holds the fd, the same shape as QEMU), bind
|
||||
it to the slot/port, then enslave the anchor into the per-link
|
||||
kernel bridge — the shared mixin flow. On a router that is not
|
||||
running the wiring is deferred to its start (no anchors exist yet).
|
||||
A port whose wiring survived a stop (nothing is torn down there) is
|
||||
left alone: re-binding would collide on both hypervisor and bridge.
|
||||
"""
|
||||
|
||||
if (slot_number, port_number) in self._tap_nios:
|
||||
return
|
||||
anchor = self._kernel_host_ifc(slot_number, port_number)
|
||||
if anchor is None:
|
||||
if self.status != "started":
|
||||
return
|
||||
raise self._kernel_error(
|
||||
"Port {slot}/{port} of router '{name}' runs on the relay datapath "
|
||||
"(this uBridge cannot create persistent TAPs) and cannot carry a kernel link".format(
|
||||
slot=slot_number, port=port_number, name=self._name
|
||||
)
|
||||
)
|
||||
|
||||
tap_nio = NIOTAP(self._hypervisor, anchor)
|
||||
await tap_nio.create()
|
||||
try:
|
||||
await self._hypervisor.send(
|
||||
'vm slot_add_nio_binding "{name}" {slot_number} {port_number} {nio}'.format(
|
||||
name=self._name, slot_number=slot_number, port_number=port_number, nio=tap_nio.name
|
||||
)
|
||||
)
|
||||
except DynamipsError:
|
||||
with contextlib.suppress(DynamipsError):
|
||||
await tap_nio.delete()
|
||||
raise
|
||||
await self.slot_enable_nio(slot_number, port_number)
|
||||
self._tap_nios[(slot_number, port_number)] = tap_nio
|
||||
await self._kernel_attach(anchor, nio)
|
||||
await self._set_adapter_carrier(slot_number, not nio.suspend, port_number)
|
||||
|
||||
async def _remove_kernel_nio_binding(self, slot_number, port_number, nio):
|
||||
"""
|
||||
Release a kernel link's NIO: tear the anchor out of the per-link
|
||||
kernel bridge (markers, tc, delif — the shared mixin flow), then
|
||||
unbind the port and delete the hypervisor's TAP NIO, releasing the
|
||||
anchor fd. The anchor device itself survives for the next link.
|
||||
"""
|
||||
|
||||
if self.ubridge and self._kernel_host_ifc(slot_number, port_number) is not None:
|
||||
await self._remove_kernel_nio(nio, slot_number, port_number)
|
||||
tap_nio = self._tap_nios.pop((slot_number, port_number), None)
|
||||
if tap_nio is not None:
|
||||
with contextlib.suppress(DynamipsError, OSError):
|
||||
await self.slot_disable_nio(slot_number, port_number)
|
||||
with contextlib.suppress(DynamipsError, OSError):
|
||||
await self._hypervisor.send(
|
||||
'vm slot_remove_nio_binding "{name}" {slot_number} {port_number}'.format(
|
||||
name=self._name, slot_number=slot_number, port_number=port_number
|
||||
)
|
||||
)
|
||||
with contextlib.suppress(DynamipsError, OSError):
|
||||
await tap_nio.delete()
|
||||
|
||||
async def _stop_ubridge(self):
|
||||
"""
|
||||
Stops uBridge, retiring the kernel datapath's host state first — in
|
||||
the QEMU order, since here it is the Dynamips hypervisor (not
|
||||
uBridge) that holds the anchor fds: the per-link kernel bridges need
|
||||
the control channel, and the anchor TAPs (persistent devices) must
|
||||
be deleted through it after the hypervisor's TAP NIOs are gone, or a
|
||||
device Dynamips still holds open would outlive its retirement. The
|
||||
next start spawns a fresh uBridge, so the tc capabilities must be
|
||||
probed again.
|
||||
"""
|
||||
|
||||
if self.ubridge:
|
||||
for (slot_number, port_number), tap_nio in list(self._tap_nios.items()):
|
||||
with contextlib.suppress(DynamipsError, OSError):
|
||||
await self._hypervisor.send(
|
||||
'vm slot_remove_nio_binding "{name}" {slot_number} {port_number}'.format(
|
||||
name=self._name, slot_number=slot_number, port_number=port_number
|
||||
)
|
||||
)
|
||||
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()
|
||||
self._ubridge_tc_caps = None
|
||||
await super()._stop_ubridge()
|
||||
|
||||
def _create_slots(self, numslots):
|
||||
"""
|
||||
Creates the appropriate number of slots for this router.
|
||||
|
||||
614
tests/compute/dynamips/test_dynamips_kernel_datapath.py
Normal file
614
tests/compute/dynamips/test_dynamips_kernel_datapath.py
Normal file
@ -0,0 +1,614 @@
|
||||
#
|
||||
# Copyright (C) 2026 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
Kernel-datapath wiring for Dynamips routers: the persistent TAP every
|
||||
Ethernet slot/port owns (Ethernet adapters and WIC-1ENET ports only — serial,
|
||||
ATM and POS adapters never anchor), the hypervisor TAP NIO (nio create_tap)
|
||||
that holds the anchor open, per-link enslavement, the deferred wiring of
|
||||
kernel links bound while the node was stopped, and the stop order (the
|
||||
hypervisor's fds released before the TAPs are unpersisted — QEMU's order,
|
||||
since here it is Dynamips that holds them).
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import contextlib
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from tests.utils import AsyncioMagicMock
|
||||
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.dynamips.nodes.router import Router
|
||||
from gns3server.compute.dynamips.dynamips_error import DynamipsError
|
||||
from gns3server.compute.dynamips.adapters.gt96100_fe import GT96100_FE
|
||||
from gns3server.compute.dynamips.adapters.pa_4t import PA_4T
|
||||
from gns3server.compute.dynamips.adapters.pa_fe_tx import PA_FE_TX
|
||||
from gns3server.compute.dynamips.adapters.wic_1enet import WIC_1ENET
|
||||
from gns3server.compute.ubridge.ubridge_error import UbridgeError
|
||||
from gns3server.compute.nios.nio_bridge import NIOBridge
|
||||
|
||||
|
||||
NODE_ID = "00010203-0405-0607-0809-0a0b0c0d0e0f"
|
||||
BRIDGE = "gns3a1b2c3d4e5f"
|
||||
TAP00 = "gd00010203e0p0" # the router fixture's node id, slot 0 port 0
|
||||
TAP01 = "gd00010203e0p1"
|
||||
TAP016 = "gd00010203e0p16" # WIC-1ENET port in WIC slot 0
|
||||
|
||||
|
||||
class FakeDynamipsHypervisor:
|
||||
"""Records the hypervisor protocol; answers status queries."""
|
||||
|
||||
def __init__(self, status_code=2):
|
||||
self.commands = []
|
||||
self._status_code = status_code
|
||||
|
||||
def is_running(self):
|
||||
return True
|
||||
|
||||
async def send(self, command):
|
||||
self.commands.append(command)
|
||||
if command.startswith("vm get_status"):
|
||||
return [str(self._status_code)]
|
||||
return ["100-OK"]
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def manager(port_manager):
|
||||
|
||||
m = Dynamips.instance()
|
||||
m.port_manager = port_manager
|
||||
return m
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def router(compute_project, manager):
|
||||
|
||||
router = Router("R1", NODE_ID, compute_project, manager)
|
||||
router._hypervisor = FakeDynamipsHypervisor()
|
||||
router._ubridge_hypervisor = MagicMock()
|
||||
router._ubridge_hypervisor.is_running.return_value = True
|
||||
router._ubridge_send = AsyncioMagicMock()
|
||||
# slot 0: a 2-port Ethernet motherboard with a WIC-1ENET in WIC slot 0
|
||||
# (port 16); slot 1: a serial PA that must never anchor.
|
||||
router._create_slots(2)
|
||||
router._slots[0] = GT96100_FE()
|
||||
router._slots[0]._wics[0] = WIC_1ENET()
|
||||
router._slots[1] = PA_4T()
|
||||
yield router
|
||||
# compute_project is the shared, id-fixed project every compute test
|
||||
# file reuses (ProjectManager caches by id); leave its dynamips module
|
||||
# directory as this fixture found it (absent) — a later manager test
|
||||
# creates it with os.makedirs and no exist_ok.
|
||||
shutil.rmtree(router._working_directory, ignore_errors=True)
|
||||
with contextlib.suppress(OSError):
|
||||
os.rmdir(compute_project.module_working_path(manager.module_name.lower()))
|
||||
|
||||
|
||||
async def _nio(router, **kwargs):
|
||||
settings = {"type": "nio_bridge", "bridge": BRIDGE}
|
||||
settings.update(kwargs)
|
||||
return await router.manager.create_nio(router, settings)
|
||||
|
||||
|
||||
def _hypervisor_commands(router):
|
||||
return list(router._hypervisor.commands)
|
||||
|
||||
|
||||
def _ubridge_commands(router):
|
||||
return [str(c[0][0]) if c[0] else "" for c in router._ubridge_send.call_args_list]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Anchor topology: which ports anchor at all
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_ethernet_slot_ports_skip_serial_and_include_wics(router):
|
||||
"""
|
||||
Only Ethernet-carrying ports anchor: the PA's two ports plus the
|
||||
WIC-1ENET port (numbered from 16 per WIC slot), never the serial PA.
|
||||
"""
|
||||
|
||||
assert router._ethernet_slot_ports() == [(0, 0), (0, 1), (0, 16)]
|
||||
|
||||
|
||||
def test_tap_name_fits_ifnamsiz(router):
|
||||
"""
|
||||
The anchor name has to fit IFNAMSIZ (15) even for WIC port numbers.
|
||||
"""
|
||||
|
||||
assert len(router._tap_name(6, 48)) <= 15
|
||||
|
||||
|
||||
def test_kernel_host_ifc_is_none_before_start(router):
|
||||
assert router._kernel_host_ifc(0, 0) is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Anchor lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_tap_datapath_creates_one_tap_per_ethernet_port(router):
|
||||
"""
|
||||
On a capable uBridge every Ethernet slot/port gets its persistent TAP —
|
||||
swept of leftovers first, ownership handed to this user (so the
|
||||
unprivileged hypervisor can open it), born down.
|
||||
"""
|
||||
|
||||
router._ubridge_send.reset_mock()
|
||||
await router._prepare_tap_datapath()
|
||||
|
||||
assert router._tap_datapath is True
|
||||
assert router._kernel_taps == {(0, 0): TAP00, (0, 1): TAP01, (0, 16): TAP016}
|
||||
router._ubridge_send.assert_any_call(f'tap delete "{TAP00}"')
|
||||
router._ubridge_send.assert_any_call(f'tap create "{TAP00}"')
|
||||
router._ubridge_send.assert_any_call(f"tap set_owner {TAP016} {os.getuid()}")
|
||||
router._ubridge_send.assert_any_call(f'link set "{TAP016}" down')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_tap_datapath_falls_back_without_the_tap_module(router):
|
||||
"""
|
||||
A uBridge without the tap module keeps the router on the relay datapath —
|
||||
it still starts, it just cannot carry kernel links.
|
||||
"""
|
||||
|
||||
async def refuse_probe(command):
|
||||
if "prob" in command:
|
||||
raise UbridgeError("Unknown command")
|
||||
return ["100-OK"]
|
||||
|
||||
router._ubridge_send.side_effect = refuse_probe
|
||||
with patch.object(router.project, "emit") as emit:
|
||||
await router._prepare_tap_datapath()
|
||||
|
||||
assert router._tap_datapath is False
|
||||
assert router._kernel_taps == {}
|
||||
assert emit.called
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_tap_datapath_skips_ghost_routers(compute_project, manager):
|
||||
"""
|
||||
Ghost IOS images share RAM with a real router and never link: no uBridge,
|
||||
no anchors.
|
||||
"""
|
||||
|
||||
ghost = Router("Ghost", NODE_ID, compute_project, manager, ghost_flag=True)
|
||||
ghost._start_ubridge = AsyncioMagicMock()
|
||||
await ghost._prepare_tap_datapath()
|
||||
|
||||
assert ghost._tap_datapath is False
|
||||
assert not ghost._start_ubridge.called
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_tap_datapath_keeps_existing_anchors_across_a_restart(router):
|
||||
"""
|
||||
Nothing is undone at node stop, so a restart must not recreate an anchor
|
||||
a surviving kernel link is still enslaved to.
|
||||
"""
|
||||
|
||||
await router._prepare_tap_datapath()
|
||||
router._ubridge_send.reset_mock()
|
||||
|
||||
await router._prepare_tap_datapath()
|
||||
|
||||
assert router._kernel_taps == {(0, 0): TAP00, (0, 1): TAP01, (0, 16): TAP016}
|
||||
assert not any("tap create" in c or "tap delete" in c for c in _ubridge_commands(router))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_tap_datapath_wires_nios_bound_while_stopped(router):
|
||||
"""
|
||||
A kernel link bound to a stopped router could only be stored (no anchors
|
||||
existed); the start wires it — the hypervisor opens the anchor and binds
|
||||
the port, the anchor joins the per-link bridge.
|
||||
"""
|
||||
|
||||
router.status = "stopped"
|
||||
nio = await _nio(router)
|
||||
router._slots[0].add_nio(0, nio)
|
||||
|
||||
router._ubridge_send.reset_mock()
|
||||
router._hypervisor.commands.clear()
|
||||
await router._prepare_tap_datapath()
|
||||
|
||||
hypervisor = _hypervisor_commands(router)
|
||||
assert any(c.startswith("nio create_tap tap-") and c.endswith(f"{TAP00}") for c in hypervisor)
|
||||
assert any(c.startswith(f"vm slot_add_nio_binding \"R1\" 0 0 tap-") for c in hypervisor)
|
||||
router._ubridge_send.assert_any_call(f'brctl addif "{BRIDGE}" "{TAP00}"')
|
||||
assert (0, 0) in router._tap_nios
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Link attach / detach
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_attach_kernel_nio_opens_the_anchor_in_the_hypervisor_and_enslaves_it(router):
|
||||
"""
|
||||
A kernel link opens the port's anchor TAP in the Dynamips hypervisor
|
||||
(Dynamips holds the fd — QEMU's shape), binds it to the slot/port, then
|
||||
enslaves the anchor into the per-link kernel bridge.
|
||||
"""
|
||||
|
||||
router.status = "started"
|
||||
router._kernel_taps[(0, 0)] = TAP00
|
||||
nio = await _nio(router)
|
||||
|
||||
await router._attach_kernel_nio(0, 0, nio)
|
||||
|
||||
hypervisor = _hypervisor_commands(router)
|
||||
assert any(c.startswith("nio create_tap tap-") and c.endswith(f"{TAP00}") for c in hypervisor)
|
||||
assert any(c.startswith('vm slot_add_nio_binding "R1" 0 0 tap-') for c in hypervisor)
|
||||
router._ubridge_send.assert_any_call(f'brctl create "{BRIDGE}"')
|
||||
router._ubridge_send.assert_any_call(f'link set "{BRIDGE}" up')
|
||||
router._ubridge_send.assert_any_call(f'brctl addif "{BRIDGE}" "{TAP00}"')
|
||||
router._ubridge_send.assert_any_call(f'link set "{TAP00}" up')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_attach_kernel_nio_without_anchor_on_a_running_node_raises(router):
|
||||
"""
|
||||
A kernel link on a serial port (or a router that never anchored) is an
|
||||
error naming the relay datapath, not a silent fallback.
|
||||
"""
|
||||
|
||||
router.status = "started"
|
||||
nio = await _nio(router)
|
||||
|
||||
with pytest.raises(DynamipsError, match="relay datapath"):
|
||||
await router._attach_kernel_nio(1, 0, nio)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_attach_kernel_nio_defers_on_a_stopped_node(router):
|
||||
"""
|
||||
On a stopped router there are no anchors yet: the NIO is stored and the
|
||||
wiring happens at start (test above), so nothing is sent.
|
||||
"""
|
||||
|
||||
router.status = "stopped"
|
||||
nio = await _nio(router)
|
||||
|
||||
await router._attach_kernel_nio(1, 0, nio)
|
||||
|
||||
assert _hypervisor_commands(router) == []
|
||||
assert not router._ubridge_send.called
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_attach_kernel_nio_is_idempotent_across_a_restart(router):
|
||||
"""
|
||||
A stop tears nothing down, so a wired port must not re-bind on the next
|
||||
start: a second nio create_tap would orphan the first NIO's fd and the
|
||||
re-enslavement would collide on the bridge.
|
||||
"""
|
||||
|
||||
router.status = "started"
|
||||
router._kernel_taps[(0, 0)] = TAP00
|
||||
nio = await _nio(router)
|
||||
await router._attach_kernel_nio(0, 0, nio)
|
||||
hypervisor_first = list(_hypervisor_commands(router))
|
||||
router._ubridge_send.reset_mock()
|
||||
|
||||
await router._attach_kernel_nio(0, 0, nio)
|
||||
|
||||
assert _hypervisor_commands(router) == hypervisor_first
|
||||
assert not router._ubridge_send.called
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_nio_binding_relay_keeps_the_udp_tunnel(router):
|
||||
"""
|
||||
A relay link is wired exactly as before: the hypervisor NIO is a UDP NIO
|
||||
tunneled through loopback into the node's uBridge relay.
|
||||
"""
|
||||
|
||||
router.status = "started"
|
||||
nio = await router.manager.create_nio(
|
||||
router, {"type": "nio_udp", "lport": 20000, "rhost": "127.0.0.1", "rport": 20001}
|
||||
)
|
||||
|
||||
await router.slot_add_nio_binding(0, 0, nio)
|
||||
|
||||
hypervisor = _hypervisor_commands(router)
|
||||
assert any(c.startswith("nio create_udp udp-") for c in hypervisor)
|
||||
assert any(c.startswith('vm slot_add_nio_binding "R1" 0 0 udp-') for c in hypervisor)
|
||||
ubridge = _ubridge_commands(router)
|
||||
assert any(c.startswith("bridge create DYNAMIPS-") for c in ubridge)
|
||||
assert any(c.startswith("bridge add_nio_udp DYNAMIPS-") for c in ubridge)
|
||||
assert any(c.startswith("bridge start DYNAMIPS-") for c in ubridge)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_nio_binding_kernel_binds_and_enslaves(router):
|
||||
"""
|
||||
The route-level add path dispatches a NIOBridge to the kernel wiring and
|
||||
stores it in the slot adapter like any NIO.
|
||||
"""
|
||||
|
||||
router.status = "started"
|
||||
router._kernel_taps[(0, 0)] = TAP00
|
||||
nio = await _nio(router)
|
||||
|
||||
await router.slot_add_nio_binding(0, 0, nio)
|
||||
|
||||
assert router._slots[0].get_nio(0) is nio
|
||||
router._ubridge_send.assert_any_call(f'brctl addif "{BRIDGE}" "{TAP00}"')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nio_binding_kernel_reapplies_on_the_anchor(router):
|
||||
"""
|
||||
A filter update on an attached kernel link reconciles on the anchor
|
||||
(tc) without re-binding the port or re-enslaving the TAP.
|
||||
"""
|
||||
|
||||
router._kernel_taps[(0, 0)] = TAP00
|
||||
nio = await _nio(router, filters={"delay": [10]})
|
||||
router._slots[0].add_nio(0, nio)
|
||||
|
||||
await router.slot_update_nio_binding(0, 0, nio)
|
||||
|
||||
router._ubridge_send.assert_any_call(f'tc reset "{TAP00}"')
|
||||
router._ubridge_send.assert_any_call(f'tc netem set "{TAP00}" delay 10')
|
||||
assert not any("slot_add_nio_binding" in c for c in _hypervisor_commands(router))
|
||||
assert not any("brctl addif" in c for c in _ubridge_commands(router))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nio_binding_kernel_suspend_flaps_the_anchor(router):
|
||||
"""
|
||||
Suspending a kernel link admin-downs the anchor — the native carrier,
|
||||
which is why the route must copy suspend onto the NIO.
|
||||
"""
|
||||
|
||||
router._kernel_taps[(0, 0)] = TAP00
|
||||
nio = await _nio(router)
|
||||
router._slots[0].add_nio(0, nio)
|
||||
nio.suspend = True
|
||||
|
||||
await router.slot_update_nio_binding(0, 0, nio)
|
||||
|
||||
router._ubridge_send.assert_any_call(f'link set "{TAP00}" down')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_nio_binding_kernel_releases_the_fd_and_detaches(router):
|
||||
"""
|
||||
Removing a kernel link detaches the anchor from the per-link bridge,
|
||||
resets its qdisc, and unbinds the port plus deletes the hypervisor TAP
|
||||
NIO — releasing Dynamips's fd while the control channels live.
|
||||
"""
|
||||
|
||||
router.status = "started"
|
||||
router._kernel_taps[(0, 0)] = TAP00
|
||||
nio = await _nio(router)
|
||||
router._slots[0].add_nio(0, nio)
|
||||
await router._attach_kernel_nio(0, 0, nio)
|
||||
router._ubridge_send.reset_mock()
|
||||
router._hypervisor.commands.clear()
|
||||
|
||||
removed = await router.slot_remove_nio_binding(0, 0)
|
||||
|
||||
assert removed is nio
|
||||
assert router._slots[0].get_nio(0) is None
|
||||
router._ubridge_send.assert_any_call(f'tc reset "{TAP00}"')
|
||||
router._ubridge_send.assert_any_call(f'brctl delif "{BRIDGE}" "{TAP00}"')
|
||||
router._ubridge_send.assert_any_call(f'brctl delete "{BRIDGE}"')
|
||||
hypervisor = _hypervisor_commands(router)
|
||||
assert any(c.startswith('vm slot_remove_nio_binding "R1" 0 0') for c in hypervisor)
|
||||
assert any(c.startswith("nio delete tap-") for c in hypervisor)
|
||||
assert (0, 0) not in router._tap_nios
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stop order (the hypervisor holds the anchor fds — QEMU's order)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_ubridge_releases_hypervisor_fds_before_deleting_taps(router):
|
||||
"""
|
||||
The hypervisor's TAP NIOs are unbound and deleted first (closing every
|
||||
anchor fd), then the TAPs are unpersisted, then the per-link bridges go —
|
||||
all before uBridge itself stops.
|
||||
"""
|
||||
|
||||
router.status = "started"
|
||||
router._kernel_taps[(0, 0)] = TAP00
|
||||
nio = await _nio(router)
|
||||
router._slots[0].add_nio(0, nio)
|
||||
await router._attach_kernel_nio(0, 0, nio)
|
||||
router._ubridge_send.reset_mock()
|
||||
router._hypervisor.commands.clear()
|
||||
|
||||
with patch(
|
||||
"gns3server.compute.base_node.BaseNode._stop_ubridge", new=AsyncioMagicMock()
|
||||
) as stop:
|
||||
await router._stop_ubridge()
|
||||
|
||||
hypervisor = _hypervisor_commands(router)
|
||||
ubridge = _ubridge_commands(router)
|
||||
unbind = next(i for i, c in enumerate(hypervisor) if c.startswith('vm slot_remove_nio_binding "R1" 0 0'))
|
||||
nio_delete = next(i for i, c in enumerate(hypervisor) if c.startswith("nio delete tap-"))
|
||||
tap_delete = next(i for i, c in enumerate(ubridge) if c == f'tap delete "{TAP00}"')
|
||||
brctl_delete = next(i for i, c in enumerate(ubridge) if c == f'brctl delete "{BRIDGE}"')
|
||||
assert unbind < nio_delete
|
||||
assert tap_delete < brctl_delete
|
||||
assert stop.called
|
||||
assert router._kernel_taps == {}
|
||||
assert router._tap_nios == {}
|
||||
assert router._ubridge_tc_caps is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_kernel_bridges_covers_wic_ports(router):
|
||||
"""
|
||||
The bridge sweep walks adapters derived from the anchor map, so a kernel
|
||||
link on a WIC-1ENET port (inside a non-Ethernet motherboard) is found.
|
||||
"""
|
||||
|
||||
router._kernel_taps[(0, 16)] = TAP016
|
||||
wic_nio = await _nio(router)
|
||||
router._slots[0].add_nio(16, wic_nio)
|
||||
|
||||
await router._remove_kernel_bridges()
|
||||
|
||||
router._ubridge_send.assert_any_call(f'brctl delete "{BRIDGE}"')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_slot_add_binding_hot_adds_anchors(router):
|
||||
"""
|
||||
An adapter inserted while the router runs (OIR) gets its anchors created
|
||||
at once, so a kernel link can attach without a restart.
|
||||
"""
|
||||
|
||||
router.status = "started"
|
||||
router._tap_datapath = True
|
||||
router._kernel_taps[(0, 0)] = TAP00
|
||||
router._slots[1] = None # free the serial slot for the hot-insert
|
||||
|
||||
await router.slot_add_binding(1, PA_FE_TX())
|
||||
|
||||
assert router._kernel_taps[(1, 0)] == "gd00010203e1p0"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Capture (kernel: the anchor; relay: the Dynamips NIO's own pcap)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capture_on_a_kernel_link_uses_the_anchor(router):
|
||||
router._kernel_taps[(0, 0)] = TAP00
|
||||
nio = await _nio(router)
|
||||
router._slots[0].add_nio(0, nio)
|
||||
|
||||
await router.start_capture(0, 0, "/tmp/test.pcap")
|
||||
router._ubridge_send.assert_any_call(f'capture start_kernel {TAP00} "/tmp/test.pcap"')
|
||||
assert not any("bind_filter" in c for c in _hypervisor_commands(router))
|
||||
|
||||
await router.stop_capture(0, 0)
|
||||
router._ubridge_send.assert_any_call("capture stop_kernel")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capture_on_a_relay_link_uses_the_dynamips_nio(router):
|
||||
nio = await router.manager.create_nio(
|
||||
router, {"type": "nio_udp", "lport": 20000, "rhost": "127.0.0.1", "rport": 20001}
|
||||
)
|
||||
router._slots[0].add_nio(0, nio)
|
||||
|
||||
await router.start_capture(0, 0, "/tmp/test.pcap")
|
||||
assert not any("capture start_kernel" in c for c in _ubridge_commands(router))
|
||||
hypervisor = _hypervisor_commands(router)
|
||||
assert any(c.startswith("nio bind_filter udp-") and c.endswith(" capture") for c in hypervisor)
|
||||
assert any(c.startswith("nio setup_filter udp-") for c in hypervisor)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Markers (the polymorphic anchor: relay bridge name on relay, TAP on kernel)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_markers_on_a_kernel_link_attach_to_the_anchor(router):
|
||||
"""
|
||||
The marker reconcile is datapath-agnostic: with the anchor as the
|
||||
location, the attach/toggle/delete primitives translate to
|
||||
marker *_kernel on the TAP.
|
||||
"""
|
||||
|
||||
router._kernel_taps[(0, 0)] = TAP00
|
||||
nio = await _nio(router)
|
||||
nio.markers = {
|
||||
"m1": {"bpf": "vlan 100", "link_id": "lk1", "enabled": True, "data_link_type": "DLT_EN10MB"},
|
||||
}
|
||||
router._slots[0].add_nio(0, nio)
|
||||
|
||||
await router._ubridge_apply_markers(TAP00, nio)
|
||||
|
||||
router._ubridge_send.assert_any_call(
|
||||
f'marker add_kernel m1 {TAP00} "vlan 100" link lk1 pcap "{router.project.markers_working_directory()}/'
|
||||
f'{router.id}_lk1_m1.pcap"'
|
||||
)
|
||||
assert router._marker_filter_bridges[("m1", "lk1")] == TAP00
|
||||
|
||||
await router._ubridge_set_marker_filter_state("m1", False)
|
||||
router._ubridge_send.assert_any_call(f"marker enable_kernel {TAP00} m1 off")
|
||||
|
||||
await router.delete_marker_capture("m1", "lk1", nio)
|
||||
router._ubridge_send.assert_any_call(f"marker delete_kernel {TAP00} m1")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_markers_on_a_relay_link_keep_the_ubridge_shape(router):
|
||||
"""
|
||||
On the relay datapath the same reconcile talks to the uBridge relay
|
||||
bridge behind the Dynamips UDP tunnel — unchanged behaviour.
|
||||
"""
|
||||
|
||||
nio = await router.manager.create_nio(
|
||||
router, {"type": "nio_udp", "lport": 20000, "rhost": "127.0.0.1", "rport": 20001}
|
||||
)
|
||||
nio.markers = {
|
||||
"m1": {"bpf": "vlan 100", "link_id": "lk1", "enabled": True, "data_link_type": "DLT_EN10MB"},
|
||||
}
|
||||
router._slots[0].add_nio(0, nio)
|
||||
tunnel_bridge = "DYNAMIPS-20000-20001"
|
||||
|
||||
await router._ubridge_apply_markers(tunnel_bridge, nio)
|
||||
|
||||
router._ubridge_send.assert_any_call(
|
||||
f'bridge add_packet_filter {tunnel_bridge} m1 mark "vlan 100" link lk1 pcap '
|
||||
f'"{router.project.markers_working_directory()}/{router.id}_lk1_m1.pcap"'
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Manager NIO construction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_manager_create_nio_builds_the_bridge_nio(router):
|
||||
"""
|
||||
The Dynamips manager accepts nio_bridge settings (the kernel-datapath
|
||||
NIO the controller POSTs) — without it the route 422s before the router
|
||||
ever sees the link.
|
||||
"""
|
||||
|
||||
nio = await router.manager.create_nio(
|
||||
router, {"type": "nio_bridge", "bridge": BRIDGE, "filters": {"delay": [5]}, "suspend": True}
|
||||
)
|
||||
assert isinstance(nio, NIOBridge)
|
||||
assert nio.bridge == BRIDGE
|
||||
assert nio.filters == {"delay": [5]}
|
||||
assert nio.suspend is True
|
||||
assert _hypervisor_commands(router) == []
|
||||
Loading…
x
Reference in New Issue
Block a user