mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 03:36:48 +00:00
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.
147 lines
9.5 KiB
Python
147 lines
9.5 KiB
Python
"""On an active VM an edit changes the persistent definition and is pending
|
|
until the next start. The edit forms show that definition, so a save does
|
|
not post the running VM's value back over a pending change."""
|
|
|
|
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):
|
|
return {
|
|
"dev": dev, "bus": "virtio", "image": f"{dev}.qcow2", "storage": "default", "path": f"/pool/{dev}.qcow2",
|
|
"format": "qcow2", "backing_file": None, "size": 1 << 30, "used": 1 << 20, "cache": cache, "io": "default",
|
|
"discard": "default", "detect_zeroes": "default", "readonly": False, "shareable": False, "serial": serial,
|
|
}
|
|
|
|
|
|
class PendingEditsTestCase(TestCase):
|
|
def setUp(self):
|
|
self.client.force_login(get_user_model().objects.create_superuser("pe-admin", "pe@example.com", "x"))
|
|
compute = Compute.objects.create(name="pe", hostname="127.0.0.1:1", login="root", password="", type=1)
|
|
self.instance = Instance.objects.create(compute=compute, name="pe-vm", uuid="aaaaaaaa-1111-2222-3333-444444444444")
|
|
|
|
def page(self, live=(), config=(), qos=({}, {}), status=1, **getters):
|
|
"""live and config: disks; qos and getters (get_vcpu=(2, 4)): values of
|
|
the live and the persistent definition."""
|
|
with patch("instances.models.wvmInstance") as wvm, patch.object(
|
|
Compute, "status", new_callable=PropertyMock, return_value=True
|
|
):
|
|
proxy = wvm.return_value
|
|
proxy.get_status.return_value = status
|
|
proxy.get_memory.return_value = proxy.get_cur_memory.return_value = 1024
|
|
proxy.get_max_memory.return_value = 4096
|
|
getters = {"get_vcpu": (1, 1), "get_cur_vcpu": (1, 1), "get_title": ("", ""),
|
|
"get_description": ("", ""), **getters}
|
|
for getter, values in getters.items():
|
|
getattr(proxy, getter).side_effect = lambda config=False, values=values: values[config]
|
|
proxy.get_max_cpus.return_value = [1, 2, 3, 4]
|
|
proxy.get_vcpus.return_value = {}
|
|
for getter in ("get_networks", "get_ifaces", "get_storages", "get_media_devices", "get_net_devices",
|
|
"get_snapshot", "get_snapshot_jobs"):
|
|
getattr(proxy, getter).return_value = []
|
|
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)
|
|
return html.fromstring(response.content)
|
|
|
|
def test_the_disk_edit_form_shows_the_pending_definition(self):
|
|
# running: cache=none and a new disk vdb pending, vda's serial live only
|
|
doc = self.page(
|
|
live=[disk("vda", "default", "live-serial")],
|
|
config=[disk("vda", "none", ""), disk("vdb", "default", "")],
|
|
)
|
|
forms = doc.xpath("//form[@action=$a]", a=reverse("instances:edit_volume", args=[self.instance.id]))
|
|
self.assertEqual([f.xpath("string(.//input[@name='dev']/@value)") for f in forms], ["vda", "vdb"])
|
|
vda = forms[0]
|
|
self.assertEqual(vda.xpath(".//select[@name='vol_cache']/option[@selected]/@value"), ["none"])
|
|
self.assertEqual(vda.xpath("string(.//input[@name='vol_serial']/@value)"), "")
|
|
|
|
def test_the_qos_form_shows_the_pending_definition(self):
|
|
mac = "52:54:10:00:00:01"
|
|
inbound = {"direction": "inbound", "average": "500", "peak": "0", "burst": "0", "floor": None}
|
|
doc = self.page(qos=({mac: [inbound]}, {mac: [{**inbound, "average": "1000"}, {**inbound, "direction": "outbound"}]}))
|
|
self.assertEqual(doc.xpath("//input[@name='qos_average']/@value"), ["1000", "500"])
|
|
# the pending outbound has a row
|
|
self.assertEqual([" ".join(t.split()) for t in doc.xpath("//label[@class='col-form-label']/text()")],
|
|
[f"{mac} Inbound", f"{mac} Outbound"])
|
|
|
|
def test_a_disk_with_a_pending_source_is_not_deleted(self):
|
|
# running: vda is A, its definition for the next start uses B
|
|
with patch("instances.models.wvmInstance") as wvm, patch("instances.views.wvmStorage") as storage:
|
|
proxy = wvm.return_value
|
|
live, pending = disk("vda", "default", ""), {**disk("vda", "default", ""), "path": "/pool/b.qcow2"}
|
|
proxy.get_disk_devices.side_effect = lambda **kw: [pending] if kw.get("config") else [live]
|
|
self.client.post(reverse("instances:delete_vol", args=[self.instance.id]), {"dev": "vda"})
|
|
proxy.detach_disk.assert_not_called()
|
|
storage.return_value.del_volume.assert_not_called()
|
|
|
|
def test_the_options_and_cpu_forms_show_the_pending_definition(self):
|
|
doc = self.page(get_vcpu=(2, 4), get_cur_vcpu=(1, 3), get_title=("old", "new"),
|
|
get_description=("live text", "pending text"))
|
|
self.assertEqual(doc.xpath("//input[@name='title']/@value"), ["new"])
|
|
self.assertEqual(doc.xpath("//textarea[@name='description']/text()"), ["pending text"])
|
|
self.assertEqual(doc.xpath("//select[@name='vcpu']/option[@selected]/@value"), ["4"])
|
|
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 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", "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_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"}
|
|
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"])
|
|
|
|
def test_an_active_vm_says_its_settings_apply_at_the_next_start(self):
|
|
for status, shown in ((1, True), (3, True), (5, False)):
|
|
with self.subTest(status=status):
|
|
self.assertEqual(bool(self.page(status=status).xpath("//p[@id='next-start-note']")), shown)
|