From 55eb63a09f7ca8052c39c4669261dc2398e98457 Mon Sep 17 00:00:00 2001 From: catborise Date: Wed, 7 Oct 2026 23:57:10 +0300 Subject: [PATCH] fix(interfaces): an ethernet interface needs no bridge member, and unreadable host devices are reported The form required a bridge member for an ethernet interface, which does not use one. When the host's network devices could not be read, the error was swallowed and every creation failed with "the device is not on this host"; the page now shows the real error and does not try. --- interfaces/forms.py | 9 +++++++-- interfaces/test_forms.py | 27 +++++++++++++++++++++++++++ interfaces/views.py | 12 ++++++++---- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/interfaces/forms.py b/interfaces/forms.py index 83923c8c..ab680f43 100644 --- a/interfaces/forms.py +++ b/interfaces/forms.py @@ -14,7 +14,8 @@ class AddInterface(forms.Form): required=True, choices=(("none", "none"), ("onboot", "onboot"), ("hotplug", "hotplug")), ) - netdev = forms.CharField(max_length=15, required=True) + # a bridge's member; an ethernet interface configures the device in name + netdev = forms.CharField(max_length=15, required=False) ipv4_type = forms.ChoiceField( required=True, choices=(("dhcp", "dhcp"), ("static", "static"), ("none", "none")), @@ -93,6 +94,8 @@ class AddInterface(forms.Form): def clean_netdev(self): netdev = self.cleaned_data["netdev"] + if not netdev: + return netdev if not re.fullmatch(r"[A-Za-z0-9_.-]{1,15}", netdev) or netdev in (".", ".."): raise forms.ValidationError( _("The device name may have up to 15 letters, digits and . _ -") @@ -111,7 +114,9 @@ class AddInterface(forms.Form): self.add_error("stp", _("A bridge needs STP on or off")) if cleaned.get("delay") is None and "delay" not in self.errors: self.add_error("delay", _("A bridge needs a forward delay")) - if netdev and netdev not in self.netdevs: + if not netdev and "netdev" not in self.errors: + self.add_error("netdev", _("A bridge needs a device")) + elif netdev and netdev not in self.netdevs: self.add_error("netdev", _("The device is not on this host")) elif netdev and netdev == name: self.add_error("netdev", _("A bridge cannot contain itself")) diff --git a/interfaces/test_forms.py b/interfaces/test_forms.py index b5a36456..cbf77e98 100644 --- a/interfaces/test_forms.py +++ b/interfaces/test_forms.py @@ -225,3 +225,30 @@ class CreateViewTestCase(TestCase): {**BASE, "netdev": netdev, "ipv6_type": "none", "create": ""}, ) self.assertEqual(conn.create_iface.called, created) + + def test_unreadable_host_devices_are_reported_not_blamed_on_the_input(self): + admin = get_user_model().objects.create_superuser("iface_admin2", "b@example.com", "x") + compute = Compute.objects.create(name="iface-c2", hostname="127.0.0.1:1", login="root", password="", type=1) + self.client.force_login(admin) + with patch("interfaces.views.wvmInterfaces") as conn_cls: + conn = conn_cls.return_value + conn.get_ifaces.return_value = [] + conn.get_net_devices.side_effect = RuntimeError("nodedev driver not running") + response = self.client.post( + reverse("interfaces", args=[compute.id]), + {**BASE, "netdev": "eth0", "ipv6_type": "none", "create": ""}, + ) + self.assertFalse(conn.create_iface.called) + messages = [str(m) for m in response.context["messages"]] + self.assertTrue(any("nodedev driver not running" in m for m in messages), messages) + self.assertFalse(any("not on this host" in m for m in messages), messages) + + +class EthernetDeviceTestCase(SimpleTestCase): + def test_an_ethernet_interface_needs_no_bridge_member(self): + self.assertTrue(form(itype="ethernet", name="eth0", netdev="", ipv6_type="none").is_valid()) + + def test_a_bridge_needs_a_member(self): + f = form(netdev="", ipv6_type="none") + self.assertFalse(f.is_valid()) + self.assertIn("netdev", f.errors) diff --git a/interfaces/views.py b/interfaces/views.py index 6453e08f..f89a91b7 100644 --- a/interfaces/views.py +++ b/interfaces/views.py @@ -1,8 +1,8 @@ -import contextlib from django.contrib import messages from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect from django.urls import reverse +from django.utils.translation import gettext_lazy as _ from libvirt import libvirtError from computes.models import Compute from interfaces.forms import AddInterface @@ -30,9 +30,12 @@ def interfaces(request, compute_id): ) ifaces = conn.get_ifaces() - netdevs = [] - with contextlib.suppress(Exception): + netdevs, netdev_error = [], None + try: netdevs = conn.get_net_devices() + except Exception as err: + netdev_error = err + messages.error(request, _("The network devices of this host could not be read: %(error)s") % {"error": err}) for iface in ifaces: interf = wvmInterface( @@ -45,7 +48,8 @@ def interfaces(request, compute_id): ifaces_all.append(interf.get_details()) if request.method == "POST": - if "create" in request.POST: + # without the host's devices nothing can be checked, so nothing is created + if "create" in request.POST and netdev_error is None: form = AddInterface(request.POST, netdevs=netdevs) if form.is_valid(): data = form.cleaned_data