mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
- The VM list with no VMs no longer also says that no VM matches the search and the state. - The New VM dialog with no compute yet says to add the first host, with a link to the Computes page (and links there when none is reachable). - The new VM page on a host without a running storage pool or a network says what to add or start and where, instead of "You haven't defined any storage pools" (also when pools exist but are stopped) and "network pools".
160 lines
7.7 KiB
Python
160 lines
7.7 KiB
Python
"""The new-VM dialog's CPU bars and capacity lines: the page carries each
|
|
compute's URLs and reads neither itself; its script fills them when the
|
|
dialog opens, two reads at a time, and ignores answers of an earlier
|
|
opening."""
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import unittest
|
|
from unittest.mock import PropertyMock, patch
|
|
|
|
from computes.models import Compute
|
|
from django.conf import settings
|
|
from django.contrib.auth import get_user_model
|
|
from django.test import TestCase
|
|
from django.urls import reverse
|
|
from lxml import html
|
|
|
|
RUN = """
|
|
const vm = require("vm");
|
|
function element(cls) {
|
|
const attrs = {};
|
|
return {style: {}, textContent: "", attrs, classList: {contains: (c) => c === cls},
|
|
setAttribute: (k, v) => { attrs[k] = String(v); }, removeAttribute: (k) => { delete attrs[k]; }};
|
|
}
|
|
const cells = CPU_URLS.map((url) => {
|
|
const cell = element("cpu-usage"), meter = element(), bar = element(), label = element();
|
|
cell.dataset = {url};
|
|
cell.querySelector = (sel) => ({".progress": meter, ".progress-bar": bar})[sel] || label;
|
|
Object.assign(cell, {meter, bar, label});
|
|
return cell;
|
|
});
|
|
const lines = LINE_URLS.map((url) => Object.assign(element("compute-capacity"), {dataset: {url}}));
|
|
const handlers = {}, requests = [];
|
|
const dialog = {addEventListener: (name, fn) => { handlers[name] = fn; },
|
|
querySelectorAll: (sel) => sel === ".cpu-usage" ? cells : cells.flatMap((c, i) => [c, lines[i]])};
|
|
const page = {
|
|
document: {getElementById: () => dialog},
|
|
$: {get: (url) => {
|
|
const r = {url, done: (f) => { r.ok = f; return r; }, fail: (f) => { r.err = f; return r; },
|
|
always: (f) => { r.end = f; return r; }};
|
|
requests.push(r);
|
|
return r;
|
|
}},
|
|
window: {location: {}},
|
|
};
|
|
vm.runInNewContext(SCRIPT, vm.createContext(page));
|
|
const state = () => [cells.map((c) => [c.bar.style.width, c.meter.attrs["aria-valuenow"] || null, c.label.textContent]),
|
|
lines.map((l) => l.textContent)];
|
|
const answer = (i, data) => { const r = requests[i]; data === null ? r.err() : r.ok(data); r.end(); };
|
|
const out = {};
|
|
handlers["show.bs.modal"]();
|
|
out.loading = state();
|
|
handlers["hide.bs.modal"]();
|
|
answer(0, {cpudata: 5});
|
|
answer(1, "stale");
|
|
out.hidden = [state(), requests.length];
|
|
handlers["show.bs.modal"]();
|
|
answer(2, {cpudata: 41.6});
|
|
answer(3, "\\nGiven 1.\\n");
|
|
answer(4, null);
|
|
answer(5, null);
|
|
out.filled = state();
|
|
out.all = requests.map((r) => r.url);
|
|
console.log(JSON.stringify(out));
|
|
"""
|
|
|
|
|
|
class CreateDialogCpuTestCase(TestCase):
|
|
def setUp(self):
|
|
self.client.force_login(get_user_model().objects.create_superuser("cd-admin", "cd@example.com", "pw"))
|
|
self.computes = [
|
|
Compute.objects.create(name=f"cd{i}", hostname=f"192.0.2.{i}", login="u", password="p", type=1)
|
|
for i in (1, 2, 3)
|
|
]
|
|
patches = [
|
|
patch.object(Compute, name, new_callable=PropertyMock, return_value=value)
|
|
for name, value in (("status", True), ("cpu_count", 1), ("ram_size", 1024), ("ram_usage", 0))
|
|
]
|
|
patches += [patch("instances.views.utils.refr"), patch("computes.models.wvmHostDetails")]
|
|
for p in patches:
|
|
self.details = p.start() # the last one: the host connection
|
|
self.addCleanup(p.stop)
|
|
|
|
def page(self):
|
|
response = self.client.get(reverse("instances:index"))
|
|
self.assertEqual(response.status_code, 200)
|
|
return response.content.decode()
|
|
|
|
def test_the_page_carries_the_statistics_urls_and_reads_no_cpu_usage(self):
|
|
page = self.page()
|
|
urls = html.fromstring(page).xpath("//*[contains(@class, 'cpu-usage ')]/@data-url")
|
|
self.assertEqual(urls, [reverse("compute_graph", args=[c.id]) for c in self.computes])
|
|
self.details.return_value.get_cpu_usage.assert_not_called()
|
|
|
|
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
|
def test_the_dialog_reads_host_by_host_two_at_a_time(self):
|
|
page = self.page()
|
|
self.assertIn("/static/js/new_vm_dialog.js", html.fromstring(page).xpath("//script/@src"))
|
|
script = (settings.BASE_DIR / "static/js/new_vm_dialog.js").read_text()
|
|
cpu = [reverse("compute_graph", args=[c.id]) for c in self.computes]
|
|
line = [reverse("compute_capacity", args=[c.id]) for c in self.computes]
|
|
js = RUN.replace("SCRIPT", json.dumps(script)).replace("CPU_URLS", json.dumps(cpu)).replace("LINE_URLS", json.dumps(line))
|
|
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)
|
|
waiting = [[["0%", None, "…"]] * 3, ["", "", ""]]
|
|
self.assertEqual(result["loading"], waiting)
|
|
# answers that come after the dialog closed change nothing and read no more
|
|
self.assertEqual(result["hidden"], [waiting, 2])
|
|
# a failed CPU read is a dash, a failed line stays empty
|
|
self.assertEqual(result["filled"], [[["42%", "42", "42%"], ["0%", None, "—"], ["0%", None, "…"]],
|
|
["Given 1.", "", ""]])
|
|
# the CPU and the line of one host, then the next host; two at a time
|
|
self.assertEqual(result["all"], [cpu[0], line[0], cpu[0], line[0], cpu[1], line[1], cpu[2], line[2]])
|
|
|
|
def test_each_host_carries_its_capacity_line_url(self):
|
|
urls = html.fromstring(self.page()).xpath("//*[@id='AddInstance']//*[contains(@class, 'compute-capacity')]/@data-url")
|
|
self.assertEqual(urls, [reverse("compute_capacity", args=[c.id]) for c in self.computes])
|
|
|
|
|
|
class CreateDialogHostsTestCase(TestCase):
|
|
"""The dialog lists the reachable computes; without one it says so."""
|
|
|
|
def setUp(self):
|
|
self.client.force_login(get_user_model().objects.create_superuser("cd-admin", "cd@example.com", "pw"))
|
|
for i in (1, 2):
|
|
Compute.objects.create(name=f"cd{i}", hostname=f"192.0.2.{i}", login="u", password="p", type=1)
|
|
for name, value in (("cpu_count", 1), ("ram_size", 1024), ("ram_usage", 0)):
|
|
p = patch.object(Compute, name, new_callable=PropertyMock, return_value=value)
|
|
p.start()
|
|
self.addCleanup(p.stop)
|
|
p = patch("instances.views.utils.refr")
|
|
p.start()
|
|
self.addCleanup(p.stop)
|
|
|
|
def dialog(self, reachable):
|
|
with patch.object(Compute, "status", property(lambda compute: compute.name in reachable)):
|
|
response = self.client.get(reverse("instances:index"))
|
|
return html.fromstring(response.content).get_element_by_id("AddInstance")
|
|
|
|
def test_only_reachable_computes_are_listed(self):
|
|
dialog = self.dialog({"cd2"})
|
|
self.assertEqual([e.text for e in dialog.xpath(".//a/div/div[1]")], ["cd2"])
|
|
self.assertNotIn("No compute is reachable", dialog.text_content())
|
|
|
|
def test_no_reachable_compute_is_said(self):
|
|
dialog = self.dialog(set())
|
|
self.assertEqual(dialog.xpath(".//a[contains(@class, 'list-group-item')]"), [])
|
|
self.assertIn("No compute is reachable: see the Computes page.", dialog.text_content())
|
|
|
|
def test_a_panel_without_computes_says_to_add_one(self):
|
|
# the first visit: the dialog and the list lead to the Computes page, and no filter is said to hide VMs
|
|
Compute.objects.all().delete()
|
|
response = self.client.get(reverse("instances:index"))
|
|
dialog = html.fromstring(response.content).get_element_by_id("AddInstance")
|
|
self.assertIn("There is no compute yet: add the first host on the Computes page.", dialog.text_content())
|
|
self.assertEqual(dialog.xpath(".//p/a/@href"), [reverse("computes")])
|
|
self.assertContains(response, "There are no VMs here yet.")
|
|
self.assertNotContains(response, "No VM matches the search")
|