mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
refresh_instance_database opened the transaction first and then asked libvirt for the domain list. A host that goes silent on an established connection keeps that call waiting until keepalive gives up (30 s). On SQLite, the default database, the open transaction holds a read lock, so every other write of the app failed meanwhile with "database is locked", logins (session writes) included. Reproduced in the test setup: with a relay to a TCP compute frozen after connecting, three session writes during the 30 s refresh each failed after 5 s. - The connection check and listAllDomains run before the transaction, still under the compute lock, so create, delete and migration of the compute stay excluded as before. - The transaction only applies the changes. It checks the compute row first: a compute deleted while libvirt was asked gets no instances (this ended in a foreign key error before). A failed listing still leaves the database untouched. With the change, the same reproduction writes the sessions at once and keeps the instances. Both new tests fail on the previous code.
183 lines
7 KiB
Python
183 lines
7 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 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_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())
|