mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 11:46:49 +00:00
feat(instances): a VM's stats in one grid, its logs a switch away
The Stats tab stacked one large card per chart, so a VM with a few devices made a long page, and its "Real Time" and "Logs" sub-tabs were shown to every user although only a superuser may read the logs. The owner chose the layout from rendered options: a state line, CPU and memory side by side, one chart per NIC and disk below, each with its latest reading next to its title, and a small Graphs/Logs switch for superusers. Codex reviewed the tab; this is fixed: - Memory is shown in MiB (it was labelled MB), and its axis grows to the RSS when the RSS passes the balloon size instead of clipping it. - The CPU reading says it is of the host's CPUs. - A NIC is titled by its MAC, not an invented "eth0", and its readings are matched to its chart by the MAC: they were matched by position, so removing a NIC while the page was open moved the next one's readings into its chart. - A VM that is not running says nothing is measured. - The logs load whenever the Logs view is shown, by keyboard too; an empty answer says "None" and a failed one says so, instead of an empty table. The tab shows the latest 50 rows, with a link to all of the VM's rows on the Logs page. - The VM's header shows memory in MiB; it said MB.
This commit is contained in:
parent
b72e9b1442
commit
5e93920d78
9 changed files with 193 additions and 110 deletions
|
|
@ -48,7 +48,7 @@
|
|||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<dt class="fw-normal text-body-secondary">{% trans "Memory" %}</dt>
|
||||
<dd class="mb-0">{{ instance.cur_memory }} {% trans "MB" %}</dd>
|
||||
<dd class="mb-0">{{ instance.cur_memory }} MiB</dd>
|
||||
</div>
|
||||
<div class="col-6 col-lg-5">
|
||||
<dt class="fw-normal text-body-secondary">{% trans "Disks" %}</dt>
|
||||
|
|
@ -263,6 +263,7 @@
|
|||
<script src="{% static 'js/poll.js' %}"></script>
|
||||
<script>
|
||||
{% trans "average over" as average_over %}
|
||||
{% trans "In" as rx_label %}{% trans "Out" as tx_label %}{% trans "Read" as rd_label %}{% trans "Write" as wr_label %}
|
||||
var stats_tab = document.querySelector('#graphics-tab');
|
||||
var stats_poll = null;
|
||||
// the seconds each CPU point averages
|
||||
|
|
@ -270,6 +271,10 @@
|
|||
// the usage sample of the previous answer: the next one averages since then
|
||||
var statsSample = null;
|
||||
// The charts are polled while the Stats tab is shown; they are made once.
|
||||
// a reading for the line above a chart: one decimal, a dash where it is missing
|
||||
function reading(value) {
|
||||
return value == null ? "–" : String(Math.round(value * 10) / 10);
|
||||
}
|
||||
stats_tab.addEventListener('hidden.bs.tab', function (event) {
|
||||
stats_poll.stop();
|
||||
});
|
||||
|
|
@ -285,14 +290,15 @@
|
|||
return span ? text + ' ({{ average_over|escapejs }} ' + span + ' s)' : text;
|
||||
},
|
||||
});
|
||||
var memChart = lineChart(document.getElementById("memChart"), "MB", ["Usage (RSS)"]);
|
||||
var memChart = lineChart(document.getElementById("memChart"), "MiB", ["Usage (RSS)"]);
|
||||
var diskChart = {};
|
||||
{% for disk in instance.disks %}
|
||||
diskChart['{{ disk.dev|escapejs }}'] = lineChart(document.getElementById("blk{{ disk.dev|escapejs }}Chart"), "MB/s", ["Read", "Write"]);
|
||||
{% endfor %}
|
||||
// two NICs with one MAC (a misconfiguration) share the first one's chart
|
||||
var netChart = {};
|
||||
{% for net in instance.networks %}
|
||||
netChart[{{ forloop.counter0 }}] = lineChart(document.getElementById("netEth{{ forloop.counter0 }}Chart"), "Mbit/s", ["Inbound", "Outbound"]);
|
||||
netChart['{{ net.mac|escapejs }}'] = netChart['{{ net.mac|escapejs }}'] || lineChart(document.getElementById("net{{ net.mac|escapejs }}Chart"), "Mbit/s", ["Inbound", "Outbound"]);
|
||||
{% endfor %}
|
||||
|
||||
// the first point at once, then every 5 s like the host graph
|
||||
|
|
@ -308,21 +314,33 @@
|
|||
cpuWindows.shift();
|
||||
}
|
||||
addPoint(cpuChart, data.timeline, [data.cpudata]);
|
||||
// a VM whose memory could not be read gives total 0: a gap, same axis
|
||||
document.getElementById("cpuNow").textContent = reading(data.cpudata) + " %";
|
||||
// a VM whose memory could not be read gives total 0: a gap, same axis;
|
||||
// the RSS may pass the balloon size: then the axis grows to it
|
||||
if (data.memdata.total) {
|
||||
memChart.options.scales.y.max = Math.round(data.memdata.total / 1024);
|
||||
addPoint(memChart, data.timeline, [Math.round(data.memdata.used / 1024)]);
|
||||
var used = Math.round(data.memdata.used / 1024), total = Math.round(data.memdata.total / 1024);
|
||||
memChart.options.scales.y.max = Math.max(total, used);
|
||||
addPoint(memChart, data.timeline, [used]);
|
||||
document.getElementById("memNow").textContent = used + " / " + total + " MiB";
|
||||
} else {
|
||||
addPoint(memChart, data.timeline, [null]);
|
||||
document.getElementById("memNow").textContent = "";
|
||||
}
|
||||
// a disk or NIC added after the page loaded has no chart until a reload
|
||||
function push(chart, values) {
|
||||
// a disk or NIC added after the page loaded has no chart until a reload,
|
||||
// and one removed keeps its last points
|
||||
function push(chart, id, labels, unit, values) {
|
||||
if (chart) {
|
||||
addPoint(chart, data.timeline, values);
|
||||
document.getElementById(id).textContent =
|
||||
labels[0] + " " + reading(values[0]) + " · " + labels[1] + " " + reading(values[1]) + " " + unit;
|
||||
}
|
||||
}
|
||||
data.blkdata.forEach(function (disk) { push(diskChart[disk.dev], disk.data); });
|
||||
data.netdata.forEach(function (nic) { push(netChart[nic.dev], nic.data); });
|
||||
data.blkdata.forEach(function (disk) {
|
||||
push(diskChart[disk.dev], "blk" + disk.dev + "Now", ["{{ rd_label|escapejs }}", "{{ wr_label|escapejs }}"], "MB/s", disk.data);
|
||||
});
|
||||
data.netdata.forEach(function (nic) {
|
||||
push(netChart[nic.mac], "net" + nic.mac + "Now", ["{{ rx_label|escapejs }}", "{{ tx_label|escapejs }}"], "Mbit/s", nic.data);
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,108 +1,113 @@
|
|||
{% load i18n %}
|
||||
{% load bootstrap_icons %}
|
||||
<div class="tab-pane" id="graphics" role="tabpanel" >
|
||||
<!-- Nav tabs -->
|
||||
<div class="tab-pane" id="graphics" role="tabpanel">
|
||||
<div class="card">
|
||||
<div class="card-header bg-transparent pb-0">
|
||||
<ul class="nav nav-underline small" role="tablist" aria-label="Instance graphs and logs menu">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#graphs" type="button" role="tab" aria-controls="graphs" aria-selected="true">
|
||||
{% trans "Real Time" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#logs" type="button" role="tab" aria-controls="logs" aria-selected="false" onclick="update_logs_table();">
|
||||
{% trans "Logs" %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content card-body">
|
||||
<div role="tabpanel" class="tab-pane active" id="graphs">
|
||||
<div class="mb-3 card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">{% bs_icon 'arrow-right' %}
|
||||
{% trans "CPU Usage" %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart position-relative"><canvas id="cpuChart" role="img" aria-label="{% trans "CPU Usage" %}"></canvas></div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mb-3">
|
||||
<p class="small text-body-secondary mb-0 me-auto">
|
||||
{% if instance.status == 1 %}
|
||||
{% trans "Updates every 5 seconds." %}
|
||||
{% else %}
|
||||
{% trans "The VM is not running: nothing is measured." %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if request.user.is_superuser %}
|
||||
<div class="btn-group btn-group-sm" role="tablist" aria-label="{% trans "Stats view" %}">
|
||||
<button class="btn btn-outline-secondary active" type="button" data-bs-toggle="tab" data-bs-target="#graphs" role="tab" aria-controls="graphs" aria-selected="true">{% trans "Graphs" %}</button>
|
||||
<button class="btn btn-outline-secondary" id="logs-tab" type="button" data-bs-toggle="tab" data-bs-target="#logs" role="tab" aria-controls="logs" aria-selected="false">{% trans "Logs" %}</button>
|
||||
</div>
|
||||
<div class="mb-3 card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">{% bs_icon 'arrow-right' %}
|
||||
{% trans "Memory Usage" %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart position-relative"><canvas id="memChart" role="img" aria-label="{% trans "Memory Usage" %}"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
{% for net in instance.networks %}
|
||||
<div class="mb-3 card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">{% bs_icon 'arrow-right' %}
|
||||
{% trans "Bandwidth Device" %}: eth{{ forloop.counter0 }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart position-relative"><canvas id="netEth{{ forloop.counter0 }}Chart" role="img" aria-label="{% trans "Bandwidth Device" %}: eth{{ forloop.counter0 }}"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for disk in instance.disks %}
|
||||
<div class="mb-3 card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">{% bs_icon 'arrow-right' %}
|
||||
{% trans "Disk I/O device" %}: {{ disk.dev }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart position-relative"><canvas id="blk{{ disk.dev }}Chart" role="img" aria-label="{% trans "Disk I/O device" %}: {{ disk.dev }}"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="logs">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped align-middle" id="logs_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Date" %}</th>
|
||||
<th scope="col">{% trans "User" %}</th>
|
||||
<th scope="col">{% trans "Message" %}</th>
|
||||
<th scope="col">{% trans "Result" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
<tr>
|
||||
<td colspan="4"><i>{% trans 'None' %}...</i></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="graphs" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<section class="col-12 col-md-6">
|
||||
<div class="d-flex flex-wrap align-items-baseline gap-2 mb-2">
|
||||
<h2 class="h6 mb-0">{% trans "CPU" %}</h2>
|
||||
<span><span id="cpuNow"></span> <span class="small text-body-secondary">{% trans "of the host" %}</span></span>
|
||||
</div>
|
||||
<div class="chart position-relative"><canvas id="cpuChart" role="img" aria-label="{% trans "CPU use, recent readings" %}"></canvas></div>
|
||||
</section>
|
||||
<section class="col-12 col-md-6">
|
||||
<div class="d-flex flex-wrap align-items-baseline gap-2 mb-2">
|
||||
<h2 class="h6 mb-0">{% trans "Memory" %}</h2>
|
||||
<span id="memNow"></span>
|
||||
</div>
|
||||
<div class="chart position-relative"><canvas id="memChart" role="img" aria-label="{% trans "Memory use, recent readings" %}"></canvas></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="border-top mt-3 pt-3">
|
||||
<div class="row g-3">
|
||||
{% for net in instance.networks %}
|
||||
<section class="col-12 col-md-6 col-lg-4">
|
||||
<h2 class="h6 mb-1">{% trans "Network" %} · {{ net.mac }}</h2>
|
||||
<p class="small text-body-secondary mb-2" id="net{{ net.mac }}Now"> </p>
|
||||
<div class="chart position-relative"><canvas id="net{{ net.mac }}Chart" role="img" aria-label="{% blocktrans with mac=net.mac %}Network {{ mac }}, recent readings{% endblocktrans %}"></canvas></div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% for disk in instance.disks %}
|
||||
<section class="col-12 col-md-6 col-lg-4">
|
||||
<h2 class="h6 mb-1">{% trans "Disk" %} · {{ disk.dev }}</h2>
|
||||
<p class="small text-body-secondary mb-2" id="blk{{ disk.dev }}Now"> </p>
|
||||
<div class="chart position-relative"><canvas id="blk{{ disk.dev }}Chart" role="img" aria-label="{% blocktrans with dev=disk.dev %}Disk {{ dev }}, recent readings{% endblocktrans %}"></canvas></div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% if request.user.is_superuser %}
|
||||
<div class="tab-pane" id="logs" role="tabpanel">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0" id="logs_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Date" %}</th>
|
||||
<th scope="col">{% trans "User" %}</th>
|
||||
<th scope="col">{% trans "Message" %}</th>
|
||||
<th scope="col">{% trans "Result" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="small text-body-secondary mt-2 mb-0">
|
||||
{% trans "The latest rows, newest first." %}
|
||||
<a href="{% url 'admin:logs' %}?q={{ instance.uuid|urlencode }}">{% trans "All of this VM's logs" %}</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if request.user.is_superuser %}
|
||||
<script>
|
||||
function update_logs_table() {
|
||||
{% trans "None" as no_rows %}
|
||||
{% trans "The logs could not be loaded." as logs_failed %}
|
||||
// a click or the arrow keys: the rows of the moment each time Logs is shown
|
||||
document.getElementById("logs-tab").addEventListener("shown.bs.tab", function () {
|
||||
var tbody = $("#logs_table > tbody");
|
||||
function note(text) {
|
||||
tbody.empty().append($("<tr>").append($("<td colspan='4' class='text-body-secondary'>").text(text)));
|
||||
}
|
||||
$.getJSON("{% url 'vm_logs' instance.id %}", function(data) {
|
||||
if (!data.length) {
|
||||
note("{{ no_rows|escapejs }}");
|
||||
return;
|
||||
}
|
||||
// Log messages contain user-entered names (snapshots, disks, clones);
|
||||
// insert them as text, never as HTML.
|
||||
var tbody = $("#logs_table > tbody").empty();
|
||||
tbody.empty();
|
||||
$.each(data, function(id, row) {
|
||||
$("<tr>")
|
||||
.append($("<td>").css("width", "150px").text(row['date']))
|
||||
.append($("<td>").addClass("text-nowrap").text(row['date']))
|
||||
.append($("<td>").text(row['user']))
|
||||
.append($("<td>").text(row['message']))
|
||||
.append($("<td>").text(row['result']))
|
||||
.appendTo(tbody);
|
||||
});
|
||||
}).fail(function () {
|
||||
note("{{ logs_failed|escapejs }}");
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
47
instances/tests/test_stats_tab.py
Normal file
47
instances/tests/test_stats_tab.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}],
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue