1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 12:46:49 +00:00

fix(instances): a running VM's boot order, video and console forms show the pending settings

On an active VM the boot order editor, the video model select and the
console form showed the running VM: a pending boot order, video model or
console setting looked unset, and the boot order editor offered devices
of the running VM only. Choosing one of those cleared the pending order,
since the setter dropped the old order and skipped devices it could not
find. They now read the persistent definition (get_bootmenu,
get_bootorder, get_media_devices, get_video_model and the console
getters take config=), and set_bootorder refuses a device that is not in
it before it changes anything.
This commit is contained in:
catborise 2026-10-11 14:18:20 +03:00
parent 042530fbfd
commit d0bd2ac32b
9 changed files with 152 additions and 35 deletions

View file

@ -185,6 +185,38 @@ class Instance(models.Model):
def config_description(self) -> str:
return self.proxy.get_description(config=True)
@cached_property
def config_media(self) -> list[dict]:
"""CD-ROMs, boot menu and boot order of the persistent definition,
which the boot order editor changes."""
return self.proxy.get_media_devices(config=True)
@cached_property
def config_bootmenu(self) -> bool:
return self.proxy.get_bootmenu(config=True)
@cached_property
def config_boot_order(self) -> dict:
return self.proxy.get_bootorder(config=True)
@cached_property
def config_video_model(self) -> str | None:
"""Video model and console settings of the persistent definition,
which their forms change."""
return self.proxy.get_video_model(config=True)
@cached_property
def config_console_listener_address(self) -> str:
return self.proxy.get_console_listener_addr(config=True)
@cached_property
def config_console_passwd(self):
return self.proxy.get_console_passwd(config=True)
@cached_property
def config_console_keymap(self) -> str:
return self.proxy.get_console_keymap(config=True)
@cached_property
def telnet_port(self):
return self.proxy.get_telnet_port()

View file

@ -255,21 +255,21 @@
});
$(document).ready(function () {
// set current console keymap or fall back to default
var keymap = "{{ instance.console_keymap }}";
var keymap = "{{ instance.config_console_keymap }}";
if (keymap != '') {
$("#console_select_keymap option[value='" + keymap + "']").prop('selected', true);
}
});
$(document).ready(function () {
// set current console listen address or fall back to default
var console_listener_address = "{{ instance.console_listener_address }}";
var console_listener_address = "{{ instance.config_console_listener_address }}";
if (console_listener_address != '') {
$("#console_select_listener_address option[value='" + console_listener_address + "']").prop('selected', true);
}
});
$(document).ready(function () {
// get video model or fall back to default
let video_model = "{{ instance.video_model }}";
let video_model = "{{ instance.config_video_model }}";
if (video_model != '') {
$("#video_model_select option[value='" + video_model + "']").prop('selected', true);
}

View file

@ -99,7 +99,7 @@
<div class="col-sm-12 text-center">
{% if instance.status == 5 %}
<p>{% trans "Enable Boot Menu for your instance when it starts up " %}
{% if instance.bootmenu == 0 %}
{% if instance.config_bootmenu == 0 %}
<form action="{% url 'instances:set_bootmenu' instance.id %}" method="post" role="form" aria-label="Enable/disable instance boot order form">{% csrf_token %}
<input type="submit" class="btn btn-success" name="set_bootmenu" title="{% trans 'Show boot menu' %}" value="{% trans "Enable" %}">
</form>
@ -109,17 +109,17 @@
</form>
{% endif %}
{% else %}
{% if instance.bootmenu == 0 %}
{% if instance.config_bootmenu == 0 %}
<p>**** {% trans "Please shutdown instance to modify boot menu" %} ****</p>
{% endif %}
{% endif %}
</div>
</div>
{% if instance.bootmenu == 1 %}
{% if instance.config_bootmenu == 1 %}
<div class="d-flex justify-content-center">
<div class="col-sm-6 bg-light rounded shadow-sm">
{% for idx, val in instance.boot_order.items %}
{% for idx, val in instance.config_boot_order.items %}
<label>{{ idx|add:1 }}:{{ val.target }}, </label>
{% endfor %}
</div>
@ -128,13 +128,13 @@
<input id="bootorder" name="bootorder" hidden>
<div class="d-flex justify-content-center">
<div id="b_order" class="multipleselect border-0">
{% for disk in instance.disks %}
{% for disk in instance.config_disks %}
<label><input type="checkbox" name="disk:{{ disk.dev }}" value="disk:{{ disk.dev }}" onclick="set_orderlist($('#bootorder'))" />{{ disk.dev }} - {{ disk.image }}</label>
{% endfor %}
{% for cd in instance.media %}
{% for cd in instance.config_media %}
<label><input type="checkbox" name="cdrom:{{ cd.dev }}" value="cdrom:{{ cd.dev }}" onclick="set_orderlist($('#bootorder'))"/>{{ cd.dev }} - {{ cd.image }}</label>
{% endfor %}
{% for net in instance.networks %}
{% for net in instance.config_networks %}
<label><input type="checkbox" name="network:{{ net.mac }}" value="network:{{ net.mac }}" onclick="set_orderlist($('#bootorder'))"/>NIC - {{ net.mac|slice:"9:" }}</label>
{% endfor %}
</div>
@ -857,7 +857,7 @@
<select id="video_model_select" class="form-select" name="video_model">
<option class="fw-bold" value="">{% trans "please choose" %}</option>
{% for vmodel in instance.video_models %}
<option value="{{ vmodel }}"{% if vmodel == instance.video_model %} selected{% endif %}>{{ vmodel }}</option>
<option value="{{ vmodel }}"{% if vmodel == instance.config_video_model %} selected{% endif %}>{{ vmodel }}</option>
{% endfor %}
</select>
{% if instance.status == 5 %}

View file

@ -401,6 +401,34 @@ class LiveDataLossTestCase(TestCase):
("wvc-title", "wvc-description", "2"))
self.assertEqual(dom.XMLDesc(0), live)
def test_pending_boot_order_and_video_are_shown_and_a_live_only_choice_changes_nothing(self):
from lxml import etree, html
nic = "<interface type='network'><mac address='52:54:00:aa:05:21'/><source network='default'/></interface>"
graphics = "<graphics type='vnc' autoport='yes' listen='127.0.0.1'/>"
dom, inst = self.vm("boot-pending", [livetest.create_volume(self.conn, P + "boot-pending")],
extra_devices=nic + graphics)
self.post("set_bootmenu", inst)
dom.create()
# pending order; a NIC the running VM has but its next start will not
self.post("set_bootorder", inst, {"bootorder": "network:52:54:00:aa:05:21,disk:vda"})
dom.attachDeviceFlags(nic.replace("05:21", "05:22"), libvirt.VIR_DOMAIN_AFFECT_LIVE)
persistent = lambda: etree.fromstring(dom.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)) # noqa: E731
video = "virtio" if persistent().find("devices/video/model").get("type") != "virtio" else "vga"
self.post("set_video_model", inst, {"video_model": video})
live = dom.XMLDesc(0)
page = html.fromstring(self.client.get(reverse("instances:instance", args=[inst.id])).content)
(boot,) = page.xpath("//form[@action=$a]", a=reverse("instances:set_bootorder", args=[inst.id]))
self.assertEqual(boot.xpath(".//input[@type='checkbox']/@value"), ["disk:vda", "network:52:54:00:aa:05:21"])
self.assertEqual(page.xpath("//select[@name='video_model']/option[@selected]/@value"), [video])
self.post("set_bootorder", inst, {"bootorder": "network:52:54:00:aa:05:22,disk:vda"})
tree = persistent()
self.assertEqual(tree.xpath("devices/interface/boot/@order"), ["1"])
self.assertEqual(tree.xpath("devices/disk[target/@dev='vda']/boot/@order"), ["2"])
self.assertEqual(dom.XMLDesc(0), live)
def test_pending_qos_change_is_shown_and_kept_by_the_next_edit(self):
from lxml import etree, html

View file

@ -6,10 +6,12 @@ from unittest.mock import PropertyMock, 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
from lxml import html
from vrtManager import util
def disk(dev, cache, serial):
@ -48,6 +50,7 @@ class PendingEditsTestCase(TestCase):
proxy.get_disk_devices.side_effect = lambda **kw: config if kw.get("config") else live
proxy.get_all_qos.side_effect = lambda **kw: qos[1] if kw.get("config") else qos[0]
proxy.get_console_type.return_value = "vnc"
proxy.get_video_models.return_value = ["vga", "virtio"]
proxy.get_cache_modes.return_value = {"default": "Default", "none": "Disabled"}
response = self.client.get(reverse("instances:instance", args=[self.instance.id]))
self.assertEqual(response.status_code, 200)
@ -102,3 +105,28 @@ class PendingEditsTestCase(TestCase):
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_boot_video_and_console_forms_show_the_pending_definition(self):
cd = {"dev": "sda", "image": "pending.iso", "storage": "default", "path": "/pool/pending.iso", "bus": "sata"}
with patch("instances.models.Instance.config_networks", new_callable=PropertyMock, return_value=[]):
doc = self.page(
live=[disk("vda", "default", "")], config=[disk("vda", "default", ""), disk("vdb", "default", "")],
get_bootmenu=(False, True), get_bootorder=({}, {0: {"type": "file", "dev": "cdrom", "target": "sda"}}),
get_media_devices=([], [cd]), get_video_model=("vga", "virtio"),
get_console_listener_addr=("127.0.0.1", "0.0.0.0"), get_console_keymap=("", "de"),
get_console_passwd=("live-pw", "pending-pw"),
)
(boot,) = doc.xpath("//form[@action=$a]", a=reverse("instances:set_bootorder", args=[self.instance.id]))
self.assertEqual(boot.xpath(".//input[@type='checkbox']/@value"), ["disk:vda", "disk:vdb", "cdrom:sda"])
self.assertIn("1:sda", doc.xpath("string(//div[label[contains(., ':')]])"))
self.assertEqual(doc.xpath("//select[@name='video_model']/option[@selected]/@value"), ["virtio"])
self.assertEqual(doc.xpath("//select[@name='keymap']/option[@selected]/@value"), ["de"])
self.assertEqual(doc.xpath("//select[@name='listen_on']/option[@selected]/@value"), ["0.0.0.0"])
self.assertEqual(doc.xpath("//input[@name='password']/@value"), ["pending-pw"])
def test_a_boot_order_choice_not_in_the_definition_is_refused(self):
with patch("instances.models.wvmInstance") as wvm:
wvm.return_value.set_bootorder.side_effect = util.OperationError("Boot device vdz is not in the definition")
response = self.client.post(reverse("instances:set_bootorder", args=[self.instance.id]),
{"bootorder": "disk:vda,disk:vdz"})
self.assertEqual([str(m) for m in get_messages(response.wsgi_request)], ["Boot device vdz is not in the definition"])

View file

@ -121,9 +121,9 @@ def _instance_page(request, pk, instance, compute):
if can_manage_console:
console_form = ConsoleForm(
initial={
"listen_on": instance.console_listener_address,
"password": instance.console_passwd,
"keymap": instance.console_keymap,
"listen_on": instance.config_console_listener_address,
"password": instance.config_console_passwd,
"keymap": instance.config_console_keymap,
},
can_set_listener=request.user.is_superuser,
)
@ -1501,7 +1501,11 @@ def set_bootorder(request, pk):
for idx, val in enumerate(bootorder.split(",")):
dev_type, dev = val.split(":", 1)
order_list[idx] = {"type": dev_type, "dev": dev}
instance.proxy.set_bootorder(order_list)
try:
instance.proxy.set_bootorder(order_list)
except util.OperationError as err:
messages.error(request, err)
return _back(request, pk, "boot_opt")
msg = _("Set boot order")
if not instance.proxy.get_status() == 5: