mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-12 21:45:41 +03:00
The live suite asserted the datapath over IPv4 only. Give every guest a ULA IPv6 address next to its v4 one and twin every v4 assertion point, sharing the impairment window (one filter application, both families asserted) so the runtime grows by minutes, not double. - harness: configure_ios(eth_ipv6=), ping() dispatches to `ping ipv6`, docker_bring_up_ipv6 (the Docker daemon leaves eth0 with disable_ipv6=1 on v6-less bridge networks, so the interfaces-file route cannot work), family-aware docker_wait_address, pcap_records scans every record (a leading DAD/MLD frame must not mask the sampled ethertype), and pcap_icmp_rtt measures the wire round trip from capture timestamps; - scenarios: connectivity/delay/suspend/isolation/churn checks in both families, captures assert 86dd next to 0800, the libpcap bpf drops are asserted family-selective (icmp vs icmp6, each killing its own family and leaving the other alive), the eBPF classifier modes on both families, marker v6 twins with an echo-specific expression (bare icmp6 also matches DAD/MLD and would break the exact counts), the v6 sharkd replay, and the link-local matrix gains the 86dd all-nodes row; serial (HDLC) legs stay v4-only; - the dynamips-emulated classic IOS under-reports its `ping ipv6` RTT by ~40 % of a delayed round trip (pcap-verified: the wire carries the v6 echo exchange in the same ~230 ms as v4 while the console reports ~130 ms), so the dynamips-family scenarios assert the v6 delay from the capture's own timestamps — recorded in tests/e2e/README.md. Full suite: 17 passed in 58m50s.
424 lines
20 KiB
Python
424 lines
20 KiB
Python
#
|
|
# 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/>.
|
|
|
|
"""
|
|
Live end-to-end test for the Ethernet switch kernel fast path (pytest
|
|
marker ``e2e``): a real switch (a Linux kernel bridge) between two real
|
|
c7200 routers, driven through the controller REST API and the IOS consoles.
|
|
Real ICMP crossing the switch proves the datapath:
|
|
|
|
* links created while the routers are stopped report ``kernel_datapath``
|
|
and defer the join; once the routers boot, the controller's re-push
|
|
makes the switch join their anchors — the switch bridge's members become
|
|
exactly the two router anchors (no relay TAPs anywhere);
|
|
* the L2-anchor spec §E.2 silence window with both routers shut: the
|
|
absorbed anchors and the switch bridge stay silent for 5 s;
|
|
* VLAN programming applies to absorbed anchors: different access VLANs
|
|
isolate the routers (ping dies), the same VLAN bridges them again;
|
|
* a ``delay 100`` filter lands as netem on the absorbed anchor (single
|
|
interface, switch-owned) and the measured RTT grows by ~100 ms — on both
|
|
link endpoint orders (peer-first and switch-first; the order decides
|
|
which side's NIO update runs last and must not decide the outcome);
|
|
* suspend admin-downs the anchor and kills the traffic; resume restores;
|
|
* capture (hosted by the switch side) writes a pcap of ICMP records;
|
|
* deleting a link detaches the anchor (which survives — the router owns
|
|
it) and re-creating re-joins it;
|
|
* a router stop/start keeps the wiring and the traffic;
|
|
* deleting the project removes the switch bridge and the anchors.
|
|
|
|
``test_ethernet_switch_relay_control`` is the negative control on an
|
|
isolated relay-configured instance: the same topology rides the per-port
|
|
uBridge relays (the switch bridge's members are its own relay TAPs, never
|
|
the routers' anchors) and still pings.
|
|
|
|
Every checkpoint runs dual-stack: the routers carry a ULA v6 address next
|
|
to their v4 one and each checkpoint pings both families (VLAN isolation
|
|
and suspend kill both; the netem delays and the capture window carry both).
|
|
"""
|
|
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from tests.e2e import harness
|
|
|
|
pytestmark = pytest.mark.e2e
|
|
|
|
# c7200 slot 0 is the fixed IO slot; the Ethernet PA goes in slot 1.
|
|
SLOTS = {"slot1": "PA-2FE-TX"}
|
|
ETH = (1, 0)
|
|
|
|
# the dual-stack half: a ULA mirroring 10.1.1.x
|
|
R1_IP6 = "fd00:1:1::1"
|
|
R2_IP6 = "fd00:1:1::2"
|
|
|
|
|
|
def _pick_image(server):
|
|
images = server.compute.dynamips_images()
|
|
image = next((name for name in images if name.startswith("c7200")), None)
|
|
if not image:
|
|
pytest.skip(f"no c7200 image available on this server (images: {images[:5]})")
|
|
return image
|
|
|
|
|
|
def _switch_bridge_name(switch_id):
|
|
# see EthernetSwitch._ensure_bridge
|
|
return "gns3" + switch_id.replace("-", "")[:6]
|
|
|
|
|
|
def _create_topology(compute, pid, image, name, idlepc=None):
|
|
switch = compute.call(
|
|
"POST",
|
|
f"/projects/{pid}/nodes",
|
|
{
|
|
"compute_id": "local",
|
|
"name": f"{name}-SW",
|
|
"node_type": "ethernet_switch",
|
|
"properties": {
|
|
"ports_mapping": [
|
|
{"name": "Ethernet0", "port_number": 0, "type": "access", "vlan": 1},
|
|
{"name": "Ethernet1", "port_number": 1, "type": "access", "vlan": 1},
|
|
]
|
|
},
|
|
},
|
|
)
|
|
r1 = compute.create_dynamips_router(pid, f"{name}-R1", image, SLOTS, idlepc=idlepc)
|
|
r2 = compute.create_dynamips_router(pid, f"{name}-R2", image, SLOTS, idlepc=idlepc)
|
|
return switch, r1, r2
|
|
|
|
|
|
def _boot(server, project_id, node_id, timeout=360):
|
|
console = harness.ios_console(server, project_id, node_id)
|
|
console.boot_wait(timeout=timeout)
|
|
return console
|
|
|
|
|
|
def _eth_shutdown(console, shut):
|
|
"""Shut/unshut the router's f1/0 — the only legitimate speaker on the
|
|
segment (IOS's own CDP/keepalives) — for §E.2's silence window; the
|
|
address stays configured and answers once it is back."""
|
|
console.run("conf t")
|
|
console.run("interface f1/0")
|
|
console.run("shutdown" if shut else "no shutdown")
|
|
console.run("end")
|
|
|
|
|
|
def test_ethernet_switch_kernel_fast_path():
|
|
server = harness.live_server(kernel=True)
|
|
compute = server.compute
|
|
if compute.capabilities().get("ubridge_tap") is not True:
|
|
pytest.skip("this compute's uBridge cannot create persistent TAPs (ubridge_tap != True)")
|
|
image = _pick_image(server)
|
|
idlepc = harness.dynamips_idlepc(server, image)
|
|
|
|
project = compute.create_project("esw-e2e")
|
|
pid = project["project_id"]
|
|
sw_bridge = None
|
|
try:
|
|
switch, r1, r2 = _create_topology(compute, pid, image, "E2E", idlepc)
|
|
switch_id = switch["node_id"]
|
|
r1_id, r2_id = r1["node_id"], r2["node_id"]
|
|
sw_bridge = _switch_bridge_name(switch_id)
|
|
a1 = harness.anchor_name(r1_id, *ETH)
|
|
a2 = harness.anchor_name(r2_id, *ETH)
|
|
|
|
# The switch bridge exists from the moment the switch node does; a
|
|
# link against a stopped router must not fail — the join defers.
|
|
assert harness.bridge_members(sw_bridge) == []
|
|
# link1 is created peer-first, link2 switch-first: the endpoint order
|
|
# decides which side's NIO update runs last, and only the switch-first
|
|
# order used to expose the passive end's tc-reset wipe (its empty-filter
|
|
# reconcile ran after the switch's apply). Both orders must work.
|
|
link1 = compute.create_link(pid, (r1_id, *ETH), (switch_id, 0, 0))
|
|
link2 = compute.create_link(pid, (switch_id, 0, 1), (r2_id, *ETH))
|
|
assert link1["kernel_datapath"] is True, link1
|
|
assert link2["kernel_datapath"] is True, link2
|
|
assert harness.bridge_members(sw_bridge) == []
|
|
|
|
print(".. starting routers and waiting for IOS boots")
|
|
compute.call("POST", f"/projects/{pid}/nodes/{r1_id}/start")
|
|
compute.call("POST", f"/projects/{pid}/nodes/{r2_id}/start")
|
|
c1 = _boot(server, pid, r1_id)
|
|
c2 = _boot(server, pid, r2_id)
|
|
|
|
# Keep NetworkManager's hands off the anchors: on NM hosts its
|
|
# netdev state machine can release a freshly re-enslaved tap from
|
|
# the bridge ~60 ms after the join (silently — the port stays up,
|
|
# the server keeps believing it is attached), which the link
|
|
# delete/recreate below triggers nondeterministically. The first
|
|
# join at node start is unaffected; this covers everything after
|
|
# it.
|
|
harness.unmanage_from_networkmanager(a1, a2)
|
|
|
|
# Deferred joins completed by the node-start re-push: the anchors
|
|
# ARE the switch ports now.
|
|
assert harness.wait_until(lambda: harness.bridge_members(sw_bridge) == sorted([a1, a2]), timeout=15), (
|
|
harness.bridge_members(sw_bridge)
|
|
)
|
|
|
|
harness.configure_ios(c1, "R1", eth_ip="10.1.1.1", eth_ipv6=R1_IP6)
|
|
harness.configure_ios(c2, "R2", eth_ip="10.1.1.2", eth_ipv6=R2_IP6)
|
|
print(".. both routers configured, pinging through the switch")
|
|
baseline = harness.wait_ping(c1, "10.1.1.2")
|
|
assert baseline["success"] == 100, baseline["raw"]
|
|
baseline6 = harness.wait_ping(c1, R2_IP6)
|
|
assert baseline6["success"] == 100, baseline6["raw"]
|
|
|
|
# L2-anchor spec §E.2: with both routers shut, the absorbed anchors
|
|
# and the switch bridge stay silent for 5 s (the bridge is created
|
|
# with multicast snooping off precisely so the bridge role can).
|
|
if harness.l2only_supported():
|
|
for console in (c1, c2):
|
|
_eth_shutdown(console, shut=True)
|
|
harness.assert_idle_silence(a1, a2, sw_bridge)
|
|
for console in (c1, c2):
|
|
_eth_shutdown(console, shut=False)
|
|
assert harness.wait_ping(c1, "10.1.1.2", attempts=5)["success"] == 100
|
|
assert harness.wait_ping(c1, R2_IP6, attempts=5)["success"] == 100
|
|
else:
|
|
print(".. uBridge without link l2only: skipping the §E.2 assertion")
|
|
|
|
# L2-anchor spec §E.1: the absorbed anchors and the switch bridge
|
|
# carry no L3 identity (skipped on a uBridge without `link l2only`),
|
|
# and the joined ports are FORWARDING — the silent-failure guard.
|
|
if harness.l2only_supported():
|
|
harness.assert_pure_l2(a1)
|
|
harness.assert_pure_l2(a2)
|
|
harness.assert_pure_l2(sw_bridge)
|
|
else:
|
|
print(".. uBridge without link l2only: skipping the §E.1 assertions")
|
|
harness.assert_forwarding(a1)
|
|
harness.assert_forwarding(a2)
|
|
|
|
# VLAN programming on absorbed anchors: different access VLANs
|
|
# isolate, the same VLAN bridges again.
|
|
compute.call(
|
|
"PUT",
|
|
f"/projects/{pid}/nodes/{switch_id}",
|
|
{
|
|
"properties": {
|
|
"ports_mapping": [
|
|
{"name": "Ethernet0", "port_number": 0, "type": "access", "vlan": 10},
|
|
{"name": "Ethernet1", "port_number": 1, "type": "access", "vlan": 20},
|
|
]
|
|
}
|
|
},
|
|
)
|
|
assert harness.bridge_members(sw_bridge) == sorted([a1, a2]), harness.bridge_members(sw_bridge)
|
|
isolated = harness.ping(c1, "10.1.1.2", repeat=3)
|
|
assert isolated["success"] == 0, isolated["raw"]
|
|
isolated6 = harness.ping(c1, R2_IP6, repeat=3)
|
|
assert isolated6["success"] == 0, isolated6["raw"]
|
|
assert harness.bridge_members(sw_bridge) == sorted([a1, a2]), harness.bridge_members(sw_bridge)
|
|
compute.call(
|
|
"PUT",
|
|
f"/projects/{pid}/nodes/{switch_id}",
|
|
{
|
|
"properties": {
|
|
"ports_mapping": [
|
|
{"name": "Ethernet0", "port_number": 0, "type": "access", "vlan": 10},
|
|
{"name": "Ethernet1", "port_number": 1, "type": "access", "vlan": 10},
|
|
]
|
|
}
|
|
},
|
|
)
|
|
rejoined = harness.wait_ping(c1, "10.1.1.2")
|
|
assert rejoined["success"] == 100, rejoined["raw"]
|
|
rejoined6 = harness.wait_ping(c1, R2_IP6)
|
|
assert rejoined6["success"] == 100, rejoined6["raw"]
|
|
|
|
# Single-owned impairment: the filter lands on the absorbed anchor
|
|
# (one interface — the switch's), RTT grows by ~100 ms one-way.
|
|
compute.call("PUT", f"/projects/{pid}/links/{link1['link_id']}", {"filters": {"delay": [100]}})
|
|
assert "netem" in harness.qdiscs(a1), harness.qdiscs(a1)
|
|
assert "netem" not in harness.qdiscs(a2), harness.qdiscs(a2)
|
|
delayed = harness.wait_ping(c1, "10.1.1.2")
|
|
assert delayed["success"] == 100, delayed["raw"]
|
|
assert 60 <= delayed["avg"] <= 250, (baseline, delayed)
|
|
# the v6 twin reads the capture's own timestamps: the guest's
|
|
# ping ipv6 under-reports a delayed round trip on dynamips (see
|
|
# test_dynamips_kernel_datapath), and here the single-owned delay
|
|
# is small enough that the quirk swallows most of the signal
|
|
capture = compute.call(
|
|
"POST", f"/projects/{pid}/links/{link1['link_id']}/capture/start", {"data_link_type": "DLT_EN10MB"}
|
|
)
|
|
harness.ping(c1, R2_IP6)
|
|
time.sleep(1)
|
|
compute.call("POST", f"/projects/{pid}/links/{link1['link_id']}/capture/stop")
|
|
wire6 = harness.pcap_icmp_rtt(capture["capture_file_path"], "86dd")
|
|
print(f".. delay 100: v4 {baseline['avg']}->{delayed['avg']} ms, v6 wire {wire6:.0f} ms")
|
|
assert wire6 is not None and wire6 >= 60, wire6
|
|
compute.call("PUT", f"/projects/{pid}/links/{link1['link_id']}", {"filters": {}})
|
|
assert "netem" not in harness.qdiscs(a1)
|
|
fast = harness.wait_ping(c1, "10.1.1.2")
|
|
assert fast["success"] == 100 and fast["avg"] < harness.FAST_RTT_MS, fast
|
|
fast6 = harness.wait_ping(c1, R2_IP6)
|
|
assert fast6["success"] == 100 and fast6["avg"] < harness.FAST_RTT_MS, fast6
|
|
|
|
# The same single-owned impairment on link2 — created switch-first,
|
|
# the order whose peer NIO update runs LAST. The passive end's empty
|
|
# reconcile used to tc-reset the shared anchor right after the switch
|
|
# applied the netem, wiping it (API green, data plane unimpaired);
|
|
# the qdisc and the RTT must both hold under this order too.
|
|
compute.call("PUT", f"/projects/{pid}/links/{link2['link_id']}", {"filters": {"delay": [100]}})
|
|
assert "netem" in harness.qdiscs(a2), harness.qdiscs(a2)
|
|
assert "netem" not in harness.qdiscs(a1), harness.qdiscs(a1)
|
|
delayed2 = harness.wait_ping(c1, "10.1.1.2")
|
|
assert delayed2["success"] == 100, delayed2["raw"]
|
|
assert 60 <= delayed2["avg"] <= 250, (baseline, delayed2)
|
|
# the capture must sit on the pinging node's anchor (link1's, i.e.
|
|
# R1's): a TX copy is timestamped post-qdisc, so on link2's own
|
|
# anchor — where this window's delay impairs the request direction —
|
|
# the request copy would already carry the delay and the measured
|
|
# delta would collapse to the turnaround. R1's anchor sees the
|
|
# request before the delay and the reply after it.
|
|
capture = compute.call(
|
|
"POST", f"/projects/{pid}/links/{link1['link_id']}/capture/start", {"data_link_type": "DLT_EN10MB"}
|
|
)
|
|
harness.ping(c1, R2_IP6)
|
|
time.sleep(1)
|
|
compute.call("POST", f"/projects/{pid}/links/{link1['link_id']}/capture/stop")
|
|
wire2_6 = harness.pcap_icmp_rtt(capture["capture_file_path"], "86dd")
|
|
print(f".. delay 100 (link2): v4 {baseline['avg']}->{delayed2['avg']} ms, v6 wire {wire2_6:.0f} ms")
|
|
assert wire2_6 is not None and wire2_6 >= 60, wire2_6
|
|
compute.call("PUT", f"/projects/{pid}/links/{link2['link_id']}", {"filters": {}})
|
|
assert "netem" not in harness.qdiscs(a2)
|
|
fast2 = harness.wait_ping(c1, "10.1.1.2")
|
|
assert fast2["success"] == 100 and fast2["avg"] < harness.FAST_RTT_MS, fast2
|
|
fast2_6 = harness.wait_ping(c1, R2_IP6)
|
|
assert fast2_6["success"] == 100 and fast2_6["avg"] < harness.FAST_RTT_MS, fast2_6
|
|
|
|
# suspend: the anchor admin-downs, the link is dead; resume restores
|
|
compute.call("PUT", f"/projects/{pid}/links/{link1['link_id']}", {"suspend": True})
|
|
assert not harness.tap_up(a1)
|
|
dead = harness.ping(c1, "10.1.1.2", repeat=3)
|
|
assert dead["success"] == 0, dead["raw"]
|
|
dead6 = harness.ping(c1, R2_IP6, repeat=3)
|
|
assert dead6["success"] == 0, dead6["raw"]
|
|
compute.call("PUT", f"/projects/{pid}/links/{link1['link_id']}", {"suspend": False})
|
|
assert harness.tap_up(a1)
|
|
assert harness.wait_ping(c1, "10.1.1.2")["success"] == 100
|
|
assert harness.wait_ping(c1, R2_IP6)["success"] == 100
|
|
|
|
# capture (the controller hosts it on the switch — always-on local
|
|
# builtin): a pcap of ICMP records written off the absorbed anchor,
|
|
# both families inside the window
|
|
capture = compute.call(
|
|
"POST", f"/projects/{pid}/links/{link1['link_id']}/capture/start", {"data_link_type": "DLT_EN10MB"}
|
|
)
|
|
harness.ping(c1, "10.1.1.2")
|
|
harness.ping(c1, R2_IP6)
|
|
time.sleep(1)
|
|
compute.call("POST", f"/projects/{pid}/links/{link1['link_id']}/capture/stop")
|
|
count, ethertypes = harness.pcap_records(capture["capture_file_path"])
|
|
assert count >= 8, capture["capture_file_path"]
|
|
assert "0800" in ethertypes, ethertypes
|
|
assert "86dd" in ethertypes, ethertypes
|
|
|
|
# link delete detaches the anchor (it survives — the router owns
|
|
# it); re-creating re-joins it
|
|
compute.call("DELETE", f"/projects/{pid}/links/{link2['link_id']}")
|
|
assert harness.bridge_members(sw_bridge) == [a1]
|
|
assert harness.tap_exists(a2)
|
|
link2 = compute.create_link(pid, (r2_id, *ETH), (switch_id, 0, 1))
|
|
assert link2["kernel_datapath"] is True, link2
|
|
assert harness.wait_until(lambda: harness.bridge_members(sw_bridge) == sorted([a1, a2]), timeout=10)
|
|
assert harness.wait_ping(c1, "10.1.1.2", attempts=5)["success"] == 100
|
|
assert harness.wait_ping(c1, R2_IP6, attempts=5)["success"] == 100
|
|
|
|
# router stop/start keeps the wiring: a Dynamips stop only halts the
|
|
# emulated router — the hypervisor, the anchor and its bridge
|
|
# membership all survive (the same stop-is-not-close semantics as
|
|
# the per-link bridge datapath)
|
|
print(".. stopping and restarting R1")
|
|
compute.call("POST", f"/projects/{pid}/nodes/{r1_id}/stop")
|
|
assert harness.tap_exists(a1)
|
|
assert harness.bridge_members(sw_bridge) == sorted([a1, a2])
|
|
compute.call("POST", f"/projects/{pid}/nodes/{r1_id}/start")
|
|
c1.close()
|
|
c1 = _boot(server, pid, r1_id)
|
|
assert harness.wait_until(lambda: harness.bridge_members(sw_bridge) == sorted([a1, a2]), timeout=15)
|
|
harness.configure_ios(c1, "R1", eth_ip="10.1.1.1", eth_ipv6=R1_IP6)
|
|
assert harness.wait_ping(c1, "10.1.1.2", attempts=5)["success"] == 100
|
|
assert harness.wait_ping(c1, R2_IP6, attempts=5)["success"] == 100
|
|
|
|
c1.close()
|
|
c2.close()
|
|
except BaseException:
|
|
harness.release(server, pid, failed=True)
|
|
raise
|
|
harness.release(server, pid, failed=False)
|
|
|
|
# project delete closed everything: switch bridge and anchors gone
|
|
assert harness.wait_until(lambda: harness.bridge_members(sw_bridge) is None, timeout=15), sw_bridge
|
|
|
|
|
|
def test_ethernet_switch_relay_control():
|
|
"""Negative control: the same topology on an isolated relay-configured
|
|
instance rides the per-port uBridge relays — the switch bridge's
|
|
members are its own relay TAPs, never the routers' anchors — and still
|
|
pings."""
|
|
server = harness.live_server(kernel=False)
|
|
compute = server.compute
|
|
image = _pick_image(server)
|
|
idlepc = harness.dynamips_idlepc(server, image)
|
|
|
|
project = compute.create_project("esw-e2e-relay")
|
|
pid = project["project_id"]
|
|
sw_bridge = None
|
|
try:
|
|
switch, r1, r2 = _create_topology(compute, pid, image, "E2E", idlepc)
|
|
sw_bridge = _switch_bridge_name(switch["node_id"])
|
|
r1_id, r2_id = r1["node_id"], r2["node_id"]
|
|
a1 = harness.anchor_name(r1_id, *ETH)
|
|
|
|
link1 = compute.create_link(pid, (r1_id, *ETH), (switch["node_id"], 0, 0))
|
|
link2 = compute.create_link(pid, (r2_id, *ETH), (switch["node_id"], 0, 1))
|
|
assert link1["kernel_datapath"] is False, link1
|
|
assert link2["kernel_datapath"] is False, link2
|
|
|
|
print(".. starting routers and waiting for IOS boots (relay)")
|
|
compute.call("POST", f"/projects/{pid}/nodes/{r1_id}/start")
|
|
compute.call("POST", f"/projects/{pid}/nodes/{r2_id}/start")
|
|
c1 = _boot(server, pid, r1_id)
|
|
c2 = _boot(server, pid, r2_id)
|
|
|
|
members = harness.bridge_members(sw_bridge)
|
|
assert members == sorted([f"{sw_bridge}-0", f"{sw_bridge}-1"]), members
|
|
assert a1 not in members
|
|
if harness.l2only_supported():
|
|
# creation-time hardening applies on the relay too: the switch's
|
|
# own port TAPs and its bridge are pure L2
|
|
for member in members:
|
|
harness.assert_pure_l2(member)
|
|
harness.assert_pure_l2(sw_bridge)
|
|
|
|
harness.configure_ios(c1, "R1", eth_ip="10.1.1.1", eth_ipv6=R1_IP6)
|
|
harness.configure_ios(c2, "R2", eth_ip="10.1.1.2", eth_ipv6=R2_IP6)
|
|
relay_ping = harness.wait_ping(c1, "10.1.1.2")
|
|
assert relay_ping["success"] == 100, relay_ping["raw"]
|
|
relay_ping6 = harness.wait_ping(c1, R2_IP6)
|
|
assert relay_ping6["success"] == 100, relay_ping6["raw"]
|
|
|
|
c1.close()
|
|
c2.close()
|
|
except BaseException:
|
|
harness.release(server, pid, failed=True)
|
|
raise
|
|
harness.release(server, pid, failed=False)
|
|
|
|
assert harness.wait_until(lambda: harness.bridge_members(sw_bridge) is None, timeout=15), sw_bridge
|