diff --git a/instances/templates/instance.html b/instances/templates/instance.html index 246eb0e3..9121cc13 100644 --- a/instances/templates/instance.html +++ b/instances/templates/instance.html @@ -48,7 +48,7 @@
{% trans "Memory" %}
-
{{ instance.cur_memory }} {% trans "MB" %}
+
{{ instance.cur_memory }} MiB
{% trans "Disks" %}
@@ -263,6 +263,7 @@ diff --git a/instances/templates/instances/stats_tab.html b/instances/templates/instances/stats_tab.html index 75184047..12f3c68e 100644 --- a/instances/templates/instances/stats_tab.html +++ b/instances/templates/instances/stats_tab.html @@ -1,108 +1,113 @@ {% load i18n %} -{% load bootstrap_icons %} -
- +
-
- -
- -
-
-
-
-
{% bs_icon 'arrow-right' %} - {% trans "CPU Usage" %} -
-
-
-
-
+
+
+

+ {% if instance.status == 1 %} + {% trans "Updates every 5 seconds." %} + {% else %} + {% trans "The VM is not running: nothing is measured." %} + {% endif %} +

+ {% if request.user.is_superuser %} +
+ +
-
-
-
{% bs_icon 'arrow-right' %} - {% trans "Memory Usage" %} -
-
-
-
-
-
- {% for net in instance.networks %} -
-
-
{% bs_icon 'arrow-right' %} - {% trans "Bandwidth Device" %}: eth{{ forloop.counter0 }} -
-
-
-
-
-
- {% endfor %} - {% for disk in instance.disks %} -
-
-
{% bs_icon 'arrow-right' %} - {% trans "Disk I/O device" %}: {{ disk.dev }} -
-
-
-
-
-
- {% endfor %} -
+ {% endif %}
-
-
- - - - - - - - - - - - - - -
{% trans "Date" %}{% trans "User" %}{% trans "Message" %}{% trans "Result" %}
{% trans 'None' %}...
+
+
+
+
+
+

{% trans "CPU" %}

+ {% trans "of the host" %} +
+
+
+
+
+

{% trans "Memory" %}

+ +
+
+
+
+
+
+ {% for net in instance.networks %} +
+

{% trans "Network" %} · {{ net.mac }}

+

 

+
+
+ {% endfor %} + {% for disk in instance.disks %} +
+

{% trans "Disk" %} · {{ disk.dev }}

+

 

+
+
+ {% endfor %} +
+
-
+ {% if request.user.is_superuser %} +
+
+ + + + + + + + + + +
{% trans "Date" %}{% trans "User" %}{% trans "Message" %}{% trans "Result" %}
+
+

+ {% trans "The latest rows, newest first." %} + {% trans "All of this VM's logs" %} +

+
+ {% endif %}
+{% if request.user.is_superuser %} +{% endif %} diff --git a/instances/tests/test_stats_sample.py b/instances/tests/test_stats_sample.py index b24d4635..1c427ad7 100644 --- a/instances/tests/test_stats_sample.py +++ b/instances/tests/test_stats_sample.py @@ -40,7 +40,7 @@ class StatsViewTestCase(TestCase): self.proxy.usage.return_value = { "cpu": 12.345, "disks": [{"dev": "vda", "rd": 2_000_000, "wr": None}], - "nics": [{"dev": 0, "rx": 8_000_000, "tx": 0}], + "nics": [{"mac": "52:54:00:00:00:01", "rx": 8_000_000, "tx": 0}], "window": 9.96, "sample": SAMPLE, } @@ -58,7 +58,7 @@ class StatsViewTestCase(TestCase): self.assertIsNone(self.previous()) self.assertEqual((data["cpudata"], data["window"]), (12.3, 10.0)) self.assertEqual(data["blkdata"], [{"dev": "vda", "data": [2.0, None]}]) - self.assertEqual(data["netdata"], [{"dev": 0, "data": [8.0, 0.0]}]) + self.assertEqual(data["netdata"], [{"mac": "52:54:00:00:00:01", "data": [8.0, 0.0]}]) self.stats(sample=data["sample"]) self.assertEqual(self.previous(), SAMPLE) @@ -178,7 +178,9 @@ class StatsScriptTestCase(TestDriverVM, TestCase): memdata: {used: 1024, total: 1024}, // vdb was attached after the page loaded blkdata: [{dev: "vdb", data: [9, 9]}, {dev: "vda", data: [1.5, null]}], - netdata: [{dev: 0, data: [2, 3]}]}); + // a NIC is known by its MAC: one added later has no chart + netdata: [{mac: "52:54:00:00:00:99", data: [7, 7]}, + {mac: "52:54:00:00:00:31", data: [2, 3]}]}); poll.request(); poll.onData(answer(3, "token-1")); poll.request(); diff --git a/instances/tests/test_stats_tab.py b/instances/tests/test_stats_tab.py new file mode 100644 index 00000000..6f8320b9 --- /dev/null +++ b/instances/tests/test_stats_tab.py @@ -0,0 +1,47 @@ +"""The Stats tab: a chart for the CPU, the memory and each NIC and disk, and +the VM's log rows for superusers only, whose log view is the only one.""" + +from types import SimpleNamespace + +from django.template.loader import render_to_string +from django.test import RequestFactory, SimpleTestCase +from lxml import html + +UUID = "abcdabcd-0000-0000-0000-000000000000" + + +def tab(status=1, superuser=True): + request = RequestFactory().get("/") + request.user = SimpleNamespace(is_superuser=superuser) + instance = SimpleNamespace( + id=1, uuid=UUID, status=status, + networks=[{"mac": "52:54:00:00:00:01"}], disks=[{"dev": "vda"}, {"dev": "vdb"}], + ) + return html.fromstring(render_to_string("instances/stats_tab.html", {"instance": instance}, request)) + + +class StatsTabTests(SimpleTestCase): + def test_every_device_has_a_chart_and_a_reading(self): + doc = tab() + for name in ("cpu", "mem", "net52:54:00:00:00:01", "blkvda", "blkvdb"): + with self.subTest(name=name): + self.assertTrue(doc.xpath(f"//canvas[@id='{name}Chart']")) + self.assertTrue(doc.xpath(f"//*[@id='{name}Now']")) + self.assertIn("52:54:00:00:00:01", doc.text_content()) + + def test_only_a_superuser_gets_the_logs(self): + for superuser in (True, False): + with self.subTest(superuser=superuser): + doc = tab(superuser=superuser) + self.assertEqual(bool(doc.xpath("//button[@data-bs-target='#logs']")), superuser) + self.assertEqual(bool(doc.xpath("//table[@id='logs_table']")), superuser) + + def test_the_logs_link_to_all_of_the_vm_s_rows(self): + self.assertEqual(tab().xpath("//a[text()=\"All of this VM's logs\"]/@href"), [f"/admin/logs/?q={UUID}"]) + + def test_a_vm_that_is_not_running_says_nothing_is_measured(self): + for status, measured in ((1, True), (3, False), (5, False)): + with self.subTest(status=status): + text = tab(status=status).text_content() + self.assertEqual("Updates every 5 seconds." in text, measured) + self.assertEqual("nothing is measured" in text, not measured) diff --git a/instances/views.py b/instances/views.py index a443e94c..b6e91a33 100755 --- a/instances/views.py +++ b/instances/views.py @@ -431,7 +431,7 @@ def stats(request, pk): "memdata": mem_usage, # MB/s and Mbit/s "blkdata": [{"dev": disk["dev"], "data": [mega(disk["rd"]), mega(disk["wr"])]} for disk in usage["disks"]], - "netdata": [{"dev": nic["dev"], "data": [mega(nic["rx"]), mega(nic["tx"])]} for nic in usage["nics"]], + "netdata": [{"mac": nic["mac"], "data": [mega(nic["rx"]), mega(nic["tx"])]} for nic in usage["nics"]], "timeline": time.strftime("%H:%M:%S"), } if "sample" in usage: diff --git a/logs/tests/test_logs.py b/logs/tests/test_logs.py index d4a2889a..cbd2444c 100644 --- a/logs/tests/test_logs.py +++ b/logs/tests/test_logs.py @@ -5,6 +5,7 @@ from django.urls import reverse from computes.models import Compute from instances.models import Instance from logs.audit import write +from logs.views import VM_LOG_ROWS UUID = "12345678-1234-1234-1234-123456789012" @@ -25,6 +26,13 @@ class VMLogsTestCase(TestCase): self.assertEqual(response.status_code, 200) return [(row["message"], row["result"]) for row in response.json()] + def test_only_the_latest_rows(self): + for n in range(VM_LOG_ROWS + 1): + write("admin", "c1", "test-vm", f"VM test-vm: row {n}", True, uuid=UUID) + shown = self.shown() + self.assertEqual(len(shown), VM_LOG_ROWS) + self.assertEqual(shown[0][0], f"VM test-vm: row {VM_LOG_ROWS}") + def test_rows_of_the_vm_with_their_result(self): write("admin", "c1", "test-vm", "VM test-vm: power on", True, uuid=UUID) write("admin", "c1", "test-vm", "VM test-vm: power off: not running", False, uuid=UUID) diff --git a/logs/views.py b/logs/views.py index 4c848f34..6829561b 100644 --- a/logs/views.py +++ b/logs/views.py @@ -6,6 +6,9 @@ from admin.decorators import superuser_only from instances.models import Instance from logs.models import Logs +# the rows of a VM on its Stats tab; the Logs page has them all +VM_LOG_ROWS = 50 + @superuser_only def vm_logs(request, pk): @@ -13,7 +16,7 @@ def vm_logs(request, pk): vm = get_object_or_404(Instance, pk=pk) results = {None: _("Unknown"), True: _("Done"), False: _("Failed")} logs = [] - for log in Logs.objects.filter(instance_uuid=vm.uuid).order_by("-date", "-id"): + for log in Logs.objects.filter(instance_uuid=vm.uuid).order_by("-date", "-id")[:VM_LOG_ROWS]: logs.append({ "user": log.user, "host": log.host, diff --git a/vrtManager/instance.py b/vrtManager/instance.py index 24ffadc2..0b579612 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -1378,7 +1378,7 @@ class wvmInstance(wvmConnect): and target.get("dev") ): disks.append(target.get("dev")) # blockStats takes the target name - # all NICs, in the order of the page's charts; one without a target is not measured + # all NICs, known by their MAC; one without a target is not measured nics = [] for nic in tree.findall("devices/interface"): mac, target = nic.find("mac"), nic.find("target") @@ -1391,7 +1391,7 @@ class wvmInstance(wvmConnect): return { "cpu": 0, "disks": [{"dev": dev, "rd": 0, "wr": 0} for dev in disks], - "nics": [{"dev": i, "rx": 0, "tx": 0} for i in range(len(nics))], + "nics": [{"mac": mac, "rx": 0, "tx": 0} for mac, target in nics], } key = [self.instance.ID(), self.host_info[2], disks, nics] @@ -1418,8 +1418,8 @@ class wvmInstance(wvmConnect): for dev, old, new in zip(disks, before[1], after[1]) ], "nics": [ - {"dev": i, "rx": _rate(old[0], new[0], seconds, 8), "tx": _rate(old[1], new[1], seconds, 8)} - for i, (old, new) in enumerate(zip(before[2], after[2])) + {"mac": mac, "rx": _rate(old[0], new[0], seconds, 8), "tx": _rate(old[1], new[1], seconds, 8)} + for (mac, target), old, new in zip(nics, before[2], after[2]) ], "window": seconds, } @@ -1436,7 +1436,7 @@ class wvmInstance(wvmConnect): usage = rates(before, counters) or { "cpu": None, "disks": [{"dev": dev, "rd": None, "wr": None} for dev in disks], - "nics": [{"dev": i, "rx": None, "tx": None} for i in range(len(nics))], + "nics": [{"mac": mac, "rx": None, "tx": None} for mac, target in nics], "window": None, } usage["sample"] = {"key": key, "counters": counters} diff --git a/vrtManager/tests/test_usage.py b/vrtManager/tests/test_usage.py index 7077d03c..0ef44192 100644 --- a/vrtManager/tests/test_usage.py +++ b/vrtManager/tests/test_usage.py @@ -86,8 +86,8 @@ class UsageTestCase(unittest.TestCase): self.assertEqual( result["disks"], [{"dev": "vda", "rd": 1000, "wr": 3000}, {"dev": "vdb", "rd": 2000, "wr": 6000}] ) - # bits; the NIC without a target is not measured and keeps its place - self.assertEqual(result["nics"], [{"dev": 0, "rx": None, "tx": None}, {"dev": 1, "rx": 800, "tx": 400}]) + # bits; the NIC without a target is not measured + self.assertEqual(result["nics"], [{"mac": "52:54:00:00:00:01", "rx": None, "tx": None}, {"mac": "52:54:00:00:00:02", "rx": 800, "tx": 400}]) self.assertEqual(result["sample"]["counters"][3], 101.0) def test_since_the_previous_sample_without_waiting(self): @@ -98,7 +98,7 @@ class UsageTestCase(unittest.TestCase): self.time.sleep.assert_not_called() self.assertEqual((result["cpu"], result["window"]), (12.5, 9.5)) self.assertEqual(result["disks"][1], {"dev": "vdb", "rd": 2000, "wr": 6000}) # per second - self.assertEqual(result["nics"][1], {"dev": 1, "rx": 800, "tx": 400}) + self.assertEqual(result["nics"][1], {"mac": "52:54:00:00:00:02", "rx": 800, "tx": 400}) self.assertEqual(result["sample"]["counters"][3], 110.5) def test_a_sample_of_another_run_host_or_devices_is_not_used(self): @@ -169,7 +169,7 @@ class UsageTestCase(unittest.TestCase): { "cpu": 0, "disks": [{"dev": "vda", "rd": 0, "wr": 0}, {"dev": "vdb", "rd": 0, "wr": 0}], - "nics": [{"dev": 0, "rx": 0, "tx": 0}, {"dev": 1, "rx": 0, "tx": 0}], + "nics": [{"mac": "52:54:00:00:00:01", "rx": 0, "tx": 0}, {"mac": "52:54:00:00:00:02", "rx": 0, "tx": 0}], }, )