mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
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).
193 lines
7.6 KiB
Python
193 lines
7.6 KiB
Python
from unittest.mock import MagicMock, PropertyMock, patch
|
|
from django.contrib.auth import get_user_model
|
|
from django.db import connection
|
|
from django.test import TestCase, TransactionTestCase
|
|
from django.test.utils import CaptureQueriesContext
|
|
|
|
from accounts.models import UserInstance
|
|
from computes.models import Compute
|
|
from computes.utils import refresh_instance_database
|
|
from instances.models import Instance
|
|
from instances.utils import refr
|
|
|
|
|
|
class MockDomain:
|
|
def __init__(self, name, uuid_str):
|
|
self._name = name
|
|
self._uuid_str = uuid_str
|
|
|
|
def name(self):
|
|
return self._name
|
|
|
|
def UUIDString(self):
|
|
return self._uuid_str
|
|
|
|
|
|
class InstanceReconciliationTestCase(TestCase):
|
|
def setUp(self):
|
|
User = get_user_model()
|
|
self.user = User.objects.create_user(username="test_reconcile_user", password="password")
|
|
self.compute = Compute.objects.create(
|
|
name="reconcile-compute",
|
|
hostname="127.0.0.1",
|
|
login="root",
|
|
password="",
|
|
type=1,
|
|
)
|
|
|
|
def test_new_domain_created_in_libvirt_is_added_to_db(self):
|
|
dom1 = MockDomain(name="vm-alpha", uuid_str="11111111-1111-1111-1111-111111111111")
|
|
dom2 = MockDomain(name="vm-beta", uuid_str="22222222-2222-2222-2222-222222222222")
|
|
|
|
mock_proxy = MagicMock()
|
|
mock_proxy.wvm.listAllDomains.return_value = [dom1, dom2]
|
|
self.compute.__dict__["proxy"] = mock_proxy
|
|
self.compute.__dict__["status"] = True # reachable
|
|
|
|
refresh_instance_database(self.compute)
|
|
|
|
instances = Instance.objects.filter(compute=self.compute).order_by("name")
|
|
self.assertEqual(instances.count(), 2)
|
|
self.assertEqual(instances[0].name, "vm-alpha")
|
|
self.assertEqual(instances[0].uuid, "11111111-1111-1111-1111-111111111111")
|
|
self.assertEqual(instances[1].name, "vm-beta")
|
|
self.assertEqual(instances[1].uuid, "22222222-2222-2222-2222-222222222222")
|
|
|
|
def test_domain_deleted_in_libvirt_is_deleted_from_db(self):
|
|
inst = Instance.objects.create(
|
|
compute=self.compute,
|
|
name="vm-to-delete",
|
|
uuid="33333333-3333-3333-3333-333333333333",
|
|
)
|
|
UserInstance.objects.create(instance=inst, user=self.user)
|
|
|
|
mock_proxy = MagicMock()
|
|
mock_proxy.wvm.listAllDomains.return_value = []
|
|
self.compute.__dict__["proxy"] = mock_proxy
|
|
self.compute.__dict__["status"] = True # reachable
|
|
|
|
refresh_instance_database(self.compute)
|
|
|
|
self.assertFalse(Instance.objects.filter(compute=self.compute, uuid="33333333-3333-3333-3333-333333333333").exists())
|
|
self.assertFalse(UserInstance.objects.filter(user=self.user).exists())
|
|
|
|
def test_domain_renamed_in_libvirt_preserves_userinstance_and_id(self):
|
|
uuid = "44444444-4444-4444-4444-444444444444"
|
|
inst = Instance.objects.create(
|
|
compute=self.compute,
|
|
name="vm-original-name",
|
|
uuid=uuid,
|
|
)
|
|
original_inst_id = inst.id
|
|
user_inst = UserInstance.objects.create(instance=inst, user=self.user, is_change=True)
|
|
|
|
dom_renamed = MockDomain(name="vm-new-name", uuid_str=uuid)
|
|
|
|
mock_proxy = MagicMock()
|
|
mock_proxy.wvm.listAllDomains.return_value = [dom_renamed]
|
|
self.compute.__dict__["proxy"] = mock_proxy
|
|
self.compute.__dict__["status"] = True # reachable
|
|
|
|
refresh_instance_database(self.compute)
|
|
|
|
inst.refresh_from_db()
|
|
self.assertEqual(inst.id, original_inst_id)
|
|
self.assertEqual(inst.name, "vm-new-name")
|
|
self.assertEqual(inst.uuid, uuid)
|
|
|
|
# Verify UserInstance still exists and still points to the same instance!
|
|
user_inst.refresh_from_db()
|
|
self.assertEqual(user_inst.instance_id, original_inst_id)
|
|
self.assertTrue(user_inst.is_change)
|
|
|
|
def test_libvirt_connection_error_leaves_db_untouched(self):
|
|
inst = Instance.objects.create(
|
|
compute=self.compute,
|
|
name="vm-persistent",
|
|
uuid="55555555-5555-5555-5555-555555555555",
|
|
)
|
|
|
|
mock_proxy = MagicMock()
|
|
mock_proxy.wvm.listAllDomains.side_effect = Exception("Libvirt socket connection refused")
|
|
self.compute.__dict__["proxy"] = mock_proxy
|
|
self.compute.__dict__["status"] = True # reachable
|
|
|
|
refresh_instance_database(self.compute)
|
|
|
|
# Instance should NOT be deleted!
|
|
self.assertTrue(Instance.objects.filter(id=inst.id).exists())
|
|
|
|
def test_broken_domain_metadata_aborts_sync_safely(self):
|
|
inst = Instance.objects.create(
|
|
compute=self.compute,
|
|
name="vm-persistent-2",
|
|
uuid="66666666-6666-6666-6666-666666666666",
|
|
)
|
|
|
|
broken_dom = MagicMock()
|
|
broken_dom.UUIDString.side_effect = Exception("Corrupted domain metadata")
|
|
|
|
mock_proxy = MagicMock()
|
|
mock_proxy.wvm.listAllDomains.return_value = [broken_dom]
|
|
self.compute.__dict__["proxy"] = mock_proxy
|
|
self.compute.__dict__["status"] = True # reachable
|
|
|
|
refresh_instance_database(self.compute)
|
|
|
|
self.assertTrue(Instance.objects.filter(id=inst.id).exists())
|
|
|
|
def test_refr_invokes_refresh_instance_database(self):
|
|
with patch("computes.utils.refresh_instance_database") as mock_refresh:
|
|
refr(self.compute)
|
|
mock_refresh.assert_called_once_with(self.compute)
|
|
|
|
|
|
class ReconciliationTransactionTestCase(TransactionTestCase):
|
|
"""libvirt is asked outside the DB transaction: on SQLite an open transaction
|
|
makes every other write of the app fail while a silent host is waited on."""
|
|
|
|
def setUp(self):
|
|
self.compute = Compute.objects.create(name="tx-compute", hostname="127.0.0.1", login="root", password="", type=1)
|
|
|
|
def proxy(self, list_domains):
|
|
mock_proxy = MagicMock()
|
|
mock_proxy.wvm.listAllDomains.side_effect = list_domains
|
|
self.compute.__dict__["proxy"] = mock_proxy
|
|
|
|
def test_libvirt_is_called_outside_the_transaction(self):
|
|
seen = {}
|
|
|
|
def status():
|
|
seen["status"] = connection.in_atomic_block
|
|
return True
|
|
|
|
def list_domains():
|
|
seen["list"] = connection.in_atomic_block
|
|
return [MockDomain(name="vm-tx", uuid_str="77777777-7777-7777-7777-777777777777")]
|
|
|
|
self.proxy(list_domains)
|
|
with patch.object(Compute, "status", new_callable=PropertyMock, side_effect=status):
|
|
refresh_instance_database(self.compute)
|
|
|
|
self.assertEqual(seen, {"status": False, "list": False})
|
|
self.assertTrue(Instance.objects.filter(compute=self.compute, name="vm-tx").exists())
|
|
|
|
def test_a_compute_that_matches_the_database_writes_nothing(self):
|
|
Instance.objects.create(compute=self.compute, name="vm-same", uuid="99999999-9999-9999-9999-999999999999")
|
|
self.proxy(lambda: [MockDomain(name="vm-same", uuid_str="99999999-9999-9999-9999-999999999999")])
|
|
self.compute.__dict__["status"] = True
|
|
with CaptureQueriesContext(connection) as queries:
|
|
refresh_instance_database(self.compute)
|
|
|
|
self.assertEqual([q["sql"].split()[0] for q in queries.captured_queries], ["SELECT", "SELECT"])
|
|
|
|
def test_compute_deleted_while_listing_gets_no_instances(self):
|
|
def list_domains():
|
|
Compute.objects.filter(pk=self.compute.pk).delete()
|
|
return [MockDomain(name="vm-orphan", uuid_str="88888888-8888-8888-8888-888888888888")]
|
|
|
|
self.proxy(list_domains)
|
|
self.compute.__dict__["status"] = True
|
|
refresh_instance_database(self.compute)
|
|
|
|
self.assertFalse(Instance.objects.filter(uuid="88888888-8888-8888-8888-888888888888").exists())
|