1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/logs/audit.py
catborise 491819ed64 feat(vrtManager): the libvirt layer's messages show in the reader's language
vrtManager's errors (some 140: a disk that does not move, a network
address outside its subnet, a host device given to another VM, the
disks a migration cannot copy) were English f-strings, so no catalog
could translate them, on pages or in jobs.

They are marked texts with their values now; a variant that differs by
a word (internal or external, running or persistent, with a subnet or
without) is a whole message of its own, and a list of problems is a
list of messages. OperationError reads in the active language, so the
pages that show one translate it as they are; its args stay English, and
so do the logs and the audit log. Host device problems and the Migrate
tab's reasons a disk is not copied are messages too.
2026-11-03 00:05:34 +03:00

121 lines
4.5 KiB
Python

import ipaddress
import logging
from contextlib import contextmanager
from dataclasses import dataclass
import libvirt
from django.db import transaction
from accounts.lockout import client_ip
from logs.models import Logs
logger = logging.getLogger("logs.audit")
def write(user, host, instance, message, success=None, ip=None, uuid=""):
"""
Save one Logs row. A failure is written to the log file instead and never
raised, so an audit write cannot break the action it records.
"""
user, host, instance, uuid = user[:150], host[:255], instance[:120], uuid[:36]
try:
with transaction.atomic():
Logs.objects.create(user=user, host=host, instance=instance, instance_uuid=uuid, message=message,
success=success, ip=ip)
except Exception as e:
try:
line = f"audit row not saved ({e!r}): {user} {host} {instance} {uuid} success={success} ip={ip} {message}"
logger.warning(line.replace("\r", "\\r").replace("\n", "\\n"))
except Exception:
pass
def request_ip(request):
"""The client address of the request, or None if it is empty or not a plain IP address."""
value = client_ip(request)
try:
ipaddress.ip_address(value)
except ValueError:
return None
# a scoped IPv6 address (fe80::1%eth0) is valid for ipaddress but not for GenericIPAddressField
return None if "%" in value else value
@dataclass
class Entry:
action: str
skip: bool = False
uuid: str = ""
def host_of(compute):
"""The host column for a Compute, or for a host name already at hand."""
if compute is None:
return "-"
return compute if isinstance(compute, str) else compute.name
def reason(exc, user):
"""Why an action failed, as text for the audit row; unknown errors give only their class name."""
from operations import claims
from operations.utils import busy_message, safe_text
from vrtManager.util import OperationError
if isinstance(exc, claims.Busy):
return busy_message(exc, user)
if isinstance(exc, OperationError):
return exc.args[0] # English: str() is the reader's language
if isinstance(exc, libvirt.libvirtError):
return safe_text(exc.get_error_message() or str(exc))[:500]
return type(exc).__name__
@contextmanager
def audit(request, compute, subject, action, instance="", uuid=""):
"""
Write one row when the block ends: done, or failed with the reason when it
raises (the exception goes on). The block may set entry.action, entry.uuid,
or entry.skip when nothing was done.
"""
entry = Entry(action, uuid=uuid)
user, host, ip = request.user.get_username(), host_of(compute), request_ip(request)
try:
yield entry
except Exception as exc:
write(user, host, instance, f"{subject}: {entry.action}: {reason(exc, request.user)}", False, ip, entry.uuid)
raise
if not entry.skip:
write(user, host, instance, f"{subject}: {entry.action}", True, ip, entry.uuid)
def failed(request, compute, subject, action, why, instance="", uuid=""):
"""Write the failure row of an action that was refused before it started."""
from vrtManager.util import OperationError
if isinstance(why, OperationError):
why = why.args[0] # English: str() is the reader's language
write(request.user.get_username(), host_of(compute), instance, f"{subject}: {action}: {why}", False,
request_ip(request), uuid)
def audit_vm(request, instance, action):
"""audit() for an operation on a VM: the row names the VM and carries its UUID."""
return audit(request, instance.compute, f"VM {instance.name}", action, instance=instance.name, uuid=instance.uuid)
def failed_vm(request, instance, action, why):
"""failed() for an operation on a VM, such as one whose error came back as a reply."""
failed(request, instance.compute, f"VM {instance.name}", action, why, instance=instance.name, uuid=instance.uuid)
def snapshot(obj):
"""The field values of a model object, for changed_fields; a many-to-many field gives its related pks."""
values = {f.attname: getattr(obj, f.attname) for f in obj._meta.concrete_fields}
for f in obj._meta.many_to_many:
values[f.name] = frozenset(o.pk for o in getattr(obj, f.name).all()) if obj.pk else frozenset()
return values
def changed_fields(before, after):
"""The sorted names of the fields whose values differ between two snapshots."""
return sorted(key.removesuffix("_id") for key in before if before[key] != after[key])