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 6abea495f1 fix(logs): failures name their job, VM and user; the new VM form shows a list it could not load
- A failed cancel hook logged "worker task cancel failed": it now names
  the job, its kind, resource and user, with the traceback for a fault
  of the panel.
- novncd logged "Console connection failed" without the VM or the user.
- django-axes wrote four INFO lines a login ("Reset 0 access
  attempts"...); logins are on the Logs page, so it logs warnings only.
- The new VM form's machine types and disk buses answered 200 with an
  empty object when libvirt failed, and nothing was logged: the list
  stayed empty, the form could not go on and said nothing. They answer
  400 with the error, as the pool's volume list does, the warning is
  logged with the compute, and the form's lists show an error they get
  with the page's messages.
2026-11-06 23:48:44 +03:00

320 lines
12 KiB
Python

import json
import logging
from admin.decorators import superuser_only
from django.conf import settings
from django.core import signing
from django.http import HttpResponse, JsonResponse
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 django.views.decorators.debug import sensitive_post_parameters
from instances.models import Instance
from instances.utils import read_list_rows
from libvirt import libvirtError
from logs.audit import audit, changed_fields, snapshot
from operations import claims
from vrtManager.connection import CONN_SOCKET, CONN_SSH, CONN_TCP, CONN_TLS
from vrtManager.hostdetails import wvmHostDetails
from vrtManager.storage import wvmStorages
from computes.forms import (
SocketComputeForm,
SshComputeForm,
TcpComputeForm,
TlsComputeForm,
)
from computes.models import Compute
from . import utils
logger = logging.getLogger(__name__)
# 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, on a scale that
# holds the host's memory or all given, whichever is larger
assigned, memory = given["assigned"], given["memory"]
assigned["stopped"] = assigned["all"] - assigned["running"]
scale = max(assigned["all"], memory) or 1
given["bar"] = {"running": assigned["running"] * 100 / scale, "stopped": assigned["stopped"] * 100 / scale,
"over": assigned["all"] > memory}
given["memory_percent"] = round(assigned["all"] * 100 / memory) if memory else 0
# the vCPU bar likewise: running and shut-off VMs' vCPUs against the CPUs
vcpus, cpus = given["vcpus"], given["cpus"]
vcpus["stopped"] = vcpus["all"] - vcpus["running"]
cpu_scale = max(vcpus["all"], cpus) or 1
given["cpu_bar"] = {"running": vcpus["running"] * 100 / cpu_scale, "stopped": vcpus["stopped"] * 100 / cpu_scale}
given["vm_count"] = sum(given["vms"].values())
return given
def host_pools(compute):
"""The compute's active storage pools, measured now: the default one (else
the first) to show, the others (a host may have dozens) to open on a
click; or None when they cannot be read."""
try:
storages = wvmStorages(compute.hostname, compute.login, compute.password, compute.type)
pools = [pool for pool in storages.get_storages_info() if pool["status"]]
except libvirtError:
return None
pools.sort(key=lambda pool: (pool["name"] != "default", pool["name"]))
return {"pool": pools[0] if pools else None, "more_pools": pools[1:]}
# how the panel reaches a compute, by Compute.type
CONNECTIONS = {CONN_TCP: "TCP", CONN_SSH: "SSH", CONN_TLS: "TLS", CONN_SOCKET: "Socket"}
@superuser_only
def computes(request):
"""The computes, with what each has given its VMs, and the totals of the
ones the panel reaches."""
computes = list(Compute.objects.order_by("name"))
totals = {"hosts": len(computes), "connected": 0, "vms": 0, "running": 0,
"vcpus": 0, "cpus": 0, "assigned": 0, "memory": 0}
for compute in computes:
compute.connection_name = CONNECTIONS.get(compute.type, "")
compute.shown_capacity = given = shown_capacity(compute)
totals["connected"] += compute.status is True
if given:
totals["vms"] += given["vm_count"]
totals["running"] += given["vms"]["running"]
totals["vcpus"] += given["vcpus"]["all"]
totals["cpus"] += given["cpus"]
totals["assigned"] += given["assigned"]["all"]
totals["memory"] += given["memory"]
return render(request, "computes/list.html", {"computes": computes, "totals": totals})
@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()
topology = conn.get_cpu_topology()
context["host"] = {
"hostname": hostname, "arch": arch, "memory": memory, "processor": processor, "uri": uri,
"topology": topology, "ghz": round(topology["mhz"] / 1000, 1),
"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)
context["storage"] = host_pools(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 = list(Instance.objects.filter(compute=compute).order_by("name").prefetch_related("userinstance_set"))
read_list_rows(instances)
return render(
request, "computes/instances.html",
{"compute": compute, "instances": instances, "job_resource": claims.compute_key(compute.id),
"states_url": reverse("instances:states") + f"?compute={compute.id}"},
)
@sensitive_post_parameters()
@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})
@sensitive_post_parameters()
@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)
try:
return HttpResponse(comp_mgr.get_disk_buses(arch, machine, disk), content_type="application/json")
except libvirtError as err:
return read_failed(comp_mgr.compute, "disk buses", err)
def get_compute_machine_types(request, compute_id, arch):
"""
:param request:
:param compute_id:
:param arch:
:return:
"""
comp_mgr = ComputeManager(compute_id)
try:
return HttpResponse(comp_mgr.get_machine_types(arch), content_type="application/json")
except libvirtError as err:
return read_failed(comp_mgr.compute, "machine types", err)
def read_failed(compute, what, err):
"""A list the new VM form loads that the host did not give: not 200 with
an empty list, which the form cannot tell from none."""
logger.warning("Reading the %s of compute %s failed: %s", what, compute.name, err)
return JsonResponse({"error": str(err)}, status=400)
class ComputeManager:
def __init__(self, compute_id):
self.compute = get_object_or_404(Compute, pk=compute_id)
def get_machine_types(self, arch):
return json.dumps({"machines": self.compute.proxy.get_machine_types(arch)})
def get_disk_buses(self, arch, machine, disk):
data = dict()
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"]
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)