mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 19:56:48 +00:00
libvirt has no timeout for opening a connection. Measured in the test setup: an unreachable host blocked qemu+tcp for 135 s and qemu+ssh for 133 s; a daemon that accepts and never answers blocks forever. Gunicorn runs sync workers and the instance list connects to every compute, so one unreachable compute held a worker for minutes per page load. - A connection is opened in a helper thread, at most one per connection at a time. The request waits LIBVIRT_CONNECT_TIMEOUT (default 5 s). An open nobody waits for any more keeps running; until it ends, requests get its error at once, and if it succeeds later the connection is used. A failed open is not repeated for LIBVIRT_RETRY_AFTER (default 30 s). A stuck host holds one thread and does not keep other hosts from connecting. - qemu+ssh runs ssh through vrtManager/libvirt-ssh (the URI's command parameter), which adds ConnectTimeout=10 and BatchMode=yes, so an abandoned ssh process ends and no prompt can hang. The user's ssh configuration (aliases, ProxyJump, keys) still applies. - The close callback acts only for the connection it was registered for: a late callback of a replaced connection dropped its successor. The reason is reported as text. A connection that closed before it was published is an error. - Compute.status is a bool (it returned the exception object, so the "host is down" check in refresh_instance_database never fired); the message is Compute.connection_error. Unit tests cover the timeout, retry delay, late success, single open, a stuck host next to a healthy one and the close callback. Live tests (vrtManager/test_live_connection.py), for TCP and SSH computes of the test setup: an unreachable address (no ssh left after 10 s), an endpoint that never answers, a slow open completing later, a host that goes silent on an established connection (keepalive ends calls within 45 s, the next request reconnects), an ssh_config alias and a ProxyJump host.
113 lines
4.4 KiB
Python
113 lines
4.4 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.utils import timezone
|
|
from instances.models import Instance, InstanceTombstone
|
|
|
|
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 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())
|
|
|
|
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())
|
|
|
|
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())
|
|
|
|
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, []))
|