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:
parent
ffb2ce6bfa
commit
3e427a41b8
9 changed files with 52 additions and 14 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ class Instance(models.Model):
|
|||
self.compute.password,
|
||||
self.compute.type,
|
||||
self.name,
|
||||
uuid=self.uuid,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue