mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-21 18:32:23 +03:00
fix(docker): delete the per-link kernel bridge when a node stops
The bridge teardown only hung off the link-removal path, but deleting a
node or closing a project never issues a per-link delete — the veths
disappeared with their containers and left the bridge behind as an empty
orphan (reproduced: create a kernel link, DELETE the project, the
gns3{link_id} bridge survives). `_remove_kernel_veths` now deletes the
node's kernel-NIO bridges alongside the veth host ends: both endpoints do
it, the peer's still-enslaved port answers EBUSY (suppressed, last one
wins) exactly like link deletion, a relay NIO's uBridge bridge is left
alone (it dies with the process), and a node restart rebuilds the bridge
from the NIO in _connect_nio.
Found while cleaning up the window_drop e2e: seven such bridges had
accumulated on this host. Re-verified live: the bridge is gone after the
project delete, and the window_drop e2e (which includes a node restart
with the kernel link attached) still passes 15/15.
This commit is contained in:
parent
316e7002c1
commit
48dbdbe4a9
@ -151,7 +151,13 @@ endpoints became eligible are upgraded to the kernel datapath automatically.
|
|||||||
attached to the host end*.
|
attached to the host end*.
|
||||||
* **Death (container stop)** — `_remove_kernel_veths` deletes the host ends
|
* **Death (container stop)** — `_remove_kernel_veths` deletes the host ends
|
||||||
explicitly: unlike a TAP (which died with the container netns), a veth host
|
explicitly: unlike a TAP (which died with the container netns), a veth host
|
||||||
end outlives the container. Deleting either end destroys the pair.
|
end outlives the container. Deleting either end destroys the pair. The
|
||||||
|
node's per-link kernel bridges go with them — deleting a node or closing a
|
||||||
|
project never runs the link-teardown path (no per-link delete is issued),
|
||||||
|
so a bridge whose ports just disappeared would stay behind as an empty
|
||||||
|
orphan. Both endpoints do this and the peer's still-enslaved port makes
|
||||||
|
the delete fail with EBUSY (suppressed, last one wins), the same contract
|
||||||
|
as link deletion; a node restart rebuilds the bridge from the NIO.
|
||||||
|
|
||||||
## Link operations
|
## Link operations
|
||||||
|
|
||||||
|
|||||||
@ -1883,12 +1883,25 @@ class DockerVM(BaseNode):
|
|||||||
with the container network namespace) the host end outlives the
|
with the container network namespace) the host end outlives the
|
||||||
container and must be removed explicitly. Deleting an enslaved veth
|
container and must be removed explicitly. Deleting an enslaved veth
|
||||||
detaches it from its bridge automatically.
|
detaches it from its bridge automatically.
|
||||||
|
|
||||||
|
The per-link kernel bridges go too: deleting a node (or closing a
|
||||||
|
project) never runs the link-teardown path, so a bridge whose ports
|
||||||
|
just disappeared would stay behind as an empty orphan. Both link
|
||||||
|
endpoints run this — the peer's port still enslaved makes the delete
|
||||||
|
fail with EBUSY (suppressed) and the last one wins, the same
|
||||||
|
contract as _remove_kernel_nio. A node restart re-creates the bridge
|
||||||
|
from the NIO in _connect_nio, so nothing is lost by deleting it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.ubridge:
|
if self.ubridge:
|
||||||
for host_ifc in self._kernel_veths.values():
|
for host_ifc in self._kernel_veths.values():
|
||||||
with contextlib.suppress(UbridgeError):
|
with contextlib.suppress(UbridgeError):
|
||||||
await self._ubridge_send(f'docker delete_veth "{host_ifc}"')
|
await self._ubridge_send(f'docker delete_veth "{host_ifc}"')
|
||||||
|
for adapter in self._ethernet_adapters:
|
||||||
|
for nio in adapter.ports.values():
|
||||||
|
if isinstance(nio, NIOBridge):
|
||||||
|
with contextlib.suppress(UbridgeError):
|
||||||
|
await self._ubridge_send(f'brctl delete "{nio.bridge}"')
|
||||||
self._kernel_veths.clear()
|
self._kernel_veths.clear()
|
||||||
|
|
||||||
async def _get_namespace(self):
|
async def _get_namespace(self):
|
||||||
|
|||||||
@ -1006,6 +1006,34 @@ async def test_remove_kernel_veths(vm):
|
|||||||
assert vm._kernel_veths == {}
|
assert vm._kernel_veths == {}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_remove_kernel_veths_deletes_per_link_bridge(vm):
|
||||||
|
"""
|
||||||
|
The per-link kernel bridge dies with the node too: deleting a project
|
||||||
|
never runs the link-teardown path, so without this the bridge stays
|
||||||
|
behind as an empty orphan (EBUSY from the peer's port is suppressed —
|
||||||
|
the last endpoint wins).
|
||||||
|
"""
|
||||||
|
|
||||||
|
vm._ubridge_hypervisor = MagicMock()
|
||||||
|
vm._ubridge_send = AsyncioMagicMock()
|
||||||
|
host_ifc, _ = vm._veth_names(0, 0)
|
||||||
|
vm._kernel_veths[(0, 0)] = host_ifc
|
||||||
|
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||||
|
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||||
|
# a relay NIO's bridge is not this node's to delete (uBridge dies with it)
|
||||||
|
relay = vm.manager.create_nio({"type": "nio_udp", "lport": 10000, "rhost": "127.0.0.1", "rport": 10001})
|
||||||
|
vm._ethernet_adapters[0].add_nio(1, relay)
|
||||||
|
|
||||||
|
await vm._remove_kernel_veths()
|
||||||
|
|
||||||
|
vm._ubridge_send.assert_any_call(f'docker delete_veth "{host_ifc}"')
|
||||||
|
# exactly the kernel bridge — the relay NIO's uBridge bridge dies with
|
||||||
|
# the uBridge process and must not be touched here
|
||||||
|
deletes = [call.args[0] for call in vm._ubridge_send.call_args_list if "brctl delete" in call.args[0]]
|
||||||
|
assert deletes == [f'brctl delete "{BRIDGE}"']
|
||||||
|
|
||||||
|
|
||||||
def test_veth_names_deterministic_and_bounded(vm):
|
def test_veth_names_deterministic_and_bounded(vm):
|
||||||
|
|
||||||
host_ifc, guest_ifc = vm._veth_names(3, 2)
|
host_ifc, guest_ifc = vm._veth_names(3, 2)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user