1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00
webvirtcloud/vrtManager/tests/test_device_edits.py
catborise 42ab293dc5 test: remove code no test run executes
- 25 vrtManager test modules configured settings themselves when Django
  had none (MAC_OUI, and in test_network_ip a fake django module): under
  manage.py test settings are always configured, so it never ran.
- webvirtcloud/tests/test_common_tags.py tested no tag: it fetched an
  unknown URL, as test_error_pages and test_logging already do.
- .gitignore covers the parallel runs' test_db_N.sqlite3 files too.
2026-11-06 13:48:03 +03:00

287 lines
15 KiB
Python

"""Device edits of a VM: each definition is changed as it describes the
device, every NIC type can be a boot device, posted values stay values, and
definition changes go through _defineXML, a QoS edit keeps the floor, and what the edit forms show is read from either definition. A boot order with a
missing device changes nothing. An active VM's memory is resized
by balloon, a stopped one's in its definition. Enabling vCPU hot plug keeps
the vCPUs enabled at boot."""
import unittest
from unittest.mock import MagicMock
from lxml import etree
from vrtManager import util
from vrtManager.instance import PERSISTENT_XML, wvmInstance
from libvirt import VIR_DOMAIN_AFFECT_CONFIG, VIR_DOMAIN_AFFECT_LIVE
PERSISTENT = """<domain><title>pending</title><vcpu current='2'>4</vcpu><os><type>hvm</type><bootmenu enable='yes'/></os><devices>
<disk type='file' device='disk'><source file='/pool/a.qcow2'/><target dev='vda' bus='virtio'/></disk>
<disk type='file' device='disk'><source file='/pool/b.qcow2'/><target dev='vdb' bus='virtio'/></disk>
<interface type='bridge'><mac address='52:54:10:00:00:02'/><source bridge='br0'/></interface>
<interface type='network'><mac address='52:54:10:00:00:01'/><source network='default'/>
<bandwidth><inbound average='1' peak='2' burst='3' floor='4'/></bandwidth></interface>
<disk type='file' device='cdrom'><target dev='sda' bus='sata'/><boot order='1'/></disk>
<video><model type='vga'/></video>
<graphics type='vnc' passwd='pending' keymap='de'><listen type='address' address='0.0.0.0'/></graphics>
</devices></domain>"""
LIVE = """<domain><vcpu>1</vcpu><os><type>hvm</type></os><devices>
<disk type='file' device='disk'><source file='/pool/a.qcow2'/><target dev='vda' bus='virtio'/><alias name='virtio-disk0'/></disk>
</devices></domain>"""
def proxy(active=True):
p = wvmInstance.__new__(wvmInstance) # no connection: only these calls
p.instance = MagicMock(**{"isActive.return_value": active})
p._XMLDesc = lambda flags: PERSISTENT if flags == PERSISTENT_XML else LIVE
p._defineXML = MagicMock()
p.wvm = MagicMock()
return p
def defined(p):
(xml,), _ = p._defineXML.call_args
return etree.fromstring(xml)
class DetachDiskTestCase(unittest.TestCase):
def test_each_definition_is_detached_as_it_describes_the_disk(self):
p = proxy()
p.detach_disk("vda")
(live, live_flags), (config, config_flags) = (c.args for c in p.instance.detachDeviceFlags.call_args_list)
self.assertEqual((live_flags, config_flags), (VIR_DOMAIN_AFFECT_LIVE, VIR_DOMAIN_AFFECT_CONFIG))
self.assertIn("virtio-disk0", live)
self.assertNotIn("alias", config)
def test_a_disk_only_in_the_persistent_definition_is_detached_there(self):
p = proxy()
p.detach_disk("vdb")
self.assertEqual([c.args[1] for c in p.instance.detachDeviceFlags.call_args_list], [VIR_DOMAIN_AFFECT_CONFIG])
def test_an_unknown_disk_is_refused(self):
p = proxy()
with self.assertRaises(util.OperationError):
p.detach_disk("vdz")
p.instance.detachDeviceFlags.assert_not_called()
class VcpuHotplugTestCase(unittest.TestCase):
def test_a_vcpu_turned_on_or_off_stays_so_at_the_next_start(self):
p = proxy()
p.set_vcpu("1", 1)
p.instance.setVcpu.assert_called_once_with("1", 1, VIR_DOMAIN_AFFECT_LIVE | VIR_DOMAIN_AFFECT_CONFIG)
def test_enabling_hotplug_keeps_the_current_vcpus_enabled(self):
p = proxy(active=False)
p.instance.info.return_value = [5]
p._XMLDesc = lambda flags: "<domain><vcpu current='2'>4</vcpu></domain>"
p.set_vcpu_hotplug(True)
self.assertEqual(defined(p).xpath("vcpus/vcpu/@enabled"), ["yes", "yes"])
class DeviceEditsTestCase(unittest.TestCase):
def test_a_block_volume_is_attached_by_its_device_path(self):
p = proxy(active=False)
p._definitions = lambda: [(PERSISTENT_XML, VIR_DOMAIN_AFFECT_CONFIG)]
p.attach_disk("vdc", "/dev/vg/c", disk_type="block", target_bus="virtio")
xml, _ = p.instance.attachDeviceFlags.call_args.args
self.assertEqual(etree.fromstring(xml).find("source").attrib, {"dev": "/dev/vg/c"})
def test_a_usb_cdrom_is_a_cd_drive_on_usb_bot(self):
# usb-storage, libvirt's choice where QEMU has no usb-bot, shows a hard disk
for bus, model in (("usb", "usb-bot"), ("sata", None)):
with self.subTest(bus=bus):
p = proxy(active=False)
p._definitions = lambda: [(PERSISTENT_XML, VIR_DOMAIN_AFFECT_CONFIG)]
p.attach_disk("sdb", "/iso/virtio-win.iso", disk_device="cdrom", target_bus=bus, readonly=True)
xml, _ = p.instance.attachDeviceFlags.call_args.args
self.assertEqual(etree.fromstring(xml).get("model"), model)
def test_a_bridge_nic_can_be_a_boot_device(self):
p = proxy()
p.set_bootorder({0: {"type": "network", "dev": "52:54:10:00:00:02"}, 1: {"type": "disk", "dev": "vdb"}})
tree = defined(p)
self.assertEqual(tree.xpath("devices/interface[@type='bridge']/boot/@order"), ["1"])
self.assertEqual(tree.xpath("devices/disk[target/@dev='vdb']/boot/@order"), ["2"])
def test_the_video_model_is_a_value(self):
p = proxy()
p.set_video_model("vga' heads='9")
model = defined(p).find("devices/video/model")
self.assertEqual(model.attrib, {"type": "vga' heads='9"})
p._defineXML.assert_called_once()
def test_qos_changes_go_through_define_xml(self):
for change in (
lambda p: p.set_qos("52:54:10:00:00:01", "outbound", 1, 2, 3),
lambda p: p.unset_qos("52:54:10:00:00:01", "inbound"),
):
p = proxy()
change(p)
p._defineXML.assert_called_once()
p.wvm.defineXML.assert_not_called()
def test_a_qos_edit_keeps_the_floor(self):
p = proxy()
p.set_qos("52:54:10:00:00:01", "inbound", 10, 20, 30)
inbound = defined(p).find("devices/interface[@type='network']/bandwidth/inbound")
self.assertEqual(inbound.attrib, {"average": "10", "peak": "20", "burst": "30", "floor": "4"})
def test_a_new_video_model_stays_the_primary_one(self):
# the old model's memory sizes are its own: they go
p = proxy()
p._XMLDesc = lambda flags: PERSISTENT.replace("<model type='vga'/>", "<model type='vga' vram='16384' primary='yes'/>")
p.set_video_model("virtio")
self.assertEqual(defined(p).find("devices/video/model").attrib, {"type": "virtio", "primary": "yes"})
def test_the_agent_channel_and_vcpu_hot_plug_are_read_from_either_definition(self):
p = proxy()
pending = ("<channel type='unix'><target type='virtio' name='org.qemu.guest_agent.0'/></channel>"
"<video>")
vcpus = "<vcpus><vcpu id='0' enabled='yes' hotpluggable='no'/><vcpu id='1' enabled='no' hotpluggable='yes'/></vcpus>"
p._XMLDesc = lambda flags: (PERSISTENT.replace("<video>", pending).replace("<devices>", vcpus + "<devices>")
if flags == PERSISTENT_XML else LIVE)
self.assertEqual((p.get_guest_agent(), p.get_vcpus()), (None, {}))
self.assertIsNotNone(p.get_guest_agent(config=True))
self.assertEqual([v["hotpluggable"] for v in p.get_vcpus(config=True).values()], ["no", "yes"])
def test_the_rng_device_is_the_panel_s_own_or_left_to_the_xml_editor(self):
p = proxy()
self.assertIsNone(p.get_rng())
p.set_rng(True)
self.assertEqual(etree.tostring(defined(p).find("devices/rng")).decode(),
"<rng model=\"virtio\"><backend model=\"random\">/dev/urandom</backend></rng>")
added = etree.tostring(defined(p)).decode()
p._XMLDesc = lambda flags: added
self.assertEqual(p.get_rng(), "virtio")
p.set_rng(False)
self.assertIsNone(defined(p).find("devices/rng"))
# the panel's device with the address libvirt gives it; another RNG device, or the panel's with
# a rate or driver, is shown as custom and changed only in the XML editor
backend = "<backend model='random'>{}</backend>"
for inner, found in ((backend.format("/dev/urandom") + "<address type='pci' bus='0x08' slot='0x00'/>", "virtio"),
(backend.format("/dev/hwrng"), "custom"),
(backend.format("/dev/urandom") + "<rate bytes='1024' period='1000'/>", "custom"),
(backend.format("/dev/urandom") + "<driver iommu='on'/>", "custom")):
with self.subTest(inner):
p = proxy()
p._XMLDesc = lambda flags, inner=inner: PERSISTENT.replace("<video>", f"<rng model='virtio'>{inner}</rng><video>")
self.assertEqual(p.get_rng(), found)
if found == "custom":
with self.assertRaises(util.OperationError):
p.set_rng(False)
p._defineXML.assert_not_called()
def test_an_emulated_tpm_2_is_added_with_the_host_s_model_or_left_to_the_xml_editor(self):
p = proxy()
p.get_tpm_model = lambda arch, machine: "tpm-crb"
p.get_arch, p.get_machine_type = lambda: "x86_64", lambda: "q35"
self.assertIsNone(p.get_tpm())
p.set_tpm(True)
tpm = defined(p).find("devices/tpm")
self.assertEqual((tpm.get("model"), dict(tpm.find("backend").attrib)), ("tpm-crb", {"type": "emulator", "version": "2.0"}))
added = etree.tostring(defined(p)).decode()
p._XMLDesc = lambda flags: added
self.assertEqual(p.get_tpm(), "emulator")
p.set_tpm(False)
self.assertIsNone(defined(p).find("devices/tpm"))
# a host without swtpm adds none
p = proxy()
p.get_tpm_model = lambda arch, machine: None
p.get_arch, p.get_machine_type = lambda: "x86_64", lambda: "q35"
with self.assertRaises(util.OperationError):
p.set_tpm(True)
p._defineXML.assert_not_called()
# libvirt's alias leaves it the panel's
p = proxy()
p._XMLDesc = lambda flags: PERSISTENT.replace(
"<video>", "<tpm model='tpm-crb'><backend type='emulator' version='2.0'/><alias name='tpm0'/></tpm><video>")
self.assertEqual(p.get_tpm(), "emulator")
# a passthrough TPM, a TPM 1.2, an encrypted one or one with its own state path is the VM's own
for backend in ("<backend type='passthrough'><device path='/dev/tpm0'/></backend>",
"<backend type='emulator' version='1.2'/>",
"<backend type='emulator' version='2.0'><encryption secret='aaaaaaaa-0000-4000-8000-000000000001'/></backend>",
"<backend type='emulator' version='2.0'><source type='dir' path='/srv/tpm'/></backend>",
"<backend type='emulator' version='2.0' persistent_state='yes'/>"):
with self.subTest(backend):
p = proxy()
p._XMLDesc = lambda flags, b=backend: PERSISTENT.replace("<video>", f"<tpm model='tpm-tis'>{b}</tpm><video>")
self.assertEqual(p.get_tpm(), "custom")
with self.assertRaises(util.OperationError):
p.set_tpm(False)
p._defineXML.assert_not_called()
def test_the_crash_action_is_restart_or_stop(self):
p = proxy()
self.assertEqual(p.get_on_crash(), "destroy") # libvirt's default
p.set_on_crash("restart")
self.assertEqual(defined(p).findtext("on_crash"), "restart")
with self.assertRaises(util.OperationError):
p.set_on_crash("preserve")
p._defineXML.assert_called_once()
def test_cpu_title_and_description_are_read_from_either_definition(self):
p = proxy()
self.assertEqual((p.get_vcpu(), p.get_cur_vcpu(), p.get_title(), p.get_description()), (1, 1, "", ""))
self.assertEqual((p.get_vcpu(config=True), p.get_cur_vcpu(config=True), p.get_title(config=True)), (4, 2, "pending"))
def test_boot_video_and_console_are_read_from_either_definition(self):
p = proxy()
live = (p.get_bootmenu(), p.get_bootorder(), p.get_media_devices(), p.get_video_model(),
p.get_console_listener_addr(), p.get_console_passwd(), p.get_console_keymap())
self.assertEqual(live, (False, {}, [], None, "127.0.0.1", None, ""))
self.assertEqual((p.get_bootmenu(config=True), p.get_bootorder(config=True), p.get_video_model(config=True)),
(True, {0: {"type": "file", "dev": "cdrom", "target": "sda"}}, "vga"))
self.assertEqual([m["dev"] for m in p.get_media_devices(config=True)], ["sda"])
self.assertEqual((p.get_console_listener_addr(config=True), p.get_console_passwd(config=True),
p.get_console_keymap(config=True)), ("0.0.0.0", "pending", "de"))
def test_a_boot_order_with_a_device_not_in_the_definition_changes_nothing(self):
p = proxy()
for choice in ({"type": "disk", "dev": "vdz"}, {"type": "network", "dev": "52:54:10:00:00:99"},
{"type": "floppy", "dev": "fda"}):
with self.subTest(choice), self.assertRaises(util.OperationError):
p.set_bootorder({0: {"type": "disk", "dev": "vda"}, 1: choice})
p._defineXML.assert_not_called()
def test_a_boot_order_holding_a_device_the_list_does_not_show_is_not_replaced(self):
for other in ("<hostdev mode='subsystem' type='pci'><boot order='2'/></hostdev>",
"<disk type='file' device='floppy'><target dev='fda'/><boot order='2'/></disk>"):
with self.subTest(other):
p = proxy()
p._XMLDesc = lambda flags, other=other: PERSISTENT.replace("<video>", other + "<video>")
with self.assertRaises(util.OperationError):
p.set_bootorder({0: {"type": "disk", "dev": "vda"}})
p._defineXML.assert_not_called()
def test_the_default_nic_model_leaves_the_choice_to_libvirt(self):
p = proxy()
p.change_network("52:54:10:00:00:01", "52:54:10:00:00:01", "default", "net", "default", "")
self.assertIsNone(defined(p).find("devices/interface[@type='network']/model"))
p = proxy(active=False)
p.add_network("52:54:10:00:00:09", "default", model="default")
xml, _ = p.instance.attachDeviceFlags.call_args.args
self.assertIsNone(etree.fromstring(xml).find("model"))
class ResizeMemTestCase(unittest.TestCase):
def test_an_active_vm_changes_only_its_current_memory(self):
# a paused VM is active too: its guest applies the balloon once resumed
p = proxy(active=True)
p.resize_mem(96, 160)
self.assertEqual(
[c.args for c in p.instance.setMemoryFlags.call_args_list],
[(96 * 1024, VIR_DOMAIN_AFFECT_LIVE), (96 * 1024, VIR_DOMAIN_AFFECT_CONFIG)],
)
p._defineXML.assert_not_called()
def test_a_stopped_vm_gets_both_values_in_its_definition(self):
p = proxy(active=False)
p._XMLDesc = lambda flags: "<domain><memory>131072</memory><currentMemory>131072</currentMemory></domain>"
p.resize_mem(96, 160)
tree = defined(p)
self.assertEqual((tree.findtext("memory"), tree.findtext("currentMemory")), (str(160 * 1024), str(96 * 1024)))
p.instance.setMemoryFlags.assert_not_called()
if __name__ == "__main__":
unittest.main()