1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00

fix(instances): find known VMs by UUID, not by name

Instance.proxy looked the domain up by its stored name. After a rename
or a name swap outside WebVirtCloud, an operation on one VM changed
another; after a VM was replaced under the same name, operations hit
the new VM.

wvmInstance takes the UUID and uses lookupByUUIDString, with no
fallback to the name: a missing UUID fails instead of picking another
domain. Every caller that has an Instance passes it: proxy, console,
quota, post-migration autostart and the VDI URL. Migration uses the
domain the proxy already resolved. getvvfile and the gstfsd requests
use the host's current name of that UUID.

Live tests cover a name swap and a VM replaced under the same name;
both fail on the previous code.
This commit is contained in:
catborise 2026-10-03 14:56:56 +03:00
parent ffb2ce6bfa
commit 3e427a41b8
9 changed files with 52 additions and 14 deletions

View file

@ -99,6 +99,7 @@ def console(request):
instance.compute.password,
instance.compute.type,
instance.name,
uuid=instance.uuid,
)
console_type = conn.get_console_type()
console_websocket_port = conn.get_console_websocket_port()

View file

@ -104,7 +104,7 @@ def get_vdi_url(request, compute_id, vname):
try:
conn = wvmInstance(
compute.hostname, compute.login, compute.password, compute.type, vname
compute.hostname, compute.login, compute.password, compute.type, vname, uuid=instance.uuid
)
fqdn = get_hostname_by_ip(compute.hostname)

View file

@ -48,6 +48,7 @@ class Instance(models.Model):
self.compute.password,
self.compute.type,
self.name,
uuid=self.uuid,
)
@cached_property

View file

@ -52,7 +52,8 @@ class CanOpenConsoleTestCase(TestCase):
def test_owner_can_download_vv_file(self):
self.client.force_login(self.owner)
with patch("instances.views.wvmInstances") as mock_conn_cls:
with patch("instances.views.wvmInstances") as mock_conn_cls, patch("instances.models.wvmInstance") as wvm:
wvm.return_value.instance.name.return_value = "cons-vm"
conn = mock_conn_cls.return_value
conn.graphics_type.return_value = "vnc"
conn.graphics_listen.return_value = "127.0.0.1"

View file

@ -215,3 +215,26 @@ class LiveDataLossTestCase(TestCase):
self.assertEqual(response.status_code, 302)
self.assertTrue(livetest.volume_exists(self.conn, shared), "deleted another VM's disk")
# R-06: operations follow the VM's UUID, not its name
def test_r06_mutation_follows_uuid_after_names_are_swapped(self):
dom_a, inst_a = self.vm("r06-a", [])
dom_b, _ = self.vm("r06-b", [])
dom_a.rename(P + "r06-tmp", 0)
dom_b.rename(P + "r06-a", 0)
dom_a.rename(P + "r06-b", 0)
self.post("change_options", inst_a, {"title": "for-a", "description": ""})
self.assertIn("<title>for-a</title>", dom_a.XMLDesc(0))
self.assertNotIn("<title>for-a</title>", dom_b.XMLDesc(0))
def test_r06_vm_replaced_under_the_same_name_is_not_touched(self):
dom_old, inst = self.vm("r06-c", [])
dom_old.undefine()
dom_new = livetest.define_vm(self.conn, P + "r06-c", [])
self.post("change_options", inst, {"title": "for-old", "description": ""})
self.assertNotIn("<title>for-old</title>", dom_new.XMLDesc(0))

View file

@ -26,6 +26,8 @@ class SetRootPassTestCase(TestCase):
@patch("socket.socket")
@patch.object(Instance, "proxy")
def test_set_root_pass_post(self, mock_proxy, mock_socket_cls):
# Renamed on the host: gstfsd must get the name of this UUID's domain (R-06)
mock_proxy.instance.name.return_value = "renamed-on-host"
mock_proxy.get_status.return_value = 5 # status: running
mock_sock = MagicMock()
mock_socket_cls.return_value = mock_sock
@ -43,7 +45,7 @@ class SetRootPassTestCase(TestCase):
sent_bytes = mock_sock.send.call_args[0][0]
data = json.loads(sent_bytes.decode())
self.assertEqual(data["action"], "password")
self.assertEqual(data["vname"], "root-pass-vm")
self.assertEqual(data["vname"], "renamed-on-host")
# SHA-512 crypt with a random salt (ROADMAP S-08), not the old fixed one
self.assertTrue(data["passwd"].startswith("$6$"))
self.assertFalse(data["passwd"].startswith("$6$kgPoiREy$"))
@ -59,6 +61,7 @@ class SetRootPassTestCase(TestCase):
@patch("socket.socket")
@patch.object(Instance, "proxy")
def test_same_password_gets_a_different_salt_each_time(self, mock_proxy, mock_socket_cls):
mock_proxy.instance.name.return_value = "root-pass-vm"
mock_proxy.get_status.return_value = 5
mock_sock = MagicMock()
mock_socket_cls.return_value = mock_sock
@ -75,6 +78,7 @@ class SetRootPassTestCase(TestCase):
@patch("socket.socket")
@patch.object(Instance, "proxy")
def test_unreachable_gstfsd_is_an_error_message_not_a_crash(self, mock_proxy, mock_socket_cls):
mock_proxy.instance.name.return_value = "root-pass-vm"
mock_proxy.get_status.return_value = 5
mock_sock = MagicMock()
mock_socket_cls.return_value = mock_sock
@ -95,6 +99,7 @@ class SetRootPassTestCase(TestCase):
admin = get_user_model().objects.get(username="admin")
key = UserSSHKey.objects.create(user=admin, keyname="k", keypublic="ssh-ed25519 AAAA k")
mock_proxy.instance.name.return_value = "root-pass-vm"
mock_proxy.get_status.return_value = 5
mock_sock = MagicMock()
mock_socket_cls.return_value = mock_sock

View file

@ -64,6 +64,7 @@ def check_user_quota(user, instance, cpu, memory, disk_size):
usr_inst.instance.compute.password,
usr_inst.instance.compute.type,
usr_inst.instance.name,
uuid=usr_inst.instance.uuid,
)
cpu += int(conn.get_vcpu())
memory += int(conn.get_memory())
@ -181,6 +182,7 @@ def migrate_instance(
new_compute.password,
new_compute.type,
instance.name,
uuid=instance.uuid,
)
if autostart:

View file

@ -696,7 +696,8 @@ def set_root_pass(request, pk):
passwd = request.POST.get("passwd", None)
if passwd:
passwd_hash = crypt.crypt(passwd, crypt.mksalt(crypt.METHOD_SHA512))
data = {"action": "password", "passwd": passwd_hash, "vname": instance.name}
# gstfsd opens the domain by name: send the host's current one.
data = {"action": "password", "passwd": passwd_hash, "vname": instance.proxy.instance.name()}
if instance.proxy.get_status() == 5:
result = gstfsd_request(instance.compute.hostname, data)
@ -727,7 +728,7 @@ def add_public_key(request, pk):
data = {
"action": "publickey",
"key": publickey.keypublic,
"vname": instance.name,
"vname": instance.proxy.instance.name(),
}
if instance.proxy.get_status() == 5:
@ -1935,6 +1936,8 @@ def getvvfile(request, pk):
instance.compute.type,
)
# The host's current name for this UUID; the stored name may be stale.
name = instance.proxy.instance.name()
msg = _("Send console.vv file")
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
response = HttpResponse(
@ -1945,14 +1948,14 @@ def getvvfile(request, pk):
charset="utf-8",
)
response.writelines("[virt-viewer]\n")
response.writelines("type=" + conn.graphics_type(instance.name) + "\n")
if conn.graphics_listen(instance.name) == "0.0.0.0":
response.writelines("type=" + conn.graphics_type(name) + "\n")
if conn.graphics_listen(name) == "0.0.0.0":
response.writelines("host=" + conn.host + "\n")
else:
response.writelines("host=" + conn.graphics_listen(instance.name) + "\n")
response.writelines("port=" + conn.graphics_port(instance.name) + "\n")
response.writelines("title=" + conn.domain_name(instance.name) + "\n")
response.writelines("password=" + conn.graphics_passwd(instance.name) + "\n")
response.writelines("host=" + conn.graphics_listen(name) + "\n")
response.writelines("port=" + conn.graphics_port(name) + "\n")
response.writelines("title=" + conn.domain_name(name) + "\n")
response.writelines("password=" + conn.graphics_passwd(name) + "\n")
response.writelines("enable-usbredir=1\n")
response.writelines("disable-effects=all\n")
response.writelines("secure-attention=ctrl+alt+ins\n")

View file

@ -126,7 +126,7 @@ class wvmInstances(wvmConnect):
if undefine:
flags |= VIR_MIGRATE_UNDEFINE_SOURCE
dom = conn.get_instance(name)
dom = conn.instance
dom_arch = conn.get_arch()
dom_emulator = conn.get_dom_emulator()
@ -177,10 +177,12 @@ class wvmInstances(wvmConnect):
class wvmInstance(wvmConnect):
def __init__(self, host, login, passwd, conn, vname):
def __init__(self, host, login, passwd, conn, vname, uuid=None):
wvmConnect.__init__(self, host, login, passwd, conn)
self._ip_cache = None
self.instance = self.get_instance(vname)
# A known VM is found by UUID: a name can be renamed or swapped
# outside WebVirtCloud. No fallback to the name.
self.instance = self.wvm.lookupByUUIDString(uuid) if uuid else self.get_instance(vname)
def osinfo(self):
info_results = qemuAgentCommand(