"""wvmInstance.cached_reads(): inside the block the domain XML (per flags), the VM's state, the host capabilities and info, and each disk's volume are read from libvirt once; outside it every read goes to libvirt, as code that changes the VM needs.""" import unittest from unittest.mock import MagicMock, patch from django.conf import settings if not settings.configured: settings.configure(MAC_OUI="52:54:10") from libvirt import libvirtError from vrtManager.connection import wvmConnect from vrtManager.instance import wvmInstance class FakeDomain: def __init__(self): self.reads = [] self.fail = False def info(self): self.reads.append("info") return [1, 0, 0, 0, 0] def XMLDesc(self, flags): if self.fail: raise RuntimeError("libvirt error") self.reads.append(flags) return f"" class FakeConn: def __init__(self): self.caps = 0 self.defined = [] def getInfo(self): self.caps += 1 return ["x86_64", 2048, 4, 0, 1, 2, 2, 1] def getCapabilities(self): self.caps += 1 return "" def defineXML(self, xml): self.defined.append(xml) def proxy(): p = wvmInstance.__new__(wvmInstance) # no connection: only these calls p.instance = FakeDomain() p.wvm = FakeConn() return p class CachedReadsTestCase(unittest.TestCase): def test_outside_the_block_every_read_goes_to_libvirt(self): p = proxy() p._XMLDesc(0) p._XMLDesc(0) p.get_cap_xml() p.get_cap_xml() self.assertEqual((p.instance.reads, p.wvm.caps), ([0, 0], 2)) def test_inside_the_block_each_flags_value_is_read_once(self): p = proxy() with p.cached_reads(): first = [p._XMLDesc(0), p._XMLDesc(1), p._XMLDesc(3)] again = [p._XMLDesc(0), p._XMLDesc(1), p._XMLDesc(3)] p.get_cap_xml() p.get_cap_xml() self.assertEqual(first, again) self.assertEqual(p.instance.reads, [0, 1, 3]) self.assertEqual(p.wvm.caps, 1) def test_the_block_ends_with_its_cache(self): p = proxy() with p.cached_reads(): p._XMLDesc(0) with p.cached_reads(): p._XMLDesc(0) p._XMLDesc(0) self.assertEqual(p.instance.reads, [0, 0, 0]) def test_an_error_inside_the_block_still_ends_it(self): p = proxy() with self.assertRaises(ValueError), p.cached_reads(): p._XMLDesc(0) raise ValueError p._XMLDesc(0) self.assertEqual(p.instance.reads, [0, 0]) def test_a_failed_read_is_not_cached(self): p = proxy() with p.cached_reads(): p.instance.fail = True with self.assertRaises(RuntimeError): p._XMLDesc(0) p.instance.fail = False p._XMLDesc(0) self.assertEqual(p.instance.reads, [0]) def test_a_definition_change_drops_the_cache(self): p = proxy() with p.cached_reads(): p._XMLDesc(3) p._defineXML("") p._XMLDesc(3) self.assertEqual(p.instance.reads, [3, 3]) def test_blocks_do_not_nest(self): p = proxy() with p.cached_reads(), self.assertRaises(RuntimeError), p.cached_reads(): pass def test_two_proxies_do_not_share_a_cache(self): a, b = proxy(), proxy() with a.cached_reads(): a._XMLDesc(0) b._XMLDesc(0) b._XMLDesc(0) self.assertEqual((a.instance.reads, b.instance.reads), ([0], [0, 0])) def test_the_state_and_host_info_are_read_once(self): p = proxy() with p.cached_reads(): for _ in range(2): p.get_status() p.get_max_memory() p.get_max_cpus() self.assertEqual((p.instance.reads, p.wvm.caps), (["info"], 1)) p.get_status() self.assertEqual(p.instance.reads, ["info", "info"]) def test_domain_capabilities_are_read_once_per_arch_and_machine(self): p = proxy() with patch.object(wvmConnect, "get_dom_cap_xml", return_value="") as read: with p.cached_reads(): for _ in range(2): p.get_dom_cap_xml("x86_64", "q35") p.get_dom_cap_xml("x86_64", "pc") p.get_dom_cap_xml("x86_64", "q35") self.assertEqual([c.args for c in read.call_args_list], [("x86_64", "q35"), ("x86_64", "pc"), ("x86_64", "q35")]) def volume_proxy(paths): """A proxy whose host has volumes at paths (pool "pool", size 10, used 4).""" p = proxy() p.lookups = [] def lookup(path): p.lookups.append(path) if path not in paths: raise libvirtError("no storage vol with matching path") vol = MagicMock() vol.name.return_value = path.rsplit("/", 1)[-1] vol.info.return_value = [0, 10, 4] vol.storagePoolLookupByVolume.return_value.name.return_value = "pool" return vol p.get_volume_by_path = lookup return p class VolumeDetailsTestCase(unittest.TestCase): def test_inside_the_block_each_path_is_looked_up_once(self): p = volume_proxy({"/p/a.qcow2", "/p/b.raw"}) with p.cached_reads(): for _ in range(2): self.assertEqual(p._volume_details("/p/a.qcow2"), ("a.qcow2", 10, 4, "pool")) p._volume_details("/p/b.raw") self.assertIsNone(p._volume_details("/dev/sdx")) self.assertEqual(p.lookups, ["/p/a.qcow2", "/p/b.raw", "/dev/sdx"]) def test_outside_the_block_and_after_a_change_the_volume_is_read_again(self): p = volume_proxy({"/p/a.qcow2"}) p._volume_details("/p/a.qcow2") with p.cached_reads(): p._volume_details("/p/a.qcow2") p._defineXML("") p._volume_details("/p/a.qcow2") p._volume_details("/p/a.qcow2") self.assertEqual(len(p.lookups), 4) def disk(path, cache): return ( f"" f"" ) class DiskListsTestCase(unittest.TestCase): """The page lists the running and the persistent disks; both share a path's volume, each keeps its own settings.""" def lists(self, live, config, volumes): p = volume_proxy(volumes) xml = {0: f"{live}", 3: f"{config}"} p.instance.XMLDesc = lambda flags: xml[flags] with p.cached_reads(): return p, p.get_disk_devices(), p.get_disk_devices(config=True) def test_one_path_in_both_lists_is_looked_up_once(self): p, live, config = self.lists(disk("/p/a.qcow2", "none"), disk("/p/a.qcow2", "writeback"), {"/p/a.qcow2"}) self.assertEqual(p.lookups, ["/p/a.qcow2"]) self.assertEqual([(d["image"], d["storage"], d["size"], d["cache"]) for d in live], [("a.qcow2", "pool", 10, "none")]) self.assertEqual([d["cache"] for d in config], ["writeback"]) self.assertIsNot(live[0], config[0]) def test_a_pending_disk_change_is_looked_up_on_its_own(self): p, live, config = self.lists(disk("/p/a.qcow2", "none"), disk("/p/b.qcow2", "none"), {"/p/a.qcow2", "/p/b.qcow2"}) self.assertEqual(p.lookups, ["/p/a.qcow2", "/p/b.qcow2"]) self.assertEqual((live[0]["image"], config[0]["image"]), ("a.qcow2", "b.qcow2")) def test_a_disk_outside_any_pool_shows_its_path(self): p, live, _ = self.lists(disk("/srv/x.img", "none"), disk("/srv/x.img", "none"), set()) self.assertEqual(p.lookups, ["/srv/x.img"]) self.assertEqual((live[0]["image"], live[0]["storage"], live[0]["size"]), ("/srv/x.img", None, None))