1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/computes/tests/test_ownership.py
catborise 37b87839c1 perf(computes): a VM list load writes nothing while the hosts match the database
Every VM list load reconciles each compute with libvirt. Even when nothing
had changed, that took the compute's claims and a transaction with
SELECT FOR UPDATE (and the ownership advisory lock on PostgreSQL): about
11 queries a compute, 4 of them writes. On SQLite each load queued behind
every other write of the app.

The reconcile now first compares libvirt's VM list with the database,
without a claim or a transaction, and stops when they match and no
removed record has expired. Otherwise it runs as before, reading libvirt
again under its claims, so two reconciles still cannot commit in reverse
order. With two matching hosts a list load goes from 38 queries (8
writes) to 18 (none).
2026-11-04 20:54:07 +03:00

198 lines
8.8 KiB
Python

"""Ownership survives a VM's temporary disappearance or an out-of-band move."""
from datetime import timedelta
from unittest.mock import MagicMock
from accounts.models import UserInstance
from computes.models import Compute
from computes.utils import refresh_instance_database
from django.contrib.auth import get_user_model
from django.test import TestCase, override_settings
from django.urls import reverse
from django.utils import timezone
from instances.models import Instance, InstanceTombstone
from logs.models import Logs
from operations import claims
from operations.models import Claim
from rest_framework.test import APIClient
UUID = "44444444-4444-4444-4444-444444444444"
class Domain:
def __init__(self, name, uuid):
self._name, self._uuid = name, uuid
def name(self):
return self._name
def UUIDString(self):
return self._uuid
class OwnershipTestCase(TestCase):
def setUp(self):
self.owner = get_user_model().objects.create_user("owner", password="pw")
self.host_a = self.compute("host-a")
self.host_b = self.compute("host-b")
def compute(self, name):
compute = Compute.objects.create(name=name, hostname=f"{name}.example", login="", password="", type=1)
compute.__dict__["proxy"] = MagicMock()
compute.__dict__["status"] = True # reachable
self.on(compute, [])
return compute
def on(self, compute, domains):
compute.proxy.wvm.listAllDomains.return_value = [Domain(n, u) for n, u in domains]
def owned_vm(self, compute):
vm = Instance.objects.create(compute=compute, name="vm", uuid=UUID, is_template=True)
UserInstance.objects.create(instance=vm, user=self.owner, is_change=True, is_vnc=True)
return vm
def ownership(self, compute):
vm = Instance.objects.get(compute=compute, uuid=UUID)
return vm.is_template, [(ui.user, ui.is_change, ui.is_delete, ui.is_vnc) for ui in vm.userinstance_set.all()]
def rows(self):
return list(Logs.objects.order_by("id").values_list("user", "host", "message"))
def test_vm_that_disappears_and_returns_keeps_its_owners(self):
self.owned_vm(self.host_a)
refresh_instance_database(self.host_a) # undefined for a while
self.assertFalse(Instance.objects.filter(uuid=UUID).exists())
self.on(self.host_a, [("vm", UUID)]) # defined again
refresh_instance_database(self.host_a)
self.assertEqual(self.ownership(self.host_a), (True, [(self.owner, True, False, True)]))
self.assertFalse(InstanceTombstone.objects.exists())
self.assertEqual(self.rows(), [
("-", "host-a", f"VM vm ({UUID}): owners kept on the Removed VMs page"),
("-", "host-a", f"VM vm ({UUID}): owners restored"),
])
def test_an_ownerless_vm_leaves_a_row(self):
Instance.objects.create(compute=self.host_a, name="vm", uuid=UUID)
refresh_instance_database(self.host_a)
self.assertEqual(self.rows(), [("-", "host-a", f"VM vm ({UUID}): left the panel (no owners)")])
def test_move_seen_on_the_new_host_first(self):
self.owned_vm(self.host_a)
self.on(self.host_a, [("vm", UUID)])
self.on(self.host_b, [("vm", UUID)])
refresh_instance_database(self.host_b) # still listed on A: a copy for now
self.on(self.host_a, [])
refresh_instance_database(self.host_a) # gone from A: it moved to B
self.assertEqual(self.ownership(self.host_b), (True, [(self.owner, True, False, True)]))
self.assertFalse(Instance.objects.filter(compute=self.host_a).exists())
self.assertEqual(self.rows(), [("-", "host-a", f"VM vm ({UUID}): owners moved to host-b")])
def test_move_seen_on_the_old_host_first(self):
self.owned_vm(self.host_a)
refresh_instance_database(self.host_a)
self.on(self.host_b, [("vm", UUID)])
refresh_instance_database(self.host_b)
self.assertEqual(self.ownership(self.host_b), (True, [(self.owner, True, False, True)]))
def test_copied_uuid_grants_nothing(self):
self.owned_vm(self.host_a)
self.on(self.host_a, [("vm", UUID)])
self.on(self.host_b, [("vm-copy", UUID)])
refresh_instance_database(self.host_a)
refresh_instance_database(self.host_b)
self.assertEqual(self.ownership(self.host_a), (True, [(self.owner, True, False, True)]))
self.assertEqual(self.ownership(self.host_b), (False, []))
@override_settings(INSTANCE_OWNERSHIP_RETENTION_DAYS=30)
def test_ownership_is_kept_for_a_bounded_time(self):
self.owned_vm(self.host_a)
refresh_instance_database(self.host_a)
InstanceTombstone.objects.update(removed=timezone.now() - timedelta(days=31))
self.on(self.host_a, [("vm", UUID)])
refresh_instance_database(self.host_a)
self.assertEqual(self.ownership(self.host_a), (False, []))
self.assertFalse(InstanceTombstone.objects.exists())
self.assertIn(("-", "-", f"VM vm ({UUID}): removed record expired"), self.rows())
@override_settings(INSTANCE_OWNERSHIP_RETENTION_DAYS=30)
def test_a_removed_record_expires_while_nothing_changes(self):
self.owned_vm(self.host_a)
refresh_instance_database(self.host_a)
InstanceTombstone.objects.update(removed=timezone.now() - timedelta(days=31))
refresh_instance_database(self.host_b) # its VMs match the database
self.assertFalse(InstanceTombstone.objects.exists())
self.assertIn(("-", "-", f"VM vm ({UUID}): removed record expired"), self.rows())
def test_deleted_users_are_skipped(self):
self.owned_vm(self.host_a)
refresh_instance_database(self.host_a)
self.owner.delete()
self.on(self.host_a, [("vm", UUID)])
refresh_instance_database(self.host_a)
self.assertEqual(self.ownership(self.host_a), (True, []))
# a deleted compute's VMs keep running on the host: their owners wait in
# tombstones as for a VM that disappeared, or follow a VM already on another compute
def delete(self, compute):
admin = get_user_model().objects.create_superuser("cd-admin", "cd@example.com", "pw")
self.client.force_login(admin)
self.client.post(reverse("compute_delete", args=[compute.id]))
self.assertFalse(Compute.objects.filter(pk=compute.pk).exists())
def test_a_host_added_again_gets_its_vms_back_with_their_owners(self):
self.owned_vm(self.host_a)
self.delete(self.host_a)
tombstone = InstanceTombstone.objects.get()
self.assertEqual((tombstone.owners[0]["user"], tombstone.host), (self.owner.id, "host-a"))
self.assertEqual(self.rows(), [
("cd-admin", "-", "Login"),
("-", "host-a", f"VM vm ({UUID}): owners kept on the Removed VMs page"),
("cd-admin", "host-a", "Compute host-a (host-a.example): delete"),
])
again = self.compute("host-a-again")
self.on(again, [("vm", UUID)])
refresh_instance_database(again)
self.assertEqual(self.ownership(again), (True, [(self.owner, True, False, True)]))
def test_a_vm_already_on_another_compute_keeps_its_owners_there(self):
self.owned_vm(self.host_a)
self.on(self.host_b, [("vm", UUID)])
refresh_instance_database(self.host_b) # listed on B too: a copy for now
self.delete(self.host_a)
self.assertEqual(self.ownership(self.host_b), (True, [(self.owner, True, False, True)]))
self.assertFalse(InstanceTombstone.objects.exists())
def test_the_api_delete_keeps_the_owners_too(self):
self.owned_vm(self.host_a)
api = APIClient()
api.force_login(get_user_model().objects.create_superuser("cd-api", "cda@example.com", "pw"))
self.assertEqual(api.delete(reverse("compute-detail", args=[self.host_a.id])).status_code, 204)
self.assertEqual(InstanceTombstone.objects.get().owners[0]["user"], self.owner.id)
def test_a_compute_in_use_is_not_deleted(self):
# a reconcile, VM operation or job holds it: the delete waits for none of them
vm = self.owned_vm(self.host_a)
claims.admit({claims.compute_key(self.host_a.id): Claim.SHARED}, holder="192.0.2.30:1:1")
admin = get_user_model().objects.create_superuser("cd-busy", "cdb@example.com", "pw")
self.client.force_login(admin)
api = APIClient()
api.force_login(admin)
url = reverse("compute_delete", args=[self.host_a.id])
self.assertRedirects(self.client.post(url, HTTP_REFERER=f"http://testserver{url}"), url,
fetch_redirect_response=False)
self.assertEqual(api.delete(reverse("compute-detail", args=[self.host_a.id])).status_code, 409)
self.assertTrue(Compute.objects.filter(pk=self.host_a.pk).exists())
self.assertEqual(list(vm.userinstance_set.values_list("user", flat=True)), [self.owner.id])
self.assertFalse(InstanceTombstone.objects.exists())