diff --git a/computes/models.py b/computes/models.py
index ec7c769b..961f3740 100644
--- a/computes/models.py
+++ b/computes/models.py
@@ -9,7 +9,6 @@ from libvirt import libvirtError, virConnect
from computes.validators import validate_hostname, validate_login, validate_migration_address
from vrtManager.connection import connection_manager
from vrtManager.hostdetails import wvmHostDetails
-from vrtManager.storage import wvmStorages
class Compute(Model):
@@ -90,10 +89,10 @@ class Compute(Model):
@cached_property
def capacity(self):
- """What the host has given its VMs (wvmHostDetails.get_capacity), its
- vCPUs per CPU, and its active storage pools, the default one first
- (None when the pools cannot be read); or None when the host cannot be
- read."""
+ """What the host has given its VMs (wvmHostDetails.get_capacity) and
+ its vCPUs per CPU; or None when the host cannot be read. Not its
+ pools: their space is measured by a refresh that scans each pool,
+ too slow for pages that read every host (computes.views.host_pools)."""
if self.status is not True:
return None
try:
@@ -101,12 +100,6 @@ class Compute(Model):
except libvirtError:
return None
given["ratio"] = given["vcpus"]["all"] / given["cpus"] if given["cpus"] else 0
- try:
- storages = wvmStorages(self.hostname, self.login, self.password, self.type)
- pools = [pool for pool in storages.get_storages_info() if pool["status"]]
- given["pools"] = sorted(pools, key=lambda pool: (pool["name"] != "default", pool["name"]))
- except libvirtError:
- given["pools"] = None
return given
def __str__(self):
diff --git a/computes/templates/computes/capacity_line.txt b/computes/templates/computes/capacity_line.txt
index 9a1d704b..06a794be 100644
--- a/computes/templates/computes/capacity_line.txt
+++ b/computes/templates/computes/capacity_line.txt
@@ -1,6 +1,5 @@
{% load i18n %}{% autoescape off %}{% if given %}
{% blocktrans count counter=given.vcpus.all with ratio=given.ratio|floatformat:1 assigned=given.assigned.all|filesizeformat %}Given to VMs: {{ counter }} vCPU ({{ ratio }}:1), {{ assigned }} memory.{% plural %}Given to VMs: {{ counter }} vCPUs ({{ ratio }}:1), {{ assigned }} memory.{% endblocktrans %}
-{% with pool=given.pools.0 %}{% if pool %}{% blocktrans with name=pool.name free=pool.free|filesizeformat %}Pool {{ name }}: {{ free }} free.{% endblocktrans %}{% endif %}{% endwith %}
{% else %}
{% trans "The capacity could not be read." %}
{% endif %}{% endautoescape %}
diff --git a/computes/templates/computes/capacity_storage.html b/computes/templates/computes/capacity_storage.html
index 569b5d7e..388f6f4e 100644
--- a/computes/templates/computes/capacity_storage.html
+++ b/computes/templates/computes/capacity_storage.html
@@ -1,15 +1,16 @@
{% load i18n %}
-{# a host's storage pools: given, from views.shown_capacity #}
-{% if given.pools is None %}
+{# a host's storage pools: storage, from views.host_pools #}
+{% if storage is None %}
{% trans "The storage pools could not be read." %}
-{% elif not given.pool %}
+{% elif not storage.pool %}
{% trans "No active storage pool." %}
+{% else %}
+{% include "computes/pool_bar.html" with pool=storage.pool %}
{% endif %}
-{% if given.pool %}{% include "computes/pool_bar.html" with pool=given.pool %}{% endif %}
-{% if given.more_pools %}
+{% if storage.more_pools %}
- {% blocktrans count counter=given.more_pools|length %}{{ counter }} more pool{% plural %}{{ counter }} more pools{% endblocktrans %}
- {% for pool in given.more_pools %}
+ {% blocktrans count counter=storage.more_pools|length %}{{ counter }} more pool{% plural %}{{ counter }} more pools{% endblocktrans %}
+ {% for pool in storage.more_pools %}
{% include "computes/pool_bar.html" %}
{% endfor %}
diff --git a/computes/templates/computes/list.html b/computes/templates/computes/list.html
index 1009cd12..1a57583e 100644
--- a/computes/templates/computes/list.html
+++ b/computes/templates/computes/list.html
@@ -29,7 +29,6 @@
{% trans "Host" %} |
{% trans "vCPUs" %} |
{% trans "Memory" %} |
- {% trans "Storage" %} |
{% trans "Actions" %} |
@@ -60,11 +59,8 @@
{% include "computes/capacity_memory.html" %}
|
-
- {% include "computes/capacity_storage.html" %}
- |
{% else %}
- {% if compute.status is True %}{% trans "The capacity could not be read." %}{% else %}{% trans "No capacity while the host cannot be reached." %}{% endif %} |
+ {% if compute.status is True %}{% trans "The capacity could not be read." %}{% else %}{% trans "No capacity while the host cannot be reached." %}{% endif %} |
{% endif %}
diff --git a/computes/tests/test_capacity.py b/computes/tests/test_capacity.py
index 5e88203a..34a99ade 100644
--- a/computes/tests/test_capacity.py
+++ b/computes/tests/test_capacity.py
@@ -14,33 +14,15 @@ GIB = 1024 ** 3
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}
-
-
class CapacityTests(TestCase):
def setUp(self):
self.compute = Compute(name="host-one", hostname="192.0.2.1", login="u", password="p", type=1)
self.compute.status = True
- @patch("computes.models.wvmStorages")
@patch("computes.models.wvmHostDetails")
- def test_a_host_tells_what_it_gave_and_its_active_pools(self, details, storages):
+ def test_a_host_tells_what_it_gave(self, details):
details.return_value.get_capacity.return_value = {"cpus": 16, "vcpus": {"all": 24, "running": 8}}
- storages.return_value.get_storages_info.return_value = [
- pool("nfs", 900, 800), pool("off", 10, 0, status=0), pool("default", 500, 120), pool("iso", 100, 90)]
- # the default pool first, the others by name
- self.assertEqual(self.compute.capacity, {
- "cpus": 16, "vcpus": {"all": 24, "running": 8}, "ratio": 1.5,
- "pools": [pool("default", 500, 120), pool("iso", 100, 90), pool("nfs", 900, 800)]})
-
- @patch("computes.models.wvmStorages")
- @patch("computes.models.wvmHostDetails")
- def test_pools_that_cannot_be_read_leave_the_cpus_and_memory(self, details, storages):
- details.return_value.get_capacity.return_value = {"cpus": 16, "vcpus": {"all": 24, "running": 8}}
- storages.return_value.get_storages_info.side_effect = libvirtError("cannot refresh pool")
- self.assertEqual(self.compute.capacity, {
- "cpus": 16, "vcpus": {"all": 24, "running": 8}, "ratio": 1.5, "pools": None})
+ self.assertEqual(self.compute.capacity, {"cpus": 16, "vcpus": {"all": 24, "running": 8}, "ratio": 1.5})
@patch("computes.models.wvmHostDetails")
def test_a_host_whose_read_fails_has_no_capacity(self, details):
@@ -62,13 +44,13 @@ class ComputesPageTests(TestCase):
c1 = Compute.objects.create(name="host-one", hostname="192.0.2.1", login="u", password="p", type=1)
Compute.objects.create(name="host-two", hostname="192.0.2.2", login="u", password="p", type=1)
found = {c1.id: {"cpus": 16, "memory": 64 * GIB, "vcpus": {"all": 40, "running": 24},
- "assigned": {"all": 48 * GIB, "running": 32 * GIB}, "ratio": 2.5, "vms": VMS,
- "pools": [pool("default", 500, 120)]}}
+ "assigned": {"all": 48 * GIB, "running": 32 * GIB}, "ratio": 2.5, "vms": VMS}}
with patch.object(Compute, "capacity", new=property(lambda c: found.get(c.id))), \
patch.object(Compute, "status", new=True):
page = html.fromstring(self.client.get(reverse("computes")).content)
headers = [" ".join(th.text_content().split()) for th in page.xpath("//thead//th")]
- self.assertEqual(headers, ["Host", "vCPUs", "Memory", "Storage", "Actions"])
+ # no pools: measuring their space scans each pool of each host (views.host_pools)
+ self.assertEqual(headers, ["Host", "vCPUs", "Memory", "Actions"])
one, two = page.xpath("//tbody/tr")
cells = [" ".join(td.text_content().split()) for td in one.xpath("td")]
self.assertIn("host-one", cells[0])
@@ -76,27 +58,9 @@ class ComputesPageTests(TestCase):
self.assertIn("40 vCPUs on 16 CPUs 24 running", cells[1])
self.assertIn("75 % 48.0 GB / 64.0 GB", cells[2])
self.assertIn("32.0 GB running", cells[2])
- self.assertIn("120.0 GB free of 500.0 GB", cells[3])
# host-two could not be read: one cell says so
self.assertIn("capacity could not be read", " ".join(two.text_content().split()))
- def test_the_default_pool_shows_and_the_others_open_on_a_click(self):
- admin = get_user_model().objects.create_superuser("cap-pools", "pools@example.com", "pw")
- self.client.force_login(admin)
- Compute.objects.create(name="host-one", hostname="192.0.2.1", login="u", password="p", type=1)
- pools = [pool("default", 500, 120), pool("backup", 900, 800), pool("iso", 100, 90), pool("nfs", 2000, 1000)]
- given = {"cpus": 16, "memory": 64 * GIB, "vcpus": {"all": 8, "running": 8}, "ratio": 0.5,
- "assigned": {"all": 8 * GIB, "running": 8 * GIB}, "pools": pools, "vms": VMS}
- with patch.object(Compute, "capacity", new=given), patch.object(Compute, "status", new=True):
- page = html.fromstring(self.client.get(reverse("computes")).content)
- (storage,) = page.xpath("//tbody/tr/td[4]")
- shown = " ".join(" ".join(storage.xpath("./div//text()")).split())
- self.assertIn("default", shown)
- self.assertNotIn("backup", shown)
- (more,) = storage.xpath(".//details")
- self.assertEqual(" ".join(more.xpath("summary")[0].text_content().split()), "3 more pools")
- self.assertEqual(more.xpath(".//div[@class='small']/text()"), ["backup", "iso", "nfs"])
-
class SingularTests(TestCase):
def test_one_vcpu_on_one_cpu_is_singular(self):
@@ -104,7 +68,7 @@ class SingularTests(TestCase):
self.client.force_login(admin)
c = Compute.objects.create(name="host-one", hostname="192.0.2.1", login="u", password="p", type=1)
given = {"cpus": 1, "memory": 4 * GIB, "vcpus": {"all": 1, "running": 1},
- "assigned": {"all": GIB, "running": GIB}, "ratio": 1.0, "pools": [], "vms": VMS}
+ "assigned": {"all": GIB, "running": GIB}, "ratio": 1.0, "vms": VMS}
with patch.object(Compute, "capacity", new=given), patch.object(Compute, "status", new=True):
page = html.fromstring(self.client.get(reverse("computes")).content)
line = self.client.get(reverse("compute_capacity", args=[c.id])).content.decode()
@@ -130,13 +94,11 @@ class NewVmDialogLineTests(TestCase):
def test_the_line_tells_what_the_host_has_given(self):
given = {"cpus": 16, "memory": 64 * GIB, "vcpus": {"all": 24, "running": 8},
"assigned": {"all": 48 * GIB, "running": 32 * GIB}, "ratio": 1.5,
- "pools": [pool("a&b", 500, 120), pool("iso", 100, 90)]}
- # plain text: a pool name is not escaped
- self.assertEqual(self.line(given), "Given to VMs: 24 vCPUs (1.5:1), 48.0 GB memory. "
- "Pool a&b: 120.0 GB free.")
+ }
+ self.assertEqual(self.line(given), "Given to VMs: 24 vCPUs (1.5:1), 48.0 GB memory.")
def test_a_host_without_pools_or_capacity_says_so(self):
given = {"cpus": 2, "memory": 4 * GIB, "vcpus": {"all": 0, "running": 0},
- "assigned": {"all": 0, "running": 0}, "ratio": 0, "pools": []}
+ "assigned": {"all": 0, "running": 0}, "ratio": 0}
self.assertEqual(self.line(given), "Given to VMs: 0 vCPUs (0.0:1), 0 bytes memory.")
self.assertEqual(self.line(None), "The capacity could not be read.")
diff --git a/computes/tests/test_cpu_graph.py b/computes/tests/test_cpu_graph.py
index 30ed4d57..6e7a4657 100644
--- a/computes/tests/test_cpu_graph.py
+++ b/computes/tests/test_cpu_graph.py
@@ -139,7 +139,7 @@ class GraphScriptTestCase(TestCase):
compute = Compute.objects.create(name="gs", hostname="192.0.2.1", login="u", password="p", type=1)
with patch("computes.views.wvmHostDetails") as details, patch.object(
Compute, "status", new_callable=PropertyMock, return_value=True
- ), patch.object(Compute, "capacity", new=None):
+ ), patch.object(Compute, "capacity", new=None), patch("computes.views.host_pools", return_value=None):
details.return_value.get_node_info.return_value = ("h", "x86_64", 1024, 2, "cpu", "qemu+tcp://h/system")
details.return_value.get_cpu_topology.return_value = TOPOLOGY
page = self.client.get(reverse("overview", args=[compute.id])).content.decode()
diff --git a/computes/tests/test_overview.py b/computes/tests/test_overview.py
index 2e33db30..7d06ff40 100644
--- a/computes/tests/test_overview.py
+++ b/computes/tests/test_overview.py
@@ -14,8 +14,11 @@ 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},
- "pools": [{"name": "default", "status": 1, "size": 500 * GIB, "free": 120 * GIB}]}
+ "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):
@@ -29,11 +32,13 @@ class OverviewTestCase(TestCase):
type=1, details="rack 4")
self.url = reverse("overview", args=[self.compute.id])
- def page(self, connected=True, given=GIVEN):
- with patch.object(Compute, "status", new_callable=PropertyMock, return_value=connected), \
+ 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)
@@ -64,6 +69,28 @@ class OverviewTestCase(TestCase):
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)
diff --git a/computes/views.py b/computes/views.py
index 827e7320..45210e48 100644
--- a/computes/views.py
+++ b/computes/views.py
@@ -13,6 +13,7 @@ from libvirt import libvirtError
from logs.audit import audit, changed_fields, snapshot
from operations import claims
from vrtManager.hostdetails import wvmHostDetails
+from vrtManager.storage import wvmStorages
from computes.forms import (
SocketComputeForm,
@@ -44,16 +45,24 @@ def shown_capacity(compute):
"over": assigned["all"] > memory}
given["memory_percent"] = round(assigned["all"] * 100 / memory) if memory else 0
given["vm_count"] = sum(given["vms"].values())
- # the default pool (else the first) shows; the others (a host may
- # have dozens) open on a click
- pools = given["pools"] or []
- for pool in pools:
- pool["used_percent"] = (pool["size"] - pool["free"]) * 100 / pool["size"] if pool["size"] else 0
- given["pool"] = pools[0] if pools else None
- given["more_pools"] = pools[1:]
return given
+def host_pools(compute):
+ """The compute's active storage pools, measured now: the default one (else
+ the first) to show, the others (a host may have dozens) to open on a
+ click; or None when they cannot be read."""
+ try:
+ storages = wvmStorages(compute.hostname, compute.login, compute.password, compute.type)
+ pools = [pool for pool in storages.get_storages_info() if pool["status"]]
+ except libvirtError:
+ return None
+ pools.sort(key=lambda pool: (pool["name"] != "default", pool["name"]))
+ for pool in pools:
+ pool["used_percent"] = (pool["size"] - pool["free"]) * 100 / pool["size"] if pool["size"] else 0
+ return {"pool": pools[0] if pools else None, "more_pools": pools[1:]}
+
+
@superuser_only
def computes(request):
"""The computes, with what each has given its VMs."""
@@ -96,6 +105,7 @@ def overview(request, compute_id):
except libvirtError as error:
context["read_error"] = str(error)
context["given"] = shown_capacity(compute)
+ context["storage"] = host_pools(compute)
return render(request, "overview.html", context)
diff --git a/vrtManager/storage.py b/vrtManager/storage.py
index 03285258..243dc229 100644
--- a/vrtManager/storage.py
+++ b/vrtManager/storage.py
@@ -7,11 +7,18 @@ from vrtManager.connection import wvmConnect
class wvmStorages(wvmConnect):
def get_storages_info(self):
+ """Each pool's state, type, volume count, size and free space. An active
+ pool is refreshed first: libvirt measures its space only when it is
+ refreshed, so two hosts sharing a pool's storage told different free
+ space, each as of its own last refresh."""
get_storages = self.get_storages()
storages = []
for pool in get_storages:
stg = self.get_storage(pool)
stg_status = stg.isActive()
+ if stg_status:
+ with contextlib.suppress(libvirt.libvirtError): # refused while a volume is cloned in it
+ stg.refresh(0)
stg_type = util.get_xml_path(stg.XMLDesc(0), "/pool/@type")
stg_vol = len(stg.listVolumes()) if stg_status else None
_state, stg_size, _allocation, stg_free = stg.info()
diff --git a/vrtManager/tests/test_host_reads.py b/vrtManager/tests/test_host_reads.py
index f544fb28..137d4355 100644
--- a/vrtManager/tests/test_host_reads.py
+++ b/vrtManager/tests/test_host_reads.py
@@ -196,3 +196,24 @@ class StoragesInfoTestCase(unittest.TestCase):
conn.get_storage = lambda name: pool
(info,) = conn.get_storages_info()
self.assertEqual((info["name"], info["size"], info["free"]), ("default", 500, 120))
+ # measured now: libvirt keeps a pool's space as of its last refresh
+ self.assertEqual([c[0] for c in pool.mock_calls[:2]], ["isActive", "refresh"])
+
+ def test_a_pool_that_refuses_a_refresh_tells_its_last_space(self):
+ conn = connection(wvmStorages)
+ pool = MagicMock(**{"isActive.return_value": 1, "XMLDesc.return_value": " ",
+ "listVolumes.return_value": [], "info.return_value": [2, 500, 380, 120],
+ "refresh.side_effect": libvirtError("pool is busy: a volume is cloned")})
+ conn.get_storages = lambda: ["default"]
+ conn.get_storage = lambda name: pool
+ (info,) = conn.get_storages_info()
+ self.assertEqual(info["free"], 120)
+
+ def test_an_inactive_pool_is_not_refreshed(self):
+ conn = connection(wvmStorages)
+ pool = MagicMock(**{"isActive.return_value": 0, "XMLDesc.return_value": " ",
+ "info.return_value": [0, 0, 0, 0]})
+ conn.get_storages = lambda: ["off"]
+ conn.get_storage = lambda name: pool
+ conn.get_storages_info()
+ pool.refresh.assert_not_called()
|