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(