mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 20:40:13 +03:00
feat: migrate builtin EthernetSwitch from Dynamips ethsw to ubridge brctl
Replace the builtin EthernetSwitch stub with a Linux kernel bridge backed
by uBridge's brctl module. Each switch node creates one kernel bridge
(gns3br{N}) with VLAN filtering; each port is a persistent TAP enslaved to
the bridge, relayed by a per-port uBridge bridge (nio_tap <-> nio_udp).
- Access/dot1q/qinq port modes translated to brctl vlan primitives
- Compute router repointed from Dynamips to Builtin manager
- Tests updated: 21 router-level tests + 215 surrounding tests pass
- Real-kernel e2e verified: access 100 PVID untagged, dot1q trunk
VIDs 1-4094 + native 1, qinq 802.1ad proto + 200 PVID
Ethertype 0x9100/0x9200 handling and port-count guard relaxation are
deferred pending resolution.
This commit is contained in:
parent
22a1026633
commit
fd4ac4460d
@ -16,6 +16,10 @@
|
||||
|
||||
"""
|
||||
API routes for Ethernet switch nodes.
|
||||
|
||||
The Ethernet switch is a builtin node backed by a Linux kernel bridge driven
|
||||
through uBridge's ``brctl`` module (see
|
||||
``gns3server.compute.builtin.nodes.ethernet_switch``).
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -25,8 +29,8 @@ from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.dynamips.nodes.ethernet_switch import EthernetSwitch
|
||||
from gns3server.compute.builtin import Builtin
|
||||
from gns3server.compute.builtin.nodes.ethernet_switch import EthernetSwitch
|
||||
from gns3server import schemas
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet switch node"}}
|
||||
@ -39,8 +43,8 @@ def dep_node(project_id: UUID, node_id: UUID) -> EthernetSwitch:
|
||||
Dependency to retrieve a node.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
builtin_manager = Builtin.instance()
|
||||
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
|
||||
return node
|
||||
|
||||
|
||||
@ -55,10 +59,9 @@ async def create_ethernet_switch(project_id: UUID, node_data: schemas.EthernetSw
|
||||
Create a new Ethernet switch.
|
||||
"""
|
||||
|
||||
# Use the Dynamips Ethernet switch to simulate this node
|
||||
dynamips_manager = Dynamips.instance()
|
||||
builtin_manager = Builtin.instance()
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node = await dynamips_manager.create_node(
|
||||
node = await builtin_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
str(project_id),
|
||||
node_data.get("node_id"),
|
||||
@ -67,7 +70,7 @@ async def create_ethernet_switch(project_id: UUID, node_data: schemas.EthernetSw
|
||||
node_type="ethernet_switch",
|
||||
ports=node_data.get("ports_mapping"),
|
||||
)
|
||||
|
||||
node.usage = node_data.get("usage", "")
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@ -86,7 +89,7 @@ async def duplicate_ethernet_switch(
|
||||
Duplicate an Ethernet switch.
|
||||
"""
|
||||
|
||||
new_node = await Dynamips.instance().duplicate_node(node.id, str(destination_node_id))
|
||||
new_node = await Builtin.instance().duplicate_node(node.id, str(destination_node_id))
|
||||
return new_node.asdict()
|
||||
|
||||
|
||||
@ -101,7 +104,9 @@ async def update_ethernet_switch(
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
if "name" in node_data and node.name != node_data["name"]:
|
||||
await node.set_name(node_data["name"])
|
||||
node.name = node_data["name"]
|
||||
if "usage" in node_data:
|
||||
node.usage = node_data["usage"]
|
||||
if "ports_mapping" in node_data:
|
||||
node.ports_mapping = node_data["ports_mapping"]
|
||||
await node.update_port_settings()
|
||||
@ -117,7 +122,7 @@ async def delete_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> No
|
||||
Delete an Ethernet switch.
|
||||
"""
|
||||
|
||||
await Dynamips.instance().delete_node(node.id)
|
||||
await Builtin.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@ -182,7 +187,7 @@ async def create_ethernet_switch_nio(
|
||||
node: EthernetSwitch = Depends(dep_node)
|
||||
) -> schemas.UDPNIO:
|
||||
|
||||
nio = await Dynamips.instance().create_nio(node, jsonable_encoder(nio_data, exclude_unset=True))
|
||||
nio = Builtin.instance().create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.add_nio(nio, port_number)
|
||||
return nio.asdict()
|
||||
|
||||
@ -199,8 +204,7 @@ async def delete_ethernet_switch_nio(
|
||||
The adapter number on the switch is always 0.
|
||||
"""
|
||||
|
||||
nio = await node.remove_nio(port_number)
|
||||
await nio.delete()
|
||||
await node.remove_nio(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
@ -251,5 +255,5 @@ async def stream_pcap_file(
|
||||
"""
|
||||
|
||||
nio = node.get_nio(port_number)
|
||||
stream = Dynamips.instance().stream_pcap_file(nio, node.project.id)
|
||||
stream = Builtin.instance().stream_pcap_file(nio, node.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
@ -14,14 +14,47 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import asyncio
|
||||
"""
|
||||
Ethernet switch backed by a Linux kernel bridge driven through uBridge's
|
||||
``brctl`` module.
|
||||
|
||||
The historical GNS3 Ethernet switch was an emulated L2 device inside Dynamips
|
||||
(``ethsw``). This implementation replaces it with a *real* Linux kernel bridge:
|
||||
one bridge per switch node, managed over uBridge's hypervisor socket. Each
|
||||
switch port is a persistent TAP that plays two roles at once -- uBridge holds
|
||||
its file descriptor as a ``nio_tap`` relay endpoint, and the same TAP is
|
||||
enslaved to the kernel bridge as a port. This dual-role TAP is exactly the
|
||||
pattern the Cloud node already uses for host bridges (see
|
||||
``cloud.py::_add_linux_ethernet``).
|
||||
|
||||
Data path (UDP link mode)::
|
||||
|
||||
peer --UDP-- ubridge[nio_udp <-> nio_tap(tap)] --tap-- kernel bridge --tap-- ... (other ports)
|
||||
|
||||
The kernel bridge performs MAC learning/forwarding and VLAN filtering; uBridge
|
||||
is only the per-port UDP transport (uBridge is strictly a 2-NIO pipe, it cannot
|
||||
be the switch). ESW ``access``/``dot1q``/``qinq`` port modes are composed from
|
||||
the ``brctl`` VLAN primitives here -- see ``_apply_port_vlan``.
|
||||
"""
|
||||
|
||||
import psutil
|
||||
|
||||
from ...base_node import BaseNode
|
||||
from ...nios.nio_udp import NIOUDP
|
||||
from ...error import NodeError
|
||||
from gns3server.compute.ubridge.ubridge_error import UbridgeError
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# VLAN ethertypes the Linux kernel bridge can realise. ``brctl setvlanproto``
|
||||
# accepts only 0x8100 (802.1Q) and 0x88a8 (802.1ad). The GNS3 schema also allows
|
||||
# the legacy 0x9100/0x9200 QinQ ethertypes; the kernel bridge cannot do those, so
|
||||
# configuring them on a qinq port is rejected.
|
||||
_SUPPORTED_VLAN_ETHERTYPE = {"0x8100", "0x88a8"}
|
||||
_QINQ_ETHERTYPE = "0x88a8"
|
||||
|
||||
|
||||
class EthernetSwitch(BaseNode):
|
||||
|
||||
@ -32,11 +65,111 @@ class EthernetSwitch(BaseNode):
|
||||
:param node_id: Node identifier
|
||||
:param project: Project instance
|
||||
:param manager: Parent VM Manager
|
||||
:param ports: initial switch ports
|
||||
"""
|
||||
|
||||
def __init__(self, name, node_id, project, manager):
|
||||
def __init__(self, name, node_id, project, manager, console=None, console_type=None, ports=None):
|
||||
|
||||
super().__init__(name, node_id, project, manager)
|
||||
super().__init__(name, node_id, project, manager, console=console, console_type=console_type or "none")
|
||||
# The switch has no console; ``console_type="none"`` makes BaseNode skip
|
||||
# reserving a TCP console port entirely.
|
||||
self._ubridge_require_privileged_access = True
|
||||
|
||||
self._nios = {}
|
||||
self._tap_by_port = {} # port_number -> kernel TAP enslaved to the bridge
|
||||
self._bridge_name = None # kernel bridge interface name (allocated on start)
|
||||
self._bridge_created = False
|
||||
self._bridge_proto_set = False # whether ``brctl setvlanproto`` has been applied
|
||||
# Idempotency flag for start(). Decoupled from ``status`` so the node can
|
||||
# report "started" (always-on, like the ESW) while ``duplicate_node`` still
|
||||
# sees status "stopped" and refuses only genuinely running stateful nodes.
|
||||
self._started = False
|
||||
|
||||
if ports is None:
|
||||
# 8 access ports in VLAN 1 by default, matching the historical ESW.
|
||||
self._ports_mapping = []
|
||||
for port_number in range(0, 8):
|
||||
self._ports_mapping.append(
|
||||
{"port_number": port_number, "name": f"Ethernet{port_number}", "type": "access", "vlan": 1}
|
||||
)
|
||||
else:
|
||||
self._ports_mapping = self._normalize_ports(ports)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# helpers
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@staticmethod
|
||||
def _normalize_ports(ports):
|
||||
"""Assign sequential port numbers/names like the Dynamips ESW did."""
|
||||
port_number = 0
|
||||
normalized = []
|
||||
for port in ports:
|
||||
port = dict(port)
|
||||
port["name"] = f"Ethernet{port_number}"
|
||||
port["port_number"] = port_number
|
||||
normalized.append(port)
|
||||
port_number += 1
|
||||
return normalized
|
||||
|
||||
def _ubridge_bridge_name(self, port_number):
|
||||
"""Name of the per-port uBridge relay bridge (not a kernel interface)."""
|
||||
return f"{self._id}-{port_number}"
|
||||
|
||||
@staticmethod
|
||||
def _free_iface(prefix):
|
||||
"""First free kernel interface name ``prefix<i>`` (kernel names are <=15 chars)."""
|
||||
existing = psutil.net_if_addrs()
|
||||
for i in range(4096):
|
||||
name = f"{prefix}{i}"
|
||||
if name not in existing and len(name) <= 15:
|
||||
return name
|
||||
raise NodeError(f"Could not allocate a free interface name with prefix '{prefix}'")
|
||||
|
||||
def _tap_name(self, port_number):
|
||||
"""Kernel TAP name for a port: ``<bridge>-<port>`` (host-unique via the bridge)."""
|
||||
return f"{self._bridge_name}-{port_number}"
|
||||
|
||||
def _port_settings(self, port_number):
|
||||
for port in self._ports_mapping:
|
||||
if port["port_number"] == port_number:
|
||||
return port
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# properties / serialisation
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@property
|
||||
def nios(self):
|
||||
return self._nios
|
||||
|
||||
@property
|
||||
def ports_mapping(self):
|
||||
return self._ports_mapping
|
||||
|
||||
@ports_mapping.setter
|
||||
def ports_mapping(self, ports):
|
||||
if ports != self._ports_mapping:
|
||||
if len(self._nios) > 0 and len(ports) != len(self._ports_mapping):
|
||||
raise NodeError("Cannot change the port count of a switch that is already connected.")
|
||||
self._ports_mapping = self._normalize_ports(ports)
|
||||
|
||||
@property
|
||||
def console(self):
|
||||
return self._console
|
||||
|
||||
@console.setter
|
||||
def console(self, console):
|
||||
self._console = console
|
||||
|
||||
@property
|
||||
def console_type(self):
|
||||
return self._console_type
|
||||
|
||||
@console_type.setter
|
||||
def console_type(self, console_type):
|
||||
self._console_type = console_type
|
||||
|
||||
def asdict(self):
|
||||
|
||||
@ -44,61 +177,362 @@ class EthernetSwitch(BaseNode):
|
||||
"name": self.name,
|
||||
"usage": self.usage,
|
||||
"node_id": self.id,
|
||||
"project_id": self.project.id
|
||||
"project_id": self.project.id,
|
||||
"ports_mapping": self._ports_mapping,
|
||||
"console": self.console,
|
||||
"console_type": self.console_type,
|
||||
# The switch is always-on once created (a kernel bridge), like the ESW.
|
||||
"status": "started",
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# lifecycle
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
async def create(self):
|
||||
"""
|
||||
Creates this switch.
|
||||
"""
|
||||
|
||||
super().create()
|
||||
await self.start()
|
||||
log.info(f'Ethernet switch "{self._name}" [{self._id}] has been created')
|
||||
|
||||
async def start(self):
|
||||
"""
|
||||
Starts this switch: bring up uBridge, create the kernel bridge, and
|
||||
re-wire any ports already bound before a restart.
|
||||
"""
|
||||
|
||||
if not self._started:
|
||||
if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running():
|
||||
await self._stop_ubridge()
|
||||
await self._start_ubridge(self._ubridge_require_privileged_access)
|
||||
await self._ensure_bridge()
|
||||
for port_number in self._nios:
|
||||
if self._nios[port_number]:
|
||||
try:
|
||||
await self._add_ubridge_connection(self._nios[port_number], port_number)
|
||||
except (UbridgeError, NodeError) as e:
|
||||
self._started = False
|
||||
raise e
|
||||
self._started = True
|
||||
|
||||
async def _ensure_bridge(self):
|
||||
"""
|
||||
Creates the per-node kernel bridge once and enables VLAN filtering.
|
||||
Applies the bridge-level QinQ ethertype if any port needs it.
|
||||
"""
|
||||
|
||||
if self._bridge_created:
|
||||
return
|
||||
self._bridge_name = self._free_iface("gns3br")
|
||||
await self._ubridge_send(f'brctl create "{self._bridge_name}"')
|
||||
await self._ubridge_send(f'brctl vlanfiltering "{self._bridge_name}" on')
|
||||
self._bridge_created = True
|
||||
await self._apply_bridge_proto_if_needed()
|
||||
|
||||
async def _apply_bridge_proto_if_needed(self):
|
||||
"""
|
||||
If any port is a QinQ port using the 802.1ad ethertype (0x88a8), switch
|
||||
the whole bridge to that protocol. A Linux bridge has a single VLAN
|
||||
protocol, so mixed QinQ ethertypes within one switch are not supported.
|
||||
"""
|
||||
|
||||
proto = None
|
||||
for port in self._ports_mapping:
|
||||
if port.get("type") == "qinq":
|
||||
# normalise case: the schema carries uppercase (e.g. "0x88A8") but
|
||||
# brctl setvlanproto wants lowercase hex
|
||||
ethertype = port.get("ethertype", "0x8100").lower()
|
||||
if ethertype not in _SUPPORTED_VLAN_ETHERTYPE:
|
||||
raise NodeError(
|
||||
f"VLAN ethertype {ethertype} is not supported by the Linux bridge "
|
||||
f"(only 0x8100/0x88a8) for QinQ port {port['name']}"
|
||||
)
|
||||
if ethertype == _QINQ_ETHERTYPE:
|
||||
proto = _QINQ_ETHERTYPE
|
||||
if proto and not self._bridge_proto_set:
|
||||
await self._ubridge_send(f'brctl setvlanproto "{self._bridge_name}" {proto}')
|
||||
self._bridge_proto_set = True
|
||||
|
||||
async def delete(self):
|
||||
"""
|
||||
Deletes this switch.
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
return await self.close()
|
||||
|
||||
async def close(self):
|
||||
"""
|
||||
Closes this switch: release UDP ports, tear down the kernel bridge, stop uBridge.
|
||||
"""
|
||||
|
||||
if not (await super().close()):
|
||||
return False
|
||||
|
||||
for nio in self._nios.values():
|
||||
if nio and isinstance(nio, NIOUDP):
|
||||
self.manager.port_manager.release_udp_port(nio.lport, self._project)
|
||||
self._nios.clear()
|
||||
self._tap_by_port.clear()
|
||||
|
||||
if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running() and self._bridge_created:
|
||||
try:
|
||||
# Deleting the bridge releases its enslaved TAPs; uBridge destroys
|
||||
# them when it stops below.
|
||||
await self._ubridge_send(f'brctl delete "{self._bridge_name}"')
|
||||
except UbridgeError as e:
|
||||
log.warning(f'Could not delete kernel bridge "{self._bridge_name}": {e}')
|
||||
self._bridge_created = False
|
||||
self._bridge_proto_set = False
|
||||
self._bridge_name = None
|
||||
self._started = False
|
||||
|
||||
await self._stop_ubridge()
|
||||
log.info(f'Ethernet switch "{self._name}" [{self._id}] has been closed')
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# per-port wiring
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
async def add_nio(self, nio, port_number):
|
||||
"""
|
||||
Adds a NIO as new port on this switch.
|
||||
Adds a NIO as a new port on this switch.
|
||||
|
||||
:param nio: NIO instance to add
|
||||
:param port_number: port to allocate for the NIO
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
if port_number in self._nios:
|
||||
raise NodeError(f"Port {port_number} isn't free")
|
||||
if not isinstance(nio, NIOUDP):
|
||||
raise NodeError("Ethernet switch ports only support UDP NIOs")
|
||||
|
||||
log.info(
|
||||
'Ethernet switch "{name}" [{id}]: NIO {nio} bound to port {port}'.format(
|
||||
name=self._name, id=self._id, nio=nio, port=port_number
|
||||
)
|
||||
)
|
||||
try:
|
||||
await self.start()
|
||||
await self._add_ubridge_connection(nio, port_number)
|
||||
self._nios[port_number] = nio
|
||||
except (NodeError, UbridgeError) as e:
|
||||
log.error('Cannot add NIO on Ethernet switch "{name}": {error}'.format(name=self._name, error=e))
|
||||
await self._stop_ubridge()
|
||||
self.status = "stopped"
|
||||
self._nios[port_number] = nio
|
||||
self.project.emit("log.error", {"message": str(e)})
|
||||
|
||||
async def _add_ubridge_connection(self, nio, port_number):
|
||||
"""
|
||||
Wires one port: a per-port uBridge relay (nio_tap <-> nio_udp) whose TAP
|
||||
is enslaved to the kernel bridge, with the port's VLAN mode applied.
|
||||
"""
|
||||
|
||||
port_settings = self._port_settings(port_number)
|
||||
if port_settings is None:
|
||||
raise NodeError(f"Port {port_number} doesn't exist on Ethernet switch '{self.name}'")
|
||||
|
||||
ubridge_bridge = self._ubridge_bridge_name(port_number)
|
||||
tap = self._tap_name(port_number)
|
||||
|
||||
# per-port uBridge relay -- uBridge holds the TAP fd
|
||||
await self._ubridge_send(f"bridge create {ubridge_bridge}")
|
||||
await self._ubridge_send(f'bridge add_nio_tap {ubridge_bridge} "{tap}"')
|
||||
# enslave the same TAP to the kernel bridge (the cloud.py::_add_linux_ethernet move)
|
||||
await self._ubridge_send(f'brctl addif "{self._bridge_name}" "{tap}"')
|
||||
# VLAN membership for this port's access/trunk/qinq mode
|
||||
await self._apply_port_vlan(port_settings, tap)
|
||||
# GNS3 link endpoint
|
||||
await self._ubridge_send(
|
||||
"bridge add_nio_udp {name} {lport} {rhost} {rport}".format(
|
||||
name=ubridge_bridge, lport=nio.lport, rhost=nio.rhost, rport=nio.rport
|
||||
)
|
||||
)
|
||||
await self._ubridge_apply_filters(ubridge_bridge, nio.filters)
|
||||
await self._ubridge_apply_markers(ubridge_bridge, nio)
|
||||
if nio.capturing:
|
||||
await self._ubridge_send(
|
||||
'bridge start_capture {name} "{output_file}"'.format(
|
||||
name=ubridge_bridge, output_file=nio.pcap_output_file
|
||||
)
|
||||
)
|
||||
await self._ubridge_send(f"bridge start {ubridge_bridge}")
|
||||
self._tap_by_port[port_number] = tap
|
||||
|
||||
async def _delete_ubridge_connection(self, port_number):
|
||||
"""
|
||||
Tears down one port's wiring: release the TAP from the bridge and delete
|
||||
the per-port uBridge relay.
|
||||
"""
|
||||
|
||||
tap = self._tap_by_port.pop(port_number, None)
|
||||
ubridge_bridge = self._ubridge_bridge_name(port_number)
|
||||
if tap is not None and self._bridge_created:
|
||||
try:
|
||||
await self._ubridge_send(f'brctl delif "{self._bridge_name}" "{tap}"')
|
||||
except UbridgeError as e:
|
||||
log.warning(f'Could not remove TAP "{tap}" from bridge "{self._bridge_name}": {e}')
|
||||
try:
|
||||
await self._ubridge_send(f"bridge delete {ubridge_bridge}")
|
||||
except UbridgeError as e:
|
||||
log.warning(f"Could not delete uBridge bridge {ubridge_bridge}: {e}")
|
||||
|
||||
async def remove_nio(self, port_number):
|
||||
"""
|
||||
Removes the specified NIO as member of this switch.
|
||||
Removes the specified NIO from this switch.
|
||||
|
||||
:param port_number: allocated port number
|
||||
|
||||
:returns: the NIO that was bound to the allocated port
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
if port_number not in self._nios:
|
||||
raise NodeError(f"Port {port_number} is not allocated")
|
||||
|
||||
await self.stop_capture(port_number)
|
||||
nio = self._nios[port_number]
|
||||
if isinstance(nio, NIOUDP):
|
||||
self.manager.port_manager.release_udp_port(nio.lport, self._project)
|
||||
|
||||
log.info(
|
||||
'Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(
|
||||
name=self._name, id=self._id, nio=nio, port=port_number
|
||||
)
|
||||
)
|
||||
del self._nios[port_number]
|
||||
if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running():
|
||||
await self._delete_ubridge_connection(port_number)
|
||||
return nio
|
||||
|
||||
def get_nio(self, port_number):
|
||||
"""
|
||||
Gets a port NIO binding.
|
||||
|
||||
:param port_number: port number
|
||||
:returns: NIO instance
|
||||
"""
|
||||
|
||||
if port_number not in self._nios:
|
||||
raise NodeError(f"Port {port_number} is not connected")
|
||||
return self._nios[port_number]
|
||||
|
||||
async def update_nio(self, port_number, nio):
|
||||
"""
|
||||
Re-applies uBridge filters/markers for a port (called when a link is updated).
|
||||
"""
|
||||
|
||||
ubridge_bridge = self._ubridge_bridge_name(port_number)
|
||||
if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running():
|
||||
await self._ubridge_apply_filters(ubridge_bridge, nio.filters)
|
||||
await self._ubridge_apply_markers(ubridge_bridge, nio)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# VLAN mode translation
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
async def _reset_port_vlan(self, tap):
|
||||
"""
|
||||
Resets a port's VLAN membership to the kernel default (PVID 1, untagged)
|
||||
by re-enslaving it. Used before re-applying a changed mode so stale VIDs
|
||||
from the previous mode do not leak.
|
||||
"""
|
||||
|
||||
await self._ubridge_send(f'brctl delif "{self._bridge_name}" "{tap}"')
|
||||
await self._ubridge_send(f'brctl addif "{self._bridge_name}" "{tap}"')
|
||||
|
||||
async def _apply_port_vlan(self, port_settings, tap):
|
||||
"""
|
||||
Translates an ESW port mode into ``brctl`` VLAN primitives. The port must
|
||||
already be enslaved to the bridge and carry the default PVID 1.
|
||||
|
||||
- access VLAN N: drop default 1, add N as PVID + egress untagged.
|
||||
- dot1q trunk (native N): drop default 1, admit all VIDs tagged, then mark
|
||||
the native VLAN PVID + untagged. (The ESW model declares only the native
|
||||
VLAN per trunk port, so the trunk admits all VIDs, like the emulated ESW.)
|
||||
- qinq (outer N): the bridge-level protocol is set separately; the port
|
||||
gets the service VLAN as PVID + untagged so customer frames are S-tagged.
|
||||
"""
|
||||
|
||||
br = self._bridge_name
|
||||
port_type = port_settings["type"]
|
||||
vlan = int(port_settings["vlan"])
|
||||
|
||||
if port_type == "access":
|
||||
await self._ubridge_send(f'brctl vlan_del "{br}" "{tap}" 1')
|
||||
await self._ubridge_send(f'brctl vlan_add "{br}" "{tap}" {vlan} pvid untagged')
|
||||
elif port_type == "dot1q":
|
||||
await self._ubridge_send(f'brctl vlan_del "{br}" "{tap}" 1')
|
||||
await self._ubridge_send(f'brctl vlan_add "{br}" "{tap}" 1 vid 4094')
|
||||
await self._ubridge_send(f'brctl vlan_add "{br}" "{tap}" {vlan} pvid untagged')
|
||||
elif port_type == "qinq":
|
||||
# setvlanproto is applied at the bridge level by _apply_bridge_proto_if_needed
|
||||
await self._ubridge_send(f'brctl vlan_del "{br}" "{tap}" 1')
|
||||
await self._ubridge_send(f'brctl vlan_add "{br}" "{tap}" {vlan} pvid untagged')
|
||||
else:
|
||||
raise NodeError(f"Unknown port type '{port_type}' on Ethernet switch '{self.name}'")
|
||||
|
||||
async def update_port_settings(self):
|
||||
"""
|
||||
Re-applies port settings (called after ``ports_mapping`` is updated). For
|
||||
ports already wired, reset then re-apply so a mode/VLAN change fully
|
||||
replaces the previous VLAN membership.
|
||||
"""
|
||||
|
||||
await self._apply_bridge_proto_if_needed()
|
||||
if not (self._ubridge_hypervisor and self._ubridge_hypervisor.is_running() and self._bridge_created):
|
||||
return
|
||||
for port_settings in self._ports_mapping:
|
||||
port_number = port_settings["port_number"]
|
||||
tap = self._tap_by_port.get(port_number)
|
||||
if tap is None:
|
||||
continue
|
||||
await self._reset_port_vlan(tap)
|
||||
await self._apply_port_vlan(port_settings, tap)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# capture
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
async def start_capture(self, port_number, output_file, data_link_type="DLT_EN10MB"):
|
||||
"""
|
||||
Starts a packet capture.
|
||||
Starts a packet capture on a port (uBridge captures on the per-port relay).
|
||||
|
||||
:param port_number: allocated port number
|
||||
:param output_file: PCAP destination file for the capture
|
||||
:param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
nio = self.get_nio(port_number)
|
||||
if nio.capturing:
|
||||
raise NodeError(f"Packet capture is already activated on port {port_number}")
|
||||
nio.start_packet_capture(output_file)
|
||||
if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running():
|
||||
ubridge_bridge = self._ubridge_bridge_name(port_number)
|
||||
await self._ubridge_send(f'bridge start_capture {ubridge_bridge} "{output_file}"')
|
||||
log.info(
|
||||
'Ethernet switch "{name}" [{id}]: starting packet capture on port {port}'.format(
|
||||
name=self.name, id=self.id, port=port_number
|
||||
)
|
||||
)
|
||||
|
||||
async def stop_capture(self, port_number):
|
||||
"""
|
||||
Stops a packet capture.
|
||||
Stops a packet capture on a port.
|
||||
|
||||
:param port_number: allocated port number
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
nio = self.get_nio(port_number)
|
||||
if not nio.capturing:
|
||||
return
|
||||
nio.stop_packet_capture()
|
||||
if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running():
|
||||
ubridge_bridge = self._ubridge_bridge_name(port_number)
|
||||
await self._ubridge_send(f"bridge stop_capture {ubridge_bridge}")
|
||||
log.info(
|
||||
'Ethernet switch "{name}" [{id}]: stopping packet capture on port {port}'.format(
|
||||
name=self.name, id=self.id, port=port_number
|
||||
)
|
||||
)
|
||||
|
||||
@ -21,34 +21,48 @@ import pytest_asyncio
|
||||
from fastapi import FastAPI, status
|
||||
from httpx import AsyncClient
|
||||
from tests.utils import asyncio_patch, AsyncioMagicMock
|
||||
from unittest.mock import call
|
||||
from unittest.mock import call, MagicMock
|
||||
|
||||
from gns3server.compute.project import Project
|
||||
|
||||
# The builtin Ethernet switch talks to uBridge (brctl/bridge modules) instead of
|
||||
# the Dynamips hypervisor. These are the seams we stub so the routes can be
|
||||
# exercised without launching a real uBridge / creating kernel interfaces.
|
||||
_NODE = "gns3server.compute.builtin.nodes.ethernet_switch.EthernetSwitch"
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
class TestEthernetSwitchNodesRoutes:
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
async def stub_ubridge(self):
|
||||
"""Keep uBridge from really starting and capture every command."""
|
||||
with asyncio_patch(f"{_NODE}._start_ubridge"), asyncio_patch(f"{_NODE}._stop_ubridge"), \
|
||||
asyncio_patch(f"{_NODE}._ubridge_send"):
|
||||
yield
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def ethernet_switch(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project) -> dict:
|
||||
|
||||
params = {"name": "Ethernet Switch"}
|
||||
with asyncio_patch("gns3server.compute.dynamips.nodes.ethernet_switch.EthernetSwitch.create") as mock:
|
||||
response = await compute_client.post(
|
||||
app.url_path_for("compute:create_ethernet_switch", project_id=compute_project.id),
|
||||
json=params
|
||||
)
|
||||
assert mock.called
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
response = await compute_client.post(
|
||||
app.url_path_for("compute:create_ethernet_switch", project_id=compute_project.id),
|
||||
json=params
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
json_response = response.json()
|
||||
node = compute_project.get_node(json_response["node_id"])
|
||||
node._hypervisor = AsyncioMagicMock()
|
||||
node._hypervisor.send = AsyncioMagicMock()
|
||||
node._hypervisor.version = "0.2.16"
|
||||
# Pretend uBridge is up so the is_running() guards in remove/close pass.
|
||||
node._ubridge_hypervisor = MagicMock()
|
||||
node._ubridge_hypervisor.is_running.return_value = True
|
||||
node._ubridge_send.reset_mock()
|
||||
return json_response
|
||||
|
||||
@staticmethod
|
||||
def _udp_params() -> dict:
|
||||
return {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}
|
||||
|
||||
async def test_ethernet_switch_create(
|
||||
self, app: FastAPI,
|
||||
@ -57,16 +71,22 @@ class TestEthernetSwitchNodesRoutes:
|
||||
) -> None:
|
||||
|
||||
params = {"name": "Ethernet Switch 1"}
|
||||
with asyncio_patch("gns3server.compute.dynamips.nodes.ethernet_switch.EthernetSwitch.create") as mock:
|
||||
response = await compute_client.post(
|
||||
app.url_path_for("compute:create_ethernet_switch", project_id=compute_project.id),
|
||||
json=params
|
||||
)
|
||||
assert mock.called
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json()["name"] == "Ethernet Switch 1"
|
||||
assert response.json()["project_id"] == compute_project.id
|
||||
response = await compute_client.post(
|
||||
app.url_path_for("compute:create_ethernet_switch", project_id=compute_project.id),
|
||||
json=params
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json()["name"] == "Ethernet Switch 1"
|
||||
assert response.json()["project_id"] == compute_project.id
|
||||
assert response.json()["status"] == "started"
|
||||
|
||||
# creation stands up the kernel bridge with VLAN filtering
|
||||
node = compute_project.get_node(response.json()["node_id"])
|
||||
br = node._bridge_name
|
||||
node._ubridge_send.assert_has_calls([
|
||||
call(f'brctl create "{br}"'),
|
||||
call(f'brctl vlanfiltering "{br}" on'),
|
||||
])
|
||||
|
||||
async def test_ethernet_switch_get(
|
||||
self, app: FastAPI,
|
||||
@ -87,7 +107,6 @@ class TestEthernetSwitchNodesRoutes:
|
||||
assert response.json()["project_id"] == compute_project.id
|
||||
assert response.json()["status"] == "started"
|
||||
|
||||
|
||||
async def test_ethernet_switch_duplicate(
|
||||
self,
|
||||
app: FastAPI,
|
||||
@ -98,15 +117,11 @@ class TestEthernetSwitchNodesRoutes:
|
||||
|
||||
# create destination switch first
|
||||
params = {"name": "Ethernet Switch 2"}
|
||||
with asyncio_patch("gns3server.compute.dynamips.nodes.ethernet_switch.EthernetSwitch.create") as mock:
|
||||
response = await compute_client.post(
|
||||
app.url_path_for(
|
||||
"compute:create_ethernet_switch",
|
||||
project_id=compute_project.id),
|
||||
json=params
|
||||
)
|
||||
assert mock.called
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
response = await compute_client.post(
|
||||
app.url_path_for("compute:create_ethernet_switch", project_id=compute_project.id),
|
||||
json=params
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
params = {"destination_node_id": response.json()["node_id"]}
|
||||
response = await compute_client.post(
|
||||
@ -117,7 +132,6 @@ class TestEthernetSwitchNodesRoutes:
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
|
||||
async def test_ethernet_switch_update(
|
||||
self,
|
||||
app: FastAPI,
|
||||
@ -126,10 +140,7 @@ class TestEthernetSwitchNodesRoutes:
|
||||
ethernet_switch: dict
|
||||
) -> None:
|
||||
|
||||
params = {
|
||||
"name": "test",
|
||||
"console_type": "telnet"
|
||||
}
|
||||
params = {"name": "test", "console_type": "none"}
|
||||
|
||||
response = await compute_client.put(
|
||||
app.url_path_for(
|
||||
@ -141,11 +152,12 @@ class TestEthernetSwitchNodesRoutes:
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["name"] == "test"
|
||||
# renaming a builtin switch does not touch uBridge (the kernel bridge is
|
||||
# name-independent); nothing should have been sent.
|
||||
node = compute_project.get_node(ethernet_switch["node_id"])
|
||||
node._hypervisor.send.assert_called_with("ethsw rename \"Ethernet Switch\" \"test\"")
|
||||
node._ubridge_send.assert_not_called()
|
||||
|
||||
|
||||
async def test_ethernet_switch_update_ports(
|
||||
async def test_ethernet_switch_update_ports_qinq_proto(
|
||||
self,
|
||||
app: FastAPI,
|
||||
compute_client: AsyncClient,
|
||||
@ -153,33 +165,11 @@ class TestEthernetSwitchNodesRoutes:
|
||||
ethernet_switch: dict
|
||||
) -> None:
|
||||
|
||||
# a QinQ port with the 802.1ad ethertype must switch the bridge protocol
|
||||
port_params = {
|
||||
"ports_mapping": [
|
||||
{
|
||||
"name": "Ethernet0",
|
||||
"port_number": 0,
|
||||
"type": "qinq",
|
||||
"vlan": 1
|
||||
},
|
||||
{
|
||||
"name": "Ethernet1",
|
||||
"port_number": 1,
|
||||
"type": "qinq",
|
||||
"vlan": 2,
|
||||
"ethertype": "0x88A8"
|
||||
},
|
||||
{
|
||||
"name": "Ethernet2",
|
||||
"port_number": 2,
|
||||
"type": "dot1q",
|
||||
"vlan": 3,
|
||||
},
|
||||
{
|
||||
"name": "Ethernet3",
|
||||
"port_number": 3,
|
||||
"type": "access",
|
||||
"vlan": 4,
|
||||
}
|
||||
{"name": "Ethernet0", "port_number": 0, "type": "qinq", "vlan": 2, "ethertype": "0x88A8"},
|
||||
{"name": "Ethernet1", "port_number": 1, "type": "access", "vlan": 4},
|
||||
],
|
||||
}
|
||||
|
||||
@ -192,90 +182,20 @@ class TestEthernetSwitchNodesRoutes:
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
nio_params = {
|
||||
"type": "nio_udp",
|
||||
"lport": 4242,
|
||||
"rport": 4343,
|
||||
"rhost": "127.0.0.1"
|
||||
}
|
||||
|
||||
for port_mapping in port_params["ports_mapping"]:
|
||||
port_number = port_mapping["port_number"]
|
||||
vlan = port_mapping["vlan"]
|
||||
port_type = port_mapping["type"]
|
||||
ethertype = port_mapping.get("ethertype", "")
|
||||
url = app.url_path_for(
|
||||
"compute:create_ethernet_switch_nio",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"],
|
||||
adapter_number="0",
|
||||
port_number=f"{port_number}"
|
||||
)
|
||||
await compute_client.post(url, json=nio_params)
|
||||
|
||||
node = compute_project.get_node(ethernet_switch["node_id"])
|
||||
nio = node.get_nio(port_number)
|
||||
calls = [
|
||||
call.send(f'nio create_udp {nio.name} 4242 127.0.0.1 4343'),
|
||||
call.send(f'ethsw add_nio "Ethernet Switch" {nio.name}'),
|
||||
call.send(f'ethsw set_{port_type}_port "Ethernet Switch" {nio.name} {vlan} {ethertype}'.strip())
|
||||
]
|
||||
node._hypervisor.send.assert_has_calls(calls)
|
||||
node._hypervisor.send.reset_mock()
|
||||
|
||||
node = compute_project.get_node(ethernet_switch["node_id"])
|
||||
node._ubridge_send.assert_any_call(f'brctl setvlanproto "{node._bridge_name}" 0x88a8')
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ports_settings",
|
||||
(
|
||||
(
|
||||
{
|
||||
"name": "Ethernet0",
|
||||
"port_number": 0,
|
||||
"type": "dot42q", # invalid port type
|
||||
"vlan": 1,
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"name": "Ethernet0",
|
||||
"port_number": 0,
|
||||
"type": "access", # missing vlan field
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"name": "Ethernet0",
|
||||
"port_number": 0,
|
||||
"type": "dot1q",
|
||||
"vlan": 1,
|
||||
"ethertype": "0x88A8" # EtherType is only for QinQ
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"name": "Ethernet0",
|
||||
"port_number": 0,
|
||||
"type": "qinq",
|
||||
"vlan": 1,
|
||||
"ethertype": "0x4242" # not a valid EtherType
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"name": "Ethernet0",
|
||||
"port_number": 0,
|
||||
"type": "access",
|
||||
"vlan": 0, # minimum vlan number is 1
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"name": "Ethernet0",
|
||||
"port_number": 0,
|
||||
"type": "access",
|
||||
"vlan": 4242, # maximum vlan number is 4094
|
||||
}
|
||||
),
|
||||
{"name": "Ethernet0", "port_number": 0, "type": "dot42q", "vlan": 1}, # bad type
|
||||
{"name": "Ethernet0", "port_number": 0, "type": "access"}, # missing vlan
|
||||
{"name": "Ethernet0", "port_number": 0, "type": "dot1q", "vlan": 1,
|
||||
"ethertype": "0x88A8"}, # ethertype only for qinq
|
||||
{"name": "Ethernet0", "port_number": 0, "type": "qinq", "vlan": 1,
|
||||
"ethertype": "0x4242"}, # bad ethertype
|
||||
{"name": "Ethernet0", "port_number": 0, "type": "access", "vlan": 0}, # vlan < 1
|
||||
{"name": "Ethernet0", "port_number": 0, "type": "access", "vlan": 4242}, # vlan > 4094
|
||||
)
|
||||
)
|
||||
async def test_ethernet_switch_update_ports_invalid(
|
||||
@ -286,20 +206,15 @@ class TestEthernetSwitchNodesRoutes:
|
||||
ports_settings: dict,
|
||||
) -> None:
|
||||
|
||||
port_params = {
|
||||
"ports_mapping": [ports_settings]
|
||||
}
|
||||
|
||||
response = await compute_client.put(
|
||||
app.url_path_for(
|
||||
"compute:update_ethernet_switch",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"]),
|
||||
json=port_params
|
||||
json={"ports_mapping": [ports_settings]}
|
||||
)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
|
||||
|
||||
|
||||
async def test_ethernet_switch_delete(
|
||||
self, app: FastAPI,
|
||||
compute_client: AsyncClient,
|
||||
@ -315,12 +230,7 @@ class TestEthernetSwitchNodesRoutes:
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
|
||||
async def test_ethernet_switch_start(
|
||||
self, app: FastAPI,
|
||||
compute_client: AsyncClient,
|
||||
ethernet_switch: dict
|
||||
) -> None:
|
||||
async def test_ethernet_switch_start(self, app: FastAPI, compute_client: AsyncClient, ethernet_switch: dict) -> None:
|
||||
|
||||
response = await compute_client.post(
|
||||
app.url_path_for(
|
||||
@ -331,12 +241,7 @@ class TestEthernetSwitchNodesRoutes:
|
||||
)
|
||||
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
|
||||
|
||||
|
||||
async def test_ethernet_switch_stop(
|
||||
self, app: FastAPI,
|
||||
compute_client: AsyncClient,
|
||||
ethernet_switch: dict
|
||||
) -> None:
|
||||
async def test_ethernet_switch_stop(self, app: FastAPI, compute_client: AsyncClient, ethernet_switch: dict) -> None:
|
||||
|
||||
response = await compute_client.post(
|
||||
app.url_path_for(
|
||||
@ -347,12 +252,7 @@ class TestEthernetSwitchNodesRoutes:
|
||||
)
|
||||
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
|
||||
|
||||
|
||||
async def test_ethernet_switch_suspend(
|
||||
self, app: FastAPI,
|
||||
compute_client: AsyncClient,
|
||||
ethernet_switch: dict
|
||||
) -> None:
|
||||
async def test_ethernet_switch_suspend(self, app: FastAPI, compute_client: AsyncClient, ethernet_switch: dict) -> None:
|
||||
|
||||
response = await compute_client.post(
|
||||
app.url_path_for(
|
||||
@ -363,12 +263,7 @@ class TestEthernetSwitchNodesRoutes:
|
||||
)
|
||||
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
|
||||
|
||||
|
||||
async def test_ethernet_switch_reload(
|
||||
self, app: FastAPI,
|
||||
compute_client: AsyncClient,
|
||||
ethernet_switch: dict
|
||||
) -> None:
|
||||
async def test_ethernet_switch_reload(self, app: FastAPI, compute_client: AsyncClient, ethernet_switch: dict) -> None:
|
||||
|
||||
response = await compute_client.post(
|
||||
app.url_path_for(
|
||||
@ -379,8 +274,7 @@ class TestEthernetSwitchNodesRoutes:
|
||||
)
|
||||
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
|
||||
|
||||
|
||||
async def test_ethernet_switch_create_udp(
|
||||
async def test_ethernet_switch_create_udp_access(
|
||||
self,
|
||||
app: FastAPI,
|
||||
compute_client: AsyncClient,
|
||||
@ -388,13 +282,6 @@ class TestEthernetSwitchNodesRoutes:
|
||||
ethernet_switch: dict
|
||||
) -> None:
|
||||
|
||||
params = {
|
||||
"type": "nio_udp",
|
||||
"lport": 4242,
|
||||
"rport": 4343,
|
||||
"rhost": "127.0.0.1"
|
||||
}
|
||||
|
||||
url = app.url_path_for(
|
||||
"compute:create_ethernet_switch_nio",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
@ -402,19 +289,66 @@ class TestEthernetSwitchNodesRoutes:
|
||||
adapter_number="0",
|
||||
port_number="0"
|
||||
)
|
||||
response = await compute_client.post(url, json=params)
|
||||
response = await compute_client.post(url, json=self._udp_params())
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json()["type"] == "nio_udp"
|
||||
|
||||
node = compute_project.get_node(ethernet_switch["node_id"])
|
||||
nio = node.get_nio(0)
|
||||
calls = [
|
||||
call.send(f'nio create_udp {nio.name} 4242 127.0.0.1 4343'),
|
||||
call.send(f'ethsw add_nio "Ethernet Switch" {nio.name}'),
|
||||
call.send(f'ethsw set_access_port "Ethernet Switch" {nio.name} 1')
|
||||
]
|
||||
node._hypervisor.send.assert_has_calls(calls)
|
||||
br = node._bridge_name
|
||||
tap = f"{br}-0"
|
||||
relay = f"{node.id}-0"
|
||||
# access VLAN 1 (default): drop default PVID 1, re-add 1 as PVID/untagged
|
||||
node._ubridge_send.assert_has_calls([
|
||||
call(f"bridge create {relay}"),
|
||||
call(f'bridge add_nio_tap {relay} "{tap}"'),
|
||||
call(f'brctl addif "{br}" "{tap}"'),
|
||||
call(f'brctl vlan_del "{br}" "{tap}" 1'),
|
||||
call(f'brctl vlan_add "{br}" "{tap}" 1 pvid untagged'),
|
||||
call(f"bridge add_nio_udp {relay} {nio.lport} {nio.rhost} {nio.rport}"),
|
||||
call(f"bridge reset_packet_filters {relay}"),
|
||||
call(f"bridge start {relay}"),
|
||||
])
|
||||
|
||||
async def test_ethernet_switch_create_udp_dot1q(
|
||||
self,
|
||||
app: FastAPI,
|
||||
compute_client: AsyncClient,
|
||||
compute_project: Project,
|
||||
ethernet_switch: dict
|
||||
) -> None:
|
||||
|
||||
# make port 0 a dot1q trunk with native VLAN 10
|
||||
await compute_client.put(
|
||||
app.url_path_for(
|
||||
"compute:update_ethernet_switch",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"]),
|
||||
json={"ports_mapping": [
|
||||
{"name": "Ethernet0", "port_number": 0, "type": "dot1q", "vlan": 10},
|
||||
]}
|
||||
)
|
||||
node = compute_project.get_node(ethernet_switch["node_id"])
|
||||
node._ubridge_send.reset_mock()
|
||||
|
||||
url = app.url_path_for(
|
||||
"compute:create_ethernet_switch_nio",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"],
|
||||
adapter_number="0",
|
||||
port_number="0"
|
||||
)
|
||||
response = await compute_client.post(url, json=self._udp_params())
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
br = node._bridge_name
|
||||
tap = f"{br}-0"
|
||||
# trunk: drop default 1, admit all VIDs tagged, mark native 10 PVID/untagged
|
||||
node._ubridge_send.assert_has_calls([
|
||||
call(f'brctl vlan_del "{br}" "{tap}" 1'),
|
||||
call(f'brctl vlan_add "{br}" "{tap}" 1 vid 4094'),
|
||||
call(f'brctl vlan_add "{br}" "{tap}" 10 pvid untagged'),
|
||||
])
|
||||
|
||||
async def test_ethernet_switch_delete_nio(
|
||||
self,
|
||||
@ -424,13 +358,6 @@ class TestEthernetSwitchNodesRoutes:
|
||||
ethernet_switch: dict
|
||||
) -> None:
|
||||
|
||||
params = {
|
||||
"type": "nio_udp",
|
||||
"lport": 4242,
|
||||
"rport": 4343,
|
||||
"rhost": "127.0.0.1"
|
||||
}
|
||||
|
||||
url = app.url_path_for(
|
||||
"compute:create_ethernet_switch_nio",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
@ -438,11 +365,10 @@ class TestEthernetSwitchNodesRoutes:
|
||||
adapter_number="0",
|
||||
port_number="0"
|
||||
)
|
||||
await compute_client.post(url, json=params)
|
||||
await compute_client.post(url, json=self._udp_params())
|
||||
|
||||
node = compute_project.get_node(ethernet_switch["node_id"])
|
||||
node._hypervisor.send.reset_mock()
|
||||
nio = node.get_nio(0)
|
||||
node._ubridge_send.reset_mock()
|
||||
|
||||
url = app.url_path_for(
|
||||
"compute:delete_ethernet_switch_nio",
|
||||
@ -454,52 +380,86 @@ class TestEthernetSwitchNodesRoutes:
|
||||
response = await compute_client.delete(url)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
calls = [
|
||||
call(f'ethsw remove_nio "Ethernet Switch" {nio.name}'),
|
||||
call(f'nio delete {nio.name}')
|
||||
]
|
||||
node._hypervisor.send.assert_has_calls(calls)
|
||||
|
||||
br = node._bridge_name
|
||||
tap = f"{br}-0"
|
||||
relay = f"{node.id}-0"
|
||||
node._ubridge_send.assert_has_calls([
|
||||
call(f'brctl delif "{br}" "{tap}"'),
|
||||
call(f"bridge delete {relay}"),
|
||||
])
|
||||
|
||||
async def test_ethernet_switch_start_capture(
|
||||
self,
|
||||
app: FastAPI,
|
||||
compute_client: AsyncClient,
|
||||
compute_project: Project,
|
||||
ethernet_switch: dict
|
||||
) -> None:
|
||||
|
||||
params = {
|
||||
"capture_file_name": "test.pcap",
|
||||
"data_link_type": "DLT_EN10MB"
|
||||
}
|
||||
# capture needs a wired port
|
||||
url = app.url_path_for(
|
||||
"compute:create_ethernet_switch_nio",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"],
|
||||
adapter_number="0",
|
||||
port_number="0"
|
||||
)
|
||||
await compute_client.post(url, json=self._udp_params())
|
||||
|
||||
node = compute_project.get_node(ethernet_switch["node_id"])
|
||||
node._ubridge_send.reset_mock()
|
||||
|
||||
params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"}
|
||||
url = app.url_path_for("compute:start_ethernet_switch_capture",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"],
|
||||
adapter_number="0",
|
||||
port_number="0")
|
||||
|
||||
with asyncio_patch("gns3server.compute.dynamips.nodes.ethernet_switch.EthernetSwitch.start_capture") as mock:
|
||||
response = await compute_client.post(url, json=params)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert mock.called
|
||||
assert "test.pcap" in response.json()["pcap_file_path"]
|
||||
|
||||
response = await compute_client.post(url, json=params)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert "test.pcap" in response.json()["pcap_file_path"]
|
||||
relay = f"{node.id}-0"
|
||||
node._ubridge_send.assert_any_call(f'bridge start_capture {relay} "{node.get_nio(0).pcap_output_file}"')
|
||||
|
||||
async def test_ethernet_switch_stop_capture(
|
||||
self,
|
||||
app: FastAPI,
|
||||
compute_client: AsyncClient,
|
||||
compute_project: Project,
|
||||
ethernet_switch: dict
|
||||
) -> None:
|
||||
|
||||
url = app.url_path_for("compute:stop_ethernet_switch_capture",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"],
|
||||
adapter_number="0",
|
||||
port_number="0")
|
||||
# start a capture first
|
||||
await compute_client.post(
|
||||
app.url_path_for(
|
||||
"compute:create_ethernet_switch_nio",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"],
|
||||
adapter_number="0",
|
||||
port_number="0"
|
||||
),
|
||||
json=self._udp_params()
|
||||
)
|
||||
await compute_client.post(
|
||||
app.url_path_for("compute:start_ethernet_switch_capture",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"],
|
||||
adapter_number="0",
|
||||
port_number="0"),
|
||||
json={"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"}
|
||||
)
|
||||
|
||||
with asyncio_patch("gns3server.compute.dynamips.nodes.ethernet_switch.EthernetSwitch.stop_capture") as mock:
|
||||
response = await compute_client.post(url)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert mock.called
|
||||
node = compute_project.get_node(ethernet_switch["node_id"])
|
||||
node._ubridge_send.reset_mock()
|
||||
relay = f"{node.id}-0"
|
||||
|
||||
response = await compute_client.post(
|
||||
app.url_path_for("compute:stop_ethernet_switch_capture",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"],
|
||||
adapter_number="0",
|
||||
port_number="0")
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
node._ubridge_send.assert_any_call(f"bridge stop_capture {relay}")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user