diff --git a/README.md b/README.md
index 6d18373d..9f821449 100644
--- a/README.md
+++ b/README.md
@@ -576,6 +576,7 @@ A version from before v3.0.0 has no worker: `systemctl disable --now webvirt-wor
- A VM's stats are polled every 5 s like the host graph, the first point as soon as the tab opens; each point is the average since the previous one, disk rates in MB/s (they were MiB/s labelled Mb/s); a counter the host does not report shows a gap instead of 0.
- An ISO uploads to an SSH compute about 3.5 times faster (SFTP writes no longer wait for each 32 KiB); a failed write still fails the upload.
- On a host whose libvirt cannot change network interfaces (the udev backend: RHEL 9 and later, Debian 12), the interface pages offer no create, start, stop or delete; they only failed there.
+- Deleting a bridge on a host with the netcf backend (RHEL/AlmaLinux 8) keeps the devices it held: each is defined again on its own with the bridge's start mode and, for a single device, its addresses, as `virsh iface-unbridge` does. They lost their configuration and stayed down after a reboot.
- On a running VM the disk list, its edit form, the QoS table, the title, description, CPU, boot order, video and console fields show the settings as they will be at the next start, so saving an edit no longer reverts another pending change. A boot order that names a device missing from that definition is refused instead of clearing the order. The settings tab of a running VM says so. A QoS edit keeps the inbound floor. Buttons shown as disabled while the VM runs can no longer be submitted with Enter.
- A new disk or CD-ROM on a running VM gets a device name that neither the running VM nor its next-start settings use; it could take the name of a pending disk and stay attached until the next start only.
- The OTP e-mail's QR code is drawn with segno; django-qr-code is no longer required. A `settings.py` kept from an older version still lists `"qr_code"` in `INSTALLED_APPS`: delete that line, or the app does not start in a venv without django-qr-code.
diff --git a/interfaces/tests/test_delete.py b/interfaces/tests/test_delete.py
new file mode 100644
index 00000000..9222791e
--- /dev/null
+++ b/interfaces/tests/test_delete.py
@@ -0,0 +1,44 @@
+"""Deleting a host interface: netcf removes a bridge's members with the
+bridge, so they are defined again on their own (as virsh iface-unbridge)."""
+
+from unittest.mock import MagicMock
+
+from django.test import SimpleTestCase
+from lxml import etree
+from vrtManager.interface import wvmInterface
+
+ADDRESS = ""
+BOND = ""
+
+
+def delete(xml):
+ conn = wvmInterface.__new__(wvmInterface)
+ conn.iface = MagicMock()
+ conn.iface.XMLDesc.return_value = xml
+ conn.wvm = MagicMock()
+ conn.delete_iface()
+ conn.iface.undefine.assert_called_once_with()
+ return [etree.fromstring(call.args[0]) for call in conn.wvm.interfaceDefineXML.call_args_list]
+
+
+class DeleteInterfaceTestCase(SimpleTestCase):
+ def test_the_only_member_takes_the_bridge_start_mode_and_addresses(self):
+ (bond,) = delete(f"{ADDRESS}{BOND}")
+ self.assertEqual(bond.get("name"), "bond0")
+ self.assertEqual(bond.xpath("bond/@mode"), ["active-backup"])
+ self.assertEqual(bond.xpath("bond/interface/@name"), ["eth1"])
+ self.assertEqual(bond.xpath("start/@mode"), ["onboot"])
+ self.assertEqual(bond.xpath("protocol/ip/@address"), ["192.0.2.10"])
+
+ def test_several_members_take_the_start_mode_but_no_address(self):
+ members = delete(
+ f"{ADDRESS}"
+ ""
+ )
+ self.assertEqual([m.get("name") for m in members], ["eth1", "eth2"])
+ for member in members:
+ self.assertEqual(member.xpath("start/@mode"), ["onboot"])
+ self.assertEqual(member.xpath("protocol"), [])
+
+ def test_an_interface_that_is_not_a_bridge_is_only_undefined(self):
+ self.assertEqual(delete(f"{ADDRESS}"), [])
diff --git a/vrtManager/interface.py b/vrtManager/interface.py
index 333d2fa7..97b59f95 100644
--- a/vrtManager/interface.py
+++ b/vrtManager/interface.py
@@ -198,4 +198,12 @@ class wvmInterface(wvmConnect):
self.iface.create()
def delete_iface(self):
+ """Undefine the interface. netcf removes a bridge's members with it, so
+ each member is defined again on its own, as virsh iface-unbridge does:
+ with the bridge's start mode and, when it is the only one, addresses."""
+ tree = ElementTree.fromstring(self._XMLDesc(VIR_INTERFACE_XML_INACTIVE))
+ members = tree.findall("bridge/interface")
self.iface.undefine()
+ for member in members:
+ member.extend(tree.findall("start") + (tree.findall("protocol") if len(members) == 1 else []))
+ self.wvm.interfaceDefineXML(ElementTree.tostring(member, encoding="unicode"), 0)