mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-27 02:32:23 +02:00
fix(ethernet-switch): release a leftover bridge's ports before the crash-recovery delete
A server killed before close() leaves the switch's kernel bridge behind with its ports still enslaved: a peer's persistent anchor TAP and cascade veth ends outlive uBridge. uBridge's brctl delete refuses a bridge that still has ports (EBUSY), so the best-effort recovery delete in _ensure_bridge() failed silently and brctl create hit EEXIST: the switch, and the project holding it, could no longer be opened (HTTP 409 "Could not create bridge gns3xxxxxx: File exists"). List the leftover bridge's ports from /sys/class/net/<bridge>/brif and release each with brctl delif before the delete. uBridge's brctl show reports no ports, and the switch already reads sysfs on the same host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4d55cb1b9c
commit
d440849886
@ -230,13 +230,27 @@ class EthernetSwitch(KernelDatapathMixin, BaseNode):
|
||||
this switch's UUID (kernel interface names are ≤ 15 chars). A stale
|
||||
bridge from a previous crash is deleted first so ``brctl create`` never
|
||||
hits EEXIST.
|
||||
|
||||
A server killed before ``close()`` leaves that bridge with its ports
|
||||
still enslaved: a peer's persistent anchor TAP or a cascade veth end
|
||||
outlives uBridge, and uBridge's ``brctl delete`` refuses a bridge that
|
||||
still has ports (EBUSY). Every leftover member is released first
|
||||
(same-host sysfs listing); no port of this switch is wired yet, and
|
||||
the re-wiring re-enslaves the ones that are still linked.
|
||||
"""
|
||||
|
||||
if self._bridge_created:
|
||||
return
|
||||
# deterministic short name — 10 chars, always fits the 15-char kernel cap
|
||||
self._bridge_name = "gns3" + self._id.replace("-", "")[:6]
|
||||
# crash recovery: best-effort delete any leftover bridge
|
||||
# crash recovery: release any leftover bridge's ports, then delete it
|
||||
try:
|
||||
leftover_members = os.listdir(f"/sys/class/net/{self._bridge_name}/brif")
|
||||
except OSError:
|
||||
leftover_members = [] # no leftover bridge
|
||||
for member in leftover_members:
|
||||
with contextlib.suppress(UbridgeError):
|
||||
await self._ubridge_send(f'brctl delif "{self._bridge_name}" "{member}"')
|
||||
try:
|
||||
await self._ubridge_send(f'brctl delete "{self._bridge_name}"')
|
||||
except UbridgeError:
|
||||
|
||||
@ -92,6 +92,69 @@ class TestEthernetSwitchNodesRoutes:
|
||||
]
|
||||
)
|
||||
|
||||
async def test_ethernet_switch_create_releases_a_leftover_bridge(
|
||||
self, app: FastAPI, compute_client: AsyncClient, compute_project: Project
|
||||
) -> None:
|
||||
"""
|
||||
A server killed before close() leaves the switch's kernel bridge
|
||||
behind, with a peer's persistent anchor TAP still enslaved (the
|
||||
anchor outlives uBridge). uBridge refuses to delete a bridge that
|
||||
still has ports (EBUSY), so the crash recovery must release the
|
||||
leftover members first: otherwise ``brctl create`` hits EEXIST and
|
||||
the switch -- and the project holding it -- can no longer be opened.
|
||||
"""
|
||||
|
||||
node_id = "ada84b51-ab2c-4d3e-8f40-0123456789ab"
|
||||
br = "gns3ada84b" # "gns3" + the first 6 hex chars of the node id
|
||||
brif = f"/sys/class/net/{br}/brif"
|
||||
leftover = "gq00010203e0p0"
|
||||
members = {leftover}
|
||||
bridge = {"exists": True}
|
||||
|
||||
async def ubridge(command):
|
||||
# uBridge's brctl: delete refuses a bridge that still has ports,
|
||||
# create refuses an existing one.
|
||||
if command == f'brctl delif "{br}" "{leftover}"':
|
||||
members.discard(leftover)
|
||||
elif command == f'brctl delete "{br}"':
|
||||
if members:
|
||||
raise UbridgeError(f"Could not delete bridge {br}: Device or resource busy")
|
||||
bridge["exists"] = False
|
||||
elif command == f'brctl create "{br}"':
|
||||
if bridge["exists"]:
|
||||
raise UbridgeError(f"Could not create bridge {br}: File exists")
|
||||
bridge["exists"] = True
|
||||
|
||||
real_listdir = os.listdir
|
||||
|
||||
def listdir(path):
|
||||
if str(path) == brif:
|
||||
if not bridge["exists"]:
|
||||
raise FileNotFoundError(path)
|
||||
return sorted(members)
|
||||
return real_listdir(path)
|
||||
|
||||
with (
|
||||
asyncio_patch(f"{_NODE}._ubridge_send", side_effect=ubridge) as send,
|
||||
patch("os.listdir", new=listdir),
|
||||
):
|
||||
response = await compute_client.post(
|
||||
app.url_path_for("compute:create_ethernet_switch", project_id=compute_project.id),
|
||||
json={"name": "Ethernet Switch 1", "node_id": node_id},
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert compute_project.get_node(node_id)._bridge_name == br
|
||||
send.assert_has_calls(
|
||||
[
|
||||
call(f'brctl delif "{br}" "{leftover}"'),
|
||||
call(f'brctl delete "{br}"'),
|
||||
call(f'brctl create "{br}"'),
|
||||
call(f'link set "{br}" up'),
|
||||
call(f'brctl vlanfiltering "{br}" on'),
|
||||
]
|
||||
)
|
||||
assert not members
|
||||
|
||||
async def test_ethernet_switch_get(
|
||||
self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, ethernet_switch: dict
|
||||
) -> None:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user