1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-10 04:06:47 +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

@ -8,9 +8,9 @@ from django.db import IntegrityError, transaction
from django.test import RequestFactory, TestCase
from computes.models import Compute
from logs.audit import audit, changed_fields, failed, request_ip, snapshot, write
from instances.models import Instance
from logs.audit import audit, audit_vm, changed_fields, failed, failed_vm, request_ip, snapshot, write
from logs.models import Logs
from logs.views import addlogmsg
from operations import claims
from vrtManager.util import OperationError
@ -62,12 +62,9 @@ class WriteTests(TestCase):
self.assertIsNone(request_ip(rf.get("/", REMOTE_ADDR="127.0.0.1", HTTP_X_REAL_IP="fe80::1%eth0")))
self.assertIsNone(request_ip(rf.get("/", REMOTE_ADDR="")))
def test_addlogmsg_still_writes(self):
addlogmsg("u", "h", "vm", "Destroy")
row = Logs.objects.get()
self.assertEqual((row.user, row.host, row.instance, row.message), ("u", "h", "vm", "Destroy"))
self.assertIsNone(row.success)
self.assertIsNone(row.ip)
def test_a_row_without_a_vm_has_no_uuid(self):
write("u", "h", "", "Network lab: start", True)
self.assertEqual(Logs.objects.get().instance_uuid, "")
class AuditTests(TestCase):
@ -135,6 +132,26 @@ class AuditTests(TestCase):
self.assertEqual(row.host, "-")
self.assertIs(row.success, False)
def test_vm_rows_name_the_vm_and_carry_its_uuid(self):
vm = Instance(compute=self.compute, name="web-1", uuid="11111111-2222-3333-4444-555555555555")
with audit_vm(self.request, vm, "power on"):
pass
with self.assertRaises(OperationError):
with audit_vm(self.request, vm, "power off"):
raise OperationError("not running")
failed_vm(self.request, vm, "reset root password", "gstfsd error: timed out")
self.assertEqual(
list(Logs.objects.order_by("id").values_list("host", "instance", "instance_uuid", "message", "success")), [
("c", "web-1", vm.uuid, "VM web-1: power on", True),
("c", "web-1", vm.uuid, "VM web-1: power off: not running", False),
("c", "web-1", vm.uuid, "VM web-1: reset root password: gstfsd error: timed out", False),
])
def test_the_block_may_set_the_uuid_it_learns(self):
with audit(self.request, self.compute, "VM web-1", "define from XML", instance="web-1") as entry:
entry.uuid = "11111111-2222-3333-4444-555555555555"
self.assertEqual(self.row().instance_uuid, "11111111-2222-3333-4444-555555555555")
def test_changed_fields(self):
compute = Compute.objects.create(name="c", hostname="192.0.2.5", type=1, password="pw")
before = snapshot(compute)

View file

@ -1,61 +1,60 @@
import json
from django.contrib.auth import get_user_model
from django.test import Client, RequestFactory, TestCase
from django.test import TestCase
from django.urls import reverse
from computes.models import Compute
from instances.models import Instance
from logs.models import Logs
from logs.views import addlogmsg
from logs.audit import write
UUID = "12345678-1234-1234-1234-123456789012"
class LogsTestCase(TestCase):
class VMLogsTestCase(TestCase):
"""The VM page's Logs tab: the rows of one VM, found by its UUID."""
def setUp(self):
User = get_user_model()
self.superuser = User.objects.create_superuser(
username="admin_logs", email="admin@example.com", password="adminpassword"
)
self.normal_user = User.objects.create_user(
username="normal_logs", email="user@example.com", password="userpassword"
)
self.compute = Compute.objects.create(
name="local_compute",
hostname="localhost",
type=1,
)
self.instance = Instance.objects.create(
compute=self.compute,
name="test-vm",
uuid="12345678-1234-1234-1234-123456789012",
)
self.superuser = User.objects.create_superuser("admin_logs", "admin@example.com", "pw")
self.normal_user = User.objects.create_user("normal_logs", "user@example.com", "pw")
self.compute = Compute.objects.create(name="c1", hostname="192.0.2.1", type=1)
self.instance = Instance.objects.create(compute=self.compute, name="test-vm", uuid=UUID)
self.client.force_login(self.superuser)
def test_addlogmsg(self):
addlogmsg("admin", "localhost", "test-vm", "Started VM")
log = Logs.objects.filter(instance="test-vm").first()
self.assertIsNotNone(log)
self.assertEqual(log.user, "admin")
self.assertEqual(log.host, "localhost")
self.assertEqual(log.message, "Started VM")
def test_vm_logs_superuser_access(self):
addlogmsg("admin", "localhost", "test-vm", "Created VM")
client = Client()
client.login(request=RequestFactory().get("/"), username="admin_logs", password="adminpassword")
response = client.get(reverse("vm_logs", args=["test-vm"]))
def shown(self, instance=None):
response = self.client.get(reverse("vm_logs", args=[(instance or self.instance).pk]))
self.assertEqual(response.status_code, 200)
return [(row["message"], row["result"]) for row in response.json()]
data = json.loads(response.content.decode("utf-8"))
self.assertIsInstance(data, list)
self.assertEqual(len(data), 1)
self.assertEqual(data[0]["user"], "admin")
self.assertEqual(data[0]["message"], "Created VM")
self.assertEqual(data[0]["instance"], "test-vm")
def test_rows_of_the_vm_with_their_result(self):
write("admin", "c1", "test-vm", "VM test-vm: power on", True, uuid=UUID)
write("admin", "c1", "test-vm", "VM test-vm: power off: not running", False, uuid=UUID)
row = self.client.get(reverse("vm_logs", args=[self.instance.pk])).json()[0]
self.assertEqual((row["user"], row["host"], row["instance"]), ("admin", "c1", "test-vm"))
self.assertEqual(self.shown(), [("VM test-vm: power off: not running", "Failed"),
("VM test-vm: power on", "Done")])
def test_vm_logs_non_superuser_forbidden(self):
client = Client()
client.login(request=RequestFactory().get("/"), username="normal_logs", password="userpassword")
def test_a_vm_is_found_by_uuid_not_by_name(self):
other = Compute.objects.create(name="c2", hostname="192.0.2.2", type=1)
namesake = Instance.objects.create(compute=other, name="test-vm", uuid="87654321-4321-4321-4321-210987654321")
write("admin", "c1", "old-name", "VM old-name: power on", True, uuid=UUID) # before a rename
write("admin", "c2", "test-vm", "VM test-vm: power on", True, uuid=namesake.uuid)
write("admin", "c1", "test-vm", "Instance is created") # a row from before the UUID column
self.assertEqual(self.shown(), [("VM old-name: power on", "Done")])
self.assertEqual(self.shown(namesake), [("VM test-vm: power on", "Done")])
response = client.get(reverse("vm_logs", args=["test-vm"]))
# superuser_only decorator redirects non-superusers to index or login
def test_one_vm_on_two_computes_shows_the_rows_of_both(self):
other = Compute.objects.create(name="c2", hostname="192.0.2.2", type=1)
moved = Instance.objects.create(compute=other, name="test-vm", uuid=UUID)
write("admin", "c1", "test-vm", "VM test-vm: migrate (live) to 192.0.2.2", True, uuid=UUID)
write("admin", "c2", "test-vm", "VM test-vm: power off", None, uuid=UUID)
self.assertEqual(self.shown(moved), [("VM test-vm: power off", "Unknown"),
("VM test-vm: migrate (live) to 192.0.2.2", "Done")])
self.assertEqual(self.shown(moved), self.shown())
def test_unknown_vm(self):
self.assertEqual(self.client.get(reverse("vm_logs", args=[999999])).status_code, 404)
def test_non_superuser_forbidden(self):
self.client.force_login(self.normal_user)
response = self.client.get(reverse("vm_logs", args=[self.instance.pk]))
self.assertNotEqual(response.status_code, 200)