mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
feat(instances): a running VM's XML can be edited for its next start
The XML editor of a running or paused VM only showed its XML. It now shows the persistent definition, the one the VM starts with next (with secrets, like the other edit forms), and Change saves it through the same review dialog; the running VM keeps its live configuration until it restarts. The tab and the saved message say so. The check against a definition changed since the editor was opened covers this case too.
This commit is contained in:
parent
3f329b50dd
commit
ec679d4d7a
6 changed files with 49 additions and 55 deletions
|
|
@ -6,12 +6,11 @@ from django.utils.translation import gettext_lazy as _
|
|||
from libvirt import (
|
||||
VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL,
|
||||
VIR_DOMAIN_SNAPSHOT_LIST_INTERNAL,
|
||||
VIR_DOMAIN_XML_SECURE,
|
||||
libvirtError,
|
||||
)
|
||||
from operations import claims
|
||||
from operations.models import Claim
|
||||
from vrtManager.instance import wvmInstance
|
||||
from vrtManager.instance import PERSISTENT_XML, wvmInstance
|
||||
from webvirtcloud.settings import QEMU_CONSOLE_LISTENER_ADDRESSES
|
||||
|
||||
|
||||
|
|
@ -291,7 +290,8 @@ class Instance(models.Model):
|
|||
|
||||
@cached_property
|
||||
def inst_xml(self):
|
||||
return self.proxy._XMLDesc(VIR_DOMAIN_XML_SECURE)
|
||||
# the definition for the next start, which is the current one while the VM is off
|
||||
return self.proxy._XMLDesc(PERSISTENT_XML)
|
||||
|
||||
@cached_property
|
||||
def has_managed_save_image(self):
|
||||
|
|
|
|||
|
|
@ -656,46 +656,38 @@
|
|||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="xmledit">
|
||||
{% if instance.status != 5 %}
|
||||
<p>{% trans "If you need to edit XML please Power Off the instance" %}</p>
|
||||
<p id="xml-next-start">{% trans "This is the definition for the VM's next start; a saved change applies then." %}</p>
|
||||
{% endif %}
|
||||
<form action="{% url 'instances:change_xml' instance.id %}" method="post" role="form" aria-label="Edit instance XML form" id="xml-form">
|
||||
{% csrf_token %}
|
||||
<div class="col-sm-12 xmlheight">
|
||||
<textarea id="editor">{{ instance.inst_xml }}</textarea>
|
||||
</div>
|
||||
{% if instance.status == 5 %}
|
||||
<button type="submit" class="btn btn-lg btn-success float-end" name="change_xml">
|
||||
{% trans "Change" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-lg btn-success float-end disabled" disabled>
|
||||
{% trans "Change" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-lg btn-success float-end" name="change_xml">
|
||||
{% trans "Change" %}
|
||||
</button>
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
{% if instance.status == 5 %}
|
||||
<div class="modal fade" id="xml-review" tabindex="-1" aria-labelledby="xml-review-title" aria-hidden="true"
|
||||
data-failed="{% trans "The XML could not be checked. Try again." %}">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="xml-review-title">{% trans "Review the XML change" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-danger d-none" role="alert" id="xml-review-error"></div>
|
||||
<p class="d-none" id="xml-review-same">{% trans "The XML is the same as the VM's definition; there is nothing to save." %}</p>
|
||||
<pre class="d-none mb-0" id="xml-review-diff"></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||
<button type="button" class="btn btn-success" id="xml-review-save">{% trans "Save" %}</button>
|
||||
</div>
|
||||
<div class="modal fade" id="xml-review" tabindex="-1" aria-labelledby="xml-review-title" aria-hidden="true"
|
||||
data-failed="{% trans "The XML could not be checked. Try again." %}">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="xml-review-title">{% trans "Review the XML change" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-danger d-none" role="alert" id="xml-review-error"></div>
|
||||
<p class="d-none" id="xml-review-same">{% trans "The XML is the same as the VM's definition; there is nothing to save." %}</p>
|
||||
<pre class="d-none mb-0" id="xml-review-diff"></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||
<button type="button" class="btn btn-success" id="xml-review-save">{% trans "Save" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="users">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -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 = "<domain>\n <name>xml-vm</name>\n <memory>1024</memory>\n</domain>"
|
||||
LIVE = "<domain id='3'>\n <name>xml-vm</name>\n <memory>512</memory>\n</domain>"
|
||||
EDITED = "<domain>\n <name>xml-vm</name>\n <memory>2048</memory>\n</domain>"
|
||||
|
||||
|
||||
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"))
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue