"""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 django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
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_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_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_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 ("",
""):
with self.subTest(other):
p = proxy()
p._XMLDesc = lambda flags, other=other: PERSISTENT.replace("