mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
CodeQL flags the <script> regex (py/bad-tag-filter); the HTML parser finds the inline scripts as the new-VM dialog test does.
167 lines
8.4 KiB
Python
167 lines
8.4 KiB
Python
"""The host CPU graph: usage is taken between the sample of the graph's
|
|
previous answer and now, without waiting, when that sample is recent and
|
|
unaltered; otherwise over one second as before."""
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import unittest
|
|
from unittest import TestCase as UnitTestCase
|
|
from unittest.mock import MagicMock, PropertyMock, patch
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.core import signing
|
|
from django.test import TestCase
|
|
from django.urls import reverse
|
|
from libvirt import libvirtError
|
|
from lxml import html
|
|
|
|
from computes.models import Compute
|
|
from computes.views import CPU_SAMPLE_MAX_AGE, CPU_SAMPLE_SIGNER
|
|
from vrtManager.hostdetails import cpu_percent, wvmHostDetails
|
|
|
|
|
|
def host(*readings):
|
|
"""A wvmHostDetails whose getCPUStats returns readings, (idle, busy) each."""
|
|
h = wvmHostDetails.__new__(wvmHostDetails)
|
|
h.wvm = MagicMock()
|
|
h.wvm.getCPUStats.side_effect = [
|
|
{"kernel": busy // 2, "user": busy - busy // 2, "idle": idle, "iowait": 0, "guest": busy // 3, "utilization": 99}
|
|
for idle, busy in readings
|
|
]
|
|
return h
|
|
|
|
|
|
class CpuPercentTestCase(UnitTestCase):
|
|
def test_busy_share_between_two_samples(self):
|
|
self.assertEqual(cpu_percent((100, 200), (130, 300)), 70.0)
|
|
self.assertEqual(cpu_percent((0, 0), (1, 3)), 66.7)
|
|
|
|
def test_samples_that_do_not_follow_on(self):
|
|
for before, after in (((100, 200), (100, 200)), ((100, 200), (50, 100)), ((100, 200), (200, 250))):
|
|
with self.subTest(before=before, after=after):
|
|
self.assertIsNone(cpu_percent(before, after))
|
|
|
|
|
|
@patch("vrtManager.hostdetails.time")
|
|
class CpuUsageTestCase(UnitTestCase):
|
|
def test_since_a_previous_sample_without_waiting(self, clock):
|
|
clock.time.return_value = 105.0
|
|
h = host((130, 70))
|
|
usage = h.get_cpu_usage(previous=(100, 100, 100.0))
|
|
clock.sleep.assert_not_called()
|
|
# total = kernel + user + idle + iowait; guest (inside user) and utilization are left out
|
|
self.assertEqual(usage, {"usage": 70.0, "sample": (130, 200, 105.0), "window": 5.0})
|
|
|
|
def test_over_one_second_without_a_sample(self, clock):
|
|
clock.time.side_effect = [10.0, 11.0]
|
|
h = host((100, 100), (110, 190))
|
|
self.assertEqual(h.get_cpu_usage(), {"usage": 90.0, "sample": (110, 300, 11.0), "window": 1.0})
|
|
clock.sleep.assert_called_once_with(1)
|
|
|
|
def test_over_one_second_after_the_host_restarted(self, clock):
|
|
clock.time.side_effect = [10.0, 11.0]
|
|
h = host((5, 5), (6, 9))
|
|
usage = h.get_cpu_usage(previous=(1000, 5000, 5.0))
|
|
clock.sleep.assert_called_once_with(1)
|
|
self.assertEqual((usage["usage"], usage["window"]), (80.0, 1.0))
|
|
|
|
|
|
class GraphTestCase(TestCase):
|
|
def setUp(self):
|
|
self.compute = Compute.objects.create(name="graph", hostname="192.0.2.1", login="u", password="p", type=1)
|
|
self.other = Compute.objects.create(name="other", hostname="192.0.2.2", login="u", password="p", type=1)
|
|
admin = get_user_model().objects.create_superuser("graph-admin", "graph@example.com", "pw")
|
|
self.client.force_login(admin)
|
|
for target in ("computes.views.wvmConnect", "computes.views.wvmHostDetails"):
|
|
patcher = patch(target)
|
|
self.addCleanup(patcher.stop)
|
|
self.conn = patcher.start().return_value # the graph reads through wvmHostDetails
|
|
self.conn.get_memory_usage.return_value = {"total": 4, "usage": 1, "percent": 25}
|
|
self.conn.get_cpu_usage.return_value = {"usage": 12.5, "sample": (10, 20, 100.0), "window": 5.04}
|
|
|
|
def graph(self, compute=None, **params):
|
|
response = self.client.get(reverse("compute_graph", args=[(compute or self.compute).id]), params)
|
|
self.assertEqual(response.status_code, 200)
|
|
return json.loads(response.content)
|
|
|
|
def previous(self):
|
|
return self.conn.get_cpu_usage.call_args.kwargs["previous"]
|
|
|
|
def test_the_answer_carries_the_sample_for_the_next_poll(self):
|
|
data = self.graph()
|
|
self.assertIsNone(self.previous())
|
|
self.assertEqual((data["cpudata"], data["window"], data["memdata"]["percent"]), (12.5, 5.0, 25))
|
|
self.graph(cpu_sample=data["cpu_sample"])
|
|
self.assertEqual(self.previous(), (10, 20, 100.0))
|
|
|
|
def test_an_altered_old_or_foreign_sample_is_not_used(self):
|
|
token = self.graph()["cpu_sample"]
|
|
foreign = self.graph(self.other)["cpu_sample"]
|
|
altered = CPU_SAMPLE_SIGNER.sign_object({"compute": self.compute.id, "idle": 0, "total": 1, "time": 0})[:-2] + "xx"
|
|
for name, sample in (("altered", altered), ("garbage", "x"), ("other compute", foreign)):
|
|
with self.subTest(name):
|
|
self.graph(cpu_sample=sample)
|
|
self.assertIsNone(self.previous())
|
|
with patch("django.core.signing.time.time", return_value=signing.time.time() + CPU_SAMPLE_MAX_AGE + 1):
|
|
self.graph(cpu_sample=token)
|
|
self.assertIsNone(self.previous())
|
|
|
|
def test_an_unreachable_host_gives_no_cpu_value_and_no_sample(self):
|
|
self.conn.get_cpu_usage.side_effect = libvirtError("unreachable")
|
|
data = self.graph()
|
|
self.assertEqual((data["cpudata"], data["memdata"]["total"]), (None, None))
|
|
self.assertNotIn("cpu_sample", data)
|
|
|
|
|
|
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
|
class GraphScriptTestCase(TestCase):
|
|
"""The overview page's script sends each answer's sample with the next poll."""
|
|
|
|
def test_the_next_poll_sends_the_previous_sample(self):
|
|
self.client.force_login(get_user_model().objects.create_superuser("gs-admin", "gs@example.com", "pw"))
|
|
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
|
|
):
|
|
details.return_value.get_node_info.return_value = ("h", "x86_64", 1024, 2, "cpu", "qemu+tcp://h/system")
|
|
details.return_value.get_memory_usage.return_value = {"total": 1024, "usage": 512, "percent": 50}
|
|
page = self.client.get(reverse("overview", args=[compute.id])).content.decode()
|
|
script = next(s for s in html.fromstring(page).xpath("//script[not(@src)]/text()") if "pollWhileVisible(" in s)
|
|
js = """
|
|
const vm = require("vm");
|
|
const charts = [], requests = [];
|
|
let poll;
|
|
const page = {
|
|
document: {getElementById: () => ({getContext: () => ({})})},
|
|
// Chart.js adds the lists a config leaves out
|
|
Chart: function (ctx, config) {
|
|
this.data = Object.assign({labels: []}, config.data);
|
|
this.data.datasets.forEach((set) => { set.data = set.data || []; });
|
|
this.options = config.options;
|
|
this.update = () => {};
|
|
charts.push(this);
|
|
},
|
|
$: {getJSON: (url, params) => { requests.push(params); return {}; }},
|
|
pollWhileVisible: (interval, request, onData) => { poll = {request, onData}; },
|
|
};
|
|
vm.runInNewContext(SCRIPT, vm.createContext(page));
|
|
const answer = (cpu, sample) => ({cpudata: cpu, window: 5.1, cpu_sample: sample, timeline: "t",
|
|
memdata: {total: 1048576, usage: 0}});
|
|
poll.request();
|
|
poll.onData(answer(3, "token-1"));
|
|
poll.request();
|
|
poll.onData({cpudata: null, memdata: {total: null, usage: null}, timeline: "t"});
|
|
poll.request();
|
|
const label = charts[0].options.tooltips.callbacks.label({datasetIndex: 0, index: 0, yLabel: 3}, charts[0].data);
|
|
console.log(JSON.stringify({requests, label, cpu: charts[0].data.datasets[0].data,
|
|
mem: charts[1].data.datasets[0].data, memMax: charts[1].options.scales.yAxes[0].ticks.max}));
|
|
""".replace("SCRIPT", json.dumps(script))
|
|
out = subprocess.run(["node", "-e", js], capture_output=True, text=True, timeout=30)
|
|
self.assertEqual(out.returncode, 0, out.stderr)
|
|
result = json.loads(out.stdout)
|
|
self.assertEqual(result["requests"], [{}, {"cpu_sample": "token-1"}, {}])
|
|
self.assertEqual(result["cpu"], [3, None])
|
|
# a host that could not be read leaves a gap and keeps the memory axis
|
|
self.assertEqual((result["mem"], result["memMax"]), ([0, None], 1))
|
|
self.assertTrue(result["label"].endswith(": 3 % (average over 5.1 s)"), result["label"])
|