mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-10 12:16: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
|
|
@ -28,3 +28,9 @@ def render_create_page(testcase, data=None):
|
|||
else:
|
||||
response = testcase.client.post(url, {"create": "", **data})
|
||||
return compute, response
|
||||
|
||||
|
||||
# an API create request (CreateInstanceSerializer) that the job takes
|
||||
BODY = {"name": "api-vm", "vcpu": 1, "vcpu_mode": "host-model", "memory": 512, "networks": "default", "mac": "",
|
||||
"nwfilter": "", "storage": "default", "hdd_size": 1, "cache_mode": "none", "meta_prealloc": False,
|
||||
"virtio": True, "qemu_ga": True, "video": "vga", "listener_addr": "0.0.0.0", "submission_key": "key-api00001"}
|
||||
|
|
|
|||
77
instances/tests/js/instance_tabs.test.js
Normal file
77
instances/tests/js/instance_tabs.test.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// node instance_tabs.test.js path/to/static/js/instance_tabs.js
|
||||
const assert = require("assert");
|
||||
const {restoreInstanceTab} = require(process.argv[2]);
|
||||
|
||||
// A minimal DOM: elements with an id, classes, a parent, children, and the
|
||||
// few selectors restoreInstanceTab uses.
|
||||
function el(id, classes, children = [], attrs = {}) {
|
||||
const node = {id, classList: new Set(classes), children, attrs, parentElement: null};
|
||||
node.classList.contains = (c) => node.classList.has(c);
|
||||
node.getAttribute = (name) => attrs[name];
|
||||
for (const child of children) child.parentElement = node;
|
||||
node.querySelector = (selector) => find(node, selector);
|
||||
return node;
|
||||
}
|
||||
|
||||
function* walk(node) {
|
||||
for (const child of node.children) {
|
||||
yield child;
|
||||
yield* walk(child);
|
||||
}
|
||||
}
|
||||
|
||||
function matches(node, selector) {
|
||||
if (selector === "[role=tablist]") return node.attrs.role === "tablist";
|
||||
if (selector === ".nav-link.active") return node.classList.has("nav-link") && node.classList.has("active");
|
||||
if (selector === ".nav-link:not(#osinfo-tab)") return node.classList.has("nav-link") && node.id !== "osinfo-tab";
|
||||
if (selector === "#osinfo-tab") return node.id === "osinfo-tab";
|
||||
throw new Error("unsupported selector " + selector);
|
||||
}
|
||||
|
||||
function find(root, selector) {
|
||||
for (const node of walk(root)) if (matches(node, selector)) return node;
|
||||
return null;
|
||||
}
|
||||
|
||||
function link(target, extra = []) {
|
||||
return el(target === "#osinfo" ? "osinfo-tab" : "", ["nav-link", ...extra], [], {"data-bs-target": target});
|
||||
}
|
||||
|
||||
// The VM page: top-level panes, Settings with its own tabs and panes.
|
||||
function page(settingsLinks) {
|
||||
const settings = el("settings", ["tab-pane"], [
|
||||
el("", ["nav", "nav-underline"], settingsLinks, {role: "tablist"}),
|
||||
el("", ["tab-content"], [el("osinfo", ["tab-pane"]), el("options", ["tab-pane"]), el("vncsettings", ["tab-pane"])]),
|
||||
]);
|
||||
const root = el("", [], [el("", ["tab-content"], [el("power", ["tab-pane", "active"]), el("access", ["tab-pane"]), settings])]);
|
||||
root.getElementById = (id) => {
|
||||
for (const node of walk(root)) if (node.id === id) return node;
|
||||
return null;
|
||||
};
|
||||
return root;
|
||||
}
|
||||
|
||||
function run(doc, hash) {
|
||||
const shown = [];
|
||||
let loaded = 0;
|
||||
restoreInstanceTab(doc, hash, (target) => shown.push(target), () => loaded++);
|
||||
return {shown, loaded};
|
||||
}
|
||||
|
||||
const settled = () => page([link("#options", ["active"])]);
|
||||
|
||||
// a top-level pane opens directly
|
||||
assert.deepStrictEqual(run(settled(), "#access").shown, ["#access"]);
|
||||
// a pane inside Settings opens Settings first
|
||||
assert.deepStrictEqual(run(settled(), "#options").shown, ["#settings", "#options"]);
|
||||
// no hash, an unknown one, or one with odd characters opens nothing
|
||||
for (const hash of ["", "#", "#nosuchpane", '#a"]b']) {
|
||||
assert.deepStrictEqual(run(settled(), hash).shown, [], hash);
|
||||
}
|
||||
// Settings without an active section opens its first one, not OS info
|
||||
assert.deepStrictEqual(run(page([link("#osinfo"), link("#vncsettings")]), "").shown, ["#vncsettings"]);
|
||||
// OS info as the only section is selected and loaded
|
||||
assert.deepStrictEqual(run(page([link("#osinfo")]), ""), {shown: ["#osinfo"], loaded: 1});
|
||||
// a redirect to OS info loads it
|
||||
assert.deepStrictEqual(run(settled(), "#osinfo"), {shown: ["#settings", "#osinfo"], loaded: 1});
|
||||
console.log("ok");
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
"""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
|
||||
|
||||
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()
|
||||
|
|
@ -16,7 +16,7 @@ from operations.jobs import Context
|
|||
from operations.models import Operation, acquire
|
||||
from vrtManager.util import OperationError
|
||||
|
||||
from instances.tests.test_create_job import BODY
|
||||
from instances.tests.create_page import BODY
|
||||
|
||||
SEED = "/srv/default/cidata-x.iso"
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ from django.contrib.messages import get_messages
|
|||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from instances.models import Instance
|
||||
from vrtManager.instance import PERSISTENT_XML, wvmInstance
|
||||
|
||||
|
||||
class ConsoleListenerTests(TestCase):
|
||||
|
|
@ -83,29 +82,3 @@ class ConsoleListenerTests(TestCase):
|
|||
response = self.client.get(reverse("instances:instance", args=[self.vm.id]))
|
||||
form = response.context["console_form"]
|
||||
self.assertTrue(form.fields["listen_on"].disabled)
|
||||
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -2,43 +2,12 @@
|
|||
model, checked by libvirt before the start. That the host offers it is
|
||||
checked when the job is submitted (test_create_job)."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from appsettings.models import AppSettings
|
||||
from appsettings.settings import get_settings
|
||||
from django.test import SimpleTestCase, TestCase
|
||||
from django.test import TestCase
|
||||
from instances.tests.create_page import render_create_page
|
||||
from lxml import etree, html
|
||||
from vrtManager.create import wvmCreate
|
||||
|
||||
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=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"))
|
||||
from lxml import html
|
||||
|
||||
|
||||
class CpuModeSelectTests(TestCase):
|
||||
|
|
|
|||
|
|
@ -13,13 +13,10 @@ from appsettings.settings import get_settings
|
|||
from computes.models import Compute
|
||||
from instances.jobs import CreateInstance
|
||||
from instances.models import Instance
|
||||
from instances.tests.create_page import BODY
|
||||
from operations.jobs import Context
|
||||
from operations.models import Operation, acquire
|
||||
|
||||
BODY = {"name": "api-vm", "vcpu": 1, "vcpu_mode": "host-model", "memory": 512, "networks": "default", "mac": "",
|
||||
"nwfilter": "", "storage": "default", "hdd_size": 1, "cache_mode": "none", "meta_prealloc": False,
|
||||
"virtio": True, "qemu_ga": True, "video": "vga", "listener_addr": "0.0.0.0", "submission_key": "key-api00001"}
|
||||
|
||||
|
||||
class CreateJobTestCase(TransactionTestCase):
|
||||
def setUp(self):
|
||||
|
|
|
|||
|
|
@ -1,126 +0,0 @@
|
|||
"""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
|
||||
|
||||
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"))
|
||||
|
|
@ -3,274 +3,22 @@ named once whether the running VM or its next start has it; the checks that
|
|||
keep a device to one VM and away from what the host uses itself, also for a
|
||||
snapshot revert; clones get none, and a VM with any is not migrated."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import libvirt
|
||||
from computes.models import Compute
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.messages import get_messages
|
||||
from django.test import SimpleTestCase, TestCase
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from instances.models import Instance
|
||||
from lxml import etree, html
|
||||
from nodedevs.tests.test_nodedevs import NODES, node
|
||||
from vrtManager.instance import wvmInstance
|
||||
|
||||
from vrtManager.util import OperationError
|
||||
|
||||
from vrtManager.tests.test_migration import FakeSource, destination, with_disks
|
||||
|
||||
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>")
|
||||
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 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, [])
|
||||
from vrtManager.tests.fakes import by_model, domain
|
||||
from vrtManager.tests.fakes import IOMMU_NODES, NODES, node
|
||||
|
||||
|
||||
class HostUsbViewTests(TestCase):
|
||||
|
|
@ -352,10 +100,9 @@ class HostUsbViewTests(TestCase):
|
|||
"VM kiosk-1: request the removal of PCI device 0000:00:0a.0"])
|
||||
|
||||
def test_a_running_vm_is_given_a_device_alone_a_shut_off_one_with_its_group(self):
|
||||
from nodedevs.tests.test_pci_rules import NODES as GROUPS
|
||||
|
||||
# the next start has the GPU's audio function managed: it goes along when the VM starts, not at a hot plug
|
||||
self.proxy.wvm.listAllDevices.return_value = [node(name, etree.tostring(n)) for name, n in GROUPS.items()]
|
||||
self.proxy.wvm.listAllDevices.return_value = [node(name, etree.tostring(n)) for name, n in IOMMU_NODES.items()]
|
||||
self.proxy.domain_trees.side_effect = lambda: []
|
||||
self.proxy.get_hostdevs.return_value = ([], [{"address": "0000:00:0e.0", "now": False, "next": True, "managed": True}], [])
|
||||
url = reverse("instances:host_devices", args=[self.instance.id])
|
||||
|
|
|
|||
23
instances/tests/test_instance_tabs_js.py
Normal file
23
instances/tests/test_instance_tabs_js.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
"""The VM page opens the tab a redirect points to (static/js/instance_tabs.js).
|
||||
Its behaviour is checked with Node."""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
BASE = Path(settings.BASE_DIR)
|
||||
|
||||
|
||||
class InstanceTabsTestCase(unittest.TestCase):
|
||||
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
||||
def test_instance_tabs_js(self):
|
||||
result = subprocess.run(
|
||||
["node", str(BASE / "instances/tests/js/instance_tabs.test.js"), str(BASE / "static/js/instance_tabs.js")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
|
@ -4,13 +4,11 @@ import hashlib
|
|||
import os
|
||||
import time
|
||||
import unittest
|
||||
from xml.etree import ElementTree
|
||||
|
||||
import libvirt
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import SimpleTestCase, TransactionTestCase
|
||||
from django.test import TransactionTestCase
|
||||
from vrtManager.connection import connection_manager
|
||||
from vrtManager.instance import wvmInstance
|
||||
|
||||
from appsettings.settings import get_settings
|
||||
from computes.models import Compute
|
||||
|
|
@ -20,115 +18,12 @@ from operations.models import Operation
|
|||
from operations.tests import fakes
|
||||
from operations.worker import Worker
|
||||
|
||||
from . import livetest
|
||||
from .test_live_jobs import wait_for
|
||||
from vrtManager.tests import livetest
|
||||
from vrtManager.tests.livetest import wait_for
|
||||
|
||||
P = livetest.PREFIX
|
||||
|
||||
|
||||
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())
|
||||
|
||||
|
||||
MIB = 1024 * 1024
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Live checks of VM creation: the queued job defines the VM, and a failed
|
||||
one leaves no volumes behind.
|
||||
|
||||
Runs only with TEST_LIBVIRT_HOST set (see instances/tests/livetest.py).
|
||||
Runs only with TEST_LIBVIRT_HOST set (see vrtManager/tests/livetest.py).
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -19,8 +19,8 @@ from operations.models import Operation
|
|||
from operations.tests import fakes
|
||||
from operations.worker import Worker
|
||||
|
||||
from . import livetest
|
||||
from .test_live_jobs import wait_for
|
||||
from vrtManager.tests import livetest
|
||||
from vrtManager.tests.livetest import wait_for
|
||||
|
||||
P = livetest.PREFIX
|
||||
|
||||
|
|
@ -214,4 +214,3 @@ class LiveCreateTestCase(TransactionTestCase):
|
|||
self.work()
|
||||
self.assertEqual(self.volumes(), set(), "a volume was left behind")
|
||||
self.assertFalse(self.domain_exists(P + "cr-api"))
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Each test asserts the safe behavior. Known unfixed cases are marked
|
||||
expectedFailure; a fix turns them into unexpected successes, which fail
|
||||
the run until the marker is removed. Runs only with
|
||||
TEST_LIBVIRT_HOST set (see instances/tests/livetest.py).
|
||||
TEST_LIBVIRT_HOST set (see vrtManager/tests/livetest.py).
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -19,7 +19,7 @@ from django.test import TestCase
|
|||
from django.urls import reverse
|
||||
from vrtManager.connection import connection_manager
|
||||
|
||||
from . import livetest
|
||||
from vrtManager.tests import livetest
|
||||
from instances.jobs import DeleteInstance
|
||||
from instances.models import Instance
|
||||
from operations.jobs import Context
|
||||
|
|
@ -671,4 +671,3 @@ class LiveDataLossTestCase(TestCase):
|
|||
inbound = etree.fromstring(dom.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)).find("devices/interface/bandwidth/inbound")
|
||||
self.assertEqual((inbound.get("average"), inbound.get("peak")), ("1000", "600"))
|
||||
self.assertEqual(dom.XMLDesc(0), live)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
"""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 instances/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 . 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)
|
||||
|
|
@ -6,7 +6,7 @@ with qemu-guest-agent enabled.
|
|||
The tests boot wvc-test VMs from qcow2 overlays in the test pool, so the
|
||||
image itself is only read. They are skipped while a running VM uses the
|
||||
image, since its content would change under the overlays. Runs only with TEST_LIBVIRT_HOST set as well (see
|
||||
instances/tests/livetest.py).
|
||||
vrtManager/tests/livetest.py).
|
||||
"""
|
||||
|
||||
import json
|
||||
|
|
@ -26,7 +26,7 @@ from vrtManager.connection import connection_manager
|
|||
|
||||
from operations.models import Operation
|
||||
|
||||
from . import livetest
|
||||
from vrtManager.tests import livetest
|
||||
from .test_create_instance import run
|
||||
from instances.models import Instance
|
||||
from instances.utils import refr
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from django.urls import reverse
|
|||
from lxml import etree
|
||||
from vrtManager.connection import connection_manager
|
||||
|
||||
from . import livetest
|
||||
from vrtManager.tests import livetest
|
||||
from instances.models import Instance
|
||||
from instances.utils import refr
|
||||
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
"""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 . 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,13 +1,12 @@
|
|||
"""Live checks for the instance jobs: the worker runs them against libvirt
|
||||
and recovery reads the same outcome back. Runs only with TEST_LIBVIRT_HOST
|
||||
set (see instances/tests/livetest.py). The migration needs a second host:
|
||||
set (see vrtManager/tests/livetest.py). The migration needs a second host:
|
||||
TEST_LIBVIRT_SSH_HOST (and _LOGIN), with TEST_LIBVIRT_MIGRATE_ADDRESS as its
|
||||
migration address. The ISO upload runs on both hosts: TCP writes through a
|
||||
libvirt stream, SSH through SFTP."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import libvirt
|
||||
|
|
@ -27,20 +26,12 @@ from operations.models import Operation
|
|||
from operations.tests import fakes
|
||||
from operations.worker import Worker
|
||||
|
||||
from . import livetest
|
||||
from vrtManager.tests import livetest
|
||||
from vrtManager.tests.livetest import wait_for
|
||||
|
||||
P = livetest.PREFIX
|
||||
|
||||
|
||||
def wait_for(predicate, seconds):
|
||||
end = time.monotonic() + seconds
|
||||
while time.monotonic() < end:
|
||||
if predicate():
|
||||
return True
|
||||
time.sleep(0.5)
|
||||
return False
|
||||
|
||||
|
||||
class LiveJobsTestCase(TransactionTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from django.urls import reverse
|
|||
from lxml import etree
|
||||
from vrtManager.connection import connection_manager
|
||||
|
||||
from . import livetest
|
||||
from vrtManager.tests import livetest
|
||||
from instances.models import Instance
|
||||
from instances.utils import refr
|
||||
|
||||
|
|
|
|||
|
|
@ -1,195 +0,0 @@
|
|||
"""Live checks for external snapshots on libvirt's own API. Runs only with
|
||||
TEST_LIBVIRT_HOST set (see instances/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 . 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")
|
||||
|
|
@ -15,7 +15,7 @@ from vrtManager.util import randomUUID
|
|||
|
||||
from operations.models import Operation
|
||||
|
||||
from . import livetest
|
||||
from vrtManager.tests import livetest
|
||||
from .test_create_instance import run
|
||||
from instances.models import Flavor, Instance
|
||||
from instances.utils import refr
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
"""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 instances/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 . 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)
|
||||
|
|
@ -1,319 +0,0 @@
|
|||
"""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 instances/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 . 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)
|
||||
|
|
@ -14,35 +14,10 @@ from instances.models import Instance
|
|||
from logs.models import Logs
|
||||
from libvirt import libvirtError
|
||||
from lxml import etree
|
||||
from vrtManager.create import PCIE_ROOT_PORTS, wvmCreate
|
||||
|
||||
DOM_CAPS = {"domain": "kvm", "os_support": "no", "cpu_modes": ["host-model"], "cpu_custom_models": [],
|
||||
"disk_bus": ["virtio", "sata", "scsi"]}
|
||||
NO_SLOT = libvirtError("internal error: No more available PCI slots")
|
||||
|
||||
|
||||
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=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"), [])
|
||||
|
||||
|
||||
class NoPciSlotTests(TestCase):
|
||||
def setUp(self):
|
||||
self.client.force_login(get_user_model().objects.create_superuser("ps-admin", "ps@example.com", "x"))
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from vrtManager.util import render
|
|||
from libvirt import VIR_DOMAIN_PAUSED, VIR_DOMAIN_RUNNING
|
||||
from operations.models import Operation
|
||||
from vrtManager.util import OperationError
|
||||
import socket
|
||||
|
||||
|
||||
def vm_reader(vcpu=2, memory=1024, disk_gib=10, pending=None):
|
||||
|
|
@ -274,3 +275,20 @@ class DetailPageQuotaTests(TestCase):
|
|||
proxy.get_status.return_value = 5
|
||||
response = self.client.get(reverse("instances:instance", args=[vm.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class QuotaWithDownHostTestCase(TestCase):
|
||||
def test_vm_on_an_unreachable_host_makes_the_quota_unverified(self):
|
||||
# its usage is unknown, so an increase is refused (fail closed), without an error page
|
||||
sock = socket.socket()
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
port = sock.getsockname()[1]
|
||||
sock.close()
|
||||
user = get_user_model().objects.create_user("quota_down", password="pw")
|
||||
UserAttributes.objects.create(user=user, max_instances=5, max_cpus=8, max_memory=8192, max_disk_size=100)
|
||||
compute = Compute.objects.create(name="down", hostname=f"127.0.0.1:{port}", login="", password="", type=1)
|
||||
vm = Instance.objects.create(compute=compute, name="vm", uuid="55555555-5555-5555-5555-555555555555")
|
||||
UserInstance.objects.create(user=user, instance=vm)
|
||||
get_settings() # app settings are normally loaded by middleware
|
||||
|
||||
self.assertEqual(check_user_quota(user, 1, 1, 128, 1), QUOTA_UNVERIFIED)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue