mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 11:46:49 +00:00
A host the panel could not reach made its overview the 500 page, without the host's sections, and the Computes list gave such a host an Overview button that led nowhere. The page now keeps the host's name, sections, "Edit connection" and "Remove from panel" (the existing confirmation), and says "Not connected" with the reason and a "Try again". Details that cannot be read are said in an alert; the VMs, capacity and load still show, as they are read on their own. A reachable host's page leads with what it runs and what it gave: - its VMs by state (running, shut off, paused, other), linked to Instances; - the vCPUs, memory and storage pools given, as on the Computes list, which now shares those parts (computes/capacity_*.html) and their preparation (views.shown_capacity) with it; - CPU and memory in use, side by side, with the current reading in words, a note on what the charts hold and when they were last updated, or since when not; memory in GB like the rest of the page; - Hardware and Software and connection: logical CPUs and clock from virNodeGetInfo, but physical cores, sockets and NUMA nodes counted from the CPUs of the capabilities' NUMA cells, as virNodeGetInfo makes a topology up for hosts it cannot describe (asymmetric NUMA); the hypervisor's own name and version, libvirt, emulator, the guest architectures as text instead of menus of dead links. A host without sysinfo shows no processor instead of failing. Empty notes no longer show "None"; labels stack over their values on a phone instead of running into them. Pools that cannot be read no longer drop a host's vCPUs and memory from its capacity: the storage part says so. get_capacity also counts the VMs by state.
278 lines
10 KiB
Python
278 lines
10 KiB
Python
import json
|
|
|
|
from admin.decorators import superuser_only
|
|
from django.conf import settings
|
|
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 django.utils.translation import gettext_lazy as _
|
|
from instances.models import Instance
|
|
from libvirt import libvirtError
|
|
from logs.audit import audit, changed_fields, snapshot
|
|
from operations import claims
|
|
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")
|
|
|
|
|
|
def shown_capacity(compute):
|
|
"""The compute's capacity, with what its bars show; None when it cannot
|
|
be read."""
|
|
given = compute.capacity
|
|
if given:
|
|
# the memory bar: running and shut-off VMs' memory, and a mark at the
|
|
# host's memory, on a scale that holds whichever is larger
|
|
assigned, memory = given["assigned"], given["memory"]
|
|
scale = max(assigned["all"], memory) or 1
|
|
given["bar"] = {"running": assigned["running"] * 100 / scale,
|
|
"stopped": (assigned["all"] - assigned["running"]) * 100 / scale,
|
|
"host": memory * 100 / scale, "over": assigned["all"] > memory}
|
|
given["memory_percent"] = round(assigned["all"] * 100 / memory) if memory else 0
|
|
given["vm_count"] = sum(given["vms"].values())
|
|
# the default pool (else the first) shows; the others (a host may
|
|
# have dozens) open on a click
|
|
pools = given["pools"] or []
|
|
for pool in pools:
|
|
pool["used_percent"] = (pool["size"] - pool["free"]) * 100 / pool["size"] if pool["size"] else 0
|
|
given["pool"] = pools[0] if pools else None
|
|
given["more_pools"] = pools[1:]
|
|
return given
|
|
|
|
|
|
@superuser_only
|
|
def computes(request):
|
|
"""The computes, with what each has given its VMs."""
|
|
computes = list(Compute.objects.order_by("name"))
|
|
for compute in computes:
|
|
compute.shown_capacity = shown_capacity(compute)
|
|
return render(request, "computes/list.html", {"computes": computes})
|
|
|
|
|
|
@superuser_only
|
|
def capacity(request, compute_id):
|
|
"""One line of what the compute has given its VMs, as plain text, for the
|
|
new-VM dialog."""
|
|
compute = get_object_or_404(Compute, pk=compute_id)
|
|
return render(request, "computes/capacity_line.txt", {"given": compute.capacity}, content_type="text/plain; charset=utf-8")
|
|
|
|
|
|
@superuser_only
|
|
def overview(request, compute_id):
|
|
"""The host's page: whether the panel reaches it, its VMs, what it has
|
|
given them, its load, hardware and software. A host it cannot reach
|
|
keeps the page, with the reason and its connection to edit."""
|
|
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
|
|
context = {"compute": compute, "connected": compute.status is True}
|
|
if context["connected"]:
|
|
try:
|
|
conn = wvmHostDetails(compute.hostname, compute.login, compute.password, compute.type)
|
|
hostname, arch, memory, _cpus, processor, uri = conn.get_node_info()
|
|
context["host"] = {
|
|
"hostname": hostname, "arch": arch, "memory": memory, "processor": processor, "uri": uri,
|
|
"topology": conn.get_cpu_topology(),
|
|
"hypervisor": conn.wvm.getType(), "hypervisor_version": conn.get_version(),
|
|
"libvirt_version": conn.get_lib_version(),
|
|
"emulator": conn.get_emulator(arch), "guest_archs": conn.get_hypervisors_domain_types(),
|
|
}
|
|
conn.close()
|
|
except libvirtError as error:
|
|
context["read_error"] = str(error)
|
|
context["given"] = shown_capacity(compute)
|
|
return render(request, "overview.html", context)
|
|
|
|
|
|
@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, "job_resource": claims.compute_key(compute.id)},
|
|
)
|
|
|
|
|
|
@superuser_only
|
|
def compute_create(request, FormClass):
|
|
form = FormClass(request.POST or None)
|
|
if form.is_valid():
|
|
compute = form.instance
|
|
with audit(request, compute, f"Compute {compute.name} ({compute.hostname})", "add"):
|
|
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
|
|
|
|
subject = f"Compute {compute.name} ({compute.hostname})"
|
|
before = snapshot(compute)
|
|
form = FormClass(request.POST or None, instance=compute)
|
|
if form.is_valid():
|
|
with audit(request, compute, subject, "change") as entry:
|
|
form.save()
|
|
fields = changed_fields(before, snapshot(compute))
|
|
entry.action = "change " + ", ".join(fields)
|
|
entry.skip = not fields
|
|
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":
|
|
with audit(request, compute, f"Compute {compute.name} ({compute.hostname})", "delete"):
|
|
compute.delete()
|
|
return redirect("computes")
|
|
|
|
return render(
|
|
request,
|
|
"common/confirm_delete.html",
|
|
{"object": compute,
|
|
"consequence": _("Its VMs keep running on the host and leave the panel; their owners are kept for "
|
|
"%(days)s days on the Removed VMs page and come back if the host is added again.")
|
|
% {"days": getattr(settings, "INSTANCE_OWNERSHIP_RETENTION_DAYS", 30)}},
|
|
)
|
|
|
|
|
|
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")
|
|
|
|
|
|
class ComputeManager:
|
|
def __init__(self, compute_id):
|
|
self.compute = get_object_or_404(Compute, pk=compute_id)
|
|
|
|
def get_machine_types(self, arch):
|
|
data = dict()
|
|
try:
|
|
data["machines"] = self.compute.proxy.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.compute.proxy.get_disk_device_types(arch, machine)
|
|
|
|
if disk in disk_device_types:
|
|
if disk == "disk":
|
|
data["bus"] = sorted(self.compute.proxy.get_disk_bus_types(arch, machine))
|
|
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)
|