mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 11:46:49 +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:
parent
4e0952aa50
commit
bdc8a6755c
73 changed files with 2231 additions and 2132 deletions
15
vrtManager/tests/driver.py
Normal file
15
vrtManager/tests/driver.py
Normal 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
318
vrtManager/tests/fakes.py
Normal 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"
|
||||
137
vrtManager/tests/livetest.py
Normal file
137
vrtManager/tests/livetest.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
183
vrtManager/tests/test_create.py
Normal file
183
vrtManager/tests/test_create.py
Normal 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"), [])
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
261
vrtManager/tests/test_host_devices.py
Normal file
261
vrtManager/tests/test_host_devices.py
Normal 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, [])
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
92
vrtManager/tests/test_interface_delete.py
Normal file
92
vrtManager/tests/test_interface_delete.py
Normal 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))
|
||||
119
vrtManager/tests/test_interface_reads.py
Normal file
119
vrtManager/tests/test_interface_reads.py
Normal 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"},
|
||||
])
|
||||
131
vrtManager/tests/test_interface_xml.py
Normal file
131
vrtManager/tests/test_interface_xml.py
Normal 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()
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
86
vrtManager/tests/test_live_disk_resize.py
Normal file
86
vrtManager/tests/test_live_disk_resize.py
Normal 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)
|
||||
96
vrtManager/tests/test_live_iso.py
Normal file
96
vrtManager/tests/test_live_iso.py
Normal 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)
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
195
vrtManager/tests/test_live_snapshots.py
Normal file
195
vrtManager/tests/test_live_snapshots.py
Normal 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")
|
||||
112
vrtManager/tests/test_live_xml.py
Normal file
112
vrtManager/tests/test_live_xml.py
Normal 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)
|
||||
319
vrtManager/tests/test_live_xml_effects.py
Normal file
319
vrtManager/tests/test_live_xml_effects.py
Normal 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)
|
||||
|
|
@ -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>"
|
||||
|
||||
|
||||
|
|
|
|||
191
vrtManager/tests/test_network.py
Normal file
191
vrtManager/tests/test_network.py
Normal 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()
|
||||
|
|
@ -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"))
|
||||
|
|
|
|||
110
vrtManager/tests/test_nodedevs.py
Normal file
110
vrtManager/tests/test_nodedevs.py
Normal 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")
|
||||
182
vrtManager/tests/test_nwfilters.py
Normal file
182
vrtManager/tests/test_nwfilters.py
Normal 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)
|
||||
104
vrtManager/tests/test_pci_rules.py
Normal file
104
vrtManager/tests/test_pci_rules.py
Normal 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")])
|
||||
42
vrtManager/tests/test_pool_xml.py
Normal file
42
vrtManager/tests/test_pool_xml.py
Normal 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()
|
||||
188
vrtManager/tests/test_secrets.py
Normal file
188
vrtManager/tests/test_secrets.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue