1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 19:56:48 +00:00
webvirtcloud/computes/tests/test_overview.py
catborise a0a31cd3f2 fix(storages): pools are measured when shown, and only on a host's own pages
libvirt measures a pool's size and free space only when the pool is
refreshed and keeps those figures until the next refresh. The pool
lists read them without a refresh, so they showed a pool's space as of
whenever libvirt last refreshed it: two hosts sharing one pool's
storage told different free space for it, each its own refresh's. On a
test host, a 1 GiB file left libvirt's free space unchanged while df
showed 1 GiB less, until the pool was refreshed.

get_storages_info refreshes each active pool before reading it; a
refresh libvirt refuses (while a volume is cloned in the pool) leaves
the last figures, as the pool pages already do. A refresh scans the
pool's volumes, so pools are read only where one host's are shown: a
host's overview (views.host_pools, its Storage card) and its Storages
page and API. The Computes list and the new-VM dialog's host lines,
which read every host at once, no longer show pools; Compute.capacity
is the vCPUs and memory given.
2026-10-27 21:30:14 +03:00

120 lines
7.3 KiB
Python

"""The host overview page: a host the panel reaches, one it cannot reach,
and one whose details cannot be read."""
from unittest.mock import PropertyMock, patch
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from libvirt import libvirtError
from lxml import html
from computes.models import Compute
GIB = 1024 ** 3
GIVEN = {"cpus": 8, "memory": 32 * GIB, "vcpus": {"all": 12, "running": 6},
"assigned": {"all": 24 * GIB, "running": 12 * GIB}, "ratio": 1.5,
"vms": {"running": 3, "paused": 1, "shutoff": 2, "other": 0}}
def pool(name, size_gib, free_gib, status=1):
return {"name": name, "status": status, "size": size_gib * GIB, "free": free_gib * GIB}
def text(page, xpath):
return " ".join(" ".join(page.xpath(xpath)[0].itertext()).split())
class OverviewTestCase(TestCase):
def setUp(self):
self.client.force_login(get_user_model().objects.create_superuser("ov-admin", "ov@example.com", "pw"))
self.compute = Compute.objects.create(name="host-one", hostname="192.0.2.1", login="u", password="p",
type=1, details="rack 4")
self.url = reverse("overview", args=[self.compute.id])
def page(self, connected=True, given=GIVEN, pools=(pool("default", 500, 120),)):
with patch("computes.views.wvmStorages") as storages, \
patch.object(Compute, "status", new_callable=PropertyMock, return_value=connected), \
patch.object(Compute, "connection_error", new_callable=PropertyMock,
return_value="Connection Failed: 192.0.2.1 did not answer within 5 s"), \
patch.object(Compute, "capacity", new=given and {**given}):
storages.return_value.get_storages_info.return_value = [dict(p) for p in pools]
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
return html.fromstring(response.content)
@patch("computes.views.wvmHostDetails")
def test_a_host_shows_its_vms_capacity_hardware_and_software(self, details):
conn = details.return_value
conn.get_node_info.return_value = ("wvc-host", "x86_64", 32 * GIB, 8, "Xeon", "qemu+tcp://192.0.2.1/system")
conn.get_cpu_topology.return_value = {"nodes": 2, "sockets": 2, "cores": 4, "mhz": 2400, "cpus": 8}
conn.wvm.getType.return_value = "QEMU"
conn.get_version.return_value = "10.1.0"
conn.get_lib_version.return_value = "11.10.0"
conn.get_emulator.return_value = "/usr/libexec/qemu-kvm"
conn.get_hypervisors_domain_types.return_value = {"i686": ["kvm", "qemu"], "x86_64": ["kvm", "qemu"]}
page = self.page()
self.assertIn("Connected", text(page, "//div[@id='host_status']"))
self.assertIn("rack 4", text(page, "//div[@id='host_status']"))
cards = [" ".join(" ".join(card.itertext()).split()) for card in page.xpath("//div[@class='card h-100']")]
self.assertIn("VMs 6 3 running, 2 shut off, 1 paused Instances →", cards)
self.assertTrue(any(card.startswith("vCPUs 1.5:1 12 vCPUs on 8 CPUs 6 running") for card in cards), cards)
self.assertTrue(any(card.startswith("Memory 75 % 24.0 GB / 32.0 GB") for card in cards), cards)
self.assertTrue(any("120.0 GB free of 500.0 GB" in card for card in cards), cards)
hardware = text(page, "//div[@class='card-header'][normalize-space()='Hardware']/..")
self.assertIn("Processor Xeon Architecture x86_64 Logical CPUs 8 Physical cores 4 Sockets 2 NUMA nodes 2 "
"Clock 2.4 GHz Memory 32.0 GB", hardware)
software = text(page, "//div[@class='card-header'][normalize-space()='Software and connection']/..")
self.assertIn("QEMU 10.1.0 libvirt 11.10.0", software)
self.assertIn("x86_64 kvm · qemu", software)
self.assertTrue(page.xpath("//canvas[@id='cpuChart']"))
@patch("computes.views.wvmHostDetails")
def test_the_default_pool_shows_and_the_others_open_on_a_click(self, details):
details.return_value.get_node_info.return_value = ("h", "x86_64", GIB, 2, None, "qemu+tcp://192.0.2.1/system")
details.return_value.get_lib_version.side_effect = libvirtError("details are not this test's")
pools = [pool("nfs", 2000, 1000), pool("off", 10, 0, status=0), pool("default", 500, 120),
pool("backup", 900, 800), pool("iso", 100, 90)]
page = self.page(pools=pools)
storage = page.xpath("//div[@class='card-header'][normalize-space()='Storage']/..")[0]
self.assertIn("default 120.0 GB free of 500.0 GB", " ".join(" ".join(storage.xpath("./div/div//text()")).split()))
(more,) = storage.xpath(".//details")
self.assertEqual(" ".join(more.xpath("summary")[0].text_content().split()), "3 more pools")
# active pools only, by name after the default one
self.assertEqual(more.xpath(".//div[@class='small']/text()"), ["backup", "iso", "nfs"])
@patch("computes.views.wvmHostDetails")
def test_pools_that_cannot_be_read_leave_the_rest(self, details):
details.return_value.get_node_info.return_value = ("h", "x86_64", GIB, 2, None, "qemu+tcp://192.0.2.1/system")
with patch("computes.views.host_pools", return_value=None):
page = self.page()
self.assertIn("The storage pools could not be read.", text(page, "//div[@class='card-header'][normalize-space()='Storage']/.."))
self.assertTrue(page.xpath("//div[@class='card-header'][normalize-space()='VMs']"))
@patch("computes.views.wvmHostDetails")
def test_a_host_the_panel_cannot_reach_keeps_its_page_with_the_reason(self, details):
page = self.page(connected=False, given=None)
details.assert_not_called()
self.assertIn("Not connected", text(page, "//div[@id='host_status']"))
self.assertIn("did not answer within 5 s", text(page, "//div[@role='alert']"))
# the way back: its sections, its connection and its removal
self.assertTrue(page.xpath("//nav[@aria-label='Host sections']"))
self.assertTrue(page.xpath(f"//a[@href='{reverse('compute_update', args=[self.compute.id])}']"))
self.assertTrue(page.xpath(f"//a[@href='{reverse('compute_delete', args=[self.compute.id])}']"))
self.assertFalse(page.xpath("//canvas"))
@patch("computes.views.wvmHostDetails")
def test_details_that_cannot_be_read_leave_the_capacity_and_the_load(self, details):
details.return_value.get_node_info.return_value = ("h", "x86_64", GIB, 2, None, "qemu+tcp://192.0.2.1/system")
details.return_value.get_lib_version.side_effect = libvirtError("cannot read the version")
page = self.page()
self.assertIn("cannot read the version", text(page, "//div[@role='alert']"))
self.assertTrue(page.xpath("//div[@class='card-header'][normalize-space()='VMs']"))
# the load is read on its own, by the charts
self.assertTrue(page.xpath("//canvas[@id='cpuChart']"))
self.assertFalse(page.xpath("//div[@class='card-header'][normalize-space()='Hardware']"))
def test_the_list_links_the_overview_of_a_host_it_cannot_reach(self):
with patch.object(Compute, "status", new_callable=PropertyMock, return_value=False):
page = html.fromstring(self.client.get(reverse("computes")).content)
self.assertTrue(page.xpath(f"//a[@title='Overview'][@href='{self.url}']"))