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:
parent
042530fbfd
commit
d0bd2ac32b
9 changed files with 152 additions and 35 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue