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