From 80a94a36b63b2a8a04d7b0422b2c31a4a92758b0 Mon Sep 17 00:00:00 2001 From: catborise Date: Sun, 4 Oct 2026 19:13:20 +0300 Subject: [PATCH] fix(instances): the XML editor saves only while the VM is shut off The editor shows its Change button only for a shut-off VM, but change_xml did not check the state: a page loaded while the VM was off could still be saved after it was started, and the posted XML then overwrote every change made to the definition in between. The view now refuses unless the VM is shut off, with a message. --- instances/test_change_xml.py | 32 ++++++++++++++++++++++++++++++++ instances/views.py | 6 +++++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 instances/test_change_xml.py diff --git a/instances/test_change_xml.py b/instances/test_change_xml.py new file mode 100644 index 00000000..dda3807b --- /dev/null +++ b/instances/test_change_xml.py @@ -0,0 +1,32 @@ +from unittest.mock import patch + +from computes.models import Compute +from django.contrib.auth import get_user_model +from django.contrib.messages import get_messages +from django.test import TestCase +from django.urls import reverse +from instances.models import Instance + + +class ChangeXmlTestCase(TestCase): + def setUp(self): + self.client.force_login(get_user_model().objects.create_superuser("xml_admin", "x@example.com", "pw")) + compute = Compute.objects.create(name="xml-compute", hostname="127.0.0.1", login="", password="", type=4) + self.instance = Instance.objects.create(compute=compute, name="xml-vm", uuid="11111111-2222-3333-4444-888888888888") + + def post(self, status): + with patch("instances.models.wvmInstance") as wvm: + wvm.return_value.get_status.return_value = status + response = self.client.post(reverse("instances:change_xml", args=[self.instance.id]), {"inst_xml": ""}) + return response, wvm.return_value._defineXML + + def test_shut_off_vm_gets_the_new_definition(self): + _, define = self.post(status=5) + define.assert_called_once_with("") + + def test_running_or_paused_vm_is_refused(self): + for status in (1, 3): + with self.subTest(status=status): + response, define = self.post(status) + define.assert_not_called() + self.assertIn("Power off", " ".join(str(m) for m in get_messages(response.wsgi_request))) diff --git a/instances/views.py b/instances/views.py index e04e4347..5d1ef7ca 100755 --- a/instances/views.py +++ b/instances/views.py @@ -1462,7 +1462,11 @@ def set_bootorder(request, pk): def change_xml(request, pk): instance = get_instance(request.user, pk) new_xml = request.POST.get("inst_xml", "") - if new_xml: + # The editor is filled while the VM is shut off; once it runs, the posted + # XML would overwrite changes made since. + if new_xml and instance.proxy.get_status() != 5: + messages.error(request, _("Power off the instance to change its XML")) + elif new_xml: instance.proxy._defineXML(new_xml) msg = _("Change instance XML") addlogmsg(request.user.username, instance.compute.name, instance.name, msg)