1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 03:36:48 +00:00

fix(interfaces): no create, start, stop or delete where libvirt cannot change interfaces

libvirt's udev interface backend (RHEL 9 and later, Debian 12) only reads
interfaces, so the create form and the start, stop and delete buttons
only failed there. The pages ask the host with an invalid definition,
which defines nothing: a backend without define refuses it as not
supported. There they show the interface state and a note instead, and
posts change nothing; the host's devices, needed only by the create
form, are not read. Any other answer keeps the pages as they were.
This commit is contained in:
catborise 2026-10-10 21:27:31 +03:00
parent 35fea424ee
commit 0bb4337043
9 changed files with 175 additions and 12 deletions

View file

@ -8,7 +8,7 @@ from computes.models import Compute
from django.contrib.auth import get_user_model
from django.test import SimpleTestCase, TestCase
from django.urls import reverse
from lxml import etree
from lxml import etree, html
from vrtManager.interface import wvmInterfaces
from interfaces.forms import AddInterface
@ -295,6 +295,7 @@ class CreateViewTestCase(TestCase):
with self.subTest(netdev=netdev), patch("interfaces.views.wvmInterfaces") as conn_cls:
conn = conn_cls.return_value
conn.get_ifaces.return_value = []
conn.can_change_interfaces.return_value = True
conn.get_net_devices.return_value = ["eth0", "eth1"]
self.client.post(
reverse("interfaces", args=[compute.id]),
@ -309,6 +310,7 @@ class CreateViewTestCase(TestCase):
with patch("interfaces.views.wvmInterfaces") as conn_cls:
conn = conn_cls.return_value
conn.get_ifaces.return_value = []
conn.can_change_interfaces.return_value = True
conn.get_net_devices.side_effect = RuntimeError("nodedev driver not running")
response = self.client.post(
reverse("interfaces", args=[compute.id]),
@ -320,6 +322,78 @@ class CreateViewTestCase(TestCase):
self.assertFalse(any("not on this host" in m for m in messages), messages)
class UnchangeableHostTestCase(TestCase):
"""A host whose libvirt cannot change interfaces (udev backend) gets no
create form and no start/stop/delete, only a note; posts change nothing."""
NOTE = "does not support changing network interfaces"
def setUp(self):
self.client.force_login(get_user_model().objects.create_superuser("iface_ro", "ro@example.com", "x"))
self.compute = Compute.objects.create(name="iface-ro", hostname="127.0.0.1:1", login="root", password="", type=1)
def interfaces_page(self, changeable, post=None):
with patch("interfaces.views.wvmInterfaces") as conn_cls:
conn = conn_cls.return_value
conn.get_ifaces.return_value = []
conn.can_change_interfaces.return_value = changeable
conn.get_net_devices.return_value = ["eth0", "eth1"]
url = reverse("interfaces", args=[self.compute.id])
response = self.client.post(url, post) if post else self.client.get(url)
return conn, response.content.decode()
def test_the_interfaces_page_offers_create_only_where_it_works(self):
for changeable in (True, False):
with self.subTest(changeable=changeable):
conn, page = self.interfaces_page(changeable)
self.assertEqual("#AddInterface" in page, changeable)
self.assertEqual(self.NOTE in page, not changeable)
# the devices are only the form's choices
self.assertEqual(conn.get_net_devices.called, changeable)
def test_a_create_post_creates_nothing(self):
conn, page = self.interfaces_page(False, {**BASE, "netdev": "eth1", "ipv6_type": "none", "create": ""})
conn.create_iface.assert_not_called()
self.assertIn(self.NOTE, page)
def interface_page(self, changeable, state, post=None):
with patch("interfaces.views.wvmInterface") as conn_cls:
conn = conn_cls.return_value
conn.can_change_interfaces.return_value = changeable
conn.is_active.return_value = state
conn.get_bridge_slave_ifaces.return_value = []
url = reverse("interface", args=[self.compute.id, "br0"])
response = self.client.post(url, post) if post else self.client.get(url)
return conn, response
def test_the_interface_page_shows_the_state_without_actions(self):
for state, shown in ((1, "Active"), (0, "Inactive")):
with self.subTest(state=state):
_conn, response = self.interface_page(False, state)
page = response.content.decode()
self.assertNotIn('name="start"', page)
self.assertNotIn('name="stop"', page)
state_cell = html.fromstring(page).xpath("//dt[normalize-space()='State']/following-sibling::dd[1]")[0]
self.assertEqual(state_cell.text_content().strip(), shown)
self.assertIn(self.NOTE, page)
_conn, response = self.interface_page(True, 0)
self.assertIn('name="start"', response.content.decode())
def test_a_host_that_could_not_be_read_keeps_the_page_as_it_was(self):
with patch("interfaces.views.wvmInterface", side_effect=libvirt.libvirtError("unreachable")):
page = self.client.get(reverse("interface", args=[self.compute.id, "br0"])).content.decode()
self.assertNotIn(self.NOTE, page)
self.assertIn("Interface start/stop/delete form", page)
def test_start_stop_and_delete_posts_change_nothing(self):
for action in ("start", "stop", "delete"):
with self.subTest(action=action):
conn, response = self.interface_page(False, 1, {action: ""})
self.assertEqual(response.status_code, 200)
for method in (conn.start_iface, conn.stop_iface, conn.delete_iface):
method.assert_not_called()
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())