1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 03:36:48 +00:00

test: each test is in the tests package of the code it tests

Tests that build vrtManager objects and touch no view, form or model of
an app lived in that app; vrtManager/tests already holds such tests, so
the convention was split. They move to vrtManager/tests: the firmware,
CPU mode and PCIe port definitions of a new VM (test_create), USB and PCI
host devices, the console listener, host CPU reads, interface reads,
deletes and XML, network filters, PCI passthrough rules, node devices,
network changes, addresses and leases, pool XML, secrets, and the live
tests that use vrtManager only (disk resize, ISO, snapshots, XML, live
clone). livetest.py, the live-test helper six apps import, and the
network test driver move with them.

Others go to their app: the quota with a host down to instances, the
401/403 Logs rows (webvirtcloud's middleware) to webvirtcloud, the VM
page's tab script test to instances, novncd's own tests out of the
console views tests, the compute lock and input validation tests out of
the computes views tests, the interface views out of the forms tests.
instances/tests/test_views.py, entirely live, is test_live_views.py.

Test modules imported fixtures from each other; shared ones are now in
vrtManager/tests/fakes.py (migration fakes, node devices, IOMMU groups,
leases, a NAT network, a Ceph secret), interfaces/tests/fakes.py,
console/tests/daemons.py (novncd loaded once), livetest.wait_for and
instances/tests/create_page.BODY. Every live test checks
livetest.enabled(). The suite runs the same 2097 tests.
This commit is contained in:
catborise 2026-11-06 11:33:42 +03:00
parent 4e0952aa50
commit bdc8a6755c
73 changed files with 2231 additions and 2132 deletions

View file

@ -0,0 +1,15 @@
"""libvirt's test driver as the network tests need it."""
from unittest.mock import patch
import libvirt
def without_inactive_flag(test):
"""The test driver refuses VIR_NETWORK_XML_INACTIVE, which real hosts take:
the saved definition is read as the running one, the same for a network
the test does not change while it runs."""
xml_desc = libvirt.virNetwork.XMLDesc
patcher = patch.object(libvirt.virNetwork, "XMLDesc", lambda net, flags=0: xml_desc(net, 0))
patcher.start()
test.addCleanup(patcher.stop)

318
vrtManager/tests/fakes.py Normal file
View file

@ -0,0 +1,318 @@
"""Fake libvirt hosts, domains and node devices that tests of several
modules share: the migration source and destination, the host devices of
a q35 host, and the IOMMU groups of the PCI passthrough rules."""
from unittest.mock import MagicMock
import libvirt
from lxml import etree
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 XMLDesc(self, flags=0):
return "<pool><target><path>/elsewhere</path></target></pool>"
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, nvram=None, **domain):
self.state = state
self.nvram = nvram
self.instance = FakeDomain(**domain)
def get_nvram(self):
return self.nvram
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
def with_disks(*disks):
return "<domain><devices>%s</devices></domain>" % "".join(disks)
def pci(slot, product, code, driver=None, group=None):
iommu = f"<iommuGroup number='{group}'><address domain='0x0000' bus='0x00' slot='{slot}' function='0x0'/></iommuGroup>" \
if group is not None else ""
driver = f"<driver><name>{driver}</name></driver>" if driver else ""
return (f"pci_0000_00_{int(slot, 16):02x}_0", f"""<device><name>pci_0000_00_{int(slot, 16):02x}_0</name><parent>computer</parent>{driver}
<capability type='pci'><class>{code}</class><domain>0</domain><bus>0</bus><slot>{int(slot, 16)}</slot><function>0</function>
<product id='0x1000'>{product}</product><vendor id='0x1af4'>Red Hat, Inc.</vendor>{iommu}</capability></device>""")
def child(name, parent, capability):
return name, f"<device><name>{name}</name><parent>{parent}</parent>{capability}</device>"
def usb(name, parent, bus, device, product="QEMU Tablet", vendor_id="0x0627", product_id="0x0001"):
return name, f"""<device><name>{name}</name><parent>{parent}</parent><capability type='usb_device'><bus>{bus}</bus>
<device>{device}</device><product id='{product_id}'>{product}</product><vendor id='{vendor_id}'>Adomax</vendor>
</capability></device>"""
def interface(name, parent, code):
return child(name, parent, f"<capability type='usb'><number>0</number><class>{code}</class></capability>")
NODES = [
pci("0x06", "Virtio network device", "0x020000", "virtio-pci", group=3),
child("net_eth0", "pci_0000_00_06_0", "<capability type='net'><interface>eth0</interface></capability>"),
pci("0x03", "Virtio block device", "0x010000", "virtio-pci", group=2),
child("virtio1", "pci_0000_00_03_0", "<capability type='system'/>"), # a level between, not a resource
child("block_vda", "virtio1", "<capability type='storage'><block>/dev/vda</block></capability>"),
pci("0x02", "Virtio 1.0 GPU", "0x030000", "virtio-pci", group=2),
child("drm_card0", "pci_0000_00_02_0", "<devnode type='dev'>/dev/dri/card0</devnode><capability type='drm'><type>primary</type></capability>"),
child("drm_render", "pci_0000_00_02_0", "<devnode type='dev'>/dev/dri/renderD128</devnode><capability type='drm'><type>render</type></capability>"),
pci("0x0a", "Ethernet controller", "0x020000", "vfio-pci", group=4),
pci("0x01", "PIIX3 USB", "0x0c0300", "uhci_hcd"),
usb("usb_usb1", "pci_0000_00_01_0", 1, 1, "1.1 root hub", "0x1d6b", "0x0001"),
interface("usb_1_0_1_0", "usb_usb1", 9),
usb("usb_1_1", "usb_usb1", 1, 2),
interface("usb_1_1_1_0", "usb_1_1", 3),
usb("usb_1_2", "usb_usb1", 1, 3),
interface("usb_1_2_1_0", "usb_1_2", 3),
]
def node(name, xml):
return MagicMock(**{"name.return_value": name, "XMLDesc.return_value": xml})
BY_MODEL = "<hostdev mode='subsystem' type='usb' managed='yes'><source{auto}><vendor id='0x0627'/><product id='0x0001'/>{address}</source></hostdev>"
def by_model(address="", auto=""):
return BY_MODEL.format(address=address, auto=auto)
def domain(*devices):
return f"<domain><name>kiosk-1</name><devices>{''.join(devices)}</devices></domain>"
def device(slot, product, code, driver, group, members=None, bridge=False):
"""A PCI node of an IOMMU test host: a device on bus 0 at slot, in IOMMU group with the slots members (its own by default)."""
members = members or [slot]
addresses = "".join(f"<address domain='0x0000' bus='0x00' slot='0x{m:02x}' function='0x0'/>" for m in members)
driver_xml = f"<driver><name>{driver}</name></driver>" if driver else ""
bridge_xml = "<capability type='pci-bridge'/>" if bridge else ""
xml = (f"<device><name>pci_0000_00_{slot:02x}_0</name><parent>computer</parent>{driver_xml}"
f"<capability type='pci'><class>{code}</class><domain>0</domain><bus>0</bus><slot>{slot}</slot><function>0</function>"
f"<product id='0x0001'>{product}</product><vendor id='0x8086'>Intel</vendor>{bridge_xml}"
f"<iommuGroup number='{group}'>{addresses}</iommuGroup></capability></device>")
return f"pci_0000_00_{slot:02x}_0", etree.fromstring(xml)
IOMMU_NODES = dict([
device(0x02, "PCIe Root port", "0x060400", "pcieport", 2, bridge=True),
device(0x03, "SATA controller", "0x010601", "ahci", 7, [0x03, 0x04, 0x05]),
device(0x04, "SMBus controller", "0x0c0500", "i801_smbus", 7, [0x03, 0x04, 0x05]),
device(0x05, "LPC controller", "0x060100", "lpc_ich", 7, [0x03, 0x04, 0x05]), # class 0x06, not a bridge
device(0x06, "Gigabit NIC", "0x020000", "e1000e", 12),
device(0x07, "Spare NIC", "0x020000", "vfio-pci", 13),
device(0x08, "NIC port 0", "0x020000", "vfio-pci", 14, [0x08, 0x09, 0x0a]),
device(0x09, "NIC port 1", "0x020000", None, 14, [0x08, 0x09, 0x0a]),
device(0x0a, "Downstream port", "0x060400", "pcieport", 14, [0x08, 0x09, 0x0a], bridge=True),
device(0x0b, "Accelerator", "0x120000", "vfio-pci", 15, [0x0b, 0x0c]), # 0x0c cannot be read
device(0x0d, "GPU", "0x030000", "nouveau", 16, [0x0d, 0x0e]),
device(0x0e, "GPU audio", "0x040300", "snd_hda_intel", 16, [0x0d, 0x0e]),
])
# what the host has below: a disk on the SATA controller, its interface on the e1000e
IOMMU_NODES["block_sda"] = etree.fromstring("<device><name>block_sda</name><parent>pci_0000_00_03_0</parent>"
"<capability type='storage'><block>/dev/sda</block></capability></device>")
IOMMU_NODES["net_eth1"] = etree.fromstring("<device><name>net_eth1</name><parent>pci_0000_00_06_0</parent>"
"<capability type='net'><interface>eth1</interface></capability></device>")
def vm(name, hostdevs, saved=None):
"""A VM with hostdevs as it runs and, given saved, persistent with those for its next start."""
def describe(flags=0):
return f"<domain><devices>{saved if flags else hostdevs}</devices></domain>"
return MagicMock(**{"name.return_value": name, "XMLDesc.side_effect": describe, "isPersistent.return_value": saved is not None})
PCI_0A = "<hostdev mode='subsystem' type='pci'><source><address domain='0x0000' bus='0x00' slot='0x0a' function='0x0'/></source></hostdev>"
USB_1_3 = "<hostdev mode='subsystem' type='usb'><source><vendor id='0x0627'/><product id='0x0001'/><address bus='1' device='3'/></source></hostdev>"
DUID = "00:01:00:01:2e:8b:3c:41:52:54:00:00:00:40"
def lease(ip, mac=None, expiry=1793265489, hostname=None, clientid=None, family=0):
return {"iface": "virbr9", "expirytime": expiry, "type": family, "mac": mac, "ipaddr": ip, "prefix": 24,
"hostname": hostname, "clientid": clientid, "iaid": None}
# a host with a NAT network that hands out addresses (libvirt's test driver node file)
NAT_NETWORK_NODE = """<node>
<network><name>lab</name><forward mode='nat'/><bridge name='virbr9'/>
<ip address='192.0.2.1' netmask='255.255.255.0'><dhcp><range start='192.0.2.10' end='192.0.2.20'/>
<host mac='52:54:00:00:00:31' name='web' ip='192.0.2.31'/></dhcp></ip>
<ip family='ipv6' address='2001:db8:1::1' prefix='64'><dhcp><host id='0:1:0:1' ip='2001:db8:1::31'/></dhcp></ip>
</network>
<network><name>lan</name><forward mode='bridge'/><bridge name='br0'/></network>
<network><name>direct</name><forward mode='bridge'><interface dev='eth0'/></forward></network>
<domain type='test'><name>web</name><memory>1024</memory><os><type>hvm</type></os>
<devices><interface type='network'><source network='lab'/></interface></devices></domain>
</node>"""
CEPH = "8f2c0000-0000-4000-8000-000000000001"

View file

@ -0,0 +1,137 @@
"""Safety helpers for the live libvirt test suite.
Everything the suite creates is named with PREFIX, and its volumes live
in a dedicated pool. Cleanup only removes prefixed domains and the
volumes of that pool, so a broken test cannot delete other VMs or images
on a shared host. inventory() lets a suite check that nothing else
changed.
"""
import os
import libvirt
import time
PREFIX = "wvc-test-"
POOL = "wvc-test"
POOL_PATH = "/var/lib/libvirt/wvc-test"
UNDEFINE_FLAGS = (
libvirt.VIR_DOMAIN_UNDEFINE_MANAGED_SAVE
| libvirt.VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA
| libvirt.VIR_DOMAIN_UNDEFINE_NVRAM
| libvirt.VIR_DOMAIN_UNDEFINE_CHECKPOINTS_METADATA
)
def enabled():
"""The suite creates and deletes VMs, so it only runs on an explicitly named host."""
return bool(os.environ.get("TEST_LIBVIRT_HOST"))
def ensure_pool(conn):
try:
pool = conn.storagePoolLookupByName(POOL)
except libvirt.libvirtError:
pool = conn.storagePoolDefineXML(
f"<pool type='dir'><name>{POOL}</name><target><path>{POOL_PATH}</path></target></pool>"
)
pool.build(0)
if not pool.isActive():
pool.create(0)
pool.refresh(0)
return pool
def cleanup(conn):
"""Remove prefixed domains and every volume of the test pool."""
for dom in conn.listAllDomains():
if not dom.name().startswith(PREFIX):
continue
persistent = dom.isPersistent()
if dom.isActive():
dom.destroy()
if persistent:
dom.undefineFlags(UNDEFINE_FLAGS)
try:
pool = conn.storagePoolLookupByName(POOL)
except libvirt.libvirtError:
return
if pool.isActive():
pool.refresh(0)
for vol in pool.listAllVolumes():
vol.delete(0)
def remove_pool(conn):
cleanup(conn)
try:
pool = conn.storagePoolLookupByName(POOL)
except libvirt.libvirtError:
return
if pool.isActive():
pool.destroy()
pool.delete(0)
pool.undefine()
def inventory(conn):
"""Domains and volumes outside the test namespace."""
domains = {(d.name(), d.UUIDString()) for d in conn.listAllDomains() if not d.name().startswith(PREFIX)}
volumes = set()
for pool in conn.listAllStoragePools():
if pool.name() == POOL or not pool.isActive():
continue
pool.refresh(0)
volumes.update((pool.name(), v.name()) for v in pool.listAllVolumes())
return domains, volumes
def create_volume(conn, name, size_mib=64, fmt="qcow2"):
"""A volume named name.fmt in the test pool; returns its path."""
assert name.startswith(PREFIX)
pool = conn.storagePoolLookupByName(POOL)
vol = pool.createXML(
f"<volume><name>{name}.{fmt}</name><capacity unit='MiB'>{size_mib}</capacity>"
f"<target><format type='{fmt}'/></target></volume>",
0,
)
return vol.path()
OVMF_CODE = "/usr/share/edk2/ovmf/OVMF_CODE.fd"
def define_vm(conn, name, disk_paths, uefi=False, extra_devices="", disk_format="qcow2"):
"""A small q35 VM without an OS; disks are attached as vda, vdb, ..."""
assert name.startswith(PREFIX)
disks = "".join(
f"<disk type='file' device='disk'><driver name='qemu' type='{disk_format}'/>"
f"<source file='{path}'/><target dev='vd{chr(ord('a') + i)}' bus='virtio'/></disk>"
for i, path in enumerate(disk_paths)
)
firmware = (
"<os><type arch='x86_64' machine='q35'>hvm</type>"
f"<loader readonly='yes' type='pflash'>{OVMF_CODE}</loader></os>"
if uefi
else "<os><type arch='x86_64' machine='q35'>hvm</type></os>"
)
return conn.defineXML(
f"<domain type='kvm'><name>{name}</name><memory unit='MiB'>128</memory><vcpu>1</vcpu>"
f"{firmware}<features><acpi/></features><devices>{disks}{extra_devices}</devices></domain>"
)
def volume_exists(conn, path):
pool = conn.storagePoolLookupByName(POOL)
pool.refresh(0)
return os.path.basename(path) in pool.listVolumes()
def wait_for(predicate, seconds):
end = time.monotonic() + seconds
while time.monotonic() < end:
if predicate():
return True
time.sleep(0.5)
return False

View file

@ -3,6 +3,7 @@ from unittest.mock import MagicMock
from lxml import etree
from vrtManager.instance import PERSISTENT_XML, wvmInstance
from django.test import TestCase
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>"
@ -39,3 +40,29 @@ class ConsoleSettersTestCase(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
class ConsoleListensLocallyTests(TestCase):
"""Only a binding the persistent XML pins to loopback (or a Unix socket) is local."""
def listens_locally(self, graphics):
proxy = wvmInstance.__new__(wvmInstance) # no connection: only the XML is read
xml = f"<domain><devices>{graphics}</devices></domain>"
proxy._XMLDesc = lambda flags: xml if flags == PERSISTENT_XML else "<wrong-flags/>"
return proxy.console_listens_locally()
def test_cases(self):
cases = {
"<graphics type='vnc' listen='127.0.0.1'><listen type='address' address='127.0.0.1'/></graphics>": True,
"<graphics type='vnc'><listen type='address' address='::1'/></graphics>": True,
"<graphics type='vnc'><listen type='socket' socket='/run/vnc.sock'/></graphics>": True,
"": True, # no console
"<graphics type='vnc' listen='0.0.0.0'><listen type='address' address='0.0.0.0'/></graphics>": False,
"<graphics type='vnc'><listen type='network' network='default'/></graphics>": False,
"<graphics type='vnc' autoport='yes'/>": False, # the host default decides
"<graphics type='vnc'><listen type='address' address='localhost'/></graphics>": False,
"<graphics type='vnc' listen='127.0.0.1'><listen type='address' address='10.0.0.5'/></graphics>": False,
}
for graphics, expected in cases.items():
with self.subTest(graphics=graphics):
self.assertIs(self.listens_locally(graphics), expected)

View file

@ -0,0 +1,183 @@
"""A new VM's firmware and TPM: the choices come from the host's domain
capabilities, UEFI is libvirt's firmware autoselection (Secure Boot with
Microsoft's keys enrolled, as Windows 11 needs) and a TPM is an emulated
TPM 2.0 (swtpm)."""
from unittest.mock import MagicMock, patch
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.connection import wvmConnect
from vrtManager.create import wvmCreate
from vrtManager.create import PCIE_ROOT_PORTS
FULL = """<domainCapabilities><os supported='yes'>
<enum name='firmware'><value>efi</value></enum>
<firmwareFeatures supported='yes'>
<enum name='secureBoot'><value>yes</value><value>no</value></enum>
<enum name='enrolledKeys'><value>yes</value><value>no</value></enum>
</firmwareFeatures>
</os><devices><tpm supported='yes'>
<enum name='model'><value>tpm-tis</value><value>tpm-crb</value></enum>
<enum name='backendModel'><value>emulator</value><value>external</value></enum>
<enum name='backendVersion'><value>2.0</value></enum>
</tpm></devices></domainCapabilities>"""
# Secure Boot firmware without Microsoft's keys, and no swtpm
BARE = """<domainCapabilities><os supported='yes'>
<enum name='firmware'><value>efi</value></enum>
<firmwareFeatures supported='yes'>
<enum name='secureBoot'><value>yes</value><value>no</value></enum>
<enum name='enrolledKeys'><value>no</value></enum>
</firmwareFeatures>
</os><devices><tpm supported='no'/></devices></domainCapabilities>"""
# only Secure Boot firmware, and a TPM of another machine
SECURE_ONLY = """<domainCapabilities><os supported='yes'>
<enum name='firmware'><value>efi</value></enum>
<firmwareFeatures supported='yes'>
<enum name='secureBoot'><value>yes</value></enum>
<enum name='enrolledKeys'><value>yes</value></enum>
</firmwareFeatures>
</os><devices><tpm supported='yes'>
<enum name='model'><value>tpm-spapr</value></enum>
<enum name='backendModel'><value>emulator</value></enum>
<enum name='backendVersion'><value>2.0</value></enum>
</tpm></devices></domainCapabilities>"""
# libvirt before 12.1: no firmwareFeatures, the loaders say whether they do Secure Boot
EARLIER = """<domainCapabilities><os supported='yes'>
<enum name='firmware'><value>efi</value></enum>
<loader supported='yes'><enum name='secure'><value>yes</value><value>no</value></enum></loader>
</os></domainCapabilities>"""
EARLIER_PLAIN = """<domainCapabilities><os supported='yes'>
<enum name='firmware'><value>efi</value></enum>
<loader supported='yes'/>
</os></domainCapabilities>"""
DOM_CAPS = {"domain": "kvm", "os_support": "yes", "cpu_modes": ["host-model"]}
def host(dom_cap_xml):
conn = wvmConnect.__new__(wvmConnect)
conn.get_dom_cap_xml = lambda arch, machine: dom_cap_xml
return conn
def defined(firmware, tpm_model=None):
conn = wvmCreate.__new__(wvmCreate)
conn.wvm = MagicMock()
with patch.object(wvmCreate, "get_capabilities", return_value={"features": ["acpi"], "os_type": "hvm"}), \
patch.object(wvmCreate, "get_dom_capabilities", return_value=DOM_CAPS):
conn.create_instance(name="vm-1", memory=512, vcpu=1, vcpu_mode="host-model",
uuid="00000000-0000-4000-8000-0000000000c2", arch="x86_64", machine="q35",
firmware=firmware, volumes=[], networks="", nwfilter="", virtio=True,
listener_addr="0.0.0.0", add_cdrom="None", add_input="None", tpm_model=tpm_model)
return etree.fromstring(conn.wvm.defineXML.call_args.args[0])
class HostOffersTests(SimpleTestCase):
def test_a_host_with_enrolled_keys_and_swtpm_offers_everything(self):
self.assertEqual(host(FULL).get_firmwares("x86_64", "q35"), ["BIOS", "UEFI", "UEFI Secure Boot"])
self.assertEqual(host(FULL).get_tpm_model("x86_64", "q35"), "tpm-crb")
def test_secure_boot_without_microsofts_keys_and_no_swtpm_are_not_offered(self):
self.assertEqual(host(BARE).get_firmwares("x86_64", "q35"), ["BIOS", "UEFI"])
self.assertIsNone(host(BARE).get_tpm_model("x86_64", "q35"))
def test_bios_is_x86_only_and_secure_boot_x86_64_only(self):
self.assertEqual(host(FULL).get_firmwares("aarch64", "virt"), ["UEFI"])
def test_uefi_is_offered_only_where_secure_boot_can_be_off(self):
self.assertEqual(host(SECURE_ONLY).get_firmwares("x86_64", "q35"), ["BIOS", "UEFI Secure Boot"])
def test_an_earlier_libvirt_offers_what_its_loaders_do(self):
self.assertEqual(host(EARLIER).get_firmwares("x86_64", "q35"), ["BIOS", "UEFI", "UEFI Secure Boot"])
self.assertEqual(host(EARLIER_PLAIN).get_firmwares("x86_64", "q35"), ["BIOS", "UEFI"])
def test_a_machine_without_crb_or_tis_takes_its_own_tpm_model(self):
self.assertEqual(host(SECURE_ONLY).get_tpm_model("ppc64le", "pseries"), "tpm-spapr")
class DefinitionTests(SimpleTestCase):
def features(self, tree):
return {f.get("name"): f.get("enabled") for f in tree.findall("os/firmware/feature")}
def test_bios_leaves_the_firmware_to_the_machine(self):
tree = defined("BIOS")
self.assertIsNone(tree.find("os").get("firmware"))
self.assertEqual((self.features(tree), tree.find("features/smm")), ({}, None))
def test_uefi_is_picked_by_libvirt_without_secure_boot(self):
tree = defined("UEFI")
self.assertEqual(tree.find("os").get("firmware"), "efi")
self.assertEqual(self.features(tree), {"secure-boot": "no"})
self.assertIsNone(tree.find("features/smm"))
def test_secure_boot_has_microsofts_keys_and_smm(self):
tree = defined("UEFI Secure Boot")
self.assertEqual(tree.find("os").get("firmware"), "efi")
self.assertEqual(self.features(tree), {"secure-boot": "yes", "enrolled-keys": "yes"})
self.assertEqual(tree.find("features/smm").get("state"), "on")
def test_a_tpm_is_an_emulated_tpm_2(self):
self.assertEqual(defined("UEFI").findall("devices/tpm"), [])
tpm = defined("UEFI Secure Boot", tpm_model="tpm-crb").find("devices/tpm")
self.assertEqual((tpm.get("model"), tpm.find("backend").get("type"), tpm.find("backend").get("version")),
("tpm-crb", "emulator", "2.0"))
CPU_DOM_CAPS = {"domain": "kvm", "os_support": "no", "cpu_modes": ["host-passthrough", "maximum", "host-model", "custom"],
"cpu_custom_models": ["Skylake-Server-noTSX-IBRS", "SandyBridge-IBRS"]}
def defined_cpu(vcpu_mode):
conn = wvmCreate.__new__(wvmCreate)
conn.wvm = MagicMock()
with patch.object(wvmCreate, "get_capabilities", return_value={"features": []}), \
patch.object(wvmCreate, "get_dom_capabilities", return_value=CPU_DOM_CAPS):
conn.create_instance(name="vm-1", memory=512, vcpu=1, vcpu_mode=vcpu_mode,
uuid="00000000-0000-4000-8000-0000000000c1", arch="x86_64", machine="q35",
firmware="BIOS", volumes=[], networks="", nwfilter="", virtio=True, listener_addr="0.0.0.0",
add_cdrom="None", add_input="None")
return etree.fromstring(conn.wvm.defineXML.call_args.args[0]).find("cpu")
class CpuModeTests(SimpleTestCase):
def test_a_mode_is_written_as_the_mode(self):
for mode in ("host-model", "host-passthrough", "maximum"):
with self.subTest(mode=mode):
cpu = defined_cpu(mode)
self.assertEqual((dict(cpu.attrib), len(cpu)), ({"mode": mode}, 0))
def test_a_named_model_is_that_model_checked_by_libvirt(self):
# a name longer than 20 characters too
cpu = defined_cpu("Skylake-Server-noTSX-IBRS")
self.assertEqual(dict(cpu.attrib), {"mode": "custom", "match": "exact"}) # no check='none'
self.assertEqual((cpu.findtext("model"), cpu.find("model").get("fallback")), ("Skylake-Server-noTSX-IBRS", "forbid"))
PORTS_DOM_CAPS = {"domain": "kvm", "os_support": "no", "cpu_modes": ["host-model"], "cpu_custom_models": [],
"disk_bus": ["virtio", "sata", "scsi"]}
def defined_controllers(machine):
conn = wvmCreate.__new__(wvmCreate)
conn.wvm = MagicMock()
with patch.object(wvmCreate, "get_capabilities", return_value={"features": []}), \
patch.object(wvmCreate, "get_dom_capabilities", return_value=PORTS_DOM_CAPS):
conn.create_instance(name="vm-1", memory=512, vcpu=1, vcpu_mode="host-model",
uuid="00000000-0000-4000-8000-0000000000c2", arch="x86_64", machine=machine,
firmware="BIOS", volumes=[], networks="default", nwfilter="", virtio=True,
listener_addr="0.0.0.0", add_cdrom="None", add_input="None")
domain = etree.fromstring(conn.wvm.defineXML.call_args.args[0])
return [c.get("model") for c in domain.findall("devices/controller[@type='pci']")]
class NewVmPortsTests(SimpleTestCase):
def test_a_new_q35_vm_has_spare_root_ports(self):
# libvirt puts the VM's devices into these and adds none: the rest are free
self.assertEqual(defined_controllers("pc-q35-rhel9.8.0"), ["pcie-root"] + ["pcie-root-port"] * PCIE_ROOT_PORTS)
def test_a_pc_vm_has_none_its_pci_bus_has_slots(self):
self.assertEqual(defined_controllers("pc-i440fx-rhel7.6.0"), [])

View file

@ -1,6 +1,6 @@
"""External snapshots go through libvirt's snapshot API; WebVirtCloud only
checks preconditions and picks the flags. libvirt is mocked here; the live
cases are in instances/tests/test_live_snapshots.py."""
cases are in vrtManager/tests/test_live_snapshots.py."""
import json
import unittest
@ -714,6 +714,7 @@ class PoolRefreshTestCase(unittest.TestCase):
p.get_volume_by_path = MagicMock()
self.assertEqual(getattr(p, action)("one"), [])
class KindTestCase(unittest.TestCase):
def test_internal_endpoints_refuse_external_names(self):
snap = fake_snap("e")

View file

@ -0,0 +1,261 @@
"""A VM's host devices in vrtManager: USB devices given and taken back,
each named once whether the running VM or its next start has it, PCI
devices plugged into a running VM, and a VM with any is not migrated."""
import unittest
from unittest.mock import MagicMock
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.instance import wvmInstance
from vrtManager.tests.fakes import FakeSource, by_model, destination, domain, with_disks
from vrtManager.util import OperationError
MIGRATABLE = libvirt.VIR_DOMAIN_XML_MIGRATABLE
CONTROLLER = "<controller type='usb' model='qemu-xhci'/>"
PCI_05 = ("<hostdev mode='subsystem' type='pci' managed='yes'><source>"
"<address domain='0x0000' bus='0x05' slot='0x00' function='0x0'/></source></hostdev>")
def vm(running, saved, active=True, migratable=None):
"""A wvmInstance whose running VM is running, as XMLDesc gives it (and as
the migratable XML marks it), and whose next start is saved."""
proxy = wvmInstance.__new__(wvmInstance)
proxy._read_cache = None
proxy.instance = MagicMock(**{"isActive.return_value": active, "hasManagedSaveImage.return_value": 0,
"info.return_value": [libvirt.VIR_DOMAIN_RUNNING if active else libvirt.VIR_DOMAIN_SHUTOFF]})
def describe(flags=0):
if flags & libvirt.VIR_DOMAIN_XML_INACTIVE:
return saved
return migratable if flags & MIGRATABLE and migratable else running
proxy.instance.XMLDesc.side_effect = describe
return proxy
class UsbHostdevTests(SimpleTestCase):
def test_a_device_chosen_by_model_is_one_row_with_the_device_it_holds(self):
# the running VM writes the address libvirt resolved; only its migratable XML marks it
proxy = vm(domain(CONTROLLER, by_model("<address bus='1' device='3'/>")), domain(CONTROLLER, by_model()),
migratable=domain(CONTROLLER, by_model("<address bus='1' device='3'/>", " autoAddress='yes'")))
usb, pci, others = proxy.get_hostdevs()
self.assertEqual(usb, [{"vendor": "0x0627", "product": "0x0001", "address": None, "now": (1, 3), "next": True}])
self.assertEqual((pci, others), ([], []))
def test_a_device_is_taken_from_each_definition_as_it_describes_it(self):
proxy = vm(domain(CONTROLLER, by_model("<address bus='1' device='3'/>")), domain(CONTROLLER, by_model()),
migratable=domain(CONTROLLER, by_model("<address bus='1' device='3'/>", " autoAddress='yes'")))
proxy.delete_usb_hostdev(("0x0627", "0x0001", None))
live, config = proxy.instance.detachDeviceFlags.call_args_list
self.assertEqual(live.args[1], libvirt.VIR_DOMAIN_AFFECT_LIVE)
self.assertEqual(etree.fromstring(live.args[0]).xpath("string(source/address/@device)"), "3")
self.assertEqual((config.args[1], etree.fromstring(config.args[0]).find("source/address")),
(libvirt.VIR_DOMAIN_AFFECT_CONFIG, None))
with self.assertRaisesRegex(OperationError, "no such USB device"):
proxy.delete_usb_hostdev(("0x0627", "0x0001", (1, 2)))
def test_a_device_is_given_by_model_or_by_address_with_a_usb_controller(self):
proxy = vm(domain(CONTROLLER), domain(CONTROLLER))
proxy.add_usb_hostdev("0x0627", "0x0001", (1, 3))
added = etree.fromstring(proxy.instance.attachDeviceFlags.call_args.args[0])
self.assertEqual((added.get("type"), added.xpath("string(source/vendor/@id)"), added.xpath("string(source/address/@device)")),
("usb", "0x0627", "3"))
for xml in (domain(), domain("<controller type='usb' model='none'/>")):
proxy = vm(xml, xml, active=False)
with self.subTest(xml=xml), self.assertRaisesRegex(OperationError, "no USB controller"):
proxy.add_usb_hostdev("0x0627", "0x0001")
proxy.instance.attachDeviceFlags.assert_not_called()
def test_a_device_the_host_has_one_of_is_given_by_model(self):
proxy = vm(domain(CONTROLLER), domain(CONTROLLER))
proxy.add_usb_hostdev("0x0627", "0x0001")
self.assertIsNone(etree.fromstring(proxy.instance.attachDeviceFlags.call_args.args[0]).find("source/address"))
def test_other_host_devices_as_the_running_vm_and_its_next_start_have_them(self):
pci = "<hostdev mode='subsystem' type='pci'><source><address domain='0x0000' bus='0x00' slot='0x0a' function='0x0'/></source></hostdev>"
nic = "<interface type='hostdev'><source><address type='pci' domain='0x0000' bus='0x00' slot='0x0b' function='0x0'/></source></interface>"
network_nic = ("<interface type='network'><source network='sriov'/><actual type='hostdev'><source>"
"<address type='pci' domain='0x0000' bus='0x00' slot='0x0c' function='0x1'/></source></actual></interface>")
mdev = "<hostdev mode='subsystem' type='mdev'><source><address uuid='{}'/></source></hostdev>"
running = domain(pci, nic, network_nic, mdev.format("00000000-0000-4000-8000-00000000a001"))
_, pci_rows, others = vm(running, domain(nic, mdev.format("00000000-0000-4000-8000-00000000a002"))).get_hostdevs()
self.assertEqual(pci_rows, [{"address": "0000:00:0a.0", "now": True, "next": False, "managed": False}])
self.assertEqual(others, [{"type": "nic", "address": "0000:00:0b.0", "now": True, "next": True},
{"type": "nic", "address": "0000:00:0c.1", "now": True, "next": False},
{"type": "mdev", "address": "00000000-0000-4000-8000-00000000a001", "now": True, "next": False},
{"type": "mdev", "address": "00000000-0000-4000-8000-00000000a002", "now": False, "next": True}])
def test_a_shut_off_vm_gets_a_pci_device_for_its_next_start(self):
proxy = vm("", domain(), active=False)
proxy.add_pci_hostdev("0000:05:00.0")
xml, flags = proxy.instance.attachDeviceFlags.call_args.args
self.assertEqual(flags, libvirt.VIR_DOMAIN_AFFECT_CONFIG)
self.assertEqual((etree.fromstring(xml).get("managed"), dict(etree.fromstring(xml).find("source/address").attrib)),
("yes", {"domain": "0x0000", "bus": "0x05", "slot": "0x00", "function": "0x0"}))
proxy = vm("", domain(PCI_05), active=False)
self.assertFalse(proxy.delete_pci_hostdev("0000:05:00.0"))
self.assertEqual([c.args[1] for c in proxy.instance.detachDeviceFlags.call_args_list], [libvirt.VIR_DOMAIN_AFFECT_CONFIG])
with self.assertRaisesRegex(OperationError, "no PCI device 0000:06:00.0"):
proxy.delete_pci_hostdev("0000:06:00.0")
def test_a_paused_vm_or_one_with_a_saved_state_keeps_its_pci_devices(self):
paused = vm(domain(PCI_05), domain(PCI_05))
paused.instance.info.return_value = [libvirt.VIR_DOMAIN_PAUSED]
saved = vm("", domain(PCI_05), active=False)
saved.instance.hasManagedSaveImage.return_value = 1
for name, proxy in (("paused", paused), ("saved", saved)):
for change in (proxy.add_pci_hostdev, proxy.delete_pci_hostdev):
with self.subTest(vm=name, change=change.__name__), self.assertRaisesRegex(OperationError, "runs, or is shut off"):
change("0000:05:00.0")
proxy.instance.attachDeviceFlags.assert_not_called()
proxy.instance.detachDeviceFlags.assert_not_called()
def test_a_nic_that_is_a_host_device_is_left_to_the_xml_editor(self):
nic = "<interface type='hostdev'><mac address='52:54:00:00:00:0a'/><source><address type='pci' bus='0x00' slot='0x0a'/></source></interface>"
proxy = vm(domain(nic), domain(nic))
with self.assertRaisesRegex(OperationError, "XML editor"):
proxy.delete_network("52:54:00:00:00:0a")
with self.assertRaisesRegex(OperationError, "XML editor"):
proxy.change_network("52:54:00:00:00:0a", "52:54:00:00:00:0b", "default", "net", "virtio", "")
proxy.instance.detachDeviceFlags.assert_not_called()
def test_a_clone_gets_no_host_device_and_its_nics_keep_their_numbers(self):
proxy = vm("", domain(by_model(),
"<interface type='hostdev'><mac address='52:54:00:00:00:0a'/></interface>",
"<interface type='network'><mac address='52:54:00:00:00:0b'/><source network='default'/></interface>"))
proxy.get_instances = lambda: []
tree, _ = proxy._clone_definition({"name": "kiosk-2", "clone-net-mac-1": "52:54:00:00:00:1b"},
"99999999-2222-3333-4444-555555555555")
self.assertEqual(tree.xpath("devices/hostdev"), [])
self.assertEqual(tree.xpath("devices/interface/mac/@address"), ["52:54:00:00:00:1b"])
class RunningVM:
"""A running VM whose definitions change as libvirt changes them: the
running one and the next start's, each with the PCI device or not.
fail: the affect flag whose change libvirt fails, after doing it or not;
keeps: the guest does not let the device go."""
def __init__(self, now=True, later=True, fail=None, done=False, keeps=False, machine="pc-i440fx-10.1"):
self.has = {libvirt.VIR_DOMAIN_AFFECT_LIVE: now, libvirt.VIR_DOMAIN_AFFECT_CONFIG: later}
self.fail, self.done, self.keeps, self.machine = fail, done, keeps, machine
self.proxy = vm("", "")
self.proxy.instance.XMLDesc.side_effect = self.describe
self.proxy.instance.attachDeviceFlags.side_effect = lambda xml, flags: self.change(flags, True)
self.proxy.instance.detachDeviceFlags.side_effect = lambda xml, flags: self.change(flags, False)
def describe(self, flags=0):
saved = flags & libvirt.VIR_DOMAIN_XML_INACTIVE
has = self.has[libvirt.VIR_DOMAIN_AFFECT_CONFIG if saved else libvirt.VIR_DOMAIN_AFFECT_LIVE]
return f"<domain><os><type machine='{self.machine}'>hvm</type></os><devices>{PCI_05 if has else ''}</devices></domain>"
def change(self, flags, adding):
for affect in (libvirt.VIR_DOMAIN_AFFECT_LIVE, libvirt.VIR_DOMAIN_AFFECT_CONFIG):
if flags & affect:
if affect == self.fail:
if self.done:
self.has[affect] = adding
raise libvirt.libvirtError("refused")
if adding or not (self.keeps and affect == libvirt.VIR_DOMAIN_AFFECT_LIVE):
self.has[affect] = adding
class PciHotplugTests(SimpleTestCase):
def test_a_running_vm_gets_it_at_once_and_for_its_next_start(self):
running = RunningVM(now=False, later=False)
running.proxy.add_pci_hostdev("0000:05:00.0")
self.assertEqual(running.proxy.instance.attachDeviceFlags.call_args.args[1],
libvirt.VIR_DOMAIN_AFFECT_LIVE | libvirt.VIR_DOMAIN_AFFECT_CONFIG)
self.assertEqual(list(running.has.values()), [True, True])
def test_a_hot_plug_that_fails_says_where_the_device_is(self):
cases = ((libvirt.VIR_DOMAIN_AFFECT_CONFIG, False, "the running VM has it, its next start does not have it"),
(libvirt.VIR_DOMAIN_AFFECT_CONFIG, True, "the running VM has it, its next start has it"),
(libvirt.VIR_DOMAIN_AFFECT_LIVE, False, "the running VM does not have it, its next start does not have it"))
for fail, done, where in cases:
# libvirt plugs into the running VM first, then saves the next start
running = RunningVM(now=False, later=False, fail=fail, done=done)
if fail == libvirt.VIR_DOMAIN_AFFECT_CONFIG:
running.has[libvirt.VIR_DOMAIN_AFFECT_LIVE] = True
with self.subTest(where=where), self.assertRaisesRegex(OperationError, f"refused; PCI device 0000:05:00.0: {where}"):
running.proxy.add_pci_hostdev("0000:05:00.0")
def test_a_hot_plug_whose_outcome_cannot_be_read_says_so(self):
running = RunningVM(now=False, later=False)
running.proxy.instance.attachDeviceFlags.side_effect = libvirt.libvirtError("connection reset")
running.proxy.instance.XMLDesc.side_effect = libvirt.libvirtError("connection reset")
with self.assertRaisesRegex(OperationError, "connection reset; whether the VM has PCI device 0000:05:00.0 now is not known"):
running.proxy.add_pci_hostdev("0000:05:00.0")
def test_a_vm_that_stops_meanwhile_is_said_to_be_not_running(self):
running = RunningVM(now=False, later=False, fail=libvirt.VIR_DOMAIN_AFFECT_CONFIG)
running.proxy.instance.isActive.return_value = False # its live XML is then the inactive one
with self.assertRaisesRegex(OperationError, "the VM is not running, its next start does not have it"):
running.proxy.add_pci_hostdev("0000:05:00.0")
def test_a_running_vm_lets_it_go_as_its_guest_does(self):
running = RunningVM()
self.assertFalse(running.proxy.delete_pci_hostdev("0000:05:00.0"))
self.assertEqual(list(running.has.values()), [False, False])
# not yet: the request stands, the next start no longer has it
running = RunningVM(keeps=True)
self.assertTrue(running.proxy.delete_pci_hostdev("0000:05:00.0"))
self.assertEqual(list(running.has.values()), [True, False])
def test_a_guest_that_refuses_keeps_it_for_its_next_start_too(self):
running = RunningVM(fail=libvirt.VIR_DOMAIN_AFFECT_LIVE)
with self.assertRaisesRegex(OperationError, "refused; .*the running VM has it, its next start has it"):
running.proxy.delete_pci_hostdev("0000:05:00.0")
self.assertEqual(list(running.has.values()), [True, True])
# removed from the running VM, then an error: said, and the next start keeps it
running = RunningVM(fail=libvirt.VIR_DOMAIN_AFFECT_LIVE, done=True)
with self.assertRaisesRegex(OperationError, "the running VM does not have it, its next start has it"):
running.proxy.delete_pci_hostdev("0000:05:00.0")
def test_a_failed_removal_from_the_next_start_of_a_running_vm_is_said(self):
running = RunningVM(now=False, later=True, fail=libvirt.VIR_DOMAIN_AFFECT_CONFIG)
with self.assertRaisesRegex(OperationError, "the running VM does not have it, its next start has it"):
running.proxy.delete_pci_hostdev("0000:05:00.0")
def test_a_removal_whose_outcome_cannot_be_read_says_so(self):
running = RunningVM()
reads = []
def describe(flags=0): # the reads before the removal work, the one after it does not
reads.append(flags)
if len(reads) > 2:
raise libvirt.libvirtError("connection reset")
return running.describe(flags)
running.proxy.instance.XMLDesc.side_effect = describe
with self.assertRaisesRegex(OperationError, "connection reset; whether the VM has PCI device 0000:05:00.0 now is not known"):
running.proxy.delete_pci_hostdev("0000:05:00.0")
def test_a_next_start_that_keeps_it_after_the_running_vm_let_it_go_is_said(self):
for keeps, where in ((False, "the running VM does not have it, its next start has it"),
(True, "the running VM has it, its next start has it")):
running = RunningVM(fail=libvirt.VIR_DOMAIN_AFFECT_CONFIG, keeps=keeps)
with self.subTest(keeps=keeps), self.assertRaisesRegex(OperationError, where):
running.proxy.delete_pci_hostdev("0000:05:00.0")
def test_a_device_in_one_definition_only_is_taken_from_it(self):
for now, later in ((True, False), (False, True)):
running = RunningVM(now=now, later=later)
with self.subTest(now=now, later=later):
self.assertFalse(running.proxy.delete_pci_hostdev("0000:05:00.0"))
self.assertEqual(list(running.has.values()), [False, False])
self.assertEqual(running.proxy.instance.detachDeviceFlags.call_count, 1)
class MigrationTests(unittest.TestCase):
def test_a_vm_with_host_devices_is_not_migrated(self):
for devices, state in ((by_model(), 1), (by_model(), 5), ("<hostdev mode='subsystem' type='pci'/>", 1)):
source = FakeSource(state, xml=with_disks(devices))
with self.subTest(state=state), self.assertRaisesRegex(OperationError, "host devices cannot be migrated"):
destination().moveto(source, "vm", live=state == 1, unsafe=False, offline=state == 5)
self.assertEqual(source.instance.calls, [])

View file

@ -23,6 +23,9 @@ from libvirt import (
from vrtManager.connection import wvmConnect
from vrtManager.hostdetails import wvmHostDetails
from vrtManager.storage import wvmStorage, wvmStorages
from unittest import TestCase as UnitTestCase
from unittest.mock import patch
from vrtManager.hostdetails import cpu_percent
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>
@ -270,3 +273,49 @@ class VolumesTestCase(unittest.TestCase):
stg = self.storage([])
stg.pool.storageVolLookupByName.return_value = volume("a.img", 4096, 0, "raw")
self.assertEqual(stg.get_volume_details("a.img"), {"name": "a.img", "size": 4096, "allocation": 0, "type": "raw"})
def host(*readings):
"""A wvmHostDetails whose getCPUStats returns readings, (idle, busy) each."""
h = wvmHostDetails.__new__(wvmHostDetails)
h.wvm = MagicMock()
h.wvm.getCPUStats.side_effect = [
{"kernel": busy // 2, "user": busy - busy // 2, "idle": idle, "iowait": 0, "guest": busy // 3, "utilization": 99}
for idle, busy in readings
]
return h
class CpuPercentTestCase(UnitTestCase):
def test_busy_share_between_two_samples(self):
self.assertEqual(cpu_percent((100, 200), (130, 300)), 70.0)
self.assertEqual(cpu_percent((0, 0), (1, 3)), 66.7)
def test_samples_that_do_not_follow_on(self):
for before, after in (((100, 200), (100, 200)), ((100, 200), (50, 100)), ((100, 200), (200, 250))):
with self.subTest(before=before, after=after):
self.assertIsNone(cpu_percent(before, after))
@patch("vrtManager.hostdetails.time")
class CpuUsageTestCase(UnitTestCase):
def test_since_a_previous_sample_without_waiting(self, clock):
clock.time.return_value = 105.0
h = host((130, 70))
usage = h.get_cpu_usage(previous=(100, 100, 100.0))
clock.sleep.assert_not_called()
# total = kernel + user + idle + iowait; guest (inside user) and utilization are left out
self.assertEqual(usage, {"usage": 70.0, "sample": (130, 200, 105.0), "window": 5.0})
def test_over_one_second_without_a_sample(self, clock):
clock.time.side_effect = [10.0, 11.0]
h = host((100, 100), (110, 190))
self.assertEqual(h.get_cpu_usage(), {"usage": 90.0, "sample": (110, 300, 11.0), "window": 1.0})
clock.sleep.assert_called_once_with(1)
def test_over_one_second_after_the_host_restarted(self, clock):
clock.time.side_effect = [10.0, 11.0]
h = host((5, 5), (6, 9))
usage = h.get_cpu_usage(previous=(1000, 5000, 5.0))
clock.sleep.assert_called_once_with(1)
self.assertEqual((usage["usage"], usage["window"]), (80.0, 1.0))

View file

@ -0,0 +1,92 @@
"""Deleting a host interface: a stopped one only; netcf removes a bridge's
members with the bridge, so they are defined again on their own (as virsh
iface-unbridge), all in one change of the host's configuration, rolled back
on a failure; a bridge with addresses and several members is refused."""
from unittest.mock import MagicMock
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.interface import wvmInterface
from vrtManager.util import OperationError
ADDRESS = "<protocol family='ipv4'><ip address='192.0.2.10' prefix='24'/></protocol>"
BOND = "<interface type='bond' name='bond0'><bond mode='active-backup'><interface type='ethernet' name='eth1'/></bond></interface>"
def interface(xml, active=False):
conn = wvmInterface.__new__(wvmInterface)
conn.iface = MagicMock(**{"XMLDesc.return_value": xml, "isActive.return_value": active, "name.return_value": "br1"})
conn.wvm = MagicMock()
return conn
def delete(xml):
conn = interface(xml)
conn.delete_iface()
conn.iface.undefine.assert_called_once_with()
conn.wvm.changeBegin.assert_called_once_with(0)
conn.wvm.changeCommit.assert_called_once_with(0)
return [etree.fromstring(call.args[0]) for call in conn.wvm.interfaceDefineXML.call_args_list]
class DeleteInterfaceTestCase(SimpleTestCase):
def test_the_only_member_takes_the_bridge_start_mode_and_addresses(self):
(bond,) = delete(f"<interface type='bridge' name='br1'><start mode='onboot'/>{ADDRESS}<bridge>{BOND}</bridge></interface>")
self.assertEqual(bond.get("name"), "bond0")
self.assertEqual(bond.xpath("bond/@mode"), ["active-backup"])
self.assertEqual(bond.xpath("bond/interface/@name"), ["eth1"])
self.assertEqual(bond.xpath("start/@mode"), ["onboot"])
self.assertEqual(bond.xpath("protocol/ip/@address"), ["192.0.2.10"])
def test_several_members_take_the_start_mode(self):
members = delete(
"<interface type='bridge' name='br1'><start mode='onboot'/><bridge>"
"<interface type='ethernet' name='eth1'/><interface type='ethernet' name='eth2'/></bridge></interface>"
)
self.assertEqual([m.get("name") for m in members], ["eth1", "eth2"])
for member in members:
self.assertEqual(member.xpath("start/@mode"), ["onboot"])
self.assertEqual(member.xpath("protocol"), [])
def test_a_bridge_with_addresses_and_several_members_is_refused(self):
# which member would take the addresses is the user's choice
conn = interface(f"<interface type='bridge' name='br1'>{ADDRESS}<bridge>"
"<interface type='ethernet' name='eth1'/><interface type='ethernet' name='eth2'/></bridge></interface>")
with self.assertRaisesRegex(OperationError, "has addresses and several members"):
conn.delete_iface()
conn.iface.undefine.assert_not_called()
def test_an_active_interface_is_refused(self):
conn = interface("<interface type='ethernet' name='eth1'/>", active=True)
with self.assertRaisesRegex(OperationError, "Stop interface br1 before deleting it"):
conn.delete_iface()
conn.iface.undefine.assert_not_called()
def test_a_member_libvirt_refuses_rolls_the_whole_change_back(self):
conn = interface(f"<interface type='bridge' name='br1'><bridge>{BOND}</bridge></interface>")
conn.wvm.interfaceDefineXML.side_effect = libvirt.libvirtError("netcf: cannot write")
with self.assertRaises(Exception):
conn.delete_iface()
conn.wvm.changeRollback.assert_called_once_with(0)
conn.wvm.changeCommit.assert_not_called()
def test_a_commit_libvirt_refuses_is_rolled_back(self):
conn = interface("<interface type='ethernet' name='eth1'/>")
conn.wvm.changeCommit.side_effect = libvirt.libvirtError("netcf: cannot commit")
with self.assertRaises(Exception):
conn.delete_iface()
conn.wvm.changeRollback.assert_called_once_with(0)
def test_an_interface_that_is_not_a_bridge_is_only_undefined(self):
self.assertEqual(delete(f"<interface type='ethernet' name='eth1'><start mode='onboot'/>{ADDRESS}</interface>"), [])
def test_a_rollback_that_fails_too_tells_both(self):
conn = interface("<interface type='ethernet' name='eth1'/>")
conn.wvm.changeCommit.side_effect = OperationError("netcf: cannot commit")
conn.wvm.changeRollback.side_effect = OperationError("netcf: cannot roll back")
with self.assertRaises(OperationError) as raised:
conn.delete_iface()
self.assertIn("netcf: cannot commit", str(raised.exception))
self.assertIn("netcf: cannot roll back", str(raised.exception))

View file

@ -0,0 +1,119 @@
"""What the panel reads of the host's interfaces: each one's XML once, its
link as reported (an unknown speed is none), its members; which devices a new
bridge can take; how each family is addressed."""
from unittest.mock import MagicMock
import libvirt
from django.test import SimpleTestCase
from vrtManager.interface import wvmInterface, wvmInterfaces
BRIDGE = ("<interface type='bridge' name='br0'><mtu size='1500'/><link speed='4294967295' state='up'/>"
"<mac address='52:54:00:aa:01:02'/><bridge stp='on' delay='0'>"
"<interface type='ethernet' name='eno1'><link speed='1000' state='up'/><mac address='52:54:00:aa:01:03'/></interface>"
"</bridge></interface>")
ETHERNET = "<interface type='ethernet' name='eno2'><link state='down'/><mac address='52:54:00:aa:01:04'/></interface>"
def iface(xml, active=1):
name = xml.split("name='")[1].split("'")[0]
mac = xml.split("mac address='")[1].split("'")[0]
return MagicMock(**{"name.return_value": name, "MACString.return_value": mac, "XMLDesc.return_value": xml,
"isActive.return_value": active})
def error(code):
err = libvirt.libvirtError("refused")
err.get_error_code = lambda: code
return err
class ReadTests(SimpleTestCase):
def interfaces(self, *xmls, networks=()):
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock(**{"listAllInterfaces.return_value": [iface(x) for x in xmls],
"listAllNetworks.return_value": [MagicMock(**{"XMLDesc.return_value": n}) for n in networks]})
return conn
def test_each_interface_is_read_once(self):
conn = self.interfaces(BRIDGE, ETHERNET)
br0, eno2 = conn.get_ifaces_info()
self.assertEqual((br0["type"], br0["link"], br0["speed"], br0["mtu"], br0["mac"]),
("bridge", "up", None, "1500", "52:54:00:aa:01:02"))
self.assertEqual(br0["members"], [{"name": "eno1", "type": "ethernet", "mac": "52:54:00:aa:01:03",
"link": "up", "speed": "1000"}])
self.assertEqual((eno2["link"], eno2["members"]), ("down", []))
for listed in conn.wvm.listAllInterfaces.return_value:
# name and MAC are held by libvirt's object: no call to the host
self.assertEqual([c[0] for c in listed.mock_calls], ["XMLDesc", "name", "MACString", "isActive"])
def test_an_interface_libvirt_cannot_write_is_read_as_saved(self):
# netcf writes a link speed of -1 for a virtio card, then refuses that XML
listed = iface(ETHERNET)
listed.XMLDesc.side_effect = lambda flags=0: ETHERNET if flags else (_ for _ in ()).throw(error(libvirt.VIR_ERR_XML_ERROR))
conn = self.interfaces()
conn.wvm.listAllInterfaces.return_value = [listed]
(eno2,) = conn.get_ifaces_info()
self.assertEqual(eno2["name"], "eno2")
def test_a_bridge_takes_no_bridge_member_network_bridge_or_tap(self):
# as configured: a member the running bridge does not have yet is taken too
conn = self.interfaces(BRIDGE, ETHERNET, networks=["<network><bridge name='virbr0'/></network>"])
self.assertEqual(conn.get_member_candidates(["br0", "eno1", "eno2", "lo", "virbr0", "vnet3", "macvtap0"]), ["eno2"])
for listed in conn.wvm.listAllInterfaces.return_value:
listed.XMLDesc.assert_called_with(libvirt.VIR_INTERFACE_XML_INACTIVE)
def test_a_device_in_a_bond_of_a_bridge_is_taken(self):
nested = ("<interface type='bridge' name='br1'><mac address='52:54:00:aa:01:05'/><bridge><interface type='bond' name='bond0'><bond>"
"<interface type='ethernet' name='eno3'/><interface type='ethernet' name='eno4'/></bond></interface>"
"</bridge></interface>")
conn = self.interfaces(nested)
self.assertEqual(conn.get_member_candidates(["br1", "bond0", "eno3", "eno4", "eno5"]), ["eno5"])
def test_each_family_says_how_it_is_addressed(self):
conn = wvmInterface.__new__(wvmInterface)
saved = ("<interface type='ethernet' name='eth0'><start mode='onboot'/>"
"<protocol family='ipv4'><ip address='198.51.100.10' prefix='24'/></protocol>"
"<protocol family='ipv6'><autoconf/></protocol></interface>")
running = ("<interface type='ethernet' name='eth0'><protocol family='ipv4'><ip address='198.51.100.10' prefix='24'/></protocol>"
"<protocol family='ipv6'><ip address='2001:db8::5' prefix='64'/><ip address='2001:db8::6' prefix='64'/></protocol></interface>")
conn.iface = MagicMock(**{"name.return_value": "eth0", "MACString.return_value": "52:54:00:00:00:01",
"XMLDesc.side_effect": lambda flags=0: saved if flags else running})
info = conn.get_info()
self.assertEqual(info["start_mode"], "onboot")
self.assertEqual(info["ipv4"], {"method": "static", "addresses": ["198.51.100.10/24"]})
self.assertEqual(info["ipv6"], {"method": "autoconf", "addresses": ["2001:db8::5/64", "2001:db8::6/64"]})
self.assertEqual(conn.iface.XMLDesc.call_count, 2) # the running and the saved definition, once each
def test_an_address_without_a_prefix_is_shown_without_one(self):
conn = wvmInterface.__new__(wvmInterface)
xml = "<interface type='ethernet' name='eth0'><protocol family='ipv4'><ip address='192.0.2.10'/></protocol></interface>"
conn.iface = MagicMock(**{"name.return_value": "eth0", "MACString.return_value": "", "XMLDesc.return_value": xml})
self.assertEqual(conn.get_info()["ipv4"]["addresses"], ["192.0.2.10"])
class BridgeUseTests(SimpleTestCase):
def test_networks_and_running_vms_on_a_bridge(self):
from vrtManager.interface import networks_by_device
nets = [MagicMock(**{"name.return_value": name, "XMLDesc.return_value": xml}) for name, xml in (
("default", "<network><bridge name='virbr0'/></network>"),
("lan", "<network><forward mode='bridge'/><bridge name='br0'/></network>"),
("direct", "<network><forward mode='bridge'><interface dev='eth0'/><interface dev='eth1'/></forward></network>"),
("sriov", "<network><forward mode='hostdev'><pf dev='eth2'/></forward></network>"))]
self.assertEqual(networks_by_device(MagicMock(**{"listAllNetworks.return_value": nets})),
{"virbr0": ["default"], "br0": ["lan"], "eth0": ["direct"], "eth1": ["direct"], "eth2": ["sriov"]})
doms = [MagicMock(**{"name.return_value": name, "XMLDesc.return_value": xml}) for name, xml in (
("web", "<domain><devices><interface type='bridge'><mac address='52:54:00:00:00:01'/><source bridge='br0'/>"
"<target dev='vnet3'/></interface></devices></domain>"),
("db", "<domain><devices><interface type='network'><mac address='52:54:00:00:00:02'/><source network='lan'/>"
"<target dev='vnet7'/></interface></devices></domain>"),
("other", "<domain><devices><interface type='network'><source network='default'/><target dev='vnet9'/>"
"</interface></devices></domain>"))]
conn = wvmInterface.__new__(wvmInterface)
conn.iface = MagicMock(**{"name.return_value": "br0"})
conn.wvm = MagicMock(**{"listAllDomains.return_value": doms})
self.assertEqual(conn.get_vms(["lan"]), [
{"vm": "db", "tap": "vnet7", "mac": "52:54:00:00:00:02"},
{"vm": "web", "tap": "vnet3", "mac": "52:54:00:00:00:01"},
])

View file

@ -0,0 +1,131 @@
"""The interface XML wvmInterfaces writes from the form's values: IPv4,
IPv6 and a bridge's member."""
from unittest.mock import MagicMock
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.interface import wvmInterfaces
class Ipv6XmlTestCase(SimpleTestCase):
def xml(self, gw):
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
conn.get_iface = MagicMock()
conn.get_iface.return_value.XMLDesc.return_value = "<interface type='ethernet' name='eth1'/>"
conn.create_iface("br1", "bridge", "onboot", "eth1", "none", "", "", "static", "2001:db8::10/64", gw, "on", 0)
return etree.fromstring(conn.define_iface.call_args[0][0])
def test_static_address_with_gateway(self):
proto = self.xml("2001:db8::1").find("protocol[@family='ipv6']")
self.assertEqual(proto.find("ip").attrib, {"address": "2001:db8::10", "prefix": "64"})
self.assertEqual(proto.find("route").get("gateway"), "2001:db8::1")
def test_no_empty_route_without_gateway(self):
self.assertIsNone(self.xml("").find("protocol[@family='ipv6']/route"))
class Ipv4XmlTestCase(SimpleTestCase):
def xml(self, addr, gw):
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
conn.get_iface = MagicMock()
conn.get_iface.return_value.XMLDesc.return_value = "<interface type='ethernet' name='eth1'/>"
conn.create_iface("br1", "bridge", "onboot", "eth1", "static", addr, gw, "none", "", "", "on", 0)
return etree.fromstring(conn.define_iface.call_args[0][0])
def test_static_address_with_gateway(self):
proto = self.xml("192.0.2.10/24", "192.0.2.1").find("protocol[@family='ipv4']")
self.assertEqual(proto.find("ip").attrib, {"address": "192.0.2.10", "prefix": "24"})
self.assertEqual(proto.find("route").get("gateway"), "192.0.2.1")
def test_no_empty_route_without_gateway(self):
self.assertIsNone(self.xml("192.0.2.10/24", "").find("protocol[@family='ipv4']/route"))
def no_interface(name):
err = libvirt.libvirtError("Interface not found: no interface with matching name '%s'" % name)
err.err = (libvirt.VIR_ERR_NO_INTERFACE, libvirt.VIR_FROM_INTERFACE, str(err), 2, name, None, None, 0, 0)
return err
class BridgeMemberXmlTestCase(SimpleTestCase):
"""A bridge member is declared as the host defines it: a bond keeps its
mode and devices, a vlan its tag and device."""
def member(self, host_xml):
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
def get_iface(name):
if host_xml is None:
raise no_interface(name)
iface = MagicMock()
iface.XMLDesc.return_value = host_xml
return iface
conn.get_iface = MagicMock(side_effect=get_iface)
conn.create_iface("br1", "bridge", "onboot", "bond0", "none", "", "", "none", "", "", "on", 0)
self.assertTrue(conn.define_iface.return_value.create.called)
members = etree.fromstring(conn.define_iface.call_args[0][0]).findall("bridge/interface")
self.assertEqual(len(members), 1)
return members[0]
def test_a_bond_keeps_its_mode_and_devices(self):
member = self.member(
"<interface type='bond' name='bond0'><start mode='onboot'/><mtu size='9000'/>"
"<protocol family='ipv4'><dhcp/></protocol>"
"<bond mode='active-backup'><miimon freq='100'/>"
"<interface type='ethernet' name='eth0'><mac address='52:54:00:00:00:01'/></interface>"
"<interface type='ethernet' name='eth1'/></bond></interface>"
)
self.assertEqual((member.get("type"), member.get("name")), ("bond", "bond0"))
self.assertEqual(member.find("bond").get("mode"), "active-backup")
self.assertEqual([i.get("name") for i in member.findall("bond/interface")], ["eth0", "eth1"])
self.assertEqual(member.find("bond/miimon").get("freq"), "100")
# what only a top-level interface has: the bridge holds the addresses
for tag in ("start", "mtu", "protocol"):
self.assertIsNone(member.find(tag), tag)
def test_a_vlan_keeps_its_tag_and_device(self):
member = self.member(
"<interface type='vlan' name='bond0'><start mode='onboot'/>"
"<vlan tag='42'><interface name='eth0'/></vlan></interface>"
)
self.assertEqual(member.get("type"), "vlan")
self.assertEqual(member.find("vlan").get("tag"), "42")
self.assertEqual(member.find("vlan/interface").get("name"), "eth0")
self.assertIsNone(member.find("start"))
def test_a_device_the_host_does_not_define_is_ethernet(self):
member = self.member(None)
self.assertEqual(member.attrib, {"type": "ethernet", "name": "bond0"})
def test_other_lookup_errors_are_not_hidden(self):
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
err = libvirt.libvirtError("cannot recv data")
err.err = (libvirt.VIR_ERR_SYSTEM_ERROR, libvirt.VIR_FROM_RPC, str(err), 2, None, None, None, 0, 0)
conn.get_iface = MagicMock(side_effect=err)
with self.assertRaises(libvirt.libvirtError):
conn.create_iface("br1", "bridge", "onboot", "bond0", "none", "", "", "none", "", "", "on", 0)
self.assertFalse(conn.define_iface.called)
def test_an_interface_that_does_not_start_is_rolled_back(self):
# the definition, and the member's own configuration it changed, as they were
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.wvm = MagicMock()
conn.define_iface = MagicMock()
iface = conn.define_iface.return_value
iface.create.side_effect = libvirt.libvirtError("failed to bring up br1")
with self.assertRaises(libvirt.libvirtError):
conn.create_iface("br1", "ethernet", "onboot", "", "none", "", "", "none", "", "", "on", 0)
conn.wvm.changeRollback.assert_called_once_with(0)
conn.wvm.changeCommit.assert_not_called()

View file

@ -15,6 +15,14 @@ from vrtManager.instance import (
VIR_DOMAIN_BACKUP_BEGIN_PRESERVE_SHUTDOWN_DOMAIN,
wvmInstance,
)
import os
from xml.etree import ElementTree
from django.test import SimpleTestCase
from vrtManager.connection import connection_manager
from vrtManager.tests import livetest
P = livetest.PREFIX
GIB = 1024**3
UUID = "abababab-0000-1111-2222-333333333333"
@ -295,3 +303,106 @@ class DefineCloneTestCase(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
class LiveCloneTestCase(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)
livetest.ensure_pool(cls.conn)
cls.inventory = livetest.inventory(cls.conn)
super().setUpClass()
@classmethod
def tearDownClass(cls):
try:
livetest.remove_pool(cls.conn)
finally:
super().tearDownClass()
if livetest.inventory(cls.conn) != cls.inventory:
raise AssertionError("Live tests changed objects outside the test namespace")
def tearDown(self):
livetest.cleanup(self.conn)
def pool_volumes(self):
pool = self.conn.storagePoolLookupByName(livetest.POOL)
pool.refresh(0)
return set(pool.listVolumes())
def source(self, name):
disks = [livetest.create_volume(self.conn, P + f"{name}-{d}") for d in ("a", "b")]
dom = livetest.define_vm(self.conn, P + name, disks)
return dom, wvmInstance(*self.args, None, uuid=dom.UUIDString())
def clone_data(self, name):
return {
"name": P + name,
"disk-vda": P + name + "-a.qcow2",
"disk-vdb": P + name + "-b.qcow2",
"disk_owner_uid": 0,
"disk_owner_gid": 0,
}
def test_failed_clone_removes_only_its_own_copies(self):
_, vm = self.source("clone-src")
taken = livetest.create_volume(self.conn, P + "clone-dst-b") # destination of vdb exists
before = self.pool_volumes()
with self.assertRaises(Exception):
vm.clone_instance(self.clone_data("clone-dst"), "abababab-0000-1111-2222-333333333333")
self.assertEqual(self.pool_volumes(), before, "copies left behind or a volume deleted")
self.assertTrue(livetest.volume_exists(self.conn, taken))
with self.assertRaises(libvirt.libvirtError):
self.conn.lookupByName(P + "clone-dst")
def test_running_vm_is_not_cloned(self):
dom, vm = self.source("clone-run")
dom.create()
before = self.pool_volumes()
with self.assertRaises(Exception):
vm.clone_instance(self.clone_data("clone-run-copy"), "abababab-0000-1111-2222-333333333333")
self.assertEqual(self.pool_volumes(), before)
def test_clone_of_a_uefi_vm_has_its_own_copy_of_the_nvram(self):
disk = livetest.create_volume(self.conn, P + "clone-uefi-a")
dom = livetest.define_vm(self.conn, P + "clone-uefi", [disk], uefi=True)
dom.createWithFlags(libvirt.VIR_DOMAIN_START_PAUSED) # libvirt makes the NVRAM file at the first start
dom.destroy()
vm = wvmInstance(*self.args, None, uuid=dom.UUIDString())
source = vm.get_nvram()
uuid = vm.clone_instance(
{"name": P + "clone-uefi-copy", "disk-vda": P + "clone-uefi-copy-a.qcow2", "disk_owner_uid": 0, "disk_owner_gid": 0}, "abababab-0000-1111-2222-333333333333"
)
clone = self.conn.lookupByUUIDString(uuid)
path = ElementTree.fromstring(clone.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)).findtext("os/nvram")
self.assertEqual(path, os.path.join(os.path.dirname(source), P + "clone-uefi-copy_VARS.fd"))
with vm._nvram_pool() as pool:
copy = pool.storageVolLookupByName(os.path.basename(path))
self.assertEqual(copy.info()[1], pool.storageVolLookupByName(os.path.basename(source)).info()[1])
clone.createWithFlags(libvirt.VIR_DOMAIN_START_PAUSED) # qemu opens the copy
self.assertEqual(
ElementTree.fromstring(clone.XMLDesc(0)).findtext("os/nvram"), path, "libvirt replaced the copy"
)
clone.destroy()
def test_clone_of_a_stopped_vm(self):
_, vm = self.source("clone-ok")
uuid = vm.clone_instance(self.clone_data("clone-ok-copy"), "abababab-0000-1111-2222-333333333333")
clone = self.conn.lookupByUUIDString(uuid)
self.assertEqual(clone.name(), P + "clone-ok-copy")
self.assertTrue({P + "clone-ok-copy-a.qcow2", P + "clone-ok-copy-b.qcow2"} <= self.pool_volumes())

View file

@ -1,6 +1,6 @@
"""Live checks of bounded connection opens against real libvirt.
Runs only with TEST_LIBVIRT_HOST set (a TCP compute, see instances/tests/livetest.py);
Runs only with TEST_LIBVIRT_HOST set (a TCP compute, see vrtManager/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.
@ -34,6 +34,7 @@ import unittest
import libvirt
from vrtManager.connection import CONN_SSH, CONN_TCP, CONNECT_TIMEOUT, connection_manager
from vrtManager.tests import livetest
UNREACHABLE = "192.0.2.1"
KEEPALIVE_DEATH = 45 # keepalive (5, 5) closes a silent connection after ~30 s
@ -118,7 +119,7 @@ class Relay:
sock.close()
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_HOST"), "Set TEST_LIBVIRT_HOST to run the live libvirt tests")
@unittest.skipUnless(livetest.enabled(), "Set TEST_LIBVIRT_HOST to run the live libvirt tests")
class LiveBoundedOpenTestCase(unittest.TestCase):
def setUp(self):
self.host = os.environ["TEST_LIBVIRT_HOST"]

View file

@ -0,0 +1,86 @@
"""Live checks that a disk grows while its VM runs or is paused.
QEMU holds a running domain's image, so the size goes through blockResize;
the guest and the volume must both see it, and the persistent definition
stays as it was. Runs only with TEST_LIBVIRT_HOST set (see vrtManager/tests/livetest.py).
"""
import os
import unittest
import libvirt
from django.test import SimpleTestCase
from vrtManager.connection import connection_manager
from vrtManager.instance import wvmInstance
from vrtManager.tests import livetest
P = livetest.PREFIX
PERSISTENT = libvirt.VIR_DOMAIN_XML_INACTIVE | libvirt.VIR_DOMAIN_XML_SECURE
class LiveDiskResizeTestCase(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)
livetest.ensure_pool(cls.conn)
cls.inventory = livetest.inventory(cls.conn)
super().setUpClass()
@classmethod
def tearDownClass(cls):
try:
livetest.remove_pool(cls.conn)
finally:
super().tearDownClass()
if livetest.inventory(cls.conn) != cls.inventory:
raise AssertionError("Live tests changed objects outside the test namespace")
def tearDown(self):
livetest.cleanup(self.conn)
def vm(self, name, fmt):
pool = self.conn.storagePoolLookupByName(livetest.POOL)
vol = pool.createXML(
f"<volume><name>{P}{name}.{fmt}</name><capacity unit='MiB'>64</capacity>"
f"<target><format type='{fmt}'/></target></volume>",
0,
)
dom = self.conn.defineXML(
f"<domain type='kvm'><name>{P}{name}</name><memory unit='MiB'>128</memory><vcpu>1</vcpu>"
"<os><type arch='x86_64' machine='q35'>hvm</type></os><features><acpi/></features><devices>"
f"<disk type='file' device='disk'><driver name='qemu' type='{fmt}'/>"
f"<source file='{vol.path()}'/><target dev='vda' bus='virtio'/></disk></devices></domain>"
)
return dom, vol, wvmInstance(*self.args, f"{P}{name}")
def test_a_disk_grows_while_the_vm_runs_or_is_paused(self):
new_size = 96 << 20
for state in ("running", "paused"):
for fmt in ("qcow2", "raw"):
with self.subTest(state=state, fmt=fmt):
try:
dom, vol, vm = self.vm(f"resize-{state}-{fmt}", fmt)
dom.create()
if state == "paused":
dom.suspend()
before = dom.XMLDesc(PERSISTENT)
disk = vm.get_disk_devices()[0]
vm.resize_disk([{"path": disk["path"], "size_new": new_size}])
self.assertEqual(dom.blockInfo(disk["path"])[0], new_size, "the guest's disk")
self.assertEqual(vol.info()[1], new_size, "the volume")
self.assertEqual(dom.XMLDesc(PERSISTENT), before)
self.assertEqual(dom.state()[0], libvirt.VIR_DOMAIN_PAUSED if state == "paused" else libvirt.VIR_DOMAIN_RUNNING)
finally:
livetest.cleanup(self.conn)

View file

@ -0,0 +1,96 @@
"""Live checks for ISO mount/unmount. Runs only with TEST_LIBVIRT_HOST set."""
import os
import unittest
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.connection import connection_manager
from vrtManager.instance import wvmInstance
from vrtManager.tests import livetest
P = livetest.PREFIX
CDROMS = (
"<disk type='file' device='cdrom'><driver name='qemu' type='raw'/><target dev='sda' bus='sata'/><readonly/></disk>"
"<disk type='file' device='cdrom'><driver name='qemu' type='raw'/><target dev='sdb' bus='sata'/><readonly/></disk>"
"<graphics type='vnc' autoport='yes' passwd='keep1234'/>"
)
PERSISTENT = libvirt.VIR_DOMAIN_XML_INACTIVE | libvirt.VIR_DOMAIN_XML_SECURE
class LiveIsoTestCase(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)
pool = livetest.ensure_pool(cls.conn)
cls.inventory = livetest.inventory(cls.conn)
cls.iso = pool.createXML(
f"<volume><name>{P}media.iso</name><capacity unit='MiB'>1</capacity><target><format type='raw'/></target></volume>", 0
).path()
super().setUpClass()
@classmethod
def tearDownClass(cls):
try:
livetest.remove_pool(cls.conn)
finally:
super().tearDownClass()
if livetest.inventory(cls.conn) != cls.inventory:
raise AssertionError("Live tests changed objects outside the test namespace")
def tearDown(self):
for dom in self.conn.listAllDomains():
if dom.name().startswith(P):
if dom.isActive():
dom.destroy()
dom.undefineFlags(livetest.UNDEFINE_FLAGS)
def vm(self, state):
dom = livetest.define_vm(self.conn, P + "iso-" + state, [], extra_devices=CDROMS)
if state in ("running", "paused"):
dom.create()
if state == "paused":
dom.suspend()
return dom, wvmInstance(*self.args, None, uuid=dom.UUIDString())
def media(self, dom, flags):
tree = etree.fromstring(dom.XMLDesc(flags))
return {d.find("target").get("dev"): d.xpath("string(source/@file)") for d in tree.xpath("devices/disk[@device='cdrom']")}
def test_mount_and_unmount_in_every_state(self):
for state in ("stopped", "running", "paused"):
with self.subTest(state):
dom, vm = self.vm(state)
try:
vm.mount_iso("sdb", os.path.basename(self.iso))
views = [PERSISTENT] + ([0] if dom.isActive() else [])
for flags in views:
self.assertEqual(self.media(dom, flags), {"sda": "", "sdb": self.iso}, f"flags={flags}")
self.assertIn("keep1234", dom.XMLDesc(PERSISTENT))
vm.umount_iso("sdb", self.iso)
for flags in views:
self.assertEqual(self.media(dom, flags), {"sda": "", "sdb": ""}, f"flags={flags}")
self.assertIn("keep1234", dom.XMLDesc(PERSISTENT))
finally:
self.tearDown()
def test_unknown_iso_or_device_changes_nothing(self):
dom, vm = self.vm("running")
before = (dom.XMLDesc(0), dom.XMLDesc(PERSISTENT))
for dev, image in (("sdb", "no-such.iso"), ("sdz", os.path.basename(self.iso))):
with self.subTest(dev=dev, image=image):
with self.assertRaises(libvirt.libvirtError):
vm.mount_iso(dev, image)
self.assertEqual((dom.XMLDesc(0), dom.XMLDesc(PERSISTENT)), before)

View file

@ -1,7 +1,7 @@
"""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
Runs only with TEST_LIBVIRT_HOST set (see vrtManager/tests/livetest.py). The
deletion needs a second host: TEST_LIBVIRT_SSH_HOST (and _LOGIN).
"""
@ -12,7 +12,7 @@ import libvirt
from django.test import SimpleTestCase
from lxml import etree
from instances.tests import livetest
from vrtManager.tests import livetest
from vrtManager.connection import CONN_SSH, NVRAM_DIR, connection_manager, wvmConnect
NAME = livetest.PREFIX + "nvram"

View file

@ -0,0 +1,195 @@
"""Live checks for external snapshots on libvirt's own API. Runs only with
TEST_LIBVIRT_HOST set (see vrtManager/tests/livetest.py)."""
import os
import unittest
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager import util
from vrtManager.connection import connection_manager
from vrtManager.instance import wvmInstance
from vrtManager.tests import livetest
P = livetest.PREFIX
class LiveExternalSnapshotTestCase(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)
livetest.ensure_pool(cls.conn)
cls.inventory = livetest.inventory(cls.conn)
super().setUpClass()
@classmethod
def tearDownClass(cls):
try:
livetest.remove_pool(cls.conn)
finally:
super().tearDownClass()
if livetest.inventory(cls.conn) != cls.inventory:
raise AssertionError("Live tests changed objects outside the test namespace")
def tearDown(self):
livetest.cleanup(self.conn)
def vm(self, name, state="stopped", uefi=False, extra_devices=""):
base = livetest.create_volume(self.conn, P + name)
dom = livetest.define_vm(self.conn, P + name, [base], uefi=uefi, extra_devices=extra_devices)
if state in ("running", "paused"):
dom.create()
if state == "paused":
dom.suspend()
return dom, wvmInstance(*self.args, None, uuid=dom.UUIDString())
def pool_files(self, name):
"""The test pool's files whose names start with the VM's name."""
pool = self.conn.storagePoolLookupByName(livetest.POOL)
pool.refresh(0)
return {vol for vol in pool.listVolumes() if vol.startswith(P + name + ".")}
def top(self, dom):
return etree.fromstring(dom.XMLDesc(0)).xpath("string(devices/disk[@device='disk']/source/@file)")
def test_create_revert_delete_keep_the_state(self):
for state, expected in (("stopped", libvirt.VIR_DOMAIN_SHUTOFF),
("running", libvirt.VIR_DOMAIN_RUNNING),
("paused", libvirt.VIR_DOMAIN_PAUSED)):
with self.subTest(state):
dom, vm = self.vm("snap-" + state, state)
base = self.top(dom)
vm.create_external_snapshot("one", "first")
vm.create_external_snapshot("two")
self.assertEqual(vm.revert_external_snapshot("one"), [], "files left behind")
self.assertEqual(dom.state()[0], expected)
self.assertEqual(vm.delete_external_snapshot("two"), [], "files left behind")
self.assertEqual(vm.delete_external_snapshot("one"), [], "files left behind")
self.assertEqual(self.pool_files("snap-" + state), {os.path.basename(base)}, "an overlay stayed on disk")
self.assertEqual(dom.snapshotNum(0), 0)
self.assertEqual(dom.state()[0], expected)
self.assertTrue(livetest.volume_exists(self.conn, base))
def test_memory_snapshot_resumes_and_its_file_goes_with_it(self):
dom, vm = self.vm("snap-mem", "running")
vm.create_external_snapshot("m", memory=True)
mem = os.path.join(livetest.POOL_PATH, f"{dom.UUIDString()}.m.mem")
self.assertTrue(livetest.volume_exists(self.conn, mem))
dom.suspend()
vm.revert_external_snapshot("m")
self.assertEqual(dom.state()[0], libvirt.VIR_DOMAIN_RUNNING)
self.assertEqual(vm.delete_external_snapshot("m"), [])
self.assertFalse(livetest.volume_exists(self.conn, mem))
def test_uefi_vm_keeps_its_loader(self):
dom, vm = self.vm("snap-uefi", "running", uefi=True)
os_before = etree.tostring(etree.fromstring(dom.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)).find("os"))
vm.create_external_snapshot("u")
vm.revert_external_snapshot("u")
vm.delete_external_snapshot("u")
self.assertEqual(etree.tostring(etree.fromstring(dom.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)).find("os")), os_before)
self.assertEqual(dom.snapshotNum(0), 0)
def test_a_shut_off_vm_without_a_merge_has_no_job(self):
# libvirt reports a shut-off VM's merge as an offline block job; none runs here
_, vm = self.vm("snap-nojob")
self.assertEqual(vm.get_snapshot_jobs(), [])
def test_back_to_back_reverts_keep_a_running_vm_running(self):
# Without the retry, the second revert within one second stopped the VM.
dom, vm = self.vm("snap-collide", "running")
vm.create_external_snapshot("a")
for _ in range(3):
vm.revert_external_snapshot("a")
vm.revert_external_snapshot("a")
self.assertEqual(dom.state()[0], libvirt.VIR_DOMAIN_RUNNING)
def test_a_disk_hot_plugged_live_only_is_named_and_detached_by_a_revert(self):
dom, vm = self.vm("snap-hotplug", "running")
vm.create_external_snapshot("a")
extra = livetest.create_volume(self.conn, P + "snap-hotplug-extra")
dom.attachDeviceFlags(
f"<disk type='file' device='disk'><driver name='qemu' type='qcow2'/><source file='{extra}'/>"
"<target dev='vdb' bus='virtio'/></disk>",
libvirt.VIR_DOMAIN_AFFECT_LIVE,
)
[snap] = vm.get_snapshot(libvirt.VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL)
self.assertEqual(snap["detaches"], [{"dev": "vdb", "path": extra}])
self.assertEqual(vm.revert_external_snapshot("a"), [], "the detached disk is not a leftover")
self.assertNotIn("vdb", etree.fromstring(dom.XMLDesc(0)).xpath("devices/disk/target/@dev"))
self.assertEqual(dom.state()[0], libvirt.VIR_DOMAIN_RUNNING)
self.assertTrue(livetest.volume_exists(self.conn, extra))
vm.delete_external_snapshot("a")
def test_a_mixed_vm_can_be_cleaned_up(self):
dom, vm = self.vm("snap-mixed-clean")
dom.snapshotCreateXML("<domainsnapshot><name>i</name></domainsnapshot>", 0)
dom.snapshotCreateXML(
"<domainsnapshot><name>e</name></domainsnapshot>", libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY
)
# libvirt refuses an external snapshot whose parent is internal, so the
# internal one goes first
vm.snapshot_delete("i")
vm.delete_external_snapshot("e")
self.assertEqual(dom.snapshotNum(0), 0)
def test_chain_stays_linear(self):
dom, vm = self.vm("snap-linear")
vm.create_external_snapshot("a")
vm.create_external_snapshot("b")
vm.revert_external_snapshot("a")
with self.assertRaisesRegex(util.OperationError, "Newer snapshots"):
vm.create_external_snapshot("c")
vm.delete_external_snapshot("b")
vm.create_external_snapshot("c")
def test_refusals(self):
iso = livetest.create_volume(self.conn, P + "snap-media")
cdrom = (f"<disk type='file' device='cdrom'><driver name='qemu' type='qcow2'/><source file='{iso}'/>"
"<target dev='sda' bus='sata'/><readonly/></disk>")
_, vm = self.vm("snap-cdrom", extra_devices=cdrom)
with self.assertRaisesRegex(util.OperationError, "Eject"):
vm.create_external_snapshot("x")
dom, vm = self.vm("snap-saved", "running")
dom.managedSave(0)
with self.assertRaisesRegex(util.OperationError, "saved state"):
vm.create_external_snapshot("x")
dom, vm = self.vm("snap-mixed")
dom.snapshotCreateXML("<domainsnapshot><name>i</name></domainsnapshot>", 0)
with self.assertRaisesRegex(util.OperationError, "internal snapshots"):
vm.create_external_snapshot("x")
def test_quiesce_without_a_connected_agent_is_refused(self):
# no guest OS runs an agent here: the channel exists but is not connected
channel = "<channel type='unix'><target type='virtio' name='org.qemu.guest_agent.0'/></channel>"
for name, extra in (("snap-noagent", ""), ("snap-agentchan", channel)):
with self.subTest(name):
dom, vm = self.vm(name, "running", extra_devices=extra)
with self.assertRaisesRegex(util.OperationError, "guest agent is not connected"):
vm.create_external_snapshot("q", quiesce=True)
self.assertEqual(dom.snapshotNum(0), 0)
def test_a_pool_without_room_for_the_overlays_is_refused(self):
# a sparse disk ten times the pool: a tenth of it is more than the pool has free
pool = self.conn.storagePoolLookupByName(livetest.POOL)
big = livetest.create_volume(self.conn, P + "snap-big", size_mib=pool.info()[1] * 10 // 1024**2)
dom = livetest.define_vm(self.conn, P + "snap-big", [big])
vm = wvmInstance(*self.args, None, uuid=dom.UUIDString())
with self.assertRaisesRegex(util.OperationError, f"Storage pool {livetest.POOL} has .* GiB free; the snapshot needs"):
vm.create_external_snapshot("x")
self.assertEqual(dom.snapshotNum(0), 0)
vm.create_external_snapshot("x", free_percent=0, free_min_gib=0)
vm.delete_external_snapshot("x")

View file

@ -0,0 +1,112 @@
"""Live checks that persistent edits keep secrets and pending changes.
Every edit that redefines the domain must start from the persistent
definition with secrets (VIR_DOMAIN_XML_INACTIVE | VIR_DOMAIN_XML_SECURE).
Runs only with TEST_LIBVIRT_HOST set (see vrtManager/tests/livetest.py).
"""
import os
import unittest
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.connection import connection_manager
from vrtManager.instance import wvmInstance
from vrtManager.tests import livetest
P = livetest.PREFIX
MAC = "52:54:00:aa:01:01"
DEVICES = (
f"<interface type='network'><mac address='{MAC}'/><source network='default'/><model type='virtio'/></interface>"
"<graphics type='vnc' autoport='yes' passwd='keep1234'/>"
"<video><model type='vga' primary='yes'/></video>"
)
PERSISTENT = libvirt.VIR_DOMAIN_XML_INACTIVE | libvirt.VIR_DOMAIN_XML_SECURE
# name -> (edit, allowed while running)
EDITS = {
"set_bootmenu": (lambda vm: vm.set_bootmenu(1), True),
"set_bootorder": (lambda vm: vm.set_bootorder({0: {"type": "disk", "dev": "vda"}}), True),
"set_vcpu_hotplug": (lambda vm: vm.set_vcpu_hotplug(True, 1), False),
"set_console_keymap": (lambda vm: vm.set_console_keymap("de"), True),
"set_console_listener_addr": (lambda vm: vm.set_console_listener_addr("127.0.0.1"), True),
"set_video_model": (lambda vm: vm.set_video_model("virtio"), True),
"resize_cpu": (lambda vm: vm.resize_cpu("1", "2"), True),
"resize_mem": (lambda vm: vm.resize_mem(128, 160), False),
"resize_disk": (lambda vm: vm.resize_disk([{"path": vm.get_disk_devices()[0]["path"], "size_new": 96 << 20}]), True),
"set_options": (lambda vm: vm.set_options({"title": "edited"}), True),
"set_qos": (lambda vm: vm.set_qos(MAC, "inbound", 100, 200, 300), True),
"unset_qos": (lambda vm: vm.unset_qos(MAC, "inbound"), True),
}
class LivePersistentEditTestCase(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)
livetest.ensure_pool(cls.conn)
cls.inventory = livetest.inventory(cls.conn)
super().setUpClass()
@classmethod
def tearDownClass(cls):
try:
livetest.remove_pool(cls.conn)
finally:
super().tearDownClass()
if livetest.inventory(cls.conn) != cls.inventory:
raise AssertionError("Live tests changed objects outside the test namespace")
def tearDown(self):
livetest.cleanup(self.conn)
def vm(self, name):
disk = livetest.create_volume(self.conn, P + name)
dom = livetest.define_vm(self.conn, P + name, [disk], extra_devices=DEVICES)
return dom, wvmInstance(*self.args, None, uuid=dom.UUIDString())
def persistent(self, dom):
return etree.fromstring(dom.XMLDesc(PERSISTENT))
def test_edits_of_a_stopped_vm_keep_the_vnc_password(self):
for name, (edit, _) in EDITS.items():
with self.subTest(name):
try:
dom, vm = self.vm("xml-stopped")
edit(vm)
self.assertEqual(self.persistent(dom).xpath("string(devices/graphics/@passwd)"), "keep1234")
finally:
livetest.cleanup(self.conn)
def test_edits_of_a_running_vm_keep_pending_changes(self):
for name, (edit, while_running) in EDITS.items():
if not while_running:
continue
with self.subTest(name):
try:
dom, vm = self.vm("xml-running")
dom.create()
# A pending change: only the persistent definition has 192 MiB.
tree = self.persistent(dom)
tree.find("memory").text = str(192 * 1024)
tree.find("currentMemory").text = str(192 * 1024)
self.conn.defineXML(etree.tostring(tree).decode())
edit(vm)
after = self.persistent(dom)
self.assertEqual(after.findtext("memory"), str(192 * 1024), "pending memory change lost")
self.assertEqual(after.xpath("string(devices/graphics/@passwd)"), "keep1234")
finally:
livetest.cleanup(self.conn)

View file

@ -0,0 +1,319 @@
"""Live checks that each domain edit does what it says and nothing else.
For every edit the persistent definition (inactive, with secrets) is read
before and after. The edit must show its effect, and once the nodes it is
allowed to touch are removed from both sides, the rest of the definition
must be identical: unknown elements, secrets and pending changes survive.
On a running VM, edits of the persistent definition must leave the live
one alone. Runs only with TEST_LIBVIRT_HOST set (see vrtManager/tests/livetest.py).
"""
import os
import unittest
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.connection import connection_manager
from vrtManager.instance import wvmInstance
from vrtManager.tests import livetest
P = livetest.PREFIX
MAC = "52:54:00:aa:02:01"
PERSISTENT = libvirt.VIR_DOMAIN_XML_INACTIVE | libvirt.VIR_DOMAIN_XML_SECURE
PARSER = etree.XMLParser(remove_blank_text=True)
def domain_xml(name, disk, iso):
"""A VM with elements WebVirtCloud does not manage, which edits must keep."""
return f"""
<domain type='kvm'>
<name>{name}</name>
<title>original title</title>
<metadata><app:info xmlns:app='http://example.com/wvc-test'>keep-metadata</app:info></metadata>
<memory unit='MiB'>128</memory>
<currentMemory unit='MiB'>128</currentMemory>
<memoryBacking><nosharepages/></memoryBacking>
<vcpu placement='static'>1</vcpu>
<cputune><shares>2048</shares></cputune>
<os><type arch='x86_64' machine='q35'>hvm</type><boot dev='hd'/></os>
<features><acpi/><apic/></features>
<on_crash>restart</on_crash>
<devices>
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2' cache='none'/>
<source file='{disk}'/>
<target dev='vda' bus='virtio'/>
<serial>keep-serial</serial>
<iotune><total_bytes_sec>10485760</total_bytes_sec></iotune>
</disk>
<disk type='file' device='cdrom'>
<driver name='qemu' type='raw'/>
<source file='{iso}'/>
<target dev='sda' bus='sata'/>
<readonly/>
</disk>
<interface type='network'>
<mac address='{MAC}'/>
<source network='default'/>
<model type='virtio'/>
<bandwidth><outbound average='1000' peak='2000' burst='1024'/></bandwidth>
</interface>
<graphics type='vnc' autoport='yes' passwd='keep1234' keymap='en-us'>
<listen type='address' address='0.0.0.0'/>
</graphics>
<video><model type='vga' primary='yes'/></video>
<serial type='pty'><target port='0'/></serial>
<rng model='virtio'><backend model='random'>/dev/urandom</backend></rng>
<watchdog model='i6300esb' action='reset'/>
<controller type='virtio-serial'/> <!-- add_guest_agent hotplugs a channel -->
</devices>
</domain>"""
def first(tree, path):
found = tree.xpath(path)
return found[0] if found else None
# name -> (edit, check of the new persistent tree, paths the edit may change,
# allowed while running, changes the running VM too)
EDITS = {
"set_bootmenu": (
lambda vm: vm.set_bootmenu(1),
lambda t: first(t, "os/bootmenu/@enable") == "yes",
["os/bootmenu"], True, False,
),
"set_bootorder": (
lambda vm: vm.set_bootorder({0: {"type": "disk", "dev": "vda"}}),
lambda t: first(t, "devices/disk[target/@dev='vda']/boot/@order") == "1" and not t.xpath("os/boot"),
["os/boot", "devices/*/boot"], True, False,
),
"set_vcpu_hotplug": (
lambda vm: vm.set_vcpu_hotplug(True, 1),
lambda t: bool(t.xpath("vcpus/vcpu")),
["vcpus"], False, False,
),
"set_console_keymap": (
lambda vm: vm.set_console_keymap("de"),
lambda t: first(t, "devices/graphics/@keymap") == "de",
["devices/graphics/@keymap"], True, False,
),
"set_console_listener_addr": (
lambda vm: vm.set_console_listener_addr("127.0.0.1"),
lambda t: first(t, "devices/graphics/listen/@address") == "127.0.0.1",
["devices/graphics/@listen", "devices/graphics/listen/@address"], True, False,
),
"set_console_type pty": (
lambda vm: vm.set_console_type("pty"),
lambda t: not t.xpath("devices/graphics") and first(t, "devices/console/@type") == "pty",
["devices/graphics", "devices/console"], True, False,
),
"set_console_type vnc": (
lambda vm: vm.set_console_type("vnc"),
lambda t: t.xpath("devices/graphics/@type") == ["vnc"] and first(t, "devices/graphics/@listen") == "127.0.0.1",
["devices/graphics"], True, False,
),
"set_console_passwd": (
lambda vm: vm.set_console_passwd("new12345"),
lambda t: first(t, "devices/graphics/@passwd") == "new12345",
["devices/graphics/@passwd"], True, False,
),
"set_video_model": (
lambda vm: vm.set_video_model("virtio"),
lambda t: first(t, "devices/video/model/@type") == "virtio",
["devices/video"], True, False,
),
"set_rng": (
lambda vm: vm.set_rng(False),
lambda t: not t.xpath("devices/rng"),
["devices/rng"], True, False,
),
"set_tpm": (
lambda vm: vm.set_tpm(True),
lambda t: first(t, "devices/tpm/backend/@version") == "2.0",
["devices/tpm"], True, False,
),
"set_on_crash": (
lambda vm: vm.set_on_crash("restart"),
lambda t: t.findtext("on_crash") == "restart",
["on_crash"], True, False,
),
"resize_cpu": (
lambda vm: vm.resize_cpu("1", "2"),
lambda t: t.findtext("vcpu") == "2" and first(t, "vcpu/@current") == "1",
["vcpu"], True, False,
),
"resize_mem": (
lambda vm: vm.resize_mem(128, 160),
lambda t: t.findtext("memory") == str(160 * 1024),
["memory", "currentMemory"], False, False,
),
"set_options": (
lambda vm: vm.set_options({"title": "new title", "description": "new description"}),
lambda t: t.findtext("title") == "new title" and t.findtext("description") == "new description",
["title", "description"], True, False,
),
"set_qos": (
lambda vm: vm.set_qos(MAC, "inbound", 100, 200, 300),
lambda t: first(t, "devices/interface/bandwidth/inbound/@average") == "100"
and first(t, "devices/interface/bandwidth/outbound/@average") == "1000",
["devices/interface/bandwidth/inbound"], True, False,
),
"unset_qos": (
lambda vm: vm.unset_qos(MAC, "outbound"),
lambda t: not t.xpath("devices/interface/bandwidth/outbound"),
["devices/interface/bandwidth"], True, False,
),
"umount_iso": (
lambda vm: vm.umount_iso("sda"),
lambda t: not t.xpath("devices/disk[target/@dev='sda']/source/@file"),
["devices/disk[target/@dev='sda']/source"], True, True,
),
"set_link_state": (
lambda vm: vm.set_link_state(MAC, "down"),
lambda t: first(t, "devices/interface/link/@state") == "down",
["devices/interface/link"], True, True,
),
"change_network (same MAC, new model)": (
lambda vm: vm.change_network(MAC, MAC, "default", "net", "e1000e", ""),
lambda t: first(t, "devices/interface/model/@type") == "e1000e"
and first(t, "devices/interface/bandwidth/outbound/@average") == "1000",
["devices/interface/model"], True, False,
),
"add_network": (
lambda vm: vm.add_network("52:54:00:aa:02:09", "default", "net", "virtio"),
lambda t: len(t.xpath("devices/interface")) == 2,
["devices/interface[mac/@address='52:54:00:aa:02:09']"], True, True,
),
"delete_network": (
lambda vm: vm.delete_network(MAC),
lambda t: not t.xpath("devices/interface"),
["devices/interface"], True, True,
),
"attach_disk": (
lambda vm: vm.attach_disk("vdc", livetest.create_volume(vm.wvm, P + "fx-extra"), target_bus="virtio", format_type="qcow2"),
lambda t: bool(t.xpath("devices/disk[target/@dev='vdc']")),
["devices/disk[target/@dev='vdc']"], True, True,
),
"add_guest_agent": (
lambda vm: vm.add_guest_agent(),
lambda t: bool(t.xpath("devices/channel/target[@name='org.qemu.guest_agent.0']")),
["devices/channel[target/@name='org.qemu.guest_agent.0']"], True, True,
),
"edit_disk (cache mode)": (
lambda vm: vm.edit_disk("vda", vm.get_disk_devices(config=True)[0]["path"], False, False, "virtio",
"keep-serial", "qcow2", "writeback", None, None, None),
lambda t: first(t, "devices/disk[target/@dev='vda']/driver/@cache") == "writeback",
["devices/disk[target/@dev='vda']/driver/@cache"], True, False,
),
}
def canonical(tree, paths):
tree = etree.fromstring(etree.tostring(tree), PARSER)
for path in paths:
for node in tree.xpath(path):
if isinstance(node, etree._ElementUnicodeResult):
node.getparent().attrib.pop(node.attrname, None)
else:
node.getparent().remove(node)
return etree.tostring(tree, method="c14n").decode()
class LiveXmlEffectsTestCase(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)
livetest.ensure_pool(cls.conn)
cls.inventory = livetest.inventory(cls.conn)
super().setUpClass()
@classmethod
def tearDownClass(cls):
try:
livetest.remove_pool(cls.conn)
finally:
super().tearDownClass()
if livetest.inventory(cls.conn) != cls.inventory:
raise AssertionError("Live tests changed objects outside the test namespace")
def vm(self, running, paused=False):
disk = livetest.create_volume(self.conn, P + "fx")
iso = self.conn.storagePoolLookupByName(livetest.POOL).createXML(
f"<volume><name>{P}fx.iso</name><capacity unit='MiB'>1</capacity><target><format type='raw'/></target></volume>", 0
).path()
dom = self.conn.defineXML(domain_xml(P + "fx", disk, iso))
if running:
dom.create()
# A pending change only in the persistent definition
tree = self.persistent(dom)
tree.find("on_crash").text = "destroy"
self.conn.defineXML(etree.tostring(tree).decode())
if paused:
dom.suspend()
return dom, wvmInstance(*self.args, None, uuid=dom.UUIDString())
def persistent(self, dom):
return etree.fromstring(dom.XMLDesc(PERSISTENT), PARSER)
def live(self, dom):
return etree.fromstring(dom.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE), PARSER)
def check(self, running, paused=False):
for name, (edit, effect, touched, allowed_running, changes_live) in EDITS.items():
if running and not allowed_running:
continue
with self.subTest(name):
try:
dom, vm = self.vm(running, paused)
before, live_before = self.persistent(dom), dom.isActive() and self.live(dom)
edit(vm)
after = self.persistent(dom)
self.assertTrue(effect(after), "edit had no effect")
self.assertEqual(canonical(after, touched), canonical(before, touched), "unrelated change")
# An unplug completes only when the guest acknowledges it; these
# VMs have no OS (test_live_guest covers it with a real guest).
if running and changes_live and name != "delete_network":
self.assertTrue(effect(self.live(dom)), "edit did not reach the running VM")
if running and not changes_live:
self.assertEqual(
canonical(self.live(dom), ["devices/graphics/@port"]),
canonical(live_before, ["devices/graphics/@port"]),
"persistent-only edit changed the running VM",
)
finally:
livetest.cleanup(self.conn)
def test_edits_of_a_stopped_vm(self):
self.check(running=False)
def test_edits_of_a_running_vm(self):
self.check(running=True)
def test_edits_of_a_paused_vm(self):
self.check(running=True, paused=True)
def test_enabling_vcpu_hotplug_keeps_the_current_vcpus(self):
try:
dom, vm = self.vm(running=False)
vm.resize_cpu("2", "4")
vm.set_vcpu_hotplug(True)
after = self.persistent(dom)
self.assertEqual((after.findtext("vcpu"), first(after, "vcpu/@current")), ("4", "2"))
self.assertEqual(after.xpath("vcpus/vcpu/@enabled"), ["yes", "yes", "no", "no"])
self.assertEqual(after.xpath("vcpus/vcpu/@hotpluggable"), ["no", "yes", "yes", "yes"])
dom.create()
self.assertEqual(dom.vcpusFlags(libvirt.VIR_DOMAIN_AFFECT_LIVE), 2)
finally:
livetest.cleanup(self.conn)

View file

@ -9,180 +9,7 @@ import libvirt
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 XMLDesc(self, flags=0):
return "<pool><target><path>/elsewhere</path></target></pool>"
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, nvram=None, **domain):
self.state = state
self.nvram = nvram
self.instance = FakeDomain(**domain)
def get_nvram(self):
return self.nvram
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
from vrtManager.tests.fakes import FakeDomain, FakeHost, FakeSource, destination, with_disks
class MigrationFlagsTestCase(unittest.TestCase):
@ -325,10 +152,6 @@ class MigrationModeTestCase(unittest.TestCase):
self.assertEqual(self.migrate(5, live=True, autoconverge=True, compress=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>"

View file

@ -0,0 +1,191 @@
"""wvmNetwork: a change starts from the saved definition, so two in a row
keep each other; a fixed address is found by the address it had; the XML
editor defines only this network; a network that does not start is not
left defined; the DHCP leases are read. Runs on a private libvirt test
driver: "test:///default" is shared by the whole process."""
import tempfile
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.network import network_size, wvmNetwork, wvmNetworks
from vrtManager.tests.driver import without_inactive_flag
from vrtManager.tests.fakes import DUID, lease
from vrtManager.util import OperationError
NODE = """<node>
<network><name>lab</name><bridge name='virbr9'/>
<ip family='ipv6' address='2001:db8:1::1' prefix='64'/>
<ip family='ipv4' address='192.0.2.1' netmask='255.255.255.0'><dhcp><range start='192.0.2.10' end='192.0.2.20'/>
<host name='paul' ip='192.0.2.30'/><host mac='52:54:00:00:00:31' ip='192.0.2.31'/></dhcp></ip>
</network>
</node>"""
class NetworkChangeTests(SimpleTestCase):
def setUp(self):
node = tempfile.NamedTemporaryFile("w", suffix=".xml")
self.addCleanup(node.close)
node.write(NODE)
node.flush()
self.conn = libvirt.open(f"test://{node.name}")
self.addCleanup(self.conn.close)
patcher = patch("vrtManager.connection.connection_manager.get_connection", return_value=self.conn)
patcher.start()
self.addCleanup(patcher.stop)
without_inactive_flag(self)
def network(self):
return wvmNetwork("localhost", "", "", 4, "lab")
def saved(self):
return etree.fromstring(self.conn.networkLookupByName("lab").XMLDesc(libvirt.VIR_NETWORK_XML_INACTIVE))
def test_the_page_reads_each_definition_once(self):
# the running one for its addresses, the saved one for its limits and the XML editor
net = self.network()
with patch.object(net.net, "XMLDesc", wraps=net.net.XMLDesc) as xml:
net.get_ip_networks(), net.get_network_mac(), net.get_qos()
net.get_bridge_device(), net.get_mode(), net.saved_xml
self.assertEqual(xml.call_count, 2)
def test_the_ipv4_address_after_an_ipv6_one_keeps_its_index(self):
ipv6, ipv4 = self.network().get_ip_networks()
self.assertEqual((ipv6["family"], ipv4["family"], ipv4["index"]), ("ipv6", "ipv4", 1))
self.network().modify_fixed_address(1, "", "web", "192.0.2.40", "52:54:00:00:00:40")
self.assertEqual(self.saved().xpath("ip[2]/dhcp/host[last()]/@ip"), ["192.0.2.40"])
def test_two_changes_in_a_row_keep_each_other(self):
# a running network keeps its running definition until restarted: a
# change made from it would drop the change saved before
net = self.network()
running = net.net.XMLDesc(0)
saved = [running]
net.net = MagicMock(**{"XMLDesc.side_effect": lambda flags=0: saved[0] if flags else running})
with patch.object(self.conn, "networkDefineXML", side_effect=lambda xml: saved.__setitem__(0, xml)):
net.set_qos("inbound", 100, 200, 300)
net.set_qos("outbound", 10, 20, 30)
self.assertEqual(etree.fromstring(saved[0]).xpath("bandwidth/*/@average"), ["100", "10"])
def test_a_fixed_address_is_found_by_the_address_it_had(self):
net = self.network()
# a name-only entry changes too, and the one deleted is the one it was
net.modify_fixed_address(1, "192.0.2.30", "paul", "192.0.2.32", "")
self.assertEqual(self.saved().xpath("ip[2]/dhcp/host/@ip"), ["192.0.2.31", "192.0.2.32"])
net.modify_fixed_address(1, "192.0.2.31", "db", "192.0.2.33", "52:54:00:00:00:31")
net.delete_fixed_address(1, "192.0.2.32")
self.assertEqual([dict(h.attrib) for h in self.saved().xpath("ip[2]/dhcp/host")],
[{"mac": "52:54:00:00:00:31", "name": "db", "ip": "192.0.2.33"}])
with self.assertRaisesRegex(OperationError, "has no fixed address 192.0.2.99"):
net.delete_fixed_address(1, "192.0.2.99")
def test_a_change_keeps_what_the_page_does_not_show_and_touches_no_other_entry(self):
net = self.network()
conn = self.conn.networkLookupByName("lab")
conn.destroy()
saved = conn.XMLDesc(0).replace("<host mac='52:54:00:00:00:31' ip='192.0.2.31'/>",
"<host mac='52:54:00:00:00:31' id='01:02:03:04' ip='192.0.2.31'><lease expiry='1' unit='hours'/></host>")
self.conn.networkDefineXML(saved)
net = self.network()
# the name of another entry is refused before anything changes
with self.assertRaisesRegex(OperationError, "The name paul has another fixed address, 192.0.2.30"):
net.modify_fixed_address(1, "192.0.2.31", "paul", "192.0.2.35", "52:54:00:00:00:31")
with self.assertRaisesRegex(OperationError, "52:54:00:00:00:31 has another fixed address"):
net.modify_fixed_address(1, "192.0.2.30", "paul", "192.0.2.30", "52:54:00:00:00:31")
net.modify_fixed_address(1, "192.0.2.31", "web", "192.0.2.35", "52:54:00:00:00:31")
(host,) = self.saved().xpath("ip[2]/dhcp/host[@ip='192.0.2.35']")
self.assertEqual((host.get("id"), host.xpath("string(lease/@expiry)"), host.get("name")), ("01:02:03:04", "1", "web"))
self.assertEqual(self.saved().xpath("ip[2]/dhcp/host/@ip"), ["192.0.2.30", "192.0.2.35"])
def test_an_ipv6_entry_known_by_its_duid_changes(self):
self.conn.networkDefineXML(
"<network><name>six</name><bridge name='virbr5'/><ip family='ipv6' address='2001:db8:5::1' prefix='64'>"
"<dhcp><host id='00:01:00:01:aa:bb' ip='2001:db8:5::10'/></dhcp></ip></network>")
net = wvmNetwork("localhost", "", "", 4, "six")
net.modify_fixed_address(0, "2001:db8:5::10", "", "2001:db8:5::11", "00:01:00:01:aa:bb")
saved = etree.fromstring(self.conn.networkLookupByName("six").XMLDesc(0))
self.assertEqual([dict(h.attrib) for h in saved.xpath("ip/dhcp/host")], [{"id": "00:01:00:01:aa:bb", "ip": "2001:db8:5::11"}])
def test_an_entry_gone_meanwhile_is_not_added_anew(self):
with self.assertRaisesRegex(OperationError, "is gone meanwhile"):
self.network().modify_fixed_address(1, "192.0.2.99", "x", "192.0.2.40", "")
self.assertEqual(self.saved().xpath("ip[2]/dhcp/host/@ip"), ["192.0.2.30", "192.0.2.31"])
def test_a_running_network_saved_with_other_addresses_is_not_changed_live(self):
# parentIndex would name another <ip> of the running network
net = self.network()
net.net = MagicMock(**{"isActive.return_value": 1, "name.return_value": "lab",
"XMLDesc.side_effect": lambda flags=0: NODE.split("<node>")[1].split("</node>")[0]
if not flags else "<network><name>lab</name><ip address='192.0.2.1' netmask='255.255.255.0'/></network>"})
with self.assertRaisesRegex(OperationError, "runs with other addresses than it has saved"):
net.update(0, 0, "<host/>", 0)
net.net.update.assert_not_called()
def test_the_xml_editor_expands_no_entity(self):
# a file of the panel's host named by an external entity is not read: no DTD is taken
net = self.network()
xml = ('<!DOCTYPE network [<!ENTITY name SYSTEM "file:///etc/hostname">]>'
"<network><name>&name;</name></network>")
with self.assertRaisesRegex(OperationError, "may not declare a DTD"):
net.edit_network(xml)
def test_the_xml_editor_takes_an_encoding_declaration(self):
self.conn.networkLookupByName("lab").destroy()
net = self.network()
net.edit_network('<?xml version="1.0" encoding="UTF-8"?>\n' + net.saved_xml.replace("virbr9", "virbr6"))
self.assertEqual(self.saved().xpath("bridge/@name"), ["virbr6"])
def test_a_fixed_address_is_of_the_subnet_and_its_own(self):
net = self.network()
for address, why in (("198.51.100.5", "is not in 192.0.2.0/24"), ("192.0.2.1", "the host's own address"),
("192.0.2.31", "another entry's fixed address"), ("x", "is not an IP address")):
with self.subTest(address=address), self.assertRaisesRegex(OperationError, why):
net.modify_fixed_address(1, "192.0.2.30", "paul", address, "")
self.assertIs(net.modify_fixed_address(1, "192.0.2.30", "paul", "192.0.2.30", ""), False)
def test_the_xml_editor_defines_this_network_only(self):
self.conn.networkLookupByName("lab").destroy() # its definition read back at once
net = self.network()
xml = net.saved_xml
for other, why in ((xml.replace("<name>lab</name>", "<name>other</name>"), "must define network lab"),
(xml.replace(net.net.UUIDString(), "11111111-2222-3333-4444-555555555555"), "keep the network's UUID"),
("<network>", "not valid")):
with self.subTest(why=why), self.assertRaisesRegex(OperationError, why):
net.edit_network(other)
net.edit_network(xml.replace("virbr9", "virbr7"))
self.assertEqual(self.saved().xpath("bridge/@name"), ["virbr7"])
def test_a_network_that_does_not_start_is_undefined(self):
gateway, mask, dhcp = network_size("192.0.2.0/24", True)
with patch.object(libvirt.virNetwork, "create", side_effect=libvirt.libvirtError("in use by virbr0")):
with self.assertRaises(libvirt.libvirtError):
wvmNetworks("localhost", "", "", 4).create_network(
"n1", "nat", True, gateway, mask, dhcp, False, None, None, None, "", False)
self.assertNotIn("n1", self.conn.listDefinedNetworks() + self.conn.listNetworks())
class LeasesTests(SimpleTestCase):
def network(self, leases, active=True):
net = wvmNetwork.__new__(wvmNetwork)
net.net = MagicMock(**{"isActive.return_value": active, "DHCPLeases.return_value": leases})
return net
def test_leases_by_address_ipv4_first(self):
net = self.network([lease("2001:db8:1::40", family=1, clientid=DUID, expiry=0),
lease("192.0.2.100", "52:54:00:AA:00:01", hostname="db"),
lease("192.0.2.9", "52:54:00:aa:00:02")])
ipv4_9, ipv4_100, ipv6 = net.get_leases()
self.assertEqual([str(ipv4_9["ip"]), str(ipv4_100["ip"]), str(ipv6["ip"])], ["192.0.2.9", "192.0.2.100", "2001:db8:1::40"])
self.assertEqual((ipv4_100["mac"], ipv4_100["hostname"], ipv4_100["expires"]),
("52:54:00:aa:00:01", "db", datetime.fromtimestamp(1793265489, timezone.utc)))
# an IPv6 lease: no MAC, its client's DUID; one that never ends
self.assertEqual((ipv6["mac"], ipv6["client_id"], ipv6["expires"]), ("", DUID, None))
def test_a_network_that_does_not_run_has_none(self):
net = self.network([lease("192.0.2.9")], active=False)
self.assertEqual(net.get_leases(), [])
net.net.DHCPLeases.assert_not_called()

View file

@ -2,6 +2,7 @@ import unittest
from lxml import etree
from vrtManager.network import ip_networks, network_size
from django.test import SimpleTestCase
class TestNetworkSize(unittest.TestCase):
@ -96,3 +97,20 @@ class TestIpNetworks(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
class AddressTests(SimpleTestCase):
def test_an_address_without_a_prefix_takes_libvirts(self):
tree = etree.fromstring("<network><ip address='192.0.2.1'/><ip address='10.1.2.3'/><ip family='ipv6' address='2001:db8::1'/></network>")
self.assertEqual([str(ip["subnet"]) for ip in ip_networks(tree)], ["192.0.2.0/24", "10.0.0.0/8", "2001:db8::/64"])
def test_every_way_libvirt_writes_an_address(self):
compact = "<network><ip address='192.0.2.1' netmask='255.255.255.0'/></network>"
static = ("<network><ip address='192.0.2.1' prefix='24'><dhcp><host mac='52:54:00:00:00:01' ip='192.0.2.5'/>"
"</dhcp></ip><ip family='ipv6' address='2001:db8::1' prefix='64'/></network>")
(ip,) = ip_networks(etree.fromstring(compact))
self.assertEqual((ip["index"], ip["family"], str(ip["subnet"]), ip["dhcp"]), (0, "ipv4", "192.0.2.0/24", False))
v4, v6 = ip_networks(etree.fromstring(static))
# DHCP with fixed addresses only is DHCP too
self.assertEqual((v4["dhcp"], v4["range"], v4["hosts"][0]["ip"]), (True, None, "192.0.2.5"))
self.assertEqual((v6["index"], v6["family"], str(v6["subnet"])), (1, "ipv6", "2001:db8::/64"))

View file

@ -0,0 +1,110 @@
"""The host's node devices as vrtManager lists them: PCI devices by IOMMU
group and USB devices, each with what the host has on it and the VMs
that name it. Node device XML as libvirt 11.10 writes it; the connection
is a mock, libvirt's test driver has no node devices."""
from unittest.mock import MagicMock
import libvirt
from django.test import SimpleTestCase
from vrtManager import util
from vrtManager.connection import wvmConnect
from vrtManager.nodedevs import hostdev_problems, node_devices, pci_address
from vrtManager.tests.fakes import NODES, PCI_0A, USB_1_3, node, pci, vm
PCI_01 = "<hostdev mode='subsystem' type='pci'><source><address domain='0x0000' bus='0x00' slot='0x01' function='0x0'/></source></hostdev>"
USB_MODEL = "<hostdev mode='subsystem' type='usb'><source><vendor id='0x0627'/><product id='0x0001'/></source></hostdev>"
def devices(nodes=NODES, domains=()):
conn = wvmConnect.__new__(wvmConnect)
conn.wvm = MagicMock(**{"listAllDevices.return_value": [node(*n) for n in nodes],
"listAllDomains.return_value": list(domains)})
return node_devices(conn)
class NodeDevicesTests(SimpleTestCase):
def test_pci_devices_by_group_with_what_the_host_has_on_them(self):
pci_devices, _ = devices()
self.assertEqual([(d["address"], d["group"]) for d in pci_devices],
[("0000:00:02.0", 2), ("0000:00:03.0", 2), ("0000:00:06.0", 3), ("0000:00:0a.0", 4), ("0000:00:01.0", None)])
gpu, disk, nic, spare, _ = pci_devices
# a graphics card's primary node is the host's; its render node is not a resource of its own
self.assertEqual((gpu["resources"], disk["resources"], nic["resources"], spare["resources"]),
(["/dev/dri/card0"], ["/dev/vda"], ["eth0"], []))
self.assertEqual((nic["kind"], nic["driver"], nic["ids"]), ("Network controller", "virtio-pci", "1af4:1000"))
def test_usb_devices_with_their_classes_hubs_and_same_models(self):
_, (hub, tablet_2, tablet_3) = devices()
self.assertEqual((hub["kinds"], hub["same_model"]), (["Hub"], False))
self.assertEqual((tablet_2["kinds"], tablet_2["same_model"], tablet_3["same_model"]), (["Human interface"], True, True))
def test_the_vms_that_name_a_device(self):
pci_devices, (_, tablet_2, tablet_3) = devices(domains=[vm("db-1", PCI_0A + USB_1_3), vm("kiosk-1", USB_MODEL)])
self.assertEqual([d["vms"] for d in pci_devices], [[], [], [], ["db-1"], []])
# by address: that device; by model only: any device of the model
self.assertEqual((tablet_3["vms"], tablet_3["by_model"]), (["db-1"], ["kiosk-1"]))
self.assertEqual((tablet_2["vms"], tablet_2["by_model"]), ([], ["kiosk-1"]))
def test_a_usb_controller_holds_the_usb_devices_below_it(self):
pci_devices, (hub, tablet_2, _) = devices(domains=[vm("kiosk-1", USB_1_3)])
controller = [d for d in pci_devices if d["address"] == "0000:00:01.0"][0]
# its root hub is no device of the host's; the tablets are, one given to a VM
self.assertEqual(controller["resources"], ["USB device 1/2 (QEMU Tablet)", "USB device 1/3 (QEMU Tablet)"])
self.assertEqual((hub["controller"], tablet_2["controller"]), ("0000:00:01.0", "0000:00:01.0"))
def test_a_usb_device_on_a_controller_given_to_a_vm_is_not_given_to_another(self):
pci_devices, usb_devices = devices(domains=[vm("db-1", PCI_01)])
reasons = [util.render(m["text"], m["values"], str) for m in hostdev_problems(
"kiosk-1", pci_devices, usb_devices, set(), [("0x0627", "0x0001", (1, 2))])]
self.assertIn("USB device 1/2 (QEMU Tablet) is on the PCI controller 0000:00:01.0, given to db-1", reasons)
def test_a_usb_device_is_named_as_libvirt_picks_one(self):
def named(hostdev):
_, (_, tablet_2, tablet_3) = devices(domains=[vm("v", hostdev)])
return [(d["vms"], d["by_model"]) for d in (tablet_2, tablet_3)]
# an address with other IDs: another device now at that address
other = USB_1_3.replace("0x0627", "0x046d")
address_only = "<hostdev mode='subsystem' type='usb'><source><address bus='1' device='2'/></source></hostdev>"
self.assertEqual(named(other), [([], []), ([], [])])
self.assertEqual(named(address_only), [(["v"], []), ([], [])])
self.assertEqual(named(USB_MODEL), [([], ["v"]), ([], ["v"])])
def test_a_pci_device_named_as_a_nic_or_only_for_the_next_start(self):
nic = ("<interface type='hostdev'><source><address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x0'/>"
"</source></interface>")
pci_devices, _ = devices(domains=[vm("nic-1", nic), vm("next-1", "", saved=PCI_0A)])
self.assertEqual({d["address"]: d["vms"] for d in pci_devices if d["vms"]},
{"0000:00:06.0": ["nic-1"], "0000:00:0a.0": ["next-1"]})
def test_sr_iov_functions_and_unnamed_hardware(self):
vf = pci("0x0b", "", "0x020000", group=5)[1].replace(
"<product id='0x1000'></product>", "<product id='0x10ed'/>").replace(
"</capability></device>", "<capability type='phys_function'><address domain='0x0000' bus='0x00' slot='0x0a'"
" function='0x0'/></capability></capability></device>")
pf = NODES[8][1].replace("</capability></device>", "<capability type='virt_functions' maxCount='7'><address domain='0x0000'"
" bus='0x00' slot='0x0b' function='0x0'/></capability></capability></device>")
pci_devices, _ = devices(nodes=[("pci_0000_00_0a_0", pf), ("pci_0000_00_0b_0", vf)])
self.assertEqual([(d["physfn"], d["vfs"], d["product"]) for d in pci_devices],
[("", 1, "Ethernet controller"), ("0000:00:0a.0", 0, "0x10ed")])
def test_a_device_gone_meanwhile_is_left_out_other_errors_are_raised(self):
for code, raised in ((libvirt.VIR_ERR_NO_NODE_DEVICE, False), (libvirt.VIR_ERR_OPERATION_FAILED, True)):
err = libvirt.libvirtError("gone")
err.get_error_code = lambda code=code: code
gone = node("pci_0000_00_0b_0", "")
gone.XMLDesc.side_effect = err
conn = wvmConnect.__new__(wvmConnect)
conn.wvm = MagicMock(**{"listAllDevices.return_value": [gone, node(*NODES[0])], "listAllDomains.return_value": []})
with self.subTest(code=code):
if raised:
self.assertRaises(libvirt.libvirtError, node_devices, conn)
else:
self.assertEqual([d["address"] for d in node_devices(conn)[0]], ["0000:00:06.0"])
def test_a_pci_address_from_libvirts_hex_or_numbers(self):
self.assertEqual(pci_address("0x0000", "0x00", "0x1f", "0x3"), pci_address(0, 0, 31, 3))
self.assertEqual(pci_address(0, 0, 31, 3), "0000:00:1f.3")

View file

@ -0,0 +1,182 @@
"""What the panel reads of the host's network filters and what it refuses
to change: which VMs use a filter, as they run and as saved; a filter in use
or referenced is not deleted; a create, clone or edit replaces no other
filter; references and rules as libvirt keeps them. libvirt's test driver has
no filters, so the connection is a mock."""
from unittest.mock import MagicMock
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.nwfilters import wvmNWFilter, wvmNWFilters
from vrtManager.util import OperationError
UUID = "6d1f7a10-0000-4000-8000-00000000c0de"
WEB = (f"<filter name='web' chain='root'><uuid>{UUID}</uuid>"
"<filterref filter='clean-traffic'><parameter name='IP' value='192.0.2.10'/>"
"<parameter name='IP' value='192.0.2.11'/></filterref>"
"<rule action='accept' direction='inout' priority='500'><arp match='no' arpsrcipaddr='192.0.2.1'/>"
"<arp opcode='request'/></rule>"
"<rule action='drop' direction='in'/></filter>")
def nwfilter(name, xml, uuid=UUID):
return MagicMock(**{"name.return_value": name, "UUIDString.return_value": uuid, "XMLDesc.return_value": xml})
def vm(name, xml, saved=None, active=False, persistent=True):
def describe(flags=0):
return (saved or xml) if flags else xml
return MagicMock(**{"name.return_value": name, "XMLDesc.side_effect": describe,
"isActive.return_value": active, "isPersistent.return_value": persistent})
def using(*names):
refs = "".join(f"<interface type='network'><filterref filter='{name}'/></interface>" for name in names)
return f"<domain><devices>{refs}</devices></domain>"
def error(code):
err = libvirt.libvirtError("refused")
err.get_error_code = lambda: code
return err
def filters(*listed, domains=()):
conn = wvmNWFilters.__new__(wvmNWFilters)
conn.wvm = MagicMock(**{"listAllNWFilters.return_value": list(listed), "listAllDomains.return_value": list(domains),
"listNWFilters.return_value": [f.name() for f in listed]})
return conn
def one(xml=WEB, names=("web", "clean-traffic", "no-spoofing")):
conn = wvmNWFilter.__new__(wvmNWFilter)
conn.nwfilter = nwfilter("web", xml)
conn.wvm = MagicMock(**{"listNWFilters.return_value": list(names)})
return conn
def defined(conn):
return etree.fromstring(conn.wvm.nwfilterDefineXML.call_args.args[0])
class UsersTests(SimpleTestCase):
def test_a_filter_is_used_as_the_vm_runs_and_as_saved(self):
conn = filters(domains=[
vm("stopped", using("a")),
vm("running", using("b", "b"), saved=using("c"), active=True), # c only from its next boot
vm("transient", using("d"), saved=using("x"), active=True, persistent=False),
])
self.assertEqual(conn.get_users(), {"a": ["stopped"], "b": ["running"], "c": ["running"], "d": ["transient"]})
stopped, running, transient = conn.wvm.listAllDomains.return_value
self.assertEqual((stopped.XMLDesc.call_count, running.XMLDesc.call_count, transient.XMLDesc.call_count), (2, 2, 1))
def test_a_vm_gone_meanwhile_uses_nothing_other_errors_are_raised(self):
gone = vm("gone", "")
gone.XMLDesc.side_effect = error(libvirt.VIR_ERR_NO_DOMAIN)
self.assertEqual(filters(domains=[gone, vm("web-1", using("a"))]).get_users(), {"a": ["web-1"]})
gone.XMLDesc.side_effect = error(libvirt.VIR_ERR_OPERATION_FAILED)
with self.assertRaises(libvirt.libvirtError):
filters(domains=[gone]).get_users()
class DeleteTests(SimpleTestCase):
def test_a_filter_a_vm_or_another_filter_uses_is_kept(self):
target = nwfilter("clean-traffic", "<filter name='clean-traffic'/>", uuid="other")
for conn, why in ((filters(target, domains=[vm("web-1", using("clean-traffic"))]), "used by VM web-1"),
(filters(target, nwfilter("web", WEB)), "referenced by filter web")):
with self.subTest(why=why), self.assertRaisesRegex(OperationError, why):
conn.delete_nwfilter("clean-traffic")
conn.wvm.nwfilterLookupByName.return_value.undefine.assert_not_called()
def test_an_unused_filter_is_undefined_libvirt_may_still_refuse(self):
conn = filters(nwfilter("web", WEB))
conn.delete_nwfilter("web")
conn.wvm.nwfilterLookupByName.return_value.undefine.assert_called_once_with()
conn.wvm.nwfilterLookupByName.return_value.undefine.side_effect = error(libvirt.VIR_ERR_OPERATION_INVALID)
with self.assertRaises(libvirt.libvirtError): # a VM started meanwhile: libvirt refuses itself
conn.delete_nwfilter("web")
class IdentityTests(SimpleTestCase):
def test_a_create_replaces_no_filter(self):
conn = filters(nwfilter("web", WEB))
for xml, why in ((WEB, "name already exists"),
(WEB.replace("name='web'", "name='other'"), "UUID already exists"),
("<rule/>", "one <filter>"),
("<filter name='a b'/>", "letters, digits"),
('<!DOCTYPE filter [<!ENTITY n "web">]><filter name="&n;"/>', "DTD"),
("", "not valid")):
with self.subTest(why=why), self.assertRaisesRegex(OperationError, why):
conn.create_nwfilter(xml)
conn.wvm.nwfilterDefineXML.assert_not_called()
conn.create_nwfilter("<filter name='other' chain='root'/>")
conn.wvm.nwfilterDefineXML.assert_called_once_with("<filter name='other' chain='root'/>")
def test_a_clone_keeps_rules_and_parameters_with_a_uuid_of_its_own(self):
conn = filters(nwfilter("web", WEB))
conn.wvm.nwfilterLookupByName.return_value.XMLDesc.return_value = WEB
for name, why in (("web", "already exists"), ("web copy", "letters, digits"), ("", "letters, digits")):
with self.subTest(name=name), self.assertRaisesRegex(OperationError, why):
conn.clone_nwfilter("web", name)
conn.clone_nwfilter("web", "web-copy")
clone = defined(conn)
self.assertEqual((clone.get("name"), clone.find("uuid")), ("web-copy", None))
self.assertEqual(clone.xpath("filterref/parameter/@value"), ["192.0.2.10", "192.0.2.11"])
self.assertEqual(len(clone.findall("rule")), 2)
def test_an_edit_is_of_the_same_filter(self):
conn = one()
for xml, why in ((WEB.replace("name='web'", "name='other'"), "must define filter web"),
(WEB.replace(f"<uuid>{UUID}</uuid>", ""), "keep the filter's UUID"),
(WEB.replace(UUID, "11111111-2222-3333-4444-555555555555"), "keep the filter's UUID")):
with self.subTest(why=why), self.assertRaisesRegex(OperationError, why):
conn.edit(xml)
conn.wvm.nwfilterDefineXML.assert_not_called()
conn.edit(WEB.replace("priority='500'", "priority='400'"))
conn.wvm.nwfilterDefineXML.assert_called_once()
class ChangeTests(SimpleTestCase):
def test_a_reference_is_to_another_filter_of_the_host_not_yet_referenced(self):
for name in ("web", "clean-traffic", "missing"):
with self.subTest(name=name), self.assertRaisesRegex(OperationError, "cannot be referenced"):
one().add_ref(name)
conn = one()
conn.add_ref("no-spoofing")
self.assertEqual(defined(conn).xpath("filterref/@filter"), ["clean-traffic", "no-spoofing"])
self.assertEqual(one().get_ref_choices(), ["no-spoofing"])
def test_a_rule_is_one_rule_element(self):
with self.assertRaisesRegex(OperationError, "one <rule>"):
one().add_rule("<filter name='web'/>")
def test_a_change_keeps_the_rest_of_the_filter(self):
conn = one()
conn.add_rule("<rule action='accept' direction='in'><udp dstportstart='53'/></rule>")
tree = defined(conn)
self.assertEqual((tree.findtext("uuid"), len(tree.findall("rule"))), (UUID, 3))
self.assertEqual(tree.xpath("filterref/parameter/@value"), ["192.0.2.10", "192.0.2.11"])
self.assertEqual(len(etree.fromstring(conn.get_xml()).findall("rule")), 2, "the page's own copy is not changed")
class ReadTests(SimpleTestCase):
def test_rules_and_references_as_libvirt_keeps_them(self):
conn = one()
arp, drop = conn.get_rules()
# two elements of one protocol, a match and a negated one; a rule of no protocol
self.assertEqual([p["attributes"] for p in arp["protocols"]], [[("match", "no"), ("arpsrcipaddr", "192.0.2.1")],
[("opcode", "request")]])
self.assertEqual((drop["protocols"], drop["priority"]), ([], None))
[ref] = conn.get_refs()
self.assertEqual(ref["parameters"], [("IP", "192.0.2.10"), ("IP", "192.0.2.11")])
self.assertEqual(conn.nwfilter.XMLDesc.call_count, 1)
def test_the_list_reads_each_filter_once(self):
conn = filters(nwfilter("web", WEB), nwfilter("clean-traffic", "<filter name='clean-traffic' chain='root'/>", "x"))
info = conn.get_nwfilters_info()
self.assertEqual([(f["name"], f["rules"], f["refs"]) for f in info],
[("clean-traffic", 0, []), ("web", 2, ["clean-traffic"])])
for listed in conn.wvm.listAllNWFilters.return_value:
self.assertEqual(listed.XMLDesc.call_count, 1)

View file

@ -0,0 +1,104 @@
"""Which PCI devices of a host a VM can be given: an endpoint in an IOMMU
group, without the host's own network interfaces or disks on it, that no
other VM names; the other endpoints of its group are managed devices of the
same definition, or have no driver or vfio-pci, and no other VM has any of
them; bridges stay with pcieport. Each definition of a snapshot is checked
on its own. Node XML as libvirt 11.10 writes it on a q35 host with an
emulated IOMMU."""
from unittest.mock import MagicMock
from django.test import SimpleTestCase
from lxml import etree
from vrtManager import util
from vrtManager.nodedevs import _pci, check_hostdevs, hostdev_problems, pci_problems
from vrtManager.tests.fakes import IOMMU_NODES
from vrtManager.util import OperationError
def inventory(vms=()):
"""The host's PCI devices, as a list; vms: (VM, its PCI addresses)."""
children = {}
for name, node in IOMMU_NODES.items():
children.setdefault(node.findtext("parent"), []).append(name)
users = [(vm, set(addresses), []) for vm, addresses in vms]
return [_pci(name, node, IOMMU_NODES, children, users) for name, node in IOMMU_NODES.items()
if node.find("capability").get("type") == "pci"]
def problems(address, managed=(), vms=()):
devices = {d["address"]: d for d in inventory(vms)}
return " ".join(util.render(m["text"], m["values"], str)
for m in pci_problems("web-1", devices[address], devices, {address, *managed}))
def hostdev(slot, managed="yes"):
return (f"<hostdev mode='subsystem' type='pci' managed='{managed}'><source>"
f"<address domain='0x0000' bus='0x00' slot='0x{slot:02x}' function='0x0'/></source></hostdev>")
class PciRulesTests(SimpleTestCase):
def test_a_lone_endpoint_bound_to_vfio_pci_can_be_given(self):
self.assertEqual(problems("0000:00:07.0"), "")
def test_a_device_the_host_uses_can_not_and_how_to_free_it(self):
reason = problems("0000:00:06.0")
self.assertIn("holds the host's eth1", reason)
self.assertIn("bind 0000:00:06.0 to vfio-pci on the host first", reason)
def test_a_bridge_of_any_kind_can_not(self):
# a PCIe port, and of the bridge class an LPC controller (a host bridge alike)
self.assertEqual(problems("0000:00:02.0"), "PCI device 0000:00:02.0 is a bridge")
self.assertEqual(problems("0000:00:05.0"), "PCI device 0000:00:05.0 is a bridge")
def test_the_other_endpoints_of_its_group_must_be_free_of_the_host(self):
# the LPC controller is of class 0x06 but no bridge: its driver keeps the group
reason = problems("0000:00:04.0")
self.assertIn("with 0000:00:03.0 (SATA controller), which the host (driver ahci) uses", reason)
self.assertIn("with 0000:00:05.0 (LPC controller), which the host (driver lpc_ich) uses", reason)
def test_unbound_and_vfio_pci_members_and_a_pcieport_bridge_keep_nothing(self):
self.assertEqual(problems("0000:00:08.0"), "")
def test_a_member_given_as_a_managed_device_too_is_taken_with_it(self):
self.assertIn("with 0000:00:0e.0 (GPU audio), which the host (driver snd_hda_intel) uses", problems("0000:00:0d.0"))
self.assertEqual(problems("0000:00:0d.0", managed={"0000:00:0e.0"}), "")
def test_a_member_another_vm_has_or_that_cannot_be_read_keeps_the_group(self):
self.assertIn("with 0000:00:09.0 (NIC port 1), which db-1 uses",
problems("0000:00:08.0", vms=[("db-1", {"0000:00:09.0"})]))
# managed in this definition or not: another VM has it
self.assertIn("which db-1 uses", problems("0000:00:0d.0", managed={"0000:00:0e.0"}, vms=[("db-1", {"0000:00:0e.0"})]))
self.assertIn("with 0000:00:0c.0, which cannot be read", problems("0000:00:0b.0"))
def test_a_device_another_vm_has_can_not(self):
self.assertIn("is given to db-1", problems("0000:00:07.0", vms=[("db-1", {"0000:00:07.0"})]))
self.assertEqual(problems("0000:00:07.0", vms=[("web-1", {"0000:00:07.0"})]), "") # its own
def test_a_device_the_host_does_not_have_can_not(self):
reasons = [util.render(m["text"], m["values"], str)
for m in hostdev_problems("web-1", inventory(), [], {"0000:00:1f.0"}, [])]
self.assertEqual(reasons, ["PCI device 0000:00:1f.0 is not on the host"])
class DefinitionsTests(SimpleTestCase):
"""A revert restores two definitions; the VM starts from one of them."""
def check(self, *devices_per_definition):
conn = MagicMock()
conn.wvm.listAllDevices.return_value = [MagicMock(**{"name.return_value": name,
"XMLDesc.return_value": etree.tostring(node)})
for name, node in IOMMU_NODES.items()]
conn.domain_trees.return_value = []
trees = [etree.fromstring(f"<domain><devices>{''.join(devices)}</devices></domain>") for devices in devices_per_definition]
check_hostdevs(conn, "web-1", trees)
def test_each_definition_takes_its_group_on_its_own(self):
self.check([hostdev(0x0d), hostdev(0x0e)])
# the running definition has the GPU, only the saved one its audio function
with self.assertRaisesRegex(OperationError, "GPU audio"):
self.check([hostdev(0x0d)], [hostdev(0x0e)])
def test_a_member_libvirt_does_not_manage_keeps_its_host_driver(self):
with self.assertRaisesRegex(OperationError, "which the host \\(driver snd_hda_intel\\) uses"):
self.check([hostdev(0x0d), hostdev(0x0e, managed="no")])

View file

@ -0,0 +1,42 @@
"""The host, pool, user and format values of a new Ceph or NFS pool go into
its XML as text, whatever characters they hold; a new pool that does not
start is not left defined."""
from unittest.mock import patch
from django.test import SimpleTestCase
from libvirt import libvirtError
from lxml import etree
from vrtManager.storage import wvmStorages
ODD = "a'b\"c<d>&e"
class PoolXmlTests(SimpleTestCase):
def defined_xml(self, create, *args):
with patch("vrtManager.connection.connection_manager.get_connection") as get:
getattr(wvmStorages("host", "login", "pw", 1), create)(*args)
return etree.fromstring(get.return_value.storagePoolDefineXML.call_args.args[0])
def test_ceph_values_are_text(self):
pool = self.defined_xml("create_storage_ceph", "rbd", "pool1", ODD + "pool", ODD + "host", ODD + "user", ODD + "secret")
self.assertEqual(pool.findtext("source/name"), ODD + "pool")
self.assertEqual(pool.find("source/host").get("name"), ODD + "host")
self.assertEqual(pool.find("source/auth").get("username"), ODD + "user")
self.assertEqual(pool.find("source/auth/secret").get("uuid"), ODD + "secret")
def test_nfs_values_are_text(self):
pool = self.defined_xml("create_storage_netfs", "netfs", "pool1", ODD + "host", "/srv/nfs", ODD + "fmt", "/mnt/pool1")
self.assertEqual(pool.find("source/host").get("name"), ODD + "host")
self.assertEqual(pool.find("source/format").get("type"), ODD + "fmt")
self.assertEqual(pool.find("source/dir").get("path"), "/srv/nfs")
def test_a_pool_that_does_not_start_is_undefined(self):
with patch("vrtManager.connection.connection_manager.get_connection") as get:
pool = get.return_value.storagePoolDefineXML.return_value
pool.create.side_effect = libvirtError("mount failed")
with self.assertRaises(libvirtError):
wvmStorages("host", "login", "pw", 1).create_storage_netfs(
"netfs", "pool1", "nfs.example.com", "/srv/nfs", "auto", "/mnt/pool1")
pool.undefine.assert_called_once_with()
pool.setAutostart.assert_not_called()

View file

@ -0,0 +1,188 @@
"""wvmSecrets: a secret's usage goes into its XML as text; its value is a
Ceph key in base64 or a password as typed, and a value refused leaves no
secret behind; what uses a secret, and deleting one."""
from unittest.mock import MagicMock, patch
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.tests.fakes import CEPH
from vrtManager.util import OperationError
from vrtManager.virtsecrets import wvmSecrets
def connection_and_libvirt():
with patch("vrtManager.connection.connection_manager.get_connection") as get:
conn = wvmSecrets("host", "login", "pw", 1)
return conn, get.return_value
class CreateSecretTests(SimpleTestCase):
def test_the_usage_is_text_of_its_element(self):
for usage_type, element, usage in (
("volume", "volume", "/var/lib/libvirt/images/vm-1.qcow2"),
("iscsi", "target", "iqn.2026-10.test.wvc:lun"),
("ceph", "name", "client.libvirt secret"),
("tls", "name", "migration"),
("vtpm", "name", "VM web-1's vTPM"),
("volume", "volume", "/a'b<c>&d\""),
):
with self.subTest(usage_type=usage_type, usage=usage):
conn, wvm = connection_and_libvirt()
conn.create_secret(usage_type, usage, private=True)
secret = etree.fromstring(wvm.secretDefineXML.call_args.args[0])
self.assertEqual((secret.get("ephemeral"), secret.get("private")), ("no", "yes"))
self.assertEqual(secret.find("usage").get("type"), usage_type)
self.assertEqual(secret.findtext(f"usage/{element}"), usage)
wvm.secretDefineXML.return_value.setValue.assert_not_called()
def test_a_new_secret_takes_its_value_at_once(self):
for usage_type, value, stored in (("ceph", "cHcA/w==", b"pw\x00\xff"), ("iscsi", " pass word ", b" pass word ")):
with self.subTest(usage_type=usage_type):
conn, wvm = connection_and_libvirt()
conn.create_secret(usage_type, "lun", value)
wvm.secretDefineXML.return_value.setValue.assert_called_once_with(stored)
def test_a_value_refused_leaves_no_secret(self):
conn, wvm = connection_and_libvirt()
with self.assertRaisesRegex(OperationError, "base64"):
conn.create_secret("ceph", "client.libvirt", "not base64!")
wvm.secretDefineXML.assert_not_called()
wvm.secretDefineXML.return_value.setValue.side_effect = libvirt.libvirtError("refused")
with self.assertRaises(libvirt.libvirtError):
conn.create_secret("iscsi", "lun", "pw")
wvm.secretDefineXML.return_value.undefine.assert_called_once_with()
class SecretValueTests(SimpleTestCase):
def set_value(self, usage_type, value):
conn, wvm = connection_and_libvirt()
secret = wvm.secretLookupByUUIDString.return_value
secret.XMLDesc.return_value = f"<secret><usage type='{usage_type}'><name>x</name></usage></secret>"
conn.set_secret_value("uuid", value)
return secret.setValue
def test_a_ceph_key_is_stored_as_its_bytes_a_password_as_typed(self):
self.set_value("ceph", "cHcA/w==").assert_called_once_with(b"pw\x00\xff")
# a password that happens to be base64 is not decoded
self.set_value("iscsi", "cHcA/w==").assert_called_once_with(b"cHcA/w==")
self.set_value("vtpm", " pässword ").assert_called_once_with(" pässword ".encode())
def test_a_ceph_key_that_is_not_base64_is_refused_without_the_value(self):
for value in ("b'cHc='", "not base64!", "kéy"):
with self.subTest(value=value), self.assertRaises(OperationError) as refused:
self.set_value("ceph", value)
self.assertNotIn(value, str(refused.exception))
ISCSI = "8f2c0000-0000-4000-8000-000000000002"
VTPM = "8f2c0000-0000-4000-8000-000000000003"
def secret(uuid, usage_type, usage):
return MagicMock(**{"UUIDString.return_value": uuid,
"XMLDesc.return_value": f"<secret><usage type='{usage_type}'><name>{usage}</name></usage></secret>"})
def owner(name, xml, saved=None, active=False, persistent=True):
"""A domain or a pool: xml as it runs, saved as defined for its next start."""
return MagicMock(**{"name.return_value": name, "XMLDesc.side_effect": lambda flags=0: (saved or xml) if flags else xml,
"isActive.return_value": active, "isPersistent.return_value": persistent})
def vm(name, devices, **kwargs):
return owner(name, f"<domain><devices>{devices}</devices></domain>", **kwargs)
def pool(name, auth, **kwargs):
return owner(name, f"<pool type='rbd'><source>{auth}</source></pool>", **kwargs)
def gone(code):
err = libvirt.libvirtError("gone")
err.get_error_code = lambda: code
return MagicMock(**{"name.return_value": "gone", "XMLDesc.side_effect": err})
# the same usage, of two types: a reference by usage names the one of its type
SECRETS = [secret(CEPH, "ceph", "client.libvirt"), secret(ISCSI, "iscsi", "client.libvirt"), secret(VTPM, "vtpm", "tpm")]
def connection(domains=(), pools=(), secrets=SECRETS):
with patch("vrtManager.connection.connection_manager.get_connection") as get:
conn = wvmSecrets("host", "login", "pw", 1)
get.return_value.configure_mock(**{"listAllDomains.return_value": list(domains),
"listAllStoragePools.return_value": list(pools),
"listAllSecrets.return_value": list(secrets)})
return conn
def users(**kwargs):
conn = connection(**kwargs)
return conn.get_users(conn.get_secrets_info())
class UsersTests(SimpleTestCase):
def test_vms_name_secrets_in_disks_host_devices_and_vtpm(self):
found = users(domains=[
vm("rbd-1", f"<disk type='network'><auth username='libvirt'><secret type='ceph' uuid='{CEPH}'/></auth></disk>"),
vm("lun-1", "<hostdev mode='subsystem' type='scsi'><source protocol='iscsi'><auth username='u'>"
"<secret type='iscsi' usage='client.libvirt'/></auth></source></hostdev>"),
vm("tpm-1", f"<tpm model='tpm-crb'><backend type='emulator'><encryption secret='{VTPM}'/></backend></tpm>"),
])
self.assertEqual(found, {CEPH: {"vms": ["rbd-1"], "pools": []}, ISCSI: {"vms": ["lun-1"], "pools": []},
VTPM: {"vms": ["tpm-1"], "pools": []}})
def test_a_pool_names_its_secret_by_the_auth_type_and_as_saved(self):
found = users(pools=[
pool("ceph-rbd", "<auth type='ceph' username='libvirt'><secret usage='client.libvirt'/></auth>"),
# chap is iSCSI; named only for its next start
pool("lun", "", saved="<pool><source><auth type='chap' username='u'><secret usage='client.libvirt'/>"
"</auth></source></pool>", active=False),
])
self.assertEqual(found, {CEPH: {"vms": [], "pools": ["ceph-rbd"]}, ISCSI: {"vms": [], "pools": ["lun"]}})
def test_a_vm_uses_a_secret_as_it_runs_and_as_saved(self):
disk = "<disk><auth username='libvirt'><secret type='ceph' usage='client.libvirt'/></auth></disk>"
# saved only: read whether it runs or not, as it may stop between the reads
for kwargs in ({"active": True, "saved": f"<domain><devices>{disk}</devices></domain>"},
{"active": False, "saved": f"<domain><devices>{disk}</devices></domain>"},
{"active": False}, {"active": True, "persistent": False}):
devices = "" if "saved" in kwargs else disk
with self.subTest(**kwargs):
self.assertEqual(users(domains=[vm("db-1", devices, **kwargs)]), {CEPH: {"vms": ["db-1"], "pools": []}})
def test_what_is_gone_meanwhile_is_left_out_other_errors_are_raised(self):
found = users(domains=[gone(libvirt.VIR_ERR_NO_DOMAIN)], pools=[gone(libvirt.VIR_ERR_NO_STORAGE_POOL)],
secrets=[gone(libvirt.VIR_ERR_NO_SECRET)])
self.assertEqual(found, {})
with self.assertRaises(libvirt.libvirtError):
users(domains=[gone(libvirt.VIR_ERR_OPERATION_FAILED)])
class NoValueReadTests(SimpleTestCase):
def test_the_list_and_its_users_read_no_value(self):
for listed in SECRETS:
listed.value.side_effect = AssertionError("a secret's value was read")
conn = connection(domains=[vm("db-1", f"<disk><auth><secret type='ceph' uuid='{CEPH}'/></auth></disk>")])
conn.get_users(conn.get_secrets_info())
conn.wvm.secretLookupByUUIDString.assert_not_called()
class DeleteTests(SimpleTestCase):
def test_a_secret_a_vm_or_a_pool_names_is_not_deleted(self):
conn = connection(domains=[vm("db-2", f"<disk><auth><secret type='ceph' uuid='{CEPH}'/></auth></disk>")],
pools=[pool("ceph-rbd", "<auth type='ceph'><secret usage='client.libvirt'/></auth>")])
with self.assertRaisesRegex(OperationError, f"Secret {CEPH} is used by VMs db-2; pools ceph-rbd"):
conn.delete_secret(CEPH)
conn.wvm.secretLookupByUUIDString.return_value.undefine.assert_not_called()
def test_an_unused_secret_is_deleted(self):
conn = connection(domains=[vm("web-1", f"<disk><auth><secret type='ceph' uuid='{CEPH}'/></auth></disk>")])
conn.delete_secret(ISCSI)
conn.wvm.secretLookupByUUIDString.assert_called_once_with(ISCSI)
conn.wvm.secretLookupByUUIDString.return_value.undefine.assert_called_once_with()