"""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 = """pending4hvm """ LIVE = """1hvm """ 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: "4" 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("", "") 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 = ("" "