1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-10 12:16:49 +00:00

test: each app keeps its tests in a tests package

131 test modules sat next to the code they test; instances alone had
51 of them beside views.py and utils.py. They move into a
tests/ package per app, and the JavaScript checks into
webvirtcloud/tests/js. Each non-empty tests.py becomes tests/test_views.py;
the empty generated stubs are removed.

Imports of the app's own modules become absolute, paths built from
__file__ go one level up, the live helper is instances.tests.livetest,
and coverage omits */tests/*. The suite discovers the same 960 tests as
before.
This commit is contained in:
catborise 2026-10-12 10:37:29 +03:00
parent 0906838193
commit c55f52f025
154 changed files with 27 additions and 39 deletions

View file

View file

@ -0,0 +1,80 @@
import unittest
from unittest.mock import MagicMock
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from libvirt import libvirtError
from vrtManager.instance import wvmInstance
DOMAIN_XML = """<domain><devices>
<interface type='network'><mac address='52:54:00:00:00:01'/><source network='default'/><model type='virtio'/></interface>
<interface type='network'><mac address='52:54:00:00:00:02'/><source network='default'/><model type='virtio'/>
<bandwidth><outbound average='1000'/></bandwidth><link state='down'/>
<address type='pci' domain='0x0000' bus='0x02' slot='0x00' function='0x0'/></interface>
</devices></domain>"""
def stopped_instance():
inst = wvmInstance.__new__(wvmInstance)
inst.instance = MagicMock()
inst.wvm = MagicMock()
inst._XMLDesc = MagicMock(return_value=DOMAIN_XML)
inst.get_status = MagicMock(return_value=5)
return inst
class TestChangeNetwork(unittest.TestCase):
def test_edits_the_nic_in_place_and_keeps_its_other_settings(self):
from lxml import etree
inst = stopped_instance()
inst.change_network("52:54:00:00:00:02", "52:54:00:00:00:03", "br0", "bridge", "e1000", "clean-traffic")
defined = etree.fromstring(inst.wvm.defineXML.call_args[0][0])
first, second = defined.findall("devices/interface")
self.assertEqual(first.find("mac").get("address"), "52:54:00:00:00:01")
self.assertEqual(second.get("type"), "bridge")
self.assertEqual(second.find("mac").get("address"), "52:54:00:00:00:03")
self.assertEqual(second.find("source").attrib, {"bridge": "br0"})
self.assertEqual(second.find("model").get("type"), "e1000")
self.assertEqual(second.find("filterref").get("filter"), "clean-traffic")
# QoS, link state and the PCI address survive
self.assertEqual(second.find("bandwidth/outbound").get("average"), "1000")
self.assertEqual(second.find("link").get("state"), "down")
self.assertEqual(second.find("address").get("bus"), "0x02")
def test_unknown_old_mac_changes_nothing(self):
inst = stopped_instance()
with self.assertRaises(libvirtError):
inst.change_network("52:54:00:00:00:09", "52:54:00:00:00:03", "default", "net", "virtio", "")
inst.wvm.defineXML.assert_not_called()
class TestAddNetworkXml(unittest.TestCase):
def attached(self, **kwargs):
from lxml import etree
inst = stopped_instance()
inst.add_network(**kwargs)
return etree.fromstring(inst.instance.attachDeviceFlags.call_args[0][0])
def test_posted_values_cannot_inject_elements(self):
iface = self.attached(
mac_address="52:54:00:00:00:05",
source="default",
model="virtio'/><filesystem type='mount'/><model type='x",
nwfilter="f'/><evil/>",
)
self.assertEqual([el.tag for el in iface], ["mac", "source", "model", "filterref"])
self.assertEqual(iface.find("model").get("type"), "virtio'/><filesystem type='mount'/><model type='x")
def test_missing_mac_is_left_to_libvirt(self):
iface = self.attached(mac_address="", source="default")
self.assertIsNone(iface.find("mac"))
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,68 @@
"""A clone of a UEFI VM gets its own copy of the VM's NVRAM, with the
original's format, owner and mode, wherever the definition names the file."""
import contextlib
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 wvmInstance
ORIGINAL = """<volume><name>vm_VARS.qcow2</name><key>k</key><capacity>540672</capacity>
<target><path>/var/lib/libvirt/qemu/nvram/vm_VARS.qcow2</path><format type='qcow2'/>
<permissions><mode>0600</mode><owner>107</owner><group>107</group></permissions></target></volume>"""
def domain(nvram):
return f"""<domain><name>vm</name><uuid>u</uuid><os><type>hvm</type>
<loader readonly='yes' type='pflash'>/usr/share/edk2/ovmf/OVMF_CODE.fd</loader>{nvram}</os><devices/></domain>"""
def proxy(nvram):
p = wvmInstance.__new__(wvmInstance) # no connection: only these calls
p.get_status = lambda: 5
p.get_instances = lambda: []
p._XMLDesc = lambda flags: domain(nvram)
p._defineXML = MagicMock()
p.pool = MagicMock()
p.pool.storageVolLookupByName.return_value.XMLDesc.return_value = ORIGINAL
p.pool.createXMLFrom.return_value.path.return_value = "/var/lib/libvirt/qemu/nvram/copy_VARS.qcow2"
p._nvram_pool = lambda: contextlib.nullcontext(p.pool)
return p
class CloneNvramTestCase(unittest.TestCase):
def test_the_copy_keeps_the_format_owner_and_mode(self):
p = proxy("<nvram format='qcow2'>/var/lib/libvirt/qemu/nvram/vm_VARS.qcow2</nvram>")
p.clone_instance({"name": "copy"})
xml, original, _ = p.pool.createXMLFrom.call_args.args
volume = etree.fromstring(xml)
self.assertEqual(volume.findtext("name"), "copy_VARS.qcow2")
self.assertEqual(volume.find("target/format").get("type"), "qcow2")
self.assertEqual(volume.findtext("target/permissions/owner"), "107")
self.assertIsNone(volume.find("target/path"))
self.assertIs(original, p.pool.storageVolLookupByName.return_value)
(defined,), _ = p._defineXML.call_args
self.assertEqual(etree.fromstring(defined).findtext("os/nvram"), "/var/lib/libvirt/qemu/nvram/copy_VARS.qcow2")
def test_an_nvram_source_element_points_at_the_copy(self):
p = proxy("<nvram type='file'>\n <source file='/var/lib/libvirt/qemu/nvram/vm_VARS.qcow2'/>\n</nvram>")
p.clone_instance({"name": "copy"})
p.pool.storageVolLookupByName.assert_called_once_with("vm_VARS.qcow2")
(defined,), _ = p._defineXML.call_args
self.assertEqual(
etree.fromstring(defined).find("os/nvram/source").get("file"), "/var/lib/libvirt/qemu/nvram/copy_VARS.qcow2"
)
def test_an_nvram_that_is_not_a_file_is_refused_before_any_copy(self):
p = proxy("<nvram type='network'><source protocol='iscsi' name='x'/></nvram>")
with self.assertRaises(util.OperationError):
p.clone_instance({"name": "copy"})
p.pool.createXMLFrom.assert_not_called()

View file

@ -0,0 +1,276 @@
"""Reachability probe and connections that do not block each other."""
import os
import socket
import threading
import time
import unittest
from unittest.mock import MagicMock, patch
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
import libvirt
from vrtManager import connection
from vrtManager.connection import CONN_SSH, CONN_TCP, SSH_COMMAND, wvmConnectionManager
def closed_port():
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.close()
return port
class HostIsUpTestCase(unittest.TestCase):
def setUp(self):
self.manager = wvmConnectionManager.__new__(wvmConnectionManager)
def test_reachable_host_is_true(self):
server = socket.socket()
server.bind(("127.0.0.1", 0))
server.listen(1)
self.addCleanup(server.close)
self.assertIs(self.manager.host_is_up(CONN_TCP, f"127.0.0.1:{server.getsockname()[1]}"), True)
def test_unreachable_host_is_false_not_an_exception(self):
self.assertIs(self.manager.host_is_up(CONN_TCP, f"127.0.0.1:{closed_port()}"), False)
class ConnectionManagerTestCase(unittest.TestCase):
def setUp(self):
patcher = patch("vrtManager.connection.wvmEventLoop")
patcher.start()
self.addCleanup(patcher.stop)
self.manager = wvmConnectionManager()
def test_a_slow_host_does_not_block_other_hosts(self):
def open_auth(uri, auth, flags):
if "slow" in uri:
time.sleep(1.5)
raise __import__("libvirt").libvirtError("refused")
with patch("vrtManager.connection.libvirt.openAuth", side_effect=open_auth):
slow = threading.Thread(target=lambda: self.assertRaises(Exception, self.manager.get_connection, "slow", "u", "p", CONN_TCP))
slow.start()
time.sleep(0.2)
started = time.monotonic()
with self.assertRaises(Exception):
self.manager.get_connection("fast", "u", "p", CONN_TCP)
self.assertLess(time.monotonic() - started, 1.0)
slow.join()
def alive_connection():
conn = MagicMock()
conn.isAlive.return_value = 1
return conn
class BoundedOpenTestCase(unittest.TestCase):
"""Opening waits at most CONNECT_TIMEOUT; a failed host is not retried
for RETRY_AFTER; one open per connection at a time."""
def setUp(self):
for name, value in (("wvmEventLoop", MagicMock()), ("CONNECT_TIMEOUT", 0.3), ("RETRY_AFTER", 1.0)):
patcher = patch(f"vrtManager.connection.{name}", value)
patcher.start()
self.addCleanup(patcher.stop)
self.manager = wvmConnectionManager()
self.release = threading.Event()
self.addCleanup(self.release.set)
self.calls = 0
def open_auth(self, result):
def open_auth(uri, auth, flags):
self.calls += 1
self.release.wait(5)
if isinstance(result, Exception):
raise result
return result
return patch("vrtManager.connection.libvirt.openAuth", side_effect=open_auth)
def get(self, host="h"):
started = time.monotonic()
try:
return self.manager.get_connection(host, "u", "p", CONN_TCP), time.monotonic() - started
except libvirt.libvirtError as err:
return err, time.monotonic() - started
def wait_for_the_open(self, host="h"):
"""Wait until the released open has published its connection."""
entry = self.manager._search_connection(host, "u", "p", CONN_TCP)
deadline = time.monotonic() + 5
while entry.connection is None and time.monotonic() < deadline:
time.sleep(0.01)
def test_a_hanging_open_is_given_up_after_the_timeout(self):
with self.open_auth(alive_connection()):
result, took = self.get()
self.assertIsInstance(result, libvirt.libvirtError)
self.assertIn("did not answer within", str(result))
self.assertLess(took, 1.0)
def test_within_retry_after_the_last_error_comes_at_once(self):
with self.open_auth(alive_connection()):
self.get()
result, took = self.get()
self.assertIn("did not answer within", str(result))
self.assertLess(took, 0.1)
self.assertEqual(self.calls, 1)
def test_a_late_success_is_used(self):
conn = alive_connection()
with self.open_auth(conn):
self.get()
self.release.set()
self.wait_for_the_open()
result, took = self.get()
self.assertIs(result, conn)
self.assertEqual(self.calls, 1)
def test_a_failed_host_is_tried_again_after_retry_after(self):
self.release.set()
with self.open_auth(libvirt.libvirtError("refused")):
self.get()
self.get()
self.assertEqual(self.calls, 1)
time.sleep(1.1)
self.get()
self.assertEqual(self.calls, 2)
def test_concurrent_requests_share_one_open(self):
with self.open_auth(alive_connection()):
threads = [threading.Thread(target=self.get) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
self.assertEqual(self.calls, 1)
def test_a_stuck_host_does_not_keep_other_hosts_from_connecting(self):
healthy = alive_connection()
def open_auth(uri, auth, flags):
if "stuck" in uri:
self.release.wait(5) # an open that never returns
return healthy
with patch("vrtManager.connection.libvirt.openAuth", side_effect=open_auth):
for i in range(12):
self.get(f"stuck{i}")
result, took = self.get("healthy")
self.assertIs(result, healthy)
def test_an_abandoned_open_is_not_waited_for_again_nor_duplicated(self):
conn = alive_connection()
with self.open_auth(conn):
self.get()
time.sleep(1.1) # past RETRY_AFTER, the open still runs
result, took = self.get()
self.assertIsInstance(result, libvirt.libvirtError)
self.assertLess(took, 0.1)
self.release.set() # the host answers at last
self.wait_for_the_open()
result, _ = self.get()
self.assertIs(result, conn)
self.assertEqual(self.calls, 1)
def test_a_late_close_callback_does_not_drop_the_new_connection(self):
self.release.set()
old, new = alive_connection(), alive_connection()
with self.open_auth(old):
self.get()
entry = self.manager._search_connection("h", "u", "p", CONN_TCP)
old.isAlive.return_value = 0 # seen dead, reconnected
with self.open_auth(new):
self.assertIs(self.get()[0], new)
entry._wvmConnection__connection_close_callback(old, 1)
self.assertIs(self.get()[0], new)
entry._wvmConnection__connection_close_callback(new, 2)
self.assertIsNone(entry.connection)
self.assertEqual(entry.last_error, "Connection closed (keepalive timeout)")
def test_a_connection_closed_before_it_is_published_is_an_error(self):
self.release.set()
dead = alive_connection()
dead.isAlive.return_value = 0
with self.open_auth(dead):
result, _ = self.get()
self.assertIn("closed right after opening", str(result))
def test_ssh_runs_ssh_with_a_connect_timeout(self):
self.release.set()
with patch("vrtManager.connection.libvirt.open", return_value=alive_connection()) as libvirt_open:
self.manager.get_connection("host", "root", "", CONN_SSH)
uri = libvirt_open.call_args[0][0]
self.assertTrue(uri.startswith("qemu+ssh://root@host/system?command="))
self.assertTrue(os.access(SSH_COMMAND, os.X_OK))
with open(SSH_COMMAND) as script:
self.assertIn("ConnectTimeout", script.read())
class EventLoopTestCase(unittest.TestCase):
"""The libvirt event loop thread starts with the first connection of a
process, not at import: novncd forks a child per console after importing
this module, and libvirt is not safe to use after a fork of a process
with running libvirt threads. A child starts its own loop, so its
keepalive works."""
def setUp(self):
for name in ("wvmEventLoop", "libvirt.virEventRegisterDefaultImpl", "libvirt.openAuth"):
patcher = patch(f"vrtManager.connection.{name}")
setattr(self, name.split(".")[-1], patcher.start())
self.addCleanup(patcher.stop)
self.openAuth.side_effect = lambda *args: alive_connection()
state = patch.dict(connection._event_loop, {"lock": threading.Lock(), "running": False, "registered": False})
state.start()
self.addCleanup(state.stop)
def test_no_thread_until_the_first_connection(self):
manager = wvmConnectionManager()
self.wvmEventLoop.assert_not_called()
self.virEventRegisterDefaultImpl.assert_not_called()
manager.get_connection("h", "u", "p", CONN_TCP)
wvmConnectionManager().get_connection("h2", "u", "p", CONN_TCP) # one loop per process
self.wvmEventLoop.return_value.start.assert_called_once()
self.virEventRegisterDefaultImpl.assert_called_once()
def test_a_forked_child_starts_its_own_loop_with_its_own_connections(self):
manager = connection.connection_manager
parent_conn = manager.get_connection("fork-host", "u", "p", CONN_TCP)
read_end, write_end = os.pipe()
pid = os.fork()
if pid == 0: # child: report and leave without running the test runner
try:
child_conn = manager.get_connection("fork-host", "u", "p", CONN_TCP)
kept = any(
entry.connection is parent_conn
for connections in manager._inherited
for entries in connections.values()
for entry in entries
)
report = (
f"{self.wvmEventLoop.return_value.start.call_count} "
f"{self.virEventRegisterDefaultImpl.call_count} {child_conn is parent_conn} {kept}"
)
except BaseException as e:
report = repr(e)
os.write(write_end, report.encode())
os._exit(0)
os.close(write_end)
os.waitpid(pid, 0)
report = os.read(read_end, 200).decode()
os.close(read_end)
# a second loop started in the child, on the inherited registration;
# the child opened its own connection instead of the parent's socket,
# and kept the parent's (releasing it would run libvirt destructors)
self.assertEqual(report, "2 1 False True")
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,46 @@
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.instance import PERSISTENT_XML, wvmInstance
LIVE = "<domain><devices><graphics type='vnc' passwd='live' keymap='en-us'><listen type='address'/></graphics></devices></domain>"
PERSISTENT = "<domain><devices><graphics type='vnc' passwd='old'><listen type='address'/></graphics></devices></domain>"
def running_vm_with_pending_changes():
vm = wvmInstance.__new__(wvmInstance)
vm.wvm = MagicMock()
vm._XMLDesc = MagicMock(side_effect=lambda flags: PERSISTENT if flags == PERSISTENT_XML else LIVE)
return vm
class ConsoleSettersTestCase(unittest.TestCase):
"""The graphics device to edit comes from the persistent definition."""
def defined_graphics(self, vm):
return etree.fromstring(vm.wvm.defineXML.call_args[0][0]).find("devices/graphics")
def test_setters_edit_the_persistent_graphics_device(self):
edits = {
"set_console_passwd": (lambda vm: vm.set_console_passwd("new"), "passwd", "new"),
"set_console_keymap": (lambda vm: vm.set_console_keymap("de"), "keymap", "de"),
"set_console_listener_addr": (lambda vm: vm.set_console_listener_addr("127.0.0.1"), "listen", "127.0.0.1"),
}
for name, (edit, attr, value) in edits.items():
with self.subTest(name):
vm = running_vm_with_pending_changes()
edit(vm)
graphics = self.defined_graphics(vm)
self.assertEqual(graphics.get(attr), value)
# Built from the persistent device, not the live one
self.assertNotEqual(graphics.get("passwd"), "live")
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,176 @@
"""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 = """<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_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_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_qos_is_read_from_either_definition_and_an_edit_keeps_the_floor(self):
p = proxy()
self.assertEqual(p.get_all_qos(config=True)["52:54:10:00:00:01"][0]["floor"], "4")
self.assertEqual(p.get_all_qos(), {}) # the running VM has none
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_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()

View file

@ -0,0 +1,36 @@
"""refresh_dhcp_leases must raise a real exception (handled as a libvirt error),
not a bare string (which Python turns into TypeError, masking the cause)."""
import unittest
from unittest.mock import Mock
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from vrtManager import util
from vrtManager.network import wvmNetwork
def network_with_leases(side_effect):
net = wvmNetwork.__new__(wvmNetwork)
net.leases = None
net.net = Mock()
net.net.DHCPLeases.side_effect = side_effect
return net
class RefreshDhcpLeasesTestCase(unittest.TestCase):
def test_a_lease_error_raises_an_operation_error(self):
net = network_with_leases(Exception("no DHCP on this network"))
with self.assertRaises(util.OperationError):
net.refresh_dhcp_leases()
def test_the_error_keeps_the_cause_and_clears_leases(self):
cause = Exception("boom")
net = network_with_leases(cause)
with self.assertRaises(util.OperationError) as caught:
net.refresh_dhcp_leases()
self.assertIs(caught.exception.__cause__, cause)
self.assertEqual(net.leases, [])

View file

@ -0,0 +1,134 @@
"""A wvmConnect reads the host's info and capabilities once, and its domain
capabilities once per emulator, arch, machine and domain type: the create
page's getters parse the capabilities some 60 times, the overview reads the
info four times. An object lives for one request. The interfaces page lists
only the host's network devices, not all of them, and knows whether the host
can change interfaces at all."""
import unittest
from unittest.mock import MagicMock
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from libvirt import VIR_CONNECT_LIST_NODE_DEVICES_CAP_NET, VIR_ERR_NO_SUPPORT, VIR_ERR_OPERATION_DENIED, VIR_ERR_XML_ERROR, libvirtError
from vrtManager.connection import wvmConnect
from vrtManager.hostdetails import wvmHostDetails
CAPABILITIES = """<capabilities><host><cpu><arch>x86_64</arch></cpu></host>
<guest><os_type>hvm</os_type><arch name='x86_64'><emulator>/usr/bin/qemu-kvm</emulator>
<machine>pc-q35-9.0</machine><machine canonical='pc-q35-9.0'>q35</machine><machine canonical='pc-i440fx-9.0'>pc</machine>
<domain type='qemu'/><domain type='kvm'/></arch></guest></capabilities>"""
class FakeConn:
def __init__(self):
self.reads = []
def getCapabilities(self):
self.reads.append("capabilities")
return CAPABILITIES
def getInfo(self):
self.reads.append("info")
return ["x86_64", 2048, 4, 0, 1, 2, 2, 1]
def getMemoryStats(self, cell, flags):
self.reads.append("memory")
return {"buffers": 0, "free": 1048576, "cached": 0}
def getHostname(self):
return "host"
def getSysinfo(self, flags):
return "<sysinfo/>"
def getURI(self):
return "qemu:///system"
def getDomainCapabilities(self, emulator, arch, machine, virttype):
self.reads.append((emulator, arch, machine, virttype))
return "<domainCapabilities/>"
def connection(cls=wvmConnect):
conn = cls.__new__(cls) # no libvirt connection: only these calls
conn.wvm = FakeConn()
return conn
class CapabilitiesOnceTestCase(unittest.TestCase):
def test_the_capabilities_are_read_once_for_all_getters(self):
conn = connection()
for _ in range(2):
conn.get_emulator("x86_64")
conn.get_machine_types("x86_64")
conn.get_hypervisors_domain_types()
conn.get_cap_xml()
self.assertEqual(conn.wvm.reads, ["capabilities"])
def test_domain_capabilities_are_read_once_per_machine(self):
conn = connection()
for _ in range(2):
conn.get_dom_cap_xml("x86_64", "q35")
conn.get_dom_cap_xml("x86_64", "pc")
conn.get_dom_cap_xml("x86_64", "unknown") # falls back to pc
self.assertEqual(
conn.wvm.reads,
["capabilities", ("/usr/bin/qemu-kvm", "x86_64", "q35", "kvm"), ("/usr/bin/qemu-kvm", "x86_64", "pc", "kvm")],
)
def test_the_overview_reads_the_info_once_and_the_free_memory_each_time(self):
conn = connection(wvmHostDetails)
conn.get_node_info()
conn.get_memory_usage()
self.assertEqual(conn.get_memory_usage()["total"], 2048 * 1048576)
self.assertEqual(conn.wvm.reads, ["info", "memory", "memory"])
def test_two_objects_do_not_share_their_reads(self):
a, b = connection(), connection()
a.get_cap_xml()
b.get_cap_xml()
self.assertEqual((a.wvm.reads, b.wvm.reads), (["capabilities"], ["capabilities"]))
def test_a_failed_read_is_not_kept(self):
conn = connection()
conn.wvm.getCapabilities = lambda: (_ for _ in ()).throw(RuntimeError("libvirt error"))
with self.assertRaises(RuntimeError):
conn.get_cap_xml()
conn.wvm = FakeConn()
conn.get_cap_xml()
self.assertEqual(conn.wvm.reads, ["capabilities"])
class NetDevicesTestCase(unittest.TestCase):
def test_only_the_network_devices_are_listed_and_read(self):
conn = connection()
devices = {
VIR_CONNECT_LIST_NODE_DEVICES_CAP_NET: [
MagicMock(**{"XMLDesc.return_value": f"<device><capability type='net'><interface>{name}</interface>"
"<capability type='80203'/></capability></device>"})
for name in ("eth0", "br0")
]
}
conn.wvm.listAllDevices = lambda flags: devices.get(flags, [])
self.assertEqual(conn.get_net_devices(), ["eth0", "br0"])
class ChangeInterfacesTestCase(unittest.TestCase):
def error(self, code):
err = libvirtError("refused")
err.get_error_code = lambda: code
return err
def test_a_backend_without_define_cannot_change_interfaces(self):
conn = connection()
# any other refusal (access denied, ...) leaves the page as it was
for code, changeable in ((VIR_ERR_NO_SUPPORT, False), (VIR_ERR_XML_ERROR, True), (VIR_ERR_OPERATION_DENIED, True)):
with self.subTest(code=code):
conn.wvm.interfaceDefineXML = MagicMock(side_effect=self.error(code))
self.assertIs(conn.can_change_interfaces(), changeable)
# an invalid definition: nothing can be defined by asking
conn.wvm.interfaceDefineXML.assert_called_once_with("<interface/>", 0)

View file

@ -0,0 +1,82 @@
"""The VM page's device getters read each device on its own: a value of one
NIC, CD-ROM or boot device does not carry over to the next."""
import unittest
from unittest.mock import MagicMock
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from libvirt import libvirtError
from vrtManager.instance import wvmInstance
XML = """<domain>
<os><type arch='x86_64'>hvm</type></os>
<devices>
<disk type='file' device='disk'><source file='/pool/a.qcow2'/><target dev='vda' bus='virtio'/><boot order='3'/></disk>
<disk type='block' device='disk'><source dev='/dev/vg/b'/><target dev='vdb' bus='virtio'/><boot order='4'/></disk>
<disk type='file' device='cdrom'><source file='/iso/x.iso'/><target dev='sda' bus='sata'/></disk>
<disk type='file' device='cdrom'><target dev='sdb' bus='sata'/></disk>
<interface type='network'>
<mac address='52:54:10:00:00:01'/><source network='default'/><model type='virtio'/>
<bandwidth><inbound average='100' peak='200' burst='300'/><outbound average='10' peak='20' burst='30'/></bandwidth>
</interface>
<interface type='bridge'><mac address='52:54:10:00:00:02'/><source bridge='br0'/><boot order='1'/></interface>
<interface type='direct'><mac address='52:54:10:00:00:03'/><boot order='2'/></interface>
</devices>
</domain>"""
def proxy():
p = wvmInstance.__new__(wvmInstance) # no connection: only these calls
p._ip_cache = {"qemuga": {}, "arp": {}}
p._XMLDesc = lambda flags: XML
volumes = {"/iso/x.iso": ("x.iso", "iso")}
def volume(path):
if path not in volumes:
raise libvirtError("no volume")
name, pool = volumes[path]
return MagicMock(**{"name.return_value": name, "info.return_value": [0, 10, 4],
"storagePoolLookupByVolume.return_value.name.return_value": pool})
p.get_volume_by_path = volume
return p
class DeviceGettersTestCase(unittest.TestCase):
def test_a_nic_without_qos_source_or_model_has_none(self):
nics = proxy().get_net_devices()
self.assertEqual(nics[0]["inbound"], {"average": "100", "peak": "200", "burst": "300"})
self.assertEqual(nics[0]["outbound"], {"average": "10", "peak": "20", "burst": "30"})
self.assertEqual([(n["inbound"], n["outbound"]) for n in nics[1:]], [([], [])] * 2)
self.assertEqual([(n["nic"], n["model"]) for n in nics], [("default", "virtio"), ("br0", ""), ("", "")])
def test_an_empty_cdrom_shows_no_pool(self):
media = proxy().get_media_devices()
self.assertEqual(
media,
[
{"dev": "sda", "image": "x.iso", "storage": "iso", "path": "/iso/x.iso", "bus": "sata"},
{"dev": "sdb", "image": None, "storage": None, "path": None, "bus": "sata"},
],
)
def test_every_boot_device_has_its_target(self):
order = proxy().get_bootorder()
self.assertEqual(
[order[i]["target"] for i in range(4)],
["nic-00:00:02", "nic-00:00:03", "vda", "vdb"],
)
class RefreshInstancePoolsTestCase(unittest.TestCase):
def test_only_pools_holding_a_disk_are_refreshed(self):
p = proxy()
p.get_disk_devices = lambda: [{"path": "/pool/a.qcow2"}, {"path": "/elsewhere/b.img"}, {"path": None}]
pool = MagicMock()
p.get_wvmStorages = lambda: MagicMock(get_pool_by_target=lambda target: pool if target == "/pool" else None)
p.refresh_instance_pools()
pool.refresh.assert_called_once_with(0)

View file

@ -0,0 +1,54 @@
"""A running VM's addresses come from the guest agent when it answers and
from the host's ARP table, so a VM without an agent shows its IPv4 address."""
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 VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT, VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP
from vrtManager.instance import wvmInstance
MAC = "52:54:00:00:00:01"
ARP = {"vnet0": {"hwaddr": MAC, "addrs": [{"type": 0, "addr": "192.0.2.10", "prefix": 0}]}}
AGENT = {"eth0": {"hwaddr": MAC, "addrs": [{"type": 0, "addr": "192.0.2.20", "prefix": 24}]}}
def vm(agent):
inst = wvmInstance.__new__(wvmInstance)
inst._ip_cache = None
inst.instance = MagicMock()
inst.instance.interfaceAddresses.side_effect = lambda source: {
VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT: AGENT,
VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP: ARP,
}[source]
patch.object(inst, "get_status", return_value=1).start()
patch.object(inst, "is_agent_ready", return_value=agent).start()
return inst
class InterfaceAddressesTestCase(unittest.TestCase):
def tearDown(self):
patch.stopall()
def test_a_vm_without_an_agent_shows_its_arp_address(self):
inst = vm(agent=False)
self.assertEqual(inst.get_interface_addresses(MAC), (["192.0.2.10"], []))
self.assertEqual(
[c.args for c in inst.instance.interfaceAddresses.call_args_list], [(VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP,)]
)
def test_the_agents_address_comes_first(self):
inst = vm(agent=True)
self.assertEqual(inst.get_interface_addresses(MAC), (["192.0.2.20"], []))
self.assertEqual(
[c.args for c in inst.instance.interfaceAddresses.call_args_list],
[(VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT,), (VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP,)],
)
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,99 @@
import unittest
from unittest.mock import MagicMock
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from libvirt import (
VIR_CONNECT_LIST_STORAGE_POOLS_ACTIVE,
VIR_DOMAIN_AFFECT_CONFIG,
VIR_DOMAIN_AFFECT_LIVE,
libvirtError,
)
from lxml import etree
from vrtManager.instance import wvmInstance
DOMAIN = """<domain><devices>
<disk type='file' device='cdrom'><driver name='qemu' type='raw'/><target dev='sda' bus='sata'/></disk>
<disk type='file' device='cdrom'><driver name='qemu' type='raw'/><target dev='sdb' bus='sata'/></disk>
</devices></domain>"""
def vm(active):
inst = wvmInstance.__new__(wvmInstance)
inst.instance = MagicMock()
inst.instance.isActive.return_value = active
inst._XMLDesc = MagicMock(return_value=DOMAIN)
return inst
class IsoMediaTestCase(unittest.TestCase):
"""Media change goes through updateDeviceFlags for the right CD-ROM."""
def updates(self, inst):
return [
(etree.fromstring(c.args[0]).find("target").get("dev"), etree.fromstring(c.args[0]).xpath("string(source/@file)"), c.args[1])
for c in inst.instance.updateDeviceFlags.call_args_list
]
def test_stopped_vm_changes_only_the_persistent_definition(self):
inst = vm(active=False)
inst._set_cdrom_media("sdb", "/iso/a.iso")
self.assertEqual(self.updates(inst), [("sdb", "/iso/a.iso", VIR_DOMAIN_AFFECT_CONFIG)])
def test_running_vm_changes_live_then_persistent(self):
inst = vm(active=True)
inst._set_cdrom_media("sdb", None)
self.assertEqual(
self.updates(inst), [("sdb", "", VIR_DOMAIN_AFFECT_LIVE), ("sdb", "", VIR_DOMAIN_AFFECT_CONFIG)]
)
def test_unknown_cdrom_changes_nothing(self):
inst = vm(active=True)
with self.assertRaises(libvirtError):
inst._set_cdrom_media("sdz", "/iso/a.iso")
inst.instance.updateDeviceFlags.assert_not_called()
def pool(volumes, refresh_error=False, list_error=False):
p = MagicMock()
if refresh_error:
p.refresh.side_effect = libvirtError("refresh failed")
if list_error:
p.listVolumes.side_effect = libvirtError("pool is not active")
else:
p.listVolumes.return_value = volumes
return p
class IsoListTestCase(unittest.TestCase):
"""The CD-ROM choices are the .iso volumes of the active pools, refreshed
so that an image copied in by hand shows up."""
def iso(self, *pools):
inst = wvmInstance.__new__(wvmInstance)
inst.wvm = MagicMock()
inst.wvm.listAllStoragePools.return_value = list(pools)
return inst, inst.get_iso_media()
def test_iso_volumes_of_the_active_pools(self):
a, b = pool(["a.iso", "disk.qcow2"]), pool(["B.ISO"])
inst, iso = self.iso(a, b)
self.assertEqual(iso, ["a.iso", "B.ISO"])
inst.wvm.listAllStoragePools.assert_called_once_with(VIR_CONNECT_LIST_STORAGE_POOLS_ACTIVE)
a.refresh.assert_called_once_with(0)
b.refresh.assert_called_once_with(0)
def test_a_failed_refresh_still_lists_the_pool(self):
_, iso = self.iso(pool(["a.iso"], refresh_error=True))
self.assertEqual(iso, ["a.iso"])
def test_a_pool_stopped_meanwhile_is_skipped(self):
_, iso = self.iso(pool([], list_error=True), pool(["b.iso"]))
self.assertEqual(iso, ["b.iso"])
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,249 @@
"""Live checks of bounded connection opens against real libvirt.
Runs only with TEST_LIBVIRT_HOST set (a TCP compute, see instances/tests/livetest.py);
TEST_LIBVIRT_SSH_HOST/_LOGIN add a reachable ssh compute. 192.0.2.1 is
TEST-NET-1: SYN packets to it are dropped, as for a host that went away
without a trace.
TEST_LIBVIRT_SSH_ALIASES=1 adds the ssh tests that need these entries in the
ssh configuration of the user running the tests (SSH_HOST is the ssh compute):
Host wvc-alias
HostName SSH_HOST
Host wvc-relay
HostName 127.0.0.1
Port 2222
HostKeyAlias SSH_HOST
Host wvc-silent
HostName 127.0.0.1
Port 2223
TEST_LIBVIRT_SSH_JUMP=1 adds a ProxyJump test through
Host wvc-jump
HostName SSH_HOST
ProxyJump user@another-host
"""
import os
import shutil
import socket
import subprocess
import threading
import time
import unittest
import libvirt
from vrtManager.connection import CONN_SSH, CONN_TCP, CONNECT_TIMEOUT, connection_manager
UNREACHABLE = "192.0.2.1"
KEEPALIVE_DEATH = 45 # keepalive (5, 5) closes a silent connection after ~30 s
def timed(*args):
started = time.monotonic()
try:
return connection_manager.get_connection(*args), time.monotonic() - started
except libvirt.libvirtError as err:
return err, time.monotonic() - started
def ssh_processes(host):
"""ssh client processes (exact name) whose arguments mention host."""
out = subprocess.run(["pgrep", "-a", "-x", "ssh"], capture_output=True, text=True).stdout
return [line for line in out.splitlines() if host in line]
class Relay:
"""TCP relay that can hold all traffic (frozen) and pass it again (thawed);
a frozen relay looks like a host that went silent."""
def __init__(self, target, port=0, frozen=False):
self.forwarding = threading.Event()
if not frozen:
self.forwarding.set()
self.server = socket.socket()
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind(("127.0.0.1", port))
self.server.listen(5)
self.port = self.server.getsockname()[1]
self.target = target
self.closed = False
self.sockets = []
threading.Thread(target=self.accept, daemon=True).start()
def accept(self):
while True:
try:
client, _ = self.server.accept()
except OSError:
return
upstream = socket.create_connection(self.target)
self.sockets += [client, upstream]
for a, b in ((client, upstream), (upstream, client)):
threading.Thread(target=self.pump, args=(a, b), daemon=True).start()
def pump(self, src, dst):
try:
while not self.closed:
if not self.forwarding.wait(0.2):
continue
src.settimeout(0.2)
try:
data = src.recv(65536)
except socket.timeout:
continue
if not data:
break
dst.sendall(data)
except OSError:
pass
def freeze(self):
self.forwarding.clear()
time.sleep(0.5) # let a recv that was already waiting run out
def thaw(self):
self.forwarding.set()
def close(self):
"""Stop, and end the relayed connections, as a host that went away would."""
self.closed = True
self.forwarding.set()
try:
self.server.shutdown(socket.SHUT_RDWR) # wakes the accept; close alone does not
except OSError:
pass
self.server.close()
for sock in self.sockets:
sock.close()
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_HOST"), "Set TEST_LIBVIRT_HOST to run the live libvirt tests")
class LiveBoundedOpenTestCase(unittest.TestCase):
def setUp(self):
self.host = os.environ["TEST_LIBVIRT_HOST"]
self.login = os.environ.get("TEST_LIBVIRT_LOGIN", "")
self.password = os.environ.get("TEST_LIBVIRT_PASSWORD", "")
self.ssh_host = os.environ.get("TEST_LIBVIRT_SSH_HOST")
self.ssh_login = os.environ.get("TEST_LIBVIRT_SSH_LOGIN", "root")
# Opening
def test_unreachable_tcp_host_fails_within_the_timeout_then_at_once(self):
args = (UNREACHABLE, "u", "p", CONN_TCP)
first, took = timed(*args)
self.assertIsInstance(first, libvirt.libvirtError)
self.assertLess(took, CONNECT_TIMEOUT + 1)
second, took = timed(*args)
self.assertIsInstance(second, libvirt.libvirtError)
self.assertLess(took, 0.5)
@unittest.skipUnless(shutil.which("ssh"), "no ssh client")
def test_unreachable_ssh_host_fails_within_the_timeout_and_leaves_no_ssh(self):
result, took = timed(UNREACHABLE, "root", "", CONN_SSH)
self.assertIsInstance(result, libvirt.libvirtError)
self.assertLess(took, CONNECT_TIMEOUT + 1)
time.sleep(12) # the wrapper's ConnectTimeout is 10 s
self.assertEqual(ssh_processes(UNREACHABLE), [], "an ssh process outlived its connect timeout")
def test_a_daemon_that_never_answers_fails_within_the_timeout(self):
silent = socket.socket()
silent.bind(("127.0.0.1", 0))
silent.listen(5) # accepts (kernel backlog), never answers
self.addCleanup(silent.close)
result, took = timed(f"127.0.0.1:{silent.getsockname()[1]}", "u", "p", CONN_TCP)
self.assertIsInstance(result, libvirt.libvirtError)
self.assertLess(took, CONNECT_TIMEOUT + 1)
def check_slow_open_completes_later(self, args, relay):
first, _ = timed(*args)
self.assertIsInstance(first, libvirt.libvirtError)
relay.thaw()
deadline = time.monotonic() + 15
while time.monotonic() < deadline:
conn, _ = timed(*args)
if not isinstance(conn, libvirt.libvirtError):
break
time.sleep(0.5)
self.assertNotIsInstance(conn, libvirt.libvirtError)
self.assertTrue(conn.getVersion())
def test_a_slow_tcp_open_that_completes_later_is_used(self):
relay = Relay((self.host, 16509), frozen=True)
self.addCleanup(relay.close)
self.check_slow_open_completes_later((f"127.0.0.1:{relay.port}", self.login, self.password, CONN_TCP), relay)
# A connection whose host goes silent
def check_silent_host_is_noticed_and_replaced(self, args, relay):
old = connection_manager.get_connection(*args)
relay.freeze()
outcome = []
def call():
try:
outcome.append(old.getVersion())
except libvirt.libvirtError as err:
outcome.append(err)
caller = threading.Thread(target=call, daemon=True)
caller.start()
caller.join(KEEPALIVE_DEATH) # keepalive must end the call by then
if caller.is_alive():
relay.close() # cut the connection so the stuck call returns
self.fail(f"a call on a silent connection still waits after {KEEPALIVE_DEATH} s")
self.assertIsInstance(outcome[0], libvirt.libvirtError)
relay.thaw()
new, _ = timed(*args)
self.assertNotIsInstance(new, libvirt.libvirtError)
self.assertIsNot(new, old)
self.assertTrue(new.getVersion())
def test_a_silent_tcp_host_is_noticed_and_reconnected(self):
relay = Relay((self.host, 16509))
self.addCleanup(relay.close)
self.check_silent_host_is_noticed_and_replaced((f"127.0.0.1:{relay.port}", self.login, self.password, CONN_TCP), relay)
# ssh
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_SSH_HOST"), "Set TEST_LIBVIRT_SSH_HOST (and _LOGIN) for an ssh compute")
def test_a_reachable_ssh_host_connects_through_the_wrapper(self):
conn, _ = timed(self.ssh_host, self.ssh_login, "", CONN_SSH)
self.assertNotIsInstance(conn, libvirt.libvirtError)
self.assertIn("command=", conn.getURI())
self.assertTrue(conn.getVersion())
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_SSH_ALIASES"), "Set TEST_LIBVIRT_SSH_ALIASES=1 (see the module docstring)")
def test_an_ssh_config_alias_is_used(self):
conn, _ = timed("wvc-alias", self.ssh_login, "", CONN_SSH)
self.assertNotIsInstance(conn, libvirt.libvirtError)
self.assertTrue(conn.getVersion())
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_SSH_ALIASES"), "Set TEST_LIBVIRT_SSH_ALIASES=1 (see the module docstring)")
def test_an_ssh_port_that_never_answers_fails_within_the_timeout(self):
silent = socket.socket()
silent.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
silent.bind(("127.0.0.1", 2223))
silent.listen(5) # no ssh banner ever comes
self.addCleanup(silent.close)
result, took = timed("wvc-silent", self.ssh_login, "", CONN_SSH)
self.assertIsInstance(result, libvirt.libvirtError)
self.assertLess(took, CONNECT_TIMEOUT + 1)
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_SSH_ALIASES"), "Set TEST_LIBVIRT_SSH_ALIASES=1 (see the module docstring)")
def test_a_slow_ssh_open_that_completes_later_is_used(self):
relay = Relay((self.ssh_host, 22), port=2222, frozen=True)
self.addCleanup(relay.close)
self.check_slow_open_completes_later(("wvc-relay", self.ssh_login, "", CONN_SSH), relay)
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_SSH_ALIASES"), "Set TEST_LIBVIRT_SSH_ALIASES=1 (see the module docstring)")
def test_a_silent_ssh_host_is_noticed_and_reconnected(self):
relay = Relay((self.ssh_host, 22), port=2222)
self.addCleanup(relay.close)
self.check_silent_host_is_noticed_and_replaced(("wvc-relay", self.ssh_login, "", CONN_SSH), relay)
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_SSH_JUMP"), "Set TEST_LIBVIRT_SSH_JUMP=1 (see the module docstring)")
def test_ssh_through_a_jump_host(self):
conn, _ = timed("wvc-jump", self.ssh_login, "", CONN_SSH)
self.assertNotIsInstance(conn, libvirt.libvirtError)
self.assertTrue(conn.getVersion())

View file

@ -0,0 +1,88 @@
"""Live check that the NVRAM file a migrated VM leaves behind is removed,
and kept when the destination sees the same directory.
Runs only with TEST_LIBVIRT_HOST set (see instances/tests/livetest.py). The
deletion needs a second host: TEST_LIBVIRT_SSH_HOST (and _LOGIN).
"""
import os
import unittest
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from instances.tests import livetest
from vrtManager.connection import CONN_SSH, NVRAM_DIR, connection_manager, wvmConnect
NAME = livetest.PREFIX + "nvram"
PATH = f"{NVRAM_DIR}/{NAME}_VARS.fd"
class LiveRemoveNvramTestCase(SimpleTestCase):
@classmethod
def setUpClass(cls):
if not livetest.enabled():
raise unittest.SkipTest("Set TEST_LIBVIRT_HOST to run the live libvirt tests")
cls.args = (
os.environ["TEST_LIBVIRT_HOST"],
os.environ.get("TEST_LIBVIRT_LOGIN", ""),
os.environ.get("TEST_LIBVIRT_PASSWORD", ""),
int(os.environ.get("TEST_LIBVIRT_TYPE", 4)),
)
cls.conn = connection_manager.get_connection(*cls.args)
livetest.cleanup(cls.conn)
cls.inventory = livetest.inventory(cls.conn)
cls.pools = {p.name() for p in cls.conn.listAllStoragePools()}
super().setUpClass()
@classmethod
def tearDownClass(cls):
livetest.cleanup(cls.conn)
super().tearDownClass()
if livetest.inventory(cls.conn) != cls.inventory:
raise AssertionError("Live tests changed objects outside the test namespace")
if {p.name() for p in cls.conn.listAllStoragePools()} != cls.pools:
raise AssertionError("A storage pool was left behind")
def leave_nvram(self):
"""What a migration with VIR_MIGRATE_UNDEFINE_SOURCE leaves: the file
without a domain."""
dom = self.conn.defineXML(
f"<domain type='kvm'><name>{NAME}</name><memory unit='MiB'>128</memory><vcpu>1</vcpu>"
"<os firmware='efi'><type arch='x86_64' machine='q35'>hvm</type></os>"
"<features><acpi/></features><devices/></domain>"
)
dom.createWithFlags(libvirt.VIR_DOMAIN_START_PAUSED) # creates the file
self.assertEqual(etree.fromstring(dom.XMLDesc(0)).findtext("os/nvram"), PATH)
dom.destroy()
dom.undefineFlags(libvirt.VIR_DOMAIN_UNDEFINE_KEEP_NVRAM)
def tearDown(self):
livetest.cleanup(self.conn)
host = wvmConnect(*self.args)
with host._nvram_pool() as pool:
vol = host._find_volume(pool, os.path.basename(PATH))
if vol is not None:
vol.delete(0)
def exists(self):
host = wvmConnect(*self.args)
with host._nvram_pool() as pool:
return host._find_volume(pool, os.path.basename(PATH)) is not None
def test_a_directory_both_connections_see_is_kept(self):
self.leave_nvram()
self.assertFalse(wvmConnect(*self.args).remove_nvram(PATH, wvmConnect(*self.args)))
self.assertTrue(self.exists())
def test_the_file_is_deleted_when_the_destination_is_another_host(self):
other_host = os.environ.get("TEST_LIBVIRT_SSH_HOST")
if not other_host or other_host == self.args[0]:
self.skipTest("Set TEST_LIBVIRT_SSH_HOST to a second host")
other = wvmConnect(other_host, os.environ.get("TEST_LIBVIRT_SSH_LOGIN", "root"), "", CONN_SSH)
other_pools = {p.name() for p in other.wvm.listAllStoragePools()}
self.leave_nvram()
self.assertTrue(wvmConnect(*self.args).remove_nvram(PATH, other))
self.assertFalse(self.exists())
self.assertEqual({p.name() for p in other.wvm.listAllStoragePools()}, other_pools)

View file

@ -0,0 +1,479 @@
"""Migration: the source definition never stays behind; a shut-off VM's
definition is moved by the app, not by libvirt's offline migration."""
import threading
import unittest
from unittest.mock import patch
import libvirt
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from vrtManager import util
from vrtManager.instance import wvmInstances
def no_domain():
error = libvirt.libvirtError("Domain not found")
error.err = (libvirt.VIR_ERR_NO_DOMAIN,)
return error
class FakeDomain:
def __init__(self, xml="<domain/>", snapshots=0, saved=False, undefine_fails=False, undefine_lost=False,
started=False):
self.calls = []
self.xml = xml
self.snapshots = snapshots
self.saved = saved
self.undefine_fails = undefine_fails
self.undefine_lost = undefine_lost # undefined, but the reply is lost
self.started = started # started by another client during the move
self.undefined = None
self.redefined = []
self.restored_autostart = None
def migrate(self, dconn, flags, dname, uri, bandwidth):
self.calls.append((flags, uri))
def name(self):
return "vm"
def UUIDString(self):
return "u-1"
inactive_xml = None # the persistent definition, when it differs
def XMLDesc(self, flags):
if flags & libvirt.VIR_DOMAIN_XML_INACTIVE and self.inactive_xml:
return self.inactive_xml
return self.xml
def snapshotNum(self, flags=0):
return self.snapshots
def hasManagedSaveImage(self, flags=0):
return self.saved
def undefineFlags(self, flags):
if self.undefine_fails:
raise libvirt.libvirtError("undefine failed")
self.undefined = flags
if self.undefine_lost:
raise libvirt.libvirtError("Cannot recv data")
def isPersistent(self):
return self.undefined is None or bool(self.redefined)
def isActive(self):
if not self.started and self.undefined is not None:
raise no_domain() # libvirt: an undefined shut-off VM is gone
return self.started
def autostart(self):
return 1
def setAutostart(self, value):
self.restored_autostart = value
def connect(self):
return self
def defineXML(self, xml):
self.redefined.append(xml)
return self
def no_volume():
error = libvirt.libvirtError("Storage volume not found")
error.err = (libvirt.VIR_ERR_NO_STORAGE_VOL,)
return error
class FakeVolume:
def __init__(self, backing):
self.backing = backing
def XMLDesc(self, flags=0):
if self.backing is None:
return "<volume><target><path>x</path></target></volume>"
return f"<volume><backingStore><path>{self.backing}</path></backingStore></volume>"
class FakePool:
def __init__(self, host, active=True):
self.host = host
self.active = active
def isActive(self):
return self.active
def refresh(self, flags=0):
self.host.refreshes += 1
self.host.volumes |= self.host.unlisted
self.host.unlisted = set()
def storageVolLookupByName(self, name):
return self.host.storageVolLookupByPath(name)
class FakeHost:
"""The destination's libvirt connection. volumes: the paths its pools
list; unlisted: files its pools show after a refresh."""
def __init__(self, existing=(), volumes=(), unlisted=(), backing=None, inactive_pools=()):
self.existing = set(existing)
self.defined = []
self.new = []
self.volumes = set(volumes)
self.unlisted = set(unlisted)
self.backing = backing or {} # path: the path of its backing file
self.inactive_pools = set(inactive_pools)
self.refreshes = 0
def storageVolLookupByPath(self, path):
if path not in self.volumes:
raise no_volume()
return FakeVolume(self.backing.get(path))
def storagePoolLookupByName(self, name):
return FakePool(self, active=name not in self.inactive_pools)
def listAllStoragePools(self, flags=0):
return [FakePool(self)]
def lookup(self, key):
if key in self.existing:
return FakeDomain()
raise no_domain()
lookupByName = lookupByUUIDString = lookup
def defineXML(self, xml):
self.defined.append(xml)
self.new.append(FakeDomain())
return self.new[-1]
class FakeSource:
def __init__(self, state, **domain):
self.state = state
self.instance = FakeDomain(**domain)
reads = 0
def get_status(self):
self.reads += 1
return self.state
def get_arch(self):
return "x86_64"
def get_dom_emulator(self):
return "/usr/bin/qemu-system-x86_64"
def destination(host=None):
dest = wvmInstances.__new__(wvmInstances)
dest.wvm = host or FakeHost()
dest.get_emulator = lambda arch: "/usr/bin/qemu-system-x86_64"
return dest
class MigrationFlagsTestCase(unittest.TestCase):
def test_the_source_definition_is_always_removed(self):
for state, live, offline in ((1, True, False), (3, False, False)):
with self.subTest(state=state):
source = FakeSource(state)
destination().moveto(source, "vm", live=live, unsafe=False, offline=offline)
flags = source.instance.calls[0][0]
self.assertTrue(flags & libvirt.VIR_MIGRATE_UNDEFINE_SOURCE)
self.assertTrue(flags & libvirt.VIR_MIGRATE_PERSIST_DEST)
def test_the_migration_uri_is_passed_on(self):
source = FakeSource(1)
destination().moveto(source, "vm", live=True, unsafe=False, offline=False, uri="tcp://192.0.2.12")
self.assertEqual(source.instance.calls[0][1], "tcp://192.0.2.12")
source = FakeSource(1)
destination().moveto(source, "vm", live=True, unsafe=False, offline=False)
self.assertIsNone(source.instance.calls[0][1])
class ShutOffMigrationTestCase(unittest.TestCase):
"""libvirt's offline migration fails on some builds (RHEL-156800); a
shut-off VM is only its definition, so the app moves that itself."""
def test_the_definition_moves_to_the_destination(self):
host = FakeHost()
source = FakeSource(5, xml="<domain><name>vm</name></domain>")
destination(host).moveto(source, "vm", live=False, unsafe=False, offline=True)
self.assertEqual(host.defined, ["<domain><name>vm</name></domain>"])
self.assertEqual(source.instance.calls, [])
self.assertEqual(source.instance.undefined, 0)
def test_a_vm_with_nvram_or_tpm_state_is_refused(self):
"""That state stays on the source host: the VM would start on the
destination with new UEFI variables and a new TPM."""
cases = {
"nvram": "<os><nvram>/var/lib/libvirt/qemu/nvram/vm_VARS.fd</nvram></os>",
"tpm": "<devices><tpm model='tpm-crb'><backend type='emulator' version='2.0'/></tpm></devices>",
}
for case, xml in cases.items():
with self.subTest(case=case):
host = FakeHost()
source = FakeSource(5, xml=f"<domain>{xml}</domain>")
with self.assertRaisesRegex(util.OperationError, "migrate it live"):
destination(host).moveto(source, "vm", live=False, unsafe=False, offline=True)
self.assertEqual(host.defined, [])
self.assertIsNone(source.instance.undefined)
def test_a_tpm_without_local_state_moves(self):
xml = "<domain><devices><tpm model='tpm-tis'><backend type='passthrough'/></tpm></devices></domain>"
host = FakeHost()
destination(host).moveto(FakeSource(5, xml=xml), "vm", live=False, unsafe=False, offline=True)
self.assertEqual(host.defined, [xml])
def test_refused_before_anything_changes(self):
cases = {
"snapshots": (FakeHost(), {"snapshots": 2}),
"saved state": (FakeHost(), {"saved": True}),
"same uuid on the destination": (FakeHost(existing={"u-1"}), {}),
"same name on the destination": (FakeHost(existing={"vm"}), {}),
}
for case, (host, domain) in cases.items():
with self.subTest(case=case):
source = FakeSource(5, **domain)
with self.assertRaises(util.OperationError):
destination(host).moveto(source, "vm", live=False, unsafe=False, offline=True)
self.assertEqual(host.defined, [])
self.assertIsNone(source.instance.undefined)
def test_a_failed_source_undefine_removes_the_new_definition(self):
host = FakeHost()
source = FakeSource(5, undefine_fails=True)
with self.assertRaises(libvirt.libvirtError):
destination(host).moveto(source, "vm", live=False, unsafe=False, offline=True)
self.assertIsNotNone(host.new[0].undefined)
def test_the_destination_definition_stays_when_the_source_has_none(self):
host = FakeHost()
source = FakeSource(5, undefine_lost=True)
with self.assertRaisesRegex(util.OperationError, "only on the destination"):
destination(host).moveto(source, "vm", live=False, unsafe=False, offline=True)
self.assertIsNone(host.new[0].undefined)
def test_a_failed_rollback_is_reported(self):
host = FakeHost()
source = FakeSource(5, undefine_fails=True)
host.defineXML = lambda xml: host.new.append(FakeDomain(undefine_fails=True)) or host.new[-1]
with self.assertRaisesRegex(util.OperationError, "undefine failed; the VM is now defined on both hosts"):
destination(host).moveto(source, "vm", live=False, unsafe=False, offline=True)
def test_a_vm_started_meanwhile_stays_on_the_source(self):
host = FakeHost()
source = FakeSource(5, xml="<domain><name>vm</name></domain>", started=True)
with self.assertRaisesRegex(util.OperationError, "started on the source"):
destination(host).moveto(source, "vm", live=False, unsafe=False, offline=True)
self.assertEqual(source.instance.redefined, ["<domain><name>vm</name></domain>"])
self.assertEqual(source.instance.restored_autostart, 1)
self.assertIsNotNone(host.new[0].undefined)
class MigrationModeTestCase(unittest.TestCase):
"""The state is read once; a request that does not fit it is refused
before anything changes, and moveto names the mode it used."""
def migrate(self, state, **request):
source = FakeSource(state)
options = {"live": False, "unsafe": False, "offline": False, **request}
mode = destination().moveto(source, "vm", **options)
return mode, source.instance.calls[0][0] if source.instance.calls else None
def test_refused_requests(self):
for state, request in ((5, {"live": True}), (1, {"offline": True}), (3, {"offline": True}),
(1, {"live": True, "offline": True}), (6, {}), (4, {}), (7, {}), (0, {})):
with self.subTest(state=state, request=request):
source = FakeSource(state)
with self.assertRaises(util.OperationError):
destination().moveto(source, "vm", unsafe=False, **{"live": False, "offline": False, **request})
self.assertEqual(source.instance.calls, [])
def test_modes(self):
live, unsafe = libvirt.VIR_MIGRATE_LIVE, libvirt.VIR_MIGRATE_UNSAFE
converge, compressed = libvirt.VIR_MIGRATE_AUTO_CONVERGE, libvirt.VIR_MIGRATE_COMPRESSED
mode, flags = self.migrate(1, live=True, autoconverge=True, compress=True, unsafe=True)
self.assertEqual(mode, "live, unsafe, auto converge, compressed")
self.assertEqual(flags & (live | unsafe | converge | compressed), live | unsafe | converge | compressed)
source = FakeSource(1)
destination().moveto(source, "vm", live=True, unsafe=False, offline=False)
self.assertEqual(source.reads, 1)
mode, flags = self.migrate(1, compress=True, autoconverge=True)
self.assertEqual(mode, "non-live, compressed")
self.assertEqual(flags & (live | converge | compressed), compressed)
# a paused VM stays paused; the tuning options are for running VMs
mode, flags = self.migrate(3, live=True, autoconverge=True, compress=True, unsafe=True)
self.assertEqual(mode, "live")
self.assertEqual(flags & (live | unsafe | converge | compressed), live)
self.assertEqual(self.migrate(5), ("offline", None))
self.assertEqual(self.migrate(5, offline=True), ("offline", None))
def with_disks(*disks):
return "<domain><devices>%s</devices></domain>" % "".join(disks)
FILE_DISK = "<disk type='file' device='disk'><source file='/pool/vm.qcow2'/><target dev='vda'/></disk>"
class DestinationDisksTestCase(unittest.TestCase):
"""Every disk of the VM must be a volume the destination knows; the
VM is refused before anything changes otherwise, naming the disks."""
def migrate(self, host, xml, state=1, inactive_xml=None):
source = FakeSource(state, xml=xml)
source.instance.inactive_xml = inactive_xml
live = state != 5
destination(host).moveto(source, "vm", live=live, unsafe=False, offline=not live)
return source
def test_known_volumes_migrate(self):
host = FakeHost(volumes={"/pool/vm.qcow2", "/pool/base.qcow2", "/iso/os.iso"})
xml = with_disks(
"<disk type='file' device='disk'><source file='/pool/vm.qcow2'/><target dev='vda'/>"
"<backingStore type='file'><source file='/pool/base.qcow2'/><backingStore/></backingStore></disk>",
"<disk type='file' device='cdrom'><source file='/iso/os.iso'/><target dev='sda'/></disk>",
"<disk type='file' device='cdrom'><target dev='sdb'/></disk>", # empty
"<disk type='network' device='disk'><source protocol='rbd' name='rbd/vm'/><target dev='vdb'/></disk>",
"<disk type='volume' device='disk'><source pool='p' volume='/pool/vm.qcow2'/><target dev='vdc'/></disk>",
)
self.assertEqual(len(self.migrate(host, xml).instance.calls), 1)
self.assertEqual(host.defined, [])
self.migrate(host, xml, state=5)
self.assertEqual(len(host.defined), 1)
def test_missing_disks_are_refused_and_named(self):
cases = {
"vda \\(/pool/vm.qcow2\\)": with_disks(FILE_DISK),
"sda \\(/iso/os.iso\\)": with_disks(
"<disk type='file' device='cdrom'><source file='/iso/os.iso'/><target dev='sda'/></disk>"),
"vda \\(/pool/base.qcow2\\)": with_disks(
"<disk type='file' device='disk'><source file='/pool/vm.qcow2'/><target dev='vda'/>"
"<backingStore type='file'><source file='/pool/base.qcow2'/></backingStore></disk>"),
"vdb \\(/dev/sdz\\)": with_disks(
"<disk type='block' device='disk'><source dev='/dev/sdz'/><target dev='vdb'/></disk>"),
"vdc \\(p/v\\)": with_disks(
"<disk type='volume' device='disk'><source pool='p' volume='v'/><target dev='vdc'/></disk>"),
}
for missing, xml in cases.items():
for state in (1, 5):
with self.subTest(missing=missing, state=state):
host = FakeHost(volumes=set() if "vm.qcow2" in missing else {"/pool/vm.qcow2"})
source = FakeSource(state, xml=xml)
with self.assertRaisesRegex(util.OperationError, missing):
destination(host).moveto(source, "vm", live=state == 1, unsafe=False, offline=state == 5)
self.assertEqual((source.instance.calls, host.defined), ([], []))
def test_a_disk_of_the_persistent_definition_is_checked_too(self):
host = FakeHost(volumes={"/pool/vm.qcow2"})
inactive = with_disks(FILE_DISK, "<disk type='file' device='disk'><source file='/pool/data.img'/>"
"<target dev='vdb'/></disk>")
with self.assertRaisesRegex(util.OperationError, "vdb \\(/pool/data.img\\)"):
self.migrate(host, with_disks(FILE_DISK), inactive_xml=inactive)
def test_pools_are_refreshed_before_a_disk_counts_as_missing(self):
# a file another host made on shared storage is listed after a refresh
host = FakeHost(unlisted={"/pool/vm.qcow2"})
self.assertEqual(len(self.migrate(host, with_disks(FILE_DISK)).instance.calls), 1)
self.assertEqual(host.refreshes, 1)
host = FakeHost(volumes={"/pool/vm.qcow2"})
self.migrate(host, with_disks(FILE_DISK))
self.assertEqual(host.refreshes, 0)
def test_backing_files_the_definition_does_not_list_are_checked(self):
# an inactive definition has no <backingStore>: the volume tells
for state in (1, 5):
with self.subTest(state=state):
host = FakeHost(volumes={"/pool/vm.qcow2", "/pool/mid.qcow2"},
backing={"/pool/vm.qcow2": "/pool/mid.qcow2", "/pool/mid.qcow2": "/local/base.qcow2"})
with self.assertRaisesRegex(util.OperationError, "vda \\(/local/base.qcow2\\)"):
self.migrate(host, with_disks(FILE_DISK), state=state)
host.volumes.add("/local/base.qcow2")
self.migrate(host, with_disks(FILE_DISK), state=state)
def test_a_network_backing_image_is_not_checked(self):
host = FakeHost(volumes={"/pool/vm.qcow2"}, backing={"/pool/vm.qcow2": "nbd://storage.example.org:10809/base"})
self.assertEqual(len(self.migrate(host, with_disks(FILE_DISK)).instance.calls), 1)
def test_disks_that_cannot_be_checked_are_refused(self):
xml = with_disks("<disk type='dir' device='disk'><source dir='/srv/vm'/><target dev='vdb'/></disk>")
with self.assertRaisesRegex(util.OperationError, "vdb \\(dir disk, cannot be checked\\)"):
self.migrate(FakeHost(), xml, state=5)
def test_a_volume_of_an_inactive_pool_is_missing(self):
host = FakeHost(volumes={"v"}, inactive_pools={"p"})
xml = with_disks("<disk type='volume' device='disk'><source pool='p' volume='v'/><target dev='vdc'/></disk>")
with self.assertRaisesRegex(util.OperationError, "vdc \\(p/v\\)"):
self.migrate(host, xml)
class SlowDomain(FakeDomain):
"""A migration that does not finish until its job is aborted."""
def __init__(self, **kw):
super().__init__(**kw)
self.aborted = threading.Event()
def migrate(self, dconn, flags, dname, uri, bandwidth):
if not self.aborted.wait(5):
raise AssertionError("the migration was not aborted")
raise libvirt.libvirtError("operation aborted: job 'migration out' canceled by client")
def abortJob(self):
self.aborted.set()
class MigrationTimeLimitTestCase(unittest.TestCase):
"""A migration that does not converge is cancelled after the time limit,
before the web server ends the request; the VM stays on the source."""
def test_a_migration_over_the_limit_is_cancelled(self):
source = FakeSource(1)
source.instance = SlowDomain()
with self.assertRaisesRegex(util.OperationError, "did not finish within 0.05 s and was cancelled.*auto converge"):
destination().moveto(source, "vm", live=True, unsafe=False, offline=False, timeout=0.05)
self.assertTrue(source.instance.aborted.is_set())
def test_a_migration_within_the_limit_stops_its_timer(self):
timers = []
class Timer(threading.Timer):
def __init__(self, *args):
super().__init__(*args)
self.joined = False
timers.append(self)
def join(self, timeout=None):
self.joined = True
super().join(timeout)
source = FakeSource(1)
with patch("vrtManager.instance.threading.Timer", Timer):
destination().moveto(source, "vm", live=True, unsafe=False, offline=False, timeout=5)
self.assertEqual(len(source.instance.calls), 1)
self.assertTrue(timers[0].finished.is_set() and timers[0].joined)
self.assertFalse(timers[0].is_alive())
def test_a_failure_the_cancel_did_not_cause_keeps_its_error(self):
source = FakeSource(1)
source.instance = SlowDomain()
def refused():
source.instance.aborted.set() # migrate() gives up, but the abort itself failed
raise libvirt.libvirtError("cannot abort migration in confirm phase")
source.instance.abortJob = refused
with self.assertRaisesRegex(libvirt.libvirtError, "operation aborted") as caught:
destination().moveto(source, "vm", live=True, unsafe=False, offline=False, timeout=0.05)
self.assertNotIsInstance(caught.exception, util.OperationError)

View file

@ -0,0 +1,169 @@
import sys
import unittest
from unittest.mock import MagicMock
try:
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
except ImportError:
django_mock = MagicMock()
django_mock.conf.settings.MAC_OUI = "52:54:10"
sys.modules["django"] = django_mock
sys.modules["django.conf"] = django_mock.conf
from vrtManager.network import network_size, wvmNetwork
class TestNetworkSize(unittest.TestCase):
"""Unit tests for the network_size function."""
def test_ipv4_slash_24_with_dhcp(self):
gateway, mask, dhcp_pool = network_size("192.168.100.0/24", dhcp=True)
self.assertEqual(gateway, "192.168.100.1")
self.assertEqual(mask, "255.255.255.0")
self.assertIsNotNone(dhcp_pool)
self.assertEqual(str(dhcp_pool[0]), "192.168.100.2")
self.assertEqual(str(dhcp_pool[1]), "192.168.100.254")
def test_ipv4_slash_24_without_dhcp(self):
gateway, mask, dhcp_pool = network_size("192.168.100.0/24", dhcp=False)
self.assertEqual(gateway, "192.168.100.1")
self.assertEqual(mask, "255.255.255.0")
self.assertIsNone(dhcp_pool)
def test_ipv4_slash_16_with_dhcp(self):
gateway, mask, dhcp_pool = network_size("172.16.0.0/16", dhcp=True)
self.assertEqual(gateway, "172.16.0.1")
self.assertEqual(mask, "255.255.0.0")
self.assertIsNotNone(dhcp_pool)
self.assertEqual(str(dhcp_pool[0]), "172.16.0.2")
self.assertEqual(str(dhcp_pool[1]), "172.16.255.254")
def test_ipv4_slash_28_with_dhcp(self):
gateway, mask, dhcp_pool = network_size("10.0.0.0/28", dhcp=True)
self.assertEqual(gateway, "10.0.0.1")
self.assertEqual(mask, "255.255.255.240")
self.assertIsNotNone(dhcp_pool)
self.assertEqual(str(dhcp_pool[0]), "10.0.0.2")
self.assertEqual(str(dhcp_pool[1]), "10.0.0.14")
def test_ipv4_slash_30_with_dhcp(self):
gateway, mask, dhcp_pool = network_size("10.0.0.0/30", dhcp=True)
self.assertEqual(gateway, "10.0.0.1")
self.assertEqual(mask, "255.255.255.252")
self.assertIsNotNone(dhcp_pool)
self.assertEqual(str(dhcp_pool[0]), "10.0.0.2")
self.assertEqual(str(dhcp_pool[1]), "10.0.0.2")
def test_ipv6_slash_64_with_dhcp(self):
gateway, mask, dhcp_pool = network_size("2001:db8:ca2:2::/64", dhcp=True)
self.assertEqual(gateway, "2001:db8:ca2:2::1")
self.assertEqual(mask, "64")
self.assertIsNotNone(dhcp_pool)
self.assertEqual(str(dhcp_pool[0]), "2001:db8:ca2:2::100")
self.assertEqual(str(dhcp_pool[1]), "2001:db8:ca2:2::1ff")
def test_ipv6_slash_64_without_dhcp(self):
gateway, mask, dhcp_pool = network_size("2001:db8:ca2:2::/64", dhcp=False)
self.assertEqual(gateway, "2001:db8:ca2:2::1")
self.assertEqual(mask, "64")
self.assertIsNone(dhcp_pool)
def test_invalid_subnet(self):
with self.assertRaises(ValueError):
network_size("invalid.subnet/99")
class DummyNetwork(wvmNetwork):
"""Subclass of wvmNetwork with mock XML for isolated IP method testing."""
def __init__(self, xml_content):
self._xml = xml_content
self.parent_count = 2
self.updates = []
def _XMLDesc(self, flags):
return self._xml
def update(self, command, section, xml, parent_index, flags):
self.updates.append((command, section, xml, parent_index, flags))
class TestWvmNetworkIPOperations(unittest.TestCase):
"""Unit tests for IP parsing, DHCP ranges, and host addresses in wvmNetwork."""
DUAL_STACK_XML = """<network>
<name>default</name>
<ip address="192.168.100.1" netmask="255.255.255.0">
<dhcp>
<range start="192.168.100.2" end="192.168.100.254"/>
<host mac="52:54:00:12:34:56" name="vm1" ip="192.168.100.50"/>
</dhcp>
</ip>
<ip family="ipv6" address="2001:db8:ca2:2::1" prefix="64">
<dhcp>
<range start="2001:db8:ca2:2::100" end="2001:db8:ca2:2::1ff"/>
<host id="0:1:0:1" name="vm2" ip="2001:db8:ca2:2::50"/>
</dhcp>
</ip>
</network>"""
NO_IP_XML = """<network>
<name>isolated</name>
<bridge name="virbr1"/>
</network>"""
def test_get_ip_networks_dual_stack(self):
net = DummyNetwork(self.DUAL_STACK_XML)
ip_networks = net.get_ip_networks()
self.assertIn("ipv4", ip_networks)
self.assertIn("ipv6", ip_networks)
self.assertEqual(str(ip_networks["ipv4"]), "192.168.100.0/24")
self.assertEqual(str(ip_networks["ipv6"]), "2001:db8:ca2:2::/64")
def test_get_ip_networks_no_ip(self):
net = DummyNetwork(self.NO_IP_XML)
ip_networks = net.get_ip_networks()
self.assertEqual(ip_networks, {})
def test_get_dhcp_range_ipv4(self):
net = DummyNetwork(self.DUAL_STACK_XML)
dhcp = net.get_dhcp_range("ipv4")
self.assertIsNotNone(dhcp)
self.assertEqual(len(dhcp), 2)
self.assertEqual(str(dhcp[0]), "192.168.100.2")
self.assertEqual(str(dhcp[1]), "192.168.100.254")
def test_get_dhcp_range_ipv6(self):
net = DummyNetwork(self.DUAL_STACK_XML)
dhcp = net.get_dhcp_range("ipv6")
self.assertIsNotNone(dhcp)
self.assertEqual(len(dhcp), 2)
self.assertEqual(str(dhcp[0]), "2001:db8:ca2:2::100")
self.assertEqual(str(dhcp[1]), "2001:db8:ca2:2::1ff")
def test_get_dhcp_range_empty(self):
net = DummyNetwork(self.NO_IP_XML)
self.assertIsNone(net.get_dhcp_range("ipv4"))
self.assertIsNone(net.get_dhcp_range("ipv6"))
def test_get_dhcp_range_start_and_end(self):
net = DummyNetwork(self.DUAL_STACK_XML)
self.assertEqual(str(net.get_dhcp_range_start("ipv4")), "192.168.100.2")
self.assertEqual(str(net.get_dhcp_range_end("ipv4")), "192.168.100.254")
self.assertEqual(str(net.get_dhcp_range_start("ipv6")), "2001:db8:ca2:2::100")
self.assertEqual(str(net.get_dhcp_range_end("ipv6")), "2001:db8:ca2:2::1ff")
def test_modify_fixed_address_validation(self):
net = DummyNetwork(self.DUAL_STACK_XML)
# Invalid IP address must raise ValueError
with self.assertRaises(ValueError):
net.modify_fixed_address("bad_host", "999.999.999.999", "52:54:00:11:22:33", family="ipv4")
with self.assertRaises(ValueError):
net.modify_fixed_address("bad_host", "not-an-ip", "0:1:0:2", family="ipv6")
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,129 @@
import unittest
from unittest.mock import MagicMock
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from vrtManager.instance import PERSISTENT_XML, wvmInstance
DOMAIN = """<domain><name>vm</name><memory>131072</memory><currentMemory>131072</currentMemory>
<vcpu>1</vcpu><os><type>hvm</type></os><devices>
<interface type='network'><mac address='52:54:00:00:00:01'/><source network='default'/></interface>
<graphics type='vnc' passwd='secret'><listen type='address' address='0.0.0.0'/></graphics>
<video><model type='vga' primary='yes'/></video></devices></domain>"""
class PersistentEditsTestCase(unittest.TestCase):
"""Edits read the persistent definition with secrets."""
def test_edits_read_the_persistent_secure_definition(self):
edits = {
"set_bootmenu": lambda vm: vm.set_bootmenu(1),
"set_console_keymap": lambda vm: vm.set_console_keymap("de"),
"set_video_model": lambda vm: vm.set_video_model("virtio"),
"resize_cpu": lambda vm: vm.resize_cpu("1", "2"),
"set_options": lambda vm: vm.set_options({"title": "t"}),
"set_qos": lambda vm: vm.set_qos("52:54:00:00:00:01", "inbound", 1, 2, 3),
}
for name, edit in edits.items():
with self.subTest(name):
vm = wvmInstance.__new__(wvmInstance)
vm.instance = MagicMock()
vm.wvm = MagicMock()
vm._XMLDesc = MagicMock(return_value=DOMAIN)
vm.get_vcpus = MagicMock(return_value=None)
vm.get_status = MagicMock(return_value=5)
edit(vm)
flags = {call.args[0] for call in vm._XMLDesc.call_args_list}
self.assertIn(PERSISTENT_XML, flags)
defined = (vm.wvm.defineXML.call_args or vm._XMLDesc.call_args)[0][0]
self.assertIn("secret", str(defined))
class XmlFidelityTestCase(unittest.TestCase):
def test_edits_keep_metadata_namespace_prefixes(self):
xml = (
"<domain><metadata><app:info xmlns:app='http://example.com/x'>v</app:info></metadata>"
"<os><type>hvm</type></os><devices><graphics type='vnc'><listen type='address'/></graphics>"
"<disk type='file' device='disk'><target dev='vda'/></disk></devices></domain>"
)
edits = {
"set_bootmenu": lambda vm: vm.set_bootmenu(1),
"set_bootorder": lambda vm: vm.set_bootorder({0: {"type": "disk", "dev": "vda"}}),
"set_console_keymap": lambda vm: vm.set_console_keymap("de"),
"set_console_passwd": lambda vm: vm.set_console_passwd("x"),
"set_console_listener_addr": lambda vm: vm.set_console_listener_addr("127.0.0.1"),
}
for name, edit in edits.items():
with self.subTest(name):
vm = wvmInstance.__new__(wvmInstance)
vm.wvm = MagicMock()
vm._XMLDesc = MagicMock(return_value=xml)
edit(vm)
self.assertIn("<app:info", vm.wvm.defineXML.call_args[0][0])
AGENT_DOMAIN = """<domain><name>vm</name><devices><channel type='unix'>
<target type='virtio' name='org.qemu.guest_agent.0'/></channel></devices></domain>"""
class DeviceEditsByStateTestCase(unittest.TestCase):
"""Disk, NIC and agent channel changes reach the running definition
whenever the VM is active, paused included, and the persistent one
always. An attach is one call for both; a removal is one per definition,
from that definition's own XML."""
EDITS = {
"attach_disk": ("attachDeviceFlags", lambda vm: vm.attach_disk("vdb", "/var/lib/libvirt/images/d.qcow2")),
"add_network": ("attachDeviceFlags", lambda vm: vm.add_network("52:54:00:00:00:09", "default")),
"add_guest_agent": ("attachDeviceFlags", lambda vm: vm.add_guest_agent()),
"remove_guest_agent": ("detachDeviceFlags", lambda vm: vm.remove_guest_agent()),
}
ATTACHES = ("attach_disk", "add_network", "add_guest_agent")
def affected(self, state, active):
from libvirt import VIR_DOMAIN_AFFECT_CONFIG, VIR_DOMAIN_AFFECT_LIVE
names = {VIR_DOMAIN_AFFECT_LIVE: "live", VIR_DOMAIN_AFFECT_CONFIG: "config",
VIR_DOMAIN_AFFECT_LIVE | VIR_DOMAIN_AFFECT_CONFIG: "live+config"}
result = {}
for name, (method, edit) in self.EDITS.items():
vm = wvmInstance.__new__(wvmInstance)
vm.instance = MagicMock()
vm.instance.isActive.return_value = active
vm.get_status = MagicMock(return_value=state)
vm._XMLDesc = MagicMock(return_value=AGENT_DOMAIN)
edit(vm)
result[name] = [names[c.args[1]] for c in getattr(vm.instance, method).call_args_list]
return result
def test_paused_vm(self):
for name, affected in self.affected(state=3, active=True).items():
self.assertEqual(affected, ["live+config"] if name in self.ATTACHES else ["live", "config"], name)
def test_running_vm(self):
for name, affected in self.affected(state=1, active=True).items():
self.assertEqual(affected, ["live+config"] if name in self.ATTACHES else ["live", "config"], name)
def test_stopped_vm(self):
for name, affected in self.affected(state=5, active=False).items():
self.assertEqual(affected, ["config"], name)
def test_a_refused_attach_is_not_tried_again(self):
from libvirt import libvirtError
for name in self.ATTACHES:
with self.subTest(name):
vm = wvmInstance.__new__(wvmInstance)
vm.instance = MagicMock()
vm.instance.isActive.return_value = True
vm.instance.attachDeviceFlags.side_effect = libvirtError("target sda already exists")
with self.assertRaises(libvirtError):
self.EDITS[name][1](vm)
vm.instance.attachDeviceFlags.assert_called_once()
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,207 @@
"""wvmInstance.cached_reads(): inside the block the domain XML (per flags),
the VM's state and each disk's volume are read from libvirt once; outside it
every read goes to libvirt, as code that changes the VM needs. The host's
info and capabilities are read once per object."""
import unittest
from unittest.mock import MagicMock
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from libvirt import libvirtError
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"<domain flags='{flags}' read='{len(self.reads)}'/>"
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 "<capabilities/>"
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_status()
p.get_status()
self.assertEqual(p.instance.reads, [0, 0, "info", "info"])
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)]
self.assertEqual(first, again)
self.assertEqual(p.instance.reads, [0, 1, 3])
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("<domain/>")
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 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("<domain/>")
p._volume_details("/p/a.qcow2")
p._volume_details("/p/a.qcow2")
self.assertEqual(len(p.lookups), 4)
def disk(path, cache):
return (
f"<disk type='file' device='disk'><driver name='qemu' type='qcow2' cache='{cache}'/>"
f"<source file='{path}'/><target dev='vda' bus='virtio'/></disk>"
)
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"<domain><devices>{live}</devices></domain>", 3: f"<domain><devices>{config}</devices></domain>"}
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))

View file

@ -0,0 +1,126 @@
"""After a migration took a VM away, its NVRAM file is removed from the
source when it is in libvirt's per-host NVRAM directory and the destination
does not share that directory; a VM later created on the source with the same
name would otherwise start with the old UEFI variables."""
from unittest import TestCase
from unittest.mock import MagicMock
import libvirt
from vrtManager.connection import NVRAM_DIR, wvmConnect
PATH = NVRAM_DIR + "/vm_VARS.fd"
def error(code):
err = libvirt.libvirtError("error %d" % code)
err.err = (code, libvirt.VIR_FROM_STORAGE, str(err), 2, None, None, None, 0, 0)
return err
def pool(path, volumes=(), create_error=None):
p = MagicMock()
p.XMLDesc.return_value = f"<pool type='dir'><name>p</name><target><path>{path}</path></target></pool>"
p.vols = {name: MagicMock() for name in volumes}
p.created = []
def lookup(name):
if name not in p.vols:
raise error(libvirt.VIR_ERR_NO_STORAGE_VOL)
return p.vols[name]
def create(xml, flags):
if create_error is not None:
raise create_error
vol = MagicMock()
p.created.append(vol)
return vol
p.storageVolLookupByName.side_effect = lookup
p.createXML.side_effect = create
return p
def connect(pools=(), transient=None):
conn = wvmConnect.__new__(wvmConnect)
conn.wvm = MagicMock()
conn.wvm.listAllStoragePools.return_value = list(pools)
conn.wvm.storagePoolCreateXML.return_value = transient
return conn
def local_destination():
return connect([pool(NVRAM_DIR)])
class RemoveNvramTestCase(TestCase):
def test_a_file_outside_the_nvram_directory_is_kept(self):
conn = connect()
for path in ("/srv/shared/nvram/vm_VARS.fd", NVRAM_DIR + "/sub/vm_VARS.fd", "relative_VARS.fd"):
with self.subTest(path=path):
self.assertFalse(conn.remove_nvram(path, local_destination()))
self.assertFalse(conn.wvm.method_calls)
def test_a_pool_on_the_directory_is_used(self):
existing = pool(NVRAM_DIR + "/", ["vm_VARS.fd"])
conn = connect([pool("/var/lib/libvirt/images"), existing])
self.assertTrue(conn.remove_nvram(PATH, local_destination()))
existing.vols["vm_VARS.fd"].delete.assert_called_once_with(0)
self.assertFalse(conn.wvm.storagePoolCreateXML.called)
self.assertFalse(existing.destroy.called)
def test_pools_without_a_target_path_are_skipped(self):
rbd = MagicMock()
rbd.XMLDesc.return_value = "<pool type='rbd'><name>ceph</name><source><name>rbd</name></source></pool>"
transient = pool(NVRAM_DIR, ["vm_VARS.fd"])
conn = connect([rbd], transient)
self.assertTrue(conn.remove_nvram(PATH, local_destination()))
transient.vols["vm_VARS.fd"].delete.assert_called_once_with(0)
def test_without_a_pool_a_transient_one_is_used_and_stopped(self):
transient = pool(NVRAM_DIR, ["vm_VARS.fd"])
conn = connect([pool("/var/lib/libvirt/images")], transient)
self.assertTrue(conn.remove_nvram(PATH, local_destination()))
self.assertIn(f"<path>{NVRAM_DIR}</path>", conn.wvm.storagePoolCreateXML.call_args[0][0])
transient.vols["vm_VARS.fd"].delete.assert_called_once_with(0)
transient.destroy.assert_called_once_with()
def test_a_directory_the_destination_shares_is_left_alone(self):
# the destination cannot create the marker the source just created
source = pool(NVRAM_DIR, ["vm_VARS.fd"])
destination = connect([pool(NVRAM_DIR, create_error=error(libvirt.VIR_ERR_STORAGE_VOL_EXIST))])
self.assertFalse(connect([source]).remove_nvram(PATH, destination))
self.assertFalse(source.vols["vm_VARS.fd"].delete.called)
source.created[0].delete.assert_called_once_with(0) # the marker is gone
def test_both_markers_are_removed(self):
source = pool(NVRAM_DIR, ["vm_VARS.fd"])
other = pool(NVRAM_DIR)
self.assertTrue(connect([source]).remove_nvram(PATH, connect([other])))
self.assertEqual(source.createXML.call_args, other.createXML.call_args) # one name
source.created[0].delete.assert_called_once_with(0)
other.created[0].delete.assert_called_once_with(0)
def test_when_the_destination_cannot_be_checked_nothing_is_deleted(self):
source = pool(NVRAM_DIR, ["vm_VARS.fd"])
destination = connect([], None)
destination.wvm.storagePoolCreateXML.side_effect = error(libvirt.VIR_ERR_INTERNAL_ERROR)
with self.assertRaises(libvirt.libvirtError):
connect([source]).remove_nvram(PATH, destination)
self.assertFalse(source.vols["vm_VARS.fd"].delete.called)
source.created[0].delete.assert_called_once_with(0)
def test_the_transient_pool_is_stopped_when_the_delete_fails(self):
transient = pool(NVRAM_DIR, ["vm_VARS.fd"])
transient.vols["vm_VARS.fd"].delete.side_effect = error(libvirt.VIR_ERR_INTERNAL_ERROR)
conn = connect([], transient)
with self.assertRaises(libvirt.libvirtError):
conn.remove_nvram(PATH, local_destination())
transient.destroy.assert_called_once_with()
def test_a_missing_file_is_nothing_to_remove(self):
transient = pool(NVRAM_DIR)
conn = connect([], transient)
self.assertFalse(conn.remove_nvram(PATH, local_destination()))
transient.destroy.assert_called_once_with()

View file

@ -0,0 +1,45 @@
import unittest
from unittest.mock import MagicMock
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from libvirt import VIR_DOMAIN_BLOCK_RESIZE_BYTES, VIR_STORAGE_VOL_BLOCK, VIR_STORAGE_VOL_FILE
from vrtManager.instance import wvmInstance
SIZE = 10 << 30
def vm(active, vol_type):
inst = wvmInstance.__new__(wvmInstance)
inst.instance = MagicMock()
inst.instance.isActive.return_value = active
vol = MagicMock()
vol.info.return_value = [vol_type, 0, 0]
inst.get_volume_by_path = MagicMock(return_value=vol)
inst.resize_disk([{"path": "/dev/vg/disk", "size_new": SIZE}])
return inst, vol
class ResizeDiskTestCase(unittest.TestCase):
def test_running_block_volume_grows_in_the_pool_then_in_qemu(self):
inst, vol = vm(active=True, vol_type=VIR_STORAGE_VOL_BLOCK)
vol.resize.assert_called_once_with(SIZE)
inst.instance.blockResize.assert_called_once_with("/dev/vg/disk", SIZE, VIR_DOMAIN_BLOCK_RESIZE_BYTES)
def test_running_image_file_grows_through_qemu_only(self):
# qemu-img cannot take the image lock QEMU holds
inst, vol = vm(active=True, vol_type=VIR_STORAGE_VOL_FILE)
vol.resize.assert_not_called()
inst.instance.blockResize.assert_called_once()
def test_stopped_vm_grows_the_volume(self):
inst, vol = vm(active=False, vol_type=VIR_STORAGE_VOL_FILE)
vol.resize.assert_called_once_with(SIZE)
inst.instance.blockResize.assert_not_called()
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,198 @@
import threading
import time
import unittest
import warnings
from vrtManager.rwlock import ReadWriteLock
class TestReadWriteLock(unittest.TestCase):
def setUp(self):
self.lock = ReadWriteLock()
def test_single_thread_read_lock(self):
"""Test basic read lock acquire and release in a single thread."""
self.lock.acquireRead()
self.lock.release()
def test_single_thread_write_lock(self):
"""Test basic write lock acquire and release in a single thread."""
self.lock.acquireWrite()
self.lock.release()
def test_nested_read_locks(self):
"""Test reentrant read locks by the same thread."""
self.lock.acquireRead()
self.lock.acquireRead()
self.lock.release()
self.lock.release()
def test_nested_write_locks(self):
"""Test reentrant write locks by the same thread."""
self.lock.acquireWrite()
self.lock.acquireWrite()
self.lock.release()
self.lock.release()
def test_release_unheld_lock_raises_value_error(self):
"""Releasing a lock when none is held should raise ValueError."""
with self.assertRaises(ValueError):
self.lock.release()
def test_read_to_write_upgrade(self):
"""A thread holding a read lock can upgrade to a write lock."""
self.lock.acquireRead()
self.lock.acquireWrite(timeout=1.0)
# Should now hold write lock with nested count
self.lock.release()
self.lock.release()
def test_multiple_concurrent_readers(self):
"""Multiple threads should be able to acquire read locks simultaneously."""
active_readers = 0
max_concurrent_readers = 0
state_lock = threading.Lock()
barrier = threading.Barrier(5)
def reader_task():
nonlocal active_readers, max_concurrent_readers
barrier.wait()
self.lock.acquireRead(timeout=2.0)
try:
with state_lock:
active_readers += 1
if active_readers > max_concurrent_readers:
max_concurrent_readers = active_readers
time.sleep(0.05)
finally:
with state_lock:
active_readers -= 1
self.lock.release()
threads = [threading.Thread(target=reader_task) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
self.assertEqual(max_concurrent_readers, 5)
def test_writer_excludes_readers(self):
"""While a writer holds the lock, no reader should be able to acquire it."""
reader_acquired = False
reader_timed_out = False
self.lock.acquireWrite()
def reader_task():
nonlocal reader_acquired, reader_timed_out
try:
self.lock.acquireRead(timeout=0.1)
reader_acquired = True
self.lock.release()
except RuntimeError:
reader_timed_out = True
t = threading.Thread(target=reader_task)
t.start()
t.join()
self.lock.release()
self.assertFalse(reader_acquired)
self.assertTrue(reader_timed_out)
def test_readers_exclude_writer(self):
"""While readers hold the lock, a writer should be blocked until released."""
writer_acquired = False
writer_timed_out = False
self.lock.acquireRead()
def writer_task():
nonlocal writer_acquired, writer_timed_out
try:
self.lock.acquireWrite(timeout=0.1)
writer_acquired = True
self.lock.release()
except RuntimeError:
writer_timed_out = True
t = threading.Thread(target=writer_task)
t.start()
t.join()
self.lock.release()
self.assertFalse(writer_acquired)
self.assertTrue(writer_timed_out)
def test_writer_blocks_subsequent_readers(self):
"""Pending writers take priority and should block subsequent new readers."""
order = []
writer_ready_event = threading.Event()
# Step 1: Initial reader holds read lock
self.lock.acquireRead()
# Step 2: Writer attempts to acquire write lock and waits
def writer_task():
writer_ready_event.set()
self.lock.acquireWrite(timeout=2.0)
try:
order.append("writer")
finally:
self.lock.release()
# Step 3: New reader attempts to acquire read lock after writer is pending
def new_reader_task():
writer_ready_event.wait()
# small delay to ensure writer is queued in pendingwriters
time.sleep(0.05)
self.lock.acquireRead(timeout=2.0)
try:
order.append("new_reader")
finally:
self.lock.release()
w_thread = threading.Thread(target=writer_task)
r_thread = threading.Thread(target=new_reader_task)
w_thread.start()
r_thread.start()
writer_ready_event.wait()
time.sleep(0.1)
# Release initial reader: writer should acquire and complete before new reader
self.lock.release()
w_thread.join()
r_thread.join()
self.assertEqual(order, ["writer", "new_reader"])
def test_no_deprecation_warnings(self):
"""Verify that lock operations do not emit DeprecationWarnings (e.g. currentThread, notifyAll)."""
with warnings.catch_warnings(record=True) as caught_warnings:
warnings.simplefilter("always", DeprecationWarning)
lock = ReadWriteLock()
lock.acquireRead()
lock.release()
lock.acquireWrite()
lock.release()
# Check for currentThread or notifyAll warnings
deprecations = [
str(w.message)
for w in caught_warnings
if issubclass(w.category, DeprecationWarning)
and ("currentThread" in str(w.message) or "notifyAll" in str(w.message))
]
self.assertEqual(
deprecations,
[],
f"Found deprecation warnings: {deprecations}",
)
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,178 @@
"""VM statistics: rates are averages since the sample of the previous answer,
without waiting, when it is from the same run of the VM on the same host and
devices; otherwise over one second. The stats view runs in a sync worker,
polled every 5 s by each open VM page."""
import unittest
from unittest.mock import patch
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from vrtManager.instance import USAGE_MAX_WINDOW, wvmInstance
XML = """<domain><devices>
<disk type='file' device='disk'><source file='/var/lib/libvirt/images/a.qcow2'/><target dev='vda'/></disk>
<disk type='volume' device='disk'><source pool='default' volume='b.qcow2'/><target dev='vdb'/></disk>
<disk type='file' device='cdrom'><target dev='sda'/></disk>
<interface type='hostdev'><mac address='52:54:00:00:00:01'/></interface>
<interface type='network'><mac address='52:54:00:00:00:02'/><target dev='vnet0'/></interface>
</devices></domain>"""
class FakeDomain:
"""Counters grow by a fixed amount per second of the clock."""
def __init__(self, state, clock):
self.state = state
self.clock = clock
self.domain_id = 7
self.unsupported = False
def now(self):
return self.clock[0]
def ID(self):
return self.domain_id
def info(self):
return [self.state, 0, 0, 2, int(self.now() * 500_000_000)] # 0.5 s of CPU time per second
def blockStats(self, dev):
n = {"vda": 1, "vdb": 2}[dev] # disks are read by target name
written = -1 if self.unsupported and dev == "vdb" else int(self.now() * n * 3000)
return (0, int(self.now() * n * 1000), 0, written, 0)
def interfaceStats(self, dev):
assert dev == "vnet0"
return (int(self.now() * 100), 0, 0, 0, int(self.now() * 50), 0, 0, 0)
def instance(state, clock):
proxy = wvmInstance.__new__(wvmInstance) # no connection: only these calls
proxy._read_cache = None
proxy.instance = FakeDomain(state, clock)
proxy._XMLDesc = lambda flags: XML
proxy.host_info = ["x86_64", 0, 4] # 4 host CPUs
proxy.get_status = lambda: state
return proxy
class UsageTestCase(unittest.TestCase):
def setUp(self):
self.clock = [100.0]
patcher = patch("vrtManager.instance.time")
self.addCleanup(patcher.stop)
self.time = patcher.start()
self.time.monotonic.side_effect = lambda: self.clock[0]
self.time.sleep.side_effect = self.advance
def advance(self, seconds):
self.clock[0] += seconds
def usage(self, proxy, previous=None):
result = proxy.usage(previous)
self.time.sleep.reset_mock()
return result
def test_without_a_sample_over_one_second(self):
proxy = instance(1, self.clock)
result = proxy.usage()
self.time.sleep.assert_called_once_with(1)
self.assertEqual((result["cpu"], result["window"]), (12.5, 1.0)) # 0.5 s of 4 host CPUs
# a pool volume is measured like a file
self.assertEqual(
result["disks"], [{"dev": "vda", "rd": 1000, "wr": 3000}, {"dev": "vdb", "rd": 2000, "wr": 6000}]
)
# bits; the NIC without a target is not measured and keeps its place
self.assertEqual(result["nics"], [{"dev": 0, "rx": None, "tx": None}, {"dev": 1, "rx": 800, "tx": 400}])
self.assertEqual(result["sample"]["counters"][3], 101.0)
def test_since_the_previous_sample_without_waiting(self):
proxy = instance(1, self.clock)
sample = self.usage(proxy)["sample"]
self.advance(9.5)
result = proxy.usage(sample)
self.time.sleep.assert_not_called()
self.assertEqual((result["cpu"], result["window"]), (12.5, 9.5))
self.assertEqual(result["disks"][1], {"dev": "vdb", "rd": 2000, "wr": 6000}) # per second
self.assertEqual(result["nics"][1], {"dev": 1, "rx": 800, "tx": 400})
self.assertEqual(result["sample"]["counters"][3], 110.5)
def test_a_sample_of_another_run_host_or_devices_is_not_used(self):
def restarted(proxy):
proxy.instance.domain_id = 8
def other_host_cpus(proxy):
proxy.host_info = ["x86_64", 0, 8]
def nic_renamed(proxy):
proxy._XMLDesc = lambda flags: XML.replace("vnet0", "vnet1")
for change in (restarted, other_host_cpus, nic_renamed):
with self.subTest(change.__name__):
proxy = instance(1, self.clock)
sample = self.usage(proxy)["sample"]
change(proxy)
self.advance(5)
proxy.instance.interfaceStats = lambda dev: (0,) * 8
self.assertEqual(proxy.usage(sample)["window"], 1.0)
self.time.sleep.assert_called_once_with(1)
self.time.sleep.reset_mock()
def test_a_sample_too_old_from_the_future_or_counting_back_is_not_used(self):
for name, seconds in (("too old", USAGE_MAX_WINDOW + 1), ("future", -5), ("same time", 0)):
with self.subTest(name):
proxy = instance(1, self.clock)
sample = self.usage(proxy)["sample"]
self.advance(seconds)
self.assertEqual(proxy.usage(sample)["window"], 1.0)
self.time.sleep.assert_called_once_with(1)
self.time.sleep.reset_mock()
proxy = instance(1, self.clock)
sample = self.usage(proxy)["sample"]
sample["counters"][0] += 10**12 # CPU time went back: a restart with the same domain ID
self.advance(5)
self.assertEqual(proxy.usage(sample)["cpu"], 12.5)
self.time.sleep.assert_called_once_with(1)
def test_a_counter_the_host_does_not_report_has_no_value(self):
proxy = instance(1, self.clock)
proxy.instance.unsupported = True
disks = proxy.usage()["disks"]
self.assertEqual(disks[1], {"dev": "vdb", "rd": 2000, "wr": None})
def test_a_vm_restarted_during_the_second_has_no_values(self):
proxy = instance(1, self.clock)
domain = proxy.instance
real_sleep = self.time.sleep.side_effect
def restart(seconds):
real_sleep(seconds)
domain.info = lambda: [1, 0, 0, 2, 0] # CPU time starts over
self.time.sleep.side_effect = restart
result = proxy.usage()
self.assertEqual((result["cpu"], result["window"]), (None, None))
self.assertEqual(result["disks"][0], {"dev": "vda", "rd": None, "wr": None})
self.assertIn("sample", result)
def test_a_paused_or_shut_off_vm_is_not_sampled(self):
for state in (3, 5):
with self.subTest(state=state):
result = instance(state, self.clock).usage()
self.time.sleep.assert_not_called()
self.assertEqual(
result,
{
"cpu": 0,
"disks": [{"dev": "vda", "rd": 0, "wr": 0}, {"dev": "vdb", "rd": 0, "wr": 0}],
"nics": [{"dev": 0, "rx": 0, "tx": 0}, {"dev": 1, "rx": 0, "tx": 0}],
},
)
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,157 @@
import unittest
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from vrtManager.util import (
compareMAC,
get_xml_path,
is_kvm_available,
pretty_bytes,
pretty_mem,
randomMAC,
randomPasswd,
randomUUID,
validate_macaddr,
validate_uuid,
vol_dev_type,
vol_driver_type,
xml_escape,
)
class TestVrtManagerUtil(unittest.TestCase):
def test_random_uuid_format(self):
uuid_str = randomUUID()
# UUIDv4 format check
pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
self.assertRegex(uuid_str, pattern)
# Ensure successive UUIDs are distinct
self.assertNotEqual(uuid_str, randomUUID())
def test_validate_uuid(self):
valid_dashed = "c9a646d3-9c61-4cd8-8024-f6b72a6642d9"
self.assertEqual(validate_uuid(valid_dashed), valid_dashed)
valid_no_dash = "c9a646d39c614cd88024f6b72a6642d9"
self.assertEqual(validate_uuid(valid_no_dash), valid_dashed)
with self.assertRaises(ValueError):
validate_uuid("invalid-uuid-too-short")
with self.assertRaises(ValueError):
validate_uuid(12345)
def test_random_mac(self):
mac = randomMAC()
self.assertTrue(mac.startswith("52:54:10:"))
parts = mac.split(":")
self.assertEqual(len(parts), 6)
for part in parts:
self.assertEqual(len(part), 2)
int(part, 16) # valid hex
def test_compare_mac(self):
mac1 = "52:54:10:00:00:01"
mac2 = "52:54:10:00:00:02"
self.assertEqual(compareMAC(mac1, mac1), 0)
self.assertEqual(compareMAC(mac1, mac2), -1)
self.assertEqual(compareMAC(mac2, mac1), 1)
# Different lengths
self.assertEqual(compareMAC("52:54", "52:54:10"), -1)
self.assertEqual(compareMAC("52:54:10", "52:54"), 1)
def test_validate_macaddr(self):
self.assertIsNone(validate_macaddr(None))
self.assertIsNone(validate_macaddr("52:54:10:aa:bb:cc"))
self.assertIsNone(validate_macaddr("0:1:2:3:4:5"))
with self.assertRaises(ValueError):
validate_macaddr("invalid_mac")
with self.assertRaises(ValueError):
validate_macaddr(12345)
def test_random_passwd(self):
pwd = randomPasswd(16)
self.assertEqual(len(pwd), 16)
# Custom alphabet
numeric_pwd = randomPasswd(10, alphabet="0123456789")
self.assertEqual(len(numeric_pwd), 10)
self.assertTrue(numeric_pwd.isdigit())
def test_xml_escape(self):
self.assertIsNone(xml_escape(None))
escaped = xml_escape('<foo name="bar" test=\'baz\' &/>')
self.assertEqual(escaped, "&lt;foo name=&quot;bar&quot; test=&apos;baz&apos; &amp;/&gt;")
def test_get_xml_path(self):
xml = "<domain type='kvm'><name>testvm</name><memory unit='KiB'>1048576</memory></domain>"
self.assertEqual(get_xml_path(xml, "/domain/name"), "testvm")
self.assertEqual(get_xml_path(xml, "/domain/memory"), "1048576")
self.assertEqual(get_xml_path(xml, "/domain/@type"), "kvm")
# Function callback mode
def extract_name(doc):
return doc.xpath("/domain/name")[0].text
self.assertEqual(get_xml_path(xml, func=extract_name), "testvm")
# Missing both path and func
with self.assertRaises(ValueError):
get_xml_path(xml)
def test_is_kvm_available(self):
kvm_xml = "<capabilities><guest><arch name='x86_64'><domain type='kvm'/></arch></guest></capabilities>"
qemu_xml = "<capabilities><guest><arch name='x86_64'><domain type='qemu'/></arch></guest></capabilities>"
self.assertTrue(is_kvm_available(kvm_xml))
self.assertFalse(is_kvm_available(qemu_xml))
def test_pretty_mem(self):
# Value in KB
self.assertEqual(pretty_mem(1024), " 1 MB")
self.assertEqual(pretty_mem(1024 * 1024), "1024 MB")
self.assertEqual(pretty_mem(16 * 1024 * 1024), "16.00 GB")
def test_pretty_bytes(self):
# Value in Bytes
self.assertEqual(pretty_bytes(10 * 1024 * 1024), "10.00 MB")
self.assertEqual(pretty_bytes(2 * 1024 * 1024 * 1024), "2.00 GB")
def test_vol_dev_type(self):
self.assertEqual(vol_dev_type("ide"), "hd")
self.assertEqual(vol_dev_type("fdc"), "fd")
self.assertEqual(vol_dev_type("virtio"), "vd")
self.assertEqual(vol_dev_type("usb"), "sd")
self.assertEqual(vol_dev_type("scsi"), "sd")
self.assertEqual(vol_dev_type("sata"), "sd")
self.assertIsNone(vol_dev_type("unknown_bus"))
def test_vol_driver_type(self):
for vol_format in ("qcow2", "qcow", "raw"):
self.assertEqual(vol_driver_type(vol_format), vol_format)
for vol_format in (None, "", "none", "unknown", "iso"):
self.assertEqual(vol_driver_type(vol_format), "raw")
self.assertEqual(vol_driver_type("linux-lvm", "disk"), "raw") # a partition
class OperationErrorTestCase(unittest.TestCase):
def test_keeps_its_own_message_after_a_libvirt_failure(self):
import libvirt
from vrtManager.util import OperationError
conn = libvirt.open("test:///default")
self.addCleanup(conn.close)
with self.assertRaises(libvirt.libvirtError):
conn.lookupByName("no-such-domain") # leaves a thread-local last error
self.assertNotEqual(str(libvirt.libvirtError("own message")), "own message")
error = OperationError("own message")
self.assertEqual(str(error), "own message")
self.assertIsInstance(error, libvirt.libvirtError)
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,445 @@
import unittest
from unittest.mock import MagicMock
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from vrtManager import util
from vrtManager.instance import wvmInstance
from vrtManager.network import wvmNetworks, wvmNetwork
class TestXmlSafety(unittest.TestCase):
def test_create_network_escaping(self):
wn = wvmNetworks.__new__(wvmNetworks)
wn.wvm = MagicMock()
mock_net = MagicMock()
wn.get_network = MagicMock(return_value=mock_net)
malicious_name = "net<script>alert(1)</script>&name"
malicious_bridge = "br0' onfocus='alert(2)"
wn.create_network(
name=malicious_name,
forward="bridge",
ipv4=False,
gateway=None,
mask=None,
dhcp4=None,
ipv6=False,
gateway6=None,
prefix6=None,
dhcp6=None,
bridge=malicious_bridge,
openvswitch=False,
)
wn.wvm.networkDefineXML.assert_called_once()
defined_xml = wn.wvm.networkDefineXML.call_args[0][0]
self.assertNotIn("<script>", defined_xml)
self.assertIn("&lt;script&gt;alert(1)&lt;/script&gt;&amp;name", defined_xml)
self.assertIn("name='br0&apos; onfocus=&apos;alert(2)'", defined_xml)
def test_network_set_qos_validation(self):
wn = wvmNetwork.__new__(wvmNetwork)
wn._XMLDesc = MagicMock(return_value="<network><name>testnet</name></network>")
wn.wvm = MagicMock()
# Invalid direction
with self.assertRaises(ValueError):
wn.set_qos("lateral", 100, 200, 300)
# Invalid numeric strings
with self.assertRaises(ValueError):
wn.set_qos("inbound", "not_a_number", 200, 300)
def test_snapshot_escaping_and_recovery_on_failure(self):
inst = wvmInstance.__new__(wvmInstance)
inst.get_status = MagicMock(return_value=5) # shutoff
inst.change_snapshot_xml = MagicMock()
inst.recover_snapshot_xml = MagicMock()
inst._XMLDesc = MagicMock(return_value="<domain></domain>")
inst._snapshotCreateXML = MagicMock(side_effect=RuntimeError("libvirt exploded"))
bad_name = "snap<inject>&quot;"
bad_desc = "desc<tag>&"
with self.assertRaises(RuntimeError):
inst.create_snapshot(bad_name, bad_desc)
inst.change_snapshot_xml.assert_not_called()
inst.recover_snapshot_xml.assert_not_called()
def test_snapshot_xml_payload_is_escaped(self):
inst = wvmInstance.__new__(wvmInstance)
inst.get_status = MagicMock(return_value=5) # shutoff
inst.change_snapshot_xml = MagicMock()
inst.recover_snapshot_xml = MagicMock()
inst._XMLDesc = MagicMock(return_value="<domain></domain>")
inst._snapshotCreateXML = MagicMock(return_value=None)
bad_name = "snap<inject>&quotes"
bad_desc = "desc<tag>&and"
inst.create_snapshot(bad_name, bad_desc)
inst._snapshotCreateXML.assert_called_once()
created_xml = inst._snapshotCreateXML.call_args[0][0]
self.assertNotIn("<inject>", created_xml)
self.assertNotIn("<tag>", created_xml)
self.assertIn("&lt;inject&gt;&amp;quotes", created_xml)
self.assertIn("&lt;tag&gt;&amp;and", created_xml)
def test_external_snapshot_recovery(self):
inst = wvmInstance.__new__(wvmInstance)
inst.get_status = MagicMock(return_value=1)
inst.change_snapshot_xml = MagicMock()
inst.recover_snapshot_xml = MagicMock()
inst._XMLDesc = MagicMock(return_value="<domain></domain>")
inst._snapshotCreateXML = MagicMock(return_value=None)
inst.refresh_instance_pools = MagicMock()
inst.create_external_snapshot("ext_snap_1", desc="normal snapshot")
inst.change_snapshot_xml.assert_called_once()
inst.recover_snapshot_xml.assert_called_once()
inst.refresh_instance_pools.assert_called_once()
def test_secure_boot_loader_change_and_recovery(self):
inst = wvmInstance.__new__(wvmInstance)
inst.wvm = MagicMock()
uefi_xml = "<domain><os><loader readonly='yes' secure='yes' type='pflash'>/usr/share/OVMF/OVMF.fd</loader></os></domain>"
inst._XMLDesc = MagicMock(return_value=uefi_xml)
inst.change_snapshot_xml()
inst.wvm.defineXML.assert_called_once()
changed_xml = inst.wvm.defineXML.call_args[0][0]
self.assertIn("type=\"rom\"", changed_xml)
self.assertIn("secure=\"yes\"", changed_xml)
# Test recovery from rom back to pflash
inst._XMLDesc = MagicMock(return_value=changed_xml)
inst.wvm.defineXML.reset_mock()
inst.recover_snapshot_xml()
inst.wvm.defineXML.assert_called_once()
recovered_xml = inst.wvm.defineXML.call_args[0][0]
self.assertIn("type=\"pflash\"", recovered_xml)
self.assertIn("secure=\"yes\"", recovered_xml)
def test_original_rom_loader_preserved(self):
inst = wvmInstance.__new__(wvmInstance)
inst._XMLDesc = MagicMock(return_value="<domain><os><loader type='rom'>/usr/share/rom.bin</loader></os></domain>")
inst._defineXML = MagicMock()
inst._snapshotCreateXML = MagicMock(return_value=None)
inst.get_status = MagicMock(return_value=5)
# change_snapshot_xml should return False and not define XML
self.assertFalse(inst.change_snapshot_xml())
inst._defineXML.assert_not_called()
inst.create_snapshot("snap_rom", "testing rom preservation")
inst._defineXML.assert_not_called()
def test_malformed_xml_raises_in_change(self):
inst = wvmInstance.__new__(wvmInstance)
inst._XMLDesc = MagicMock(return_value="<malformed << xml")
with self.assertRaises(Exception):
inst.change_snapshot_xml()
def test_recovery_failure_raises(self):
inst = wvmInstance.__new__(wvmInstance)
inst._XMLDesc = MagicMock(return_value="<domain><os><loader type='rom'>/path</loader></os></domain>")
inst._defineXML = MagicMock(side_effect=RuntimeError("libvirt defineXML failed"))
with self.assertRaises(RuntimeError):
inst.recover_snapshot_xml()
def test_snapshot_revert_rom_loader_preserved(self):
inst = wvmInstance.__new__(wvmInstance)
inst.instance = MagicMock()
mock_snap = MagicMock()
inst.instance.snapshotLookupByName = MagicMock(return_value=mock_snap)
inst.instance.revertToSnapshot = MagicMock()
inst._defineXML = MagicMock()
inst.snapshot_revert("snap_rom")
inst.instance.snapshotLookupByName.assert_called_once_with("snap_rom", 0)
inst.instance.revertToSnapshot.assert_called_once_with(mock_snap, 0)
inst._defineXML.assert_not_called()
def test_internal_snapshot_never_rewrites_a_pflash_loader(self):
# libvirt decides; the definition is not switched to rom.
inst = wvmInstance.__new__(wvmInstance)
inst.get_status = MagicMock(return_value=5)
inst._XMLDesc = MagicMock(
return_value="<domain><os><loader readonly='yes' type='pflash'>/usr/share/OVMF/OVMF.fd</loader></os></domain>"
)
inst._defineXML = MagicMock()
inst._snapshotCreateXML = MagicMock()
inst.create_snapshot("snap", "uefi")
inst._defineXML.assert_not_called()
self.assertIn("type='pflash'", inst._snapshotCreateXML.call_args[0][0])
class TestCloneRollback(unittest.TestCase):
"""A failed clone removes only the volumes it created."""
def test_copies_are_removed_when_the_definition_fails(self):
import libvirt
inst = wvmInstance.__new__(wvmInstance)
inst.wvm = MagicMock()
inst.get_status = MagicMock(return_value=5)
inst.get_instances = MagicMock(return_value=[])
inst.get_nvram = MagicMock(return_value=None)
inst._XMLDesc = MagicMock(return_value=(
"<domain><name>src</name><devices>"
"<disk type='file' device='disk'><source file='/pool/src.qcow2'/><target dev='vda'/></disk>"
"</devices></domain>"
))
source_vol = MagicMock()
source_vol.XMLDesc.return_value = "<volume><target><format type='qcow2'/></target></volume>"
copy = MagicMock()
copy.path.return_value = "/pool/copy.qcow2"
source_vol.storagePoolLookupByVolume.return_value.createXMLFrom.return_value = copy
inst.get_volume_by_path = MagicMock(side_effect=lambda path: copy if path == "/pool/copy.qcow2" else source_vol)
inst._defineXML = MagicMock(side_effect=libvirt.libvirtError("bad definition"))
with self.assertRaises(libvirt.libvirtError):
inst.clone_instance({"name": "copy", "disk-vda": "copy.qcow2", "disk_owner_uid": 0, "disk_owner_gid": 0})
copy.delete.assert_called_once_with(0)
source_vol.delete.assert_not_called()
def test_running_vm_is_refused_before_anything_is_read(self):
import libvirt
inst = wvmInstance.__new__(wvmInstance)
inst.get_status = MagicMock(return_value=1)
inst._XMLDesc = MagicMock()
with self.assertRaisesRegex(libvirt.libvirtError, "Shut the VM down"):
inst.clone_instance({"name": "copy"})
inst._XMLDesc.assert_not_called()
if __name__ == "__main__":
unittest.main()
class TestDiskXmlEscaping(unittest.TestCase):
def _instance(self):
inst = wvmInstance.__new__(wvmInstance)
inst.get_status = MagicMock(return_value=5) # shutoff
inst.instance = MagicMock()
inst._defineXML = MagicMock()
return inst
def test_attach_disk_escapes_source_and_serial(self):
inst = self._instance()
inst.attach_disk(
"vdb",
"/images/a'/><disk type='file",
target_bus="virtio",
disk_type="file",
serial="S1</serial><evil/>",
)
xml = inst.instance.attachDeviceFlags.call_args[0][0]
self.assertIn("file='/images/a&apos;/&gt;&lt;disk type=&apos;file'", xml)
self.assertIn("<serial>S1&lt;/serial&gt;&lt;evil/&gt;</serial>", xml)
self.assertNotIn("<evil/>", xml)
def test_edit_disk_escapes_source_and_serial(self):
inst = self._instance()
inst._XMLDesc = MagicMock(
return_value=(
"<domain><devices><disk type='file' device='disk'>"
"<driver name='qemu' type='qcow2'/>"
"<source file='/images/vm.qcow2'/>"
"<target dev='vda' bus='virtio'/>"
"</disk></devices></domain>"
)
)
inst.edit_disk(
"vda",
"/images/b'/><x y='",
False,
False,
"virtio",
"S2<evil/>",
"qcow2",
"default",
"default",
"default",
"default",
)
xml = inst._defineXML.call_args.args[0]
from lxml import etree
self.assertEqual(etree.fromstring(xml).find("devices/disk/source").get("file"), "/images/b'/><x y='")
self.assertNotIn("<evil/>", xml)
def test_detach_disk_does_not_evaluate_dev_as_xpath(self):
inst = self._instance()
inst._XMLDesc = MagicMock(
return_value=(
"<domain><devices><disk type='file' device='disk'>"
"<source file='/images/vm.qcow2'/>"
"<target dev='vda' bus='virtio'/>"
"</disk></devices></domain>"
)
)
with self.assertRaises(util.OperationError):
inst.detach_disk("x' or '1'='1")
inst.instance.detachDeviceFlags.assert_not_called()
class TestPersistentDiskEditing(unittest.TestCase):
def setUp(self):
import libvirt
from lxml import etree
self.conn = libvirt.open("test:///default")
self.addCleanup(self.conn.close)
self.domain = self.conn.lookupByName("test")
original = self.domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE | libvirt.VIR_DOMAIN_XML_SECURE)
self.addCleanup(self.conn.defineXML, original)
config = etree.fromstring(original)
disk = config.find("devices/disk")
disk.set("type", "file")
driver = disk.find("driver")
if driver is None:
driver = etree.SubElement(disk, "driver")
driver.set("name", "qemu")
driver.set("type", "qcow2")
driver.set("queues", "4")
etree.SubElement(disk, "address", type="pci", slot="0x05")
encryption = etree.SubElement(disk, "encryption", format="luks")
etree.SubElement(encryption, "secret", type="passphrase", uuid="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
etree.SubElement(etree.SubElement(disk, "iotune"), "total_bytes_sec").text = "1024"
# A pending change and a VNC password outside the edited disk must survive.
config.find("memory").text = "123456"
etree.SubElement(config.find("devices"), "graphics", type="vnc", passwd="keep-secret")
self.conn.defineXML(etree.tostring(config).decode())
self.inst = wvmInstance.__new__(wvmInstance)
self.inst.wvm = self.conn
self.inst.instance = self.domain
self.target = disk.find("target").get("dev")
def config(self):
import libvirt
from lxml import etree
return etree.fromstring(self.domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE | libvirt.VIR_DOMAIN_XML_SECURE))
def edit(self, bus="virtio", format="qcow2"):
self.inst.edit_disk(
self.target, "/images/a'<new>&.qcow2", False, False, bus,
"serial<&", format, "none", "native", "unmap", "on",
)
def test_running_bus_change_preserves_disk_and_pending_domain_configuration(self):
self.assertTrue(self.domain.isActive())
live_before = self.domain.XMLDesc(0)
self.edit(bus="sata")
config = self.config()
disk = config.find("devices/disk")
self.assertEqual(disk.find("target").get("bus"), "sata")
self.assertTrue(disk.find("target").get("dev").startswith("sd"))
self.assertNotEqual(disk.find("address").get("type"), "pci")
self.assertIsNone(disk.find("driver").get("queues"))
self.assertEqual(disk.find("driver").get("cache"), "none")
self.assertEqual(disk.findtext("iotune/total_bytes_sec"), "1024")
self.assertEqual(disk.find("encryption/secret").get("uuid"), "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
self.assertEqual(disk.find("source").get("file"), "/images/a'<new>&.qcow2")
self.assertEqual(disk.findtext("serial"), "serial<&")
self.assertEqual(config.findtext("memory"), "123456")
self.assertEqual(config.find("devices/graphics").get("passwd"), "keep-secret")
self.assertEqual(self.domain.XMLDesc(0), live_before)
# The resulting disk can be edited again before the VM restarts.
self.target = disk.find("target").get("dev")
self.edit(bus="sata")
self.assertEqual(self.config().find("devices/disk/target").get("dev"), self.target)
def test_failed_definition_leaves_the_original_configuration_intact(self):
from libvirt import libvirtError
from unittest.mock import patch
before = self.domain.XMLDesc(2)
with patch.object(self.inst, "_defineXML", side_effect=libvirtError("definition failed")):
with self.assertRaises(libvirtError):
self.edit(bus="sata")
self.assertEqual(self.domain.XMLDesc(2), before)
def test_network_disk_keeps_auth_source_and_encryption(self):
from lxml import etree
config = self.config()
disk = config.find("devices/disk")
disk.set("type", "network")
disk.remove(disk.find("source"))
source = etree.SubElement(disk, "source", protocol="rbd", name="pool/vm-disk")
etree.SubElement(source, "host", name="mon1", port="6789")
auth = etree.SubElement(disk, "auth", username="libvirt")
etree.SubElement(auth, "secret", type="ceph", uuid="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
self.conn.defineXML(etree.tostring(config).decode())
self.edit(bus="sata")
result = self.config().find("devices/disk")
self.assertEqual(result.find("source").get("name"), "pool/vm-disk")
self.assertEqual(result.find("source/host").get("name"), "mon1")
self.assertEqual(result.find("auth").get("username"), "libvirt")
self.assertIsNotNone(result.find("encryption"))
def test_empty_format_omits_driver_type(self):
self.edit(format="")
self.assertIsNone(self.config().find("devices/disk/driver").get("type"))
def test_virtio_options_survive_editing_on_the_same_bus(self):
self.edit()
self.assertEqual(self.config().find("devices/disk/driver").get("queues"), "4")
class TestEmptyDiskClone(unittest.TestCase):
def test_clone_omits_empty_disks_before_real_libvirt_definition(self):
import libvirt
from lxml import etree
conn = libvirt.open("test:///default")
self.addCleanup(conn.close)
domain = conn.lookupByName("test")
config = etree.fromstring(domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE))
devices = config.find("devices")
for disk in devices.findall("disk"):
source = disk.find("source")
if source is not None:
disk.remove(source)
for interface in devices.findall("interface"):
devices.remove(interface)
inst = wvmInstance.__new__(wvmInstance)
inst.wvm = conn
inst.instance = domain
# Exercise defensive handling of incomplete disk metadata, while the
# clone is still defined and looked up through the real test driver.
inst._XMLDesc = MagicMock(return_value=etree.tostring(config).decode())
inst.get_status = MagicMock(return_value=5) # only shut-off VMs are cloned
clone_uuid = inst.clone_instance({"name": "empty-disk-clone"})
clone = conn.lookupByUUIDString(clone_uuid)
self.addCleanup(clone.undefine)
clone_config = etree.fromstring(clone.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE))
self.assertEqual(clone_config.findtext("name"), "empty-disk-clone")
self.assertEqual(clone_config.findall("devices/disk"), [])
self.assertTrue(config.findall("devices/disk"))
def test_clone_rejects_existing_name_before_allocating_storage(self):
import libvirt
conn = libvirt.open("test:///default")
self.addCleanup(conn.close)
inst = wvmInstance.__new__(wvmInstance)
inst.wvm = conn
inst.instance = conn.lookupByName("test")
inst._XMLDesc = MagicMock()
inst.get_status = MagicMock(return_value=5)
with self.assertRaisesRegex(ValueError, "already exists"):
inst.clone_instance({"name": "test"})
inst._XMLDesc.assert_not_called()

View file

@ -0,0 +1,65 @@
"""Values from requests do not change the XPath queries they are used in."""
import unittest
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from vrtManager.connection import wvmConnect
from vrtManager.network import wvmNetwork
from vrtManager.nwfilters import wvmNWFilter
CAPS = """<capabilities><guest><arch name='x86_64'><wordsize>64</wordsize><emulator>/usr/bin/qemu-x86</emulator>
<machine canonical='pc-q35'>q35</machine></arch>
<arch name='aarch64'><emulator>/usr/bin/qemu-arm</emulator><machine>virt</machine></arch></guest></capabilities>"""
FILTER = """<filter name='f'><rule action='drop' direction='in' priority='1'><tcp/></rule>
<rule action='drop' direction='in' priority='2'><udp/></rule></filter>"""
class FakeConn:
def __init__(self):
self.defined = None
def networkDefineXML(self, xml):
self.defined = xml
class XPathValueTests(unittest.TestCase):
def setUp(self):
self.conn = wvmConnect.__new__(wvmConnect)
self.conn.get_cap_xml = lambda: CAPS
def test_arch_lookups(self):
self.assertEqual(self.conn.get_emulator("x86_64"), "/usr/bin/qemu-x86")
self.assertEqual(self.conn.get_machine_types("aarch64"), ["virt"])
injected = "nope' or '1'='1"
self.assertIsNone(self.conn.get_emulator(injected))
self.assertEqual(self.conn.get_machine_types(injected), [])
self.assertEqual(self.conn.get_capabilities(injected), {})
def test_nwfilter_rule_is_matched_by_its_attributes(self):
nwf = wvmNWFilter.__new__(wvmNWFilter)
nwf._XMLDesc = lambda flags: FILTER
# values with quotes or brackets are compared, not parsed as XPath
self.assertEqual(nwf.delete_rule("drop'][@priority='2", "in", "1").count("<rule "), 2)
self.assertEqual(nwf.delete_rule("it's", "in", "1").count("<rule "), 2)
kept = nwf.delete_rule("drop", "in", "2")
self.assertEqual(kept.count("<rule "), 1)
self.assertIn("priority=\"1\"", kept)
# add_rule appends to the matching rule only
added = nwf.add_rule("<rule action='drop' direction='in' priority='2'><icmp/></rule>")
self.assertEqual(added.count("<rule "), 2)
self.assertIn("<icmp", added.split('priority="2"')[1])
def test_qos_direction_must_be_known(self):
net = wvmNetwork.__new__(wvmNetwork)
net.wvm = FakeConn()
net._XMLDesc = lambda flags: "<network><bandwidth><inbound average='1'/><outbound average='2'/></bandwidth></network>"
with self.assertRaises(ValueError):
net.unset_qos("*")
net.unset_qos("inbound")
self.assertNotIn("inbound", net.wvm.defined)
self.assertIn("outbound", net.wvm.defined)