-
-
-
-
{% trans "The XML is the same as the VM's definition; there is nothing to save." %}
-
-
-
+
diff --git a/instances/tests/test_change_xml.py b/instances/tests/test_change_xml.py
index 9faaee08..058d0d82 100644
--- a/instances/tests/test_change_xml.py
+++ b/instances/tests/test_change_xml.py
@@ -7,15 +7,18 @@ from django.test import TestCase
from django.urls import reverse
from instances.models import Instance
from libvirt import libvirtError
+from vrtManager.instance import PERSISTENT_XML
from logs.models import Logs
CURRENT = "\n xml-vm\n 1024\n"
+LIVE = "\n xml-vm\n 512\n"
EDITED = "\n xml-vm\n 2048\n"
class ChangeXmlTestCase(TestCase):
"""The XML editor shows what a change does before it is saved, and saves
- only over the definition it was opened with."""
+ only over the definition it was opened with: the persistent one, which a
+ running VM uses at its next start."""
def setUp(self):
self.client.force_login(get_user_model().objects.create_superuser("xml_admin", "x@example.com", "pw"))
@@ -28,7 +31,7 @@ class ChangeXmlTestCase(TestCase):
data["confirm"] = "1"
with patch("instances.models.wvmInstance") as wvm:
wvm.return_value.get_status.return_value = status
- wvm.return_value._XMLDesc.return_value = CURRENT
+ wvm.return_value._XMLDesc.side_effect = lambda flags: CURRENT if flags == PERSISTENT_XML else LIVE
wvm.return_value._defineXML.side_effect = define_error
response = self.client.post(reverse("instances:change_xml", args=[self.instance.id]), data)
return response, wvm.return_value._defineXML
@@ -67,13 +70,12 @@ class ChangeXmlTestCase(TestCase):
row = Logs.objects.get(instance_uuid=self.instance.uuid)
self.assertEqual((row.message, row.success), ("VM xml-vm: change XML: unsupported configuration: bad memory", False))
- def test_running_or_paused_vm_is_refused(self):
+ def test_a_running_or_paused_vm_gets_the_definition_for_its_next_start(self):
for status in (1, 3):
with self.subTest(status=status):
response, define = self.post(status, confirm=True)
- define.assert_not_called()
- self.assertEqual(response.status_code, 409)
- self.assertIn("Power off", response.json()["error"])
+ define.assert_called_once_with(EDITED)
+ self.assertIn("next start", " ".join(str(m) for m in get_messages(response.wsgi_request)))
def test_the_line_breaks_a_form_posts_are_the_same_definition(self):
response, _ = self.post(base=CURRENT.replace("\n", "\r\n"))
diff --git a/instances/tests/test_pending_edits.py b/instances/tests/test_pending_edits.py
index 2c54a02e..77612ef3 100644
--- a/instances/tests/test_pending_edits.py
+++ b/instances/tests/test_pending_edits.py
@@ -96,22 +96,25 @@ class PendingEditsTestCase(TestCase):
self.assertEqual(doc.xpath("//select[@name='cur_vcpu']/option[@selected]/@value"), ["3"])
def test_buttons_shown_as_disabled_cannot_submit(self):
- # running: the CPU, options, video and XML forms wait for a shutdown
+ # running: the CPU, options and video forms wait for a shutdown
doc = self.page(live=[disk("vda", "default", "")], config=[disk("vda", "default", "")])
shown = doc.xpath("//button[contains(concat(' ', @class, ' '), ' disabled ')]")
self.assertGreater(len(shown), 5)
self.assertEqual([b for b in shown if b.get("disabled") is None], [])
- for action in ("resizevm_cpu", "change_options", "set_video_model", "change_xml", "detach_vol", "delete_vol"):
+ for action in ("resizevm_cpu", "change_options", "set_video_model", "detach_vol", "delete_vol"):
with self.subTest(action):
(form,) = doc.xpath("//form[@action=$a]", a=reverse(f"instances:{action}", args=[self.instance.id]))
self.assertTrue(all(b.get("disabled") is not None for b in form.xpath(".//button")))
- def test_the_xml_editor_reviews_a_change_only_while_the_vm_is_off(self):
- # static/js/xml_editor.js opens the review dialog on Change
- doc = self.page(status=5)
- self.assertEqual(len(doc.xpath("//form[@id='xml-form']//button[@type='submit']")), 1)
- self.assertEqual(len(doc.xpath("//div[@id='xml-review']//button[@id='xml-review-save']")), 1)
- self.assertEqual(self.page().xpath("//div[@id='xml-review']"), [])
+ def test_the_xml_editor_shows_the_definition_for_the_next_start(self):
+ # static/js/xml_editor.js opens the review dialog on Change, also while the VM runs
+ for status in (5, 1):
+ with self.subTest(status=status):
+ doc = self.page(status=status)
+ (button,) = doc.xpath("//form[@id='xml-form']//button[@type='submit']")
+ self.assertIsNone(button.get("disabled"))
+ self.assertEqual(len(doc.xpath("//div[@id='xml-review']//button[@id='xml-review-save']")), 1)
+ self.assertEqual(bool(doc.xpath("//p[@id='xml-next-start']")), status != 5)
def test_the_boot_video_and_console_forms_show_the_pending_definition(self):
cd = {"dev": "sda", "image": "pending.iso", "storage": "default", "path": "/pool/pending.iso", "bus": "sata"}
diff --git a/instances/views.py b/instances/views.py
index db433c0f..f27008d3 100755
--- a/instances/views.py
+++ b/instances/views.py
@@ -1457,17 +1457,14 @@ def set_bootorder(request, pk):
def change_xml(request, pk):
"""
The XML editor's dialog: without confirm, the lines the edited XML changes
- in the definition, to review; with confirm, the edited XML is defined.
+ in the persistent definition, to review; with confirm, the edited XML is
+ defined. A running VM uses it at its next start.
Only over the definition the editor was opened with (base), so a change
made since is not overwritten; a form posts its line breaks as CRLF. A refusal comes back to the dialog and the
edited XML stays in the editor.
"""
instance = get_instance(request.user, pk)
new_xml = request.POST.get("inst_xml", "")
- # The editor is filled while the VM is shut off; once it runs, the posted
- # XML would overwrite changes made since.
- if instance.proxy.get_status() != 5:
- return JsonResponse({"error": _("Power off the instance to change its XML")}, status=409)
if not new_xml.strip():
return JsonResponse({"error": _("The XML is empty")}, status=400)
current = instance.inst_xml
@@ -1482,7 +1479,10 @@ def change_xml(request, pk):
instance.proxy._defineXML(new_xml)
except libvirtError as err:
return JsonResponse({"error": error_text(request, err)}, status=400)
- messages.success(request, _("The XML definition was saved."))
+ if instance.proxy.get_status() == 5:
+ messages.success(request, _("The XML definition was saved."))
+ else:
+ messages.success(request, _("The XML definition was saved; the VM uses it from its next start."))
return JsonResponse({"saved": True})
diff --git a/static/js/xml_editor.js b/static/js/xml_editor.js
index ffda4cb6..fa7b6aa7 100644
--- a/static/js/xml_editor.js
+++ b/static/js/xml_editor.js
@@ -10,9 +10,6 @@
editor.getSession().setMode("ace/mode/xml");
var base = editor.getValue(); // the definition the editor opened with
var dialog = document.getElementById("xml-review");
- if (!dialog) {
- return; // a running VM's editor only shows its XML
- }
var modal = bootstrap.Modal.getOrCreateInstance(dialog);
var error = document.getElementById("xml-review-error");
var same = document.getElementById("xml-review-same");