fix(qemu): release the datapath after the process is gone

QEMU is the guest side of every adapter TAP, and uBridge's tap delete
refuses a device another process holds open ('Device or resource busy') —
a best-effort delete whose failure is suppressed. QEMU's stop did the
delete first and stopped the process after, so every node stop or project
delete leaked the persistent TAPs: found by inspecting the host after a
project delete (16 taps left behind, carrier down, no holder), and the
held-tap behaviour reproduced against uBridge directly.

Stop the QEMU process first, then remove the TAPs and the per-link kernel
bridges — process side first, then the devices, the same lesson as IOU's
reverse stop order. Verified live: node stop and project delete now leave
no taps and no processes behind, while a restart recreates the TAPs.

The adapter anchor naming also goes through the shared
utils.kernel_anchor helper, like the other node types.
This commit is contained in:
YueGuobin committed 2026-10-06 00:21:09 +08:00
1 parent c343ce0b8f
commit 0b78d313eb
3 files changed
+58 -12

No files matched your search

+13 -6
View File
@@ -40,6 +40,7 @@ import gns3server
from gns3server.schemas.compute.qemu_nodes import Qemu, QemuPlatform
from gns3server.utils import parse_version
from gns3server.utils.asyncio import cancellable_wait_run_in_executor, subprocess_check_output
from gns3server.utils.kernel_anchor import kernel_anchor_name
from ...utils import int_to_macaddress, is_ipv6_enabled, macaddress_to_int
from ...utils.asyncio import monitor_process
@@ -1232,7 +1233,6 @@ class QemuVM(KernelDatapathMixin, BaseNode):
Stops this QEMU VM.
"""
await self._stop_ubridge()
async with self._execute_lock:
# stop the QEMU process
self._hw_virtualization = False
@@ -1274,6 +1274,14 @@ class QemuVM(KernelDatapathMixin, BaseNode):
await self._export_config()
await super().stop()
# Release the datapath only once QEMU is gone: the process holds the
# adapter TAPs open (it is the guest side), and uBridge's tap delete
# refuses a held device ("Device or resource busy") — deleting them
# while QEMU still ran leaked the persistent TAPs, because that
# best-effort delete is suppressed. Order: process side first, then
# the devices (the same lesson as IOU's reverse stop order).
await self._stop_ubridge()
async def _open_qemu_monitor_connection_vm(self, timeout=10):
"""
Opens a connection to the QEMU monitor.
@@ -1494,13 +1502,12 @@ class QemuVM(KernelDatapathMixin, BaseNode):
def _tap_name(self, adapter_number, port_number=0):
"""
Deterministic anchor TAP name for an adapter port. The ``gq`` prefix
keeps it out of the ``gns3`` bridge/TAP name space (and apart from
Docker's ``gv``/``gc`` veth names); 8 hex chars of the node id plus
adapter/port keep it unique and within IFNAMSIZ (15).
Deterministic anchor TAP name for an adapter port (the shared
utils.kernel_anchor naming contract — the controller names a peer's
anchor with the same function when an Ethernet switch absorbs it).
"""
return f"gq{self._id.replace('-', '')[:8]}e{adapter_number}p{port_number}"
return kernel_anchor_name("qemu", self._id, adapter_number, port_number)
def _kernel_host_ifc(self, adapter_number, port_number=0):
"""