1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/computes/views.py
catborise 27f4845086 perf(computes): the host CPU graph no longer waits a second per poll
Each poll of a host's overview graph held a worker for one second between
two CPU samples. The answer now carries its sample, signed with the time
and the compute; the next poll sends it back and the usage is taken since
then, so a point is the average since the previous one (normally 5 s, at
most 15 s). Without a sample, or with one that is altered, older or from
another host, or when the host restarted, it is measured over one second
as before.

A sample is one reading; the busy share was mixed from separate readings,
counted guest time twice (it is part of user) and added half a percent.
A host that cannot be read leaves a gap in both graphs instead of a zero
CPU point and a broken memory axis.
2026-10-09 09:53:12 +03:00

265 lines
7.5 KiB
Python

import json
from admin.decorators import superuser_only
from django.core import signing
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from instances.models import Instance
from libvirt import libvirtError
from vrtManager.connection import wvmConnect
from vrtManager.hostdetails import wvmHostDetails
from computes.forms import (
SocketComputeForm,
SshComputeForm,
TcpComputeForm,
TlsComputeForm,
)
from computes.models import Compute
from . import utils
# The host CPU graph sends back the CPU sample of its previous answer; usage
# is taken since then when it is this recent, otherwise over one second.
CPU_SAMPLE_MAX_AGE = 15
CPU_SAMPLE_SIGNER = signing.TimestampSigner(salt="computes.cpu-sample")
@superuser_only
def computes(request):
"""
:param request:
:return:
"""
computes = Compute.objects.filter().order_by("name")
return render(request, "computes/list.html", {"computes": computes})
@superuser_only
def overview(request, compute_id):
compute = get_object_or_404(Compute, pk=compute_id)
# the connection itself, not a TCP probe: ssh aliases and ProxyJump hosts
# are not reachable by name and port from here
status = "true" if compute.status else "false"
conn = wvmHostDetails(
compute.hostname,
compute.login,
compute.password,
compute.type,
)
(
hostname,
host_arch,
host_memory,
logical_cpu,
model_cpu,
uri_conn,
) = conn.get_node_info()
hypervisor = conn.get_hypervisors_domain_types()
mem_usage = conn.get_memory_usage()
emulator = conn.get_emulator(host_arch)
version = conn.get_version()
lib_version = conn.get_lib_version()
conn.close()
return render(request, "overview.html", locals())
@superuser_only
def instances(request, compute_id):
compute = get_object_or_404(Compute, pk=compute_id)
utils.refresh_instance_database(compute)
instances = Instance.objects.filter(compute=compute).prefetch_related(
"userinstance_set"
)
return render(
request, "computes/instances.html", {"compute": compute, "instances": instances}
)
@superuser_only
def compute_create(request, FormClass):
form = FormClass(request.POST or None)
if form.is_valid():
form.save()
return redirect(reverse("computes"))
return render(request, "computes/form.html", {"form": form})
@superuser_only
def compute_update(request, compute_id):
compute = get_object_or_404(Compute, pk=compute_id)
if compute.type == 1:
FormClass = TcpComputeForm
elif compute.type == 2:
FormClass = SshComputeForm
elif compute.type == 3:
FormClass = TlsComputeForm
elif compute.type == 4:
FormClass = SocketComputeForm
form = FormClass(request.POST or None, instance=compute)
if form.is_valid():
form.save()
return redirect(reverse("computes"))
return render(request, "computes/form.html", {"form": form})
@superuser_only
def compute_delete(request, compute_id):
compute = get_object_or_404(Compute, pk=compute_id)
if request.method == "POST":
compute.delete()
return redirect("computes")
return render(
request,
"common/confirm_delete.html",
{"object": compute},
)
def compute_graph(request, compute_id):
"""
:param request:
:param compute_id:
:return:
"""
comp_mgr = ComputeManager(compute_id)
data = comp_mgr.compute_graph(request.GET.get("cpu_sample"))
return HttpResponse(data, content_type="application/json")
def get_compute_disk_buses(request, compute_id, arch, machine, disk):
"""
:param request:
:param compute_id:
:param arch:
:param machine:
:param disk:
:return:
"""
comp_mgr = ComputeManager(compute_id)
return HttpResponse(comp_mgr.get_disk_buses(arch, machine, disk), content_type="application/json")
def get_compute_machine_types(request, compute_id, arch):
"""
:param request:
:param compute_id:
:param arch:
:return:
"""
comp_mgr = ComputeManager(compute_id)
return HttpResponse(comp_mgr.get_machine_types(arch), content_type="application/json")
def get_dom_capabilities(request, compute_id, arch, machine):
"""
:param request:
:param compute_id:
:param arch:
:param machine:
:return:
"""
comp_mgr = ComputeManager(compute_id)
return HttpResponse(comp_mgr.get_dom_capabilities(arch, machine), content_type="application/json")
class ComputeManager:
def __init__(self, compute_id):
self.compute = get_object_or_404(Compute, pk=compute_id)
self.conn = wvmConnect(
self.compute.hostname,
self.compute.login,
self.compute.password,
self.compute.type,
)
def get_dom_capabilities(self, arch, machine):
data = dict()
try:
data["videos"] = self.conn.get_disk_device_types(arch, machine)
data["bus"] = self.conn.get_disk_device_types(arch, machine)
except libvirtError:
pass
return json.dumps(data)
def get_machine_types(self, arch):
data = dict()
try:
data["machines"] = self.conn.get_machine_types(arch)
except libvirtError:
pass
return json.dumps(data)
def get_disk_buses(self, arch, machine, disk):
data = dict()
try:
disk_device_types = self.conn.get_disk_device_types(arch, machine)
if disk in disk_device_types:
if disk == "disk":
data["bus"] = sorted(disk_device_types)
elif disk == "cdrom":
data["bus"] = ["ide", "sata", "scsi"]
elif disk == "floppy":
data["bus"] = ["fdc"]
elif disk == "lun":
data["bus"] = ["scsi"]
except libvirtError:
pass
return json.dumps(data)
def _cpu_sample(self, token):
"""The (idle, total, time) sample in a token of this compute's graph,
or None when there is none, it is altered or too old."""
if not token:
return None
try:
data = CPU_SAMPLE_SIGNER.unsign_object(token, max_age=CPU_SAMPLE_MAX_AGE)
except signing.BadSignature:
return None
if data.get("compute") != self.compute.id:
return None
return data["idle"], data["total"], data["time"]
def compute_graph(self, cpu_sample=None):
result = {"cpudata": None, "memdata": {"total": None, "usage": None}, "timeline": timezone.now().strftime("%H:%M:%S")}
try:
conn = wvmHostDetails(
self.compute.hostname,
self.compute.login,
self.compute.password,
self.compute.type,
)
cpu = conn.get_cpu_usage(previous=self._cpu_sample(cpu_sample))
idle, total, taken = cpu["sample"]
result = {
"cpudata": cpu["usage"],
"memdata": conn.get_memory_usage(),
"timeline": timezone.now().strftime("%H:%M:%S"),
"window": round(cpu["window"], 1),
"cpu_sample": CPU_SAMPLE_SIGNER.sign_object(
{"compute": self.compute.id, "idle": idle, "total": total, "time": taken}
),
}
conn.close()
except libvirtError:
pass
return json.dumps(result)