1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 12:46:49 +00:00

feat(logs): every VM operation writes an audit row the VM's Logs tab finds by UUID

Operations on a VM now write their rows the way the admin audit does: one
row per operation, done or failed with a safe reason, with the user, IP and
host. Each row names the VM ("VM web-1: delete snapshot snap1") and carries
its UUID in the new Logs.instance_uuid column.

- audit_vm and failed_vm wrap audit() and failed() for a VM. A failure the
  view turns into a message is caught outside the block, so its failed row
  is written; busy claims, permission and quota refusals and invalid input
  write nothing. No password, key or XML goes into a row.
- All VM page operations move from addlogmsg to audit_vm, and addlogmsg
  goes. QoS changes and defining a VM from XML are logged now; a gstfsd
  error reply (root password, SSH key) is a failed row, not a done one.
- Partial results are rows of their own: the VM delete, then each disk
  delete; the disk detach, then the volume delete; the migration, then a
  failed autostart restore, NVRAM removal or record on the target.
- Migration rows use the compute name as the host column, as every other
  row does.
- VM job rows (create, clone, snapshots) and their resolutions carry the
  VM's UUID; snapshot job intents now record it. The owner rows of the VM
  page and the accounts pages, and the ownership system rows, carry it too.
- The Logs tab loads vm_logs/<pk>/ and shows the rows with the VM's UUID,
  so a rename keeps the history, two VMs with one name are told apart and
  a VM on two computes is one VM. It gains a Result column. Rows from
  before this change have no UUID and stay on the Logs page only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
catborise 2026-10-24 14:05:11 +03:00
parent 49cf16ecbe
commit c1d36053d6
24 changed files with 639 additions and 524 deletions

View file

@ -61,11 +61,12 @@ class AdminAccountRowTests(TestCase):
self.client.post(url, {"is_vnc": "on"}) # no change
self.client.post(reverse("accounts:user_instance_delete", args=[ui.pk]))
self.assertEqual(rows(), [
("VM vm owner bob: add", True),
("VM vm owner bob: change is_vnc", True),
("VM vm owner bob: remove", True),
("VM vm: add owner bob", True),
("VM vm: change owner bob: is_vnc", True),
("VM vm: remove owner bob", True),
])
self.assertEqual(set(Logs.objects.values_list("user", "host", "instance")), {("audit-admin", "c1", "vm")})
self.assertEqual(set(Logs.objects.values_list("user", "host", "instance", "instance_uuid")),
{("audit-admin", "c1", "vm", self.vm.uuid)})
def test_otp_mail_without_an_email(self):
from smtplib import SMTPException

View file

@ -19,7 +19,7 @@ from django.views.decorators.http import require_POST
from accounts.apps import admin_password_path
from instances.models import Instance
from logs.audit import audit, changed_fields, failed, snapshot
from logs.audit import audit, audit_vm, changed_fields, failed, snapshot
from accounts.forms import EmailOTPForm, ProfileForm, UserSSHKeyForm
from accounts.models import User, UserAttributes, UserInstance, UserSSHKey
@ -146,8 +146,7 @@ def user_instance_create(request, user_id):
form = forms.UserInstanceForm(request.POST or None, initial={"user": user})
if form.is_valid():
instance = form.cleaned_data["instance"]
subject = f"VM {instance.name} owner {form.cleaned_data['user'].username}"
with audit(request, instance.compute, subject, "add", instance=instance.name):
with audit_vm(request, instance, f"add owner {form.cleaned_data['user'].username}"):
form.save()
return redirect(reverse("accounts:account", args=[user.id]))
@ -167,11 +166,10 @@ def user_instance_update(request, pk):
instance, before = user_instance.instance, snapshot(user_instance)
form = forms.UserInstanceForm(request.POST or None, instance=user_instance)
if form.is_valid():
subject = f"VM {instance.name} owner {user_instance.user.username}"
with audit(request, instance.compute, subject, "change", instance=instance.name) as entry:
with audit_vm(request, instance, "change owner") as entry:
form.save()
fields = changed_fields(before, snapshot(user_instance))
entry.action = "change " + ", ".join(fields)
entry.action = f"change owner {user_instance.user.username}: " + ", ".join(fields)
entry.skip = not fields
return redirect(reverse("accounts:account", args=[user_instance.user.id]))
@ -190,8 +188,7 @@ def user_instance_delete(request, pk):
user_instance = get_object_or_404(UserInstance, pk=pk)
if request.method == "POST":
user, instance = user_instance.user, user_instance.instance
with audit(request, instance.compute, f"VM {instance.name} owner {user.username}", "remove",
instance=instance.name):
with audit_vm(request, instance, f"remove owner {user.username}"):
user_instance.delete()
next_url = request.GET.get("next", None)
if next_url and url_has_allowed_host_and_scheme(