diff --git a/console/views.py b/console/views.py index da233e73..3a770556 100644 --- a/console/views.py +++ b/console/views.py @@ -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() diff --git a/datasource/views.py b/datasource/views.py index f82fe4aa..78392bbb 100644 --- a/datasource/views.py +++ b/datasource/views.py @@ -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) diff --git a/instances/models.py b/instances/models.py index 7985707d..99485724 100644 --- a/instances/models.py +++ b/instances/models.py @@ -48,6 +48,7 @@ class Instance(models.Model): self.compute.password, self.compute.type, self.name, + uuid=self.uuid, ) @cached_property diff --git a/instances/test_console_rule.py b/instances/test_console_rule.py index ee3664e8..903b5604 100644 --- a/instances/test_console_rule.py +++ b/instances/test_console_rule.py @@ -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" diff --git a/instances/test_live_dataloss.py b/instances/test_live_dataloss.py index 42965a65..6764013a 100644 --- a/instances/test_live_dataloss.py +++ b/instances/test_live_dataloss.py @@ -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("for-a", dom_a.XMLDesc(0)) + self.assertNotIn("for-a", 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("for-old", dom_new.XMLDesc(0)) diff --git a/instances/test_root_pass.py b/instances/test_root_pass.py index e4b3107f..82ba6a83 100644 --- a/instances/test_root_pass.py +++ b/instances/test_root_pass.py @@ -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 diff --git a/instances/utils.py b/instances/utils.py index b293c115..7629158d 100644 --- a/instances/utils.py +++ b/instances/utils.py @@ -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: diff --git a/instances/views.py b/instances/views.py index 3610c00a..bcc34b5d 100755 --- a/instances/views.py +++ b/instances/views.py @@ -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") diff --git a/vrtManager/instance.py b/vrtManager/instance.py index 6e1b352d..38531516 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -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(