mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
Creating each option on a RHEL qemu-kvm host showed:
- CD-ROM: the create page offered every disk bus, but the empty CD-ROM
of a new VM works only on ide, sata and scsi. fdc and virtio failed
("Invalid bus type", "does not support ejectable media"), usb failed
with a device named 'Nonea' because vol_dev_type() had no prefix for
usb, and after that libvirt refuses an empty usb disk. Only ide, sata
and scsi are offered now, and another value is refused before any
storage is allocated. vol_dev_type() names usb disks sdX, which also
fixes data volumes on the usb bus.
- USB controller: any input device added a bare <controller
type='usb'/>, which libvirt turns into piix3-uhci (USB 1.1) on q35
instead of its default qemu-xhci. The bare controller is gone; libvirt
adds the machine's default for USB inputs.
- MACs: with more networks than MAC addresses, create_instance indexed
past the list and the request failed with a 500. NICs without a given
MAC now get one from libvirt. MAC addresses are validated (format, not
more than networks) by the page and the API before anything is
created; they went into the XML unchecked.
Unit tests cover the refused values, the MAC rule and the usb prefix.
Live tests create a VM with every offered CD-ROM bus and start it,
check the USB controller with a USB input device and create a VM with
two networks and one MAC.
198 lines
7.9 KiB
Python
198 lines
7.9 KiB
Python
"""Live checks that a failed VM creation leaves no volumes behind.
|
|
|
|
Runs only with TEST_LIBVIRT_HOST set (see instances/livetest.py).
|
|
"""
|
|
|
|
import os
|
|
import unittest
|
|
|
|
import libvirt
|
|
from computes.models import Compute
|
|
from django.contrib.auth import get_user_model
|
|
from django.test import TestCase
|
|
from django.urls import reverse
|
|
from lxml import etree
|
|
from vrtManager.connection import connection_manager
|
|
|
|
from . import livetest
|
|
|
|
P = livetest.PREFIX
|
|
|
|
|
|
class LiveCreateTestCase(TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
if not livetest.enabled():
|
|
raise unittest.SkipTest("Set TEST_LIBVIRT_HOST to run the live libvirt tests")
|
|
cls.compute_args = dict(
|
|
hostname=os.environ["TEST_LIBVIRT_HOST"],
|
|
login=os.environ.get("TEST_LIBVIRT_LOGIN", ""),
|
|
password=os.environ.get("TEST_LIBVIRT_PASSWORD", ""),
|
|
type=int(os.environ.get("TEST_LIBVIRT_TYPE", 4)),
|
|
)
|
|
cls.conn = connection_manager.get_connection(*cls.compute_args.values())
|
|
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 setUp(self):
|
|
self.compute = Compute.objects.create(name="live-create", details="test", **self.compute_args)
|
|
admin = get_user_model().objects.create_superuser("create_admin", "c@example.com", "pw")
|
|
self.client.force_login(admin)
|
|
self.client.raise_request_exception = False
|
|
|
|
def tearDown(self):
|
|
livetest.cleanup(self.conn)
|
|
|
|
def create(self, name, **overrides):
|
|
data = {
|
|
"name": P + name,
|
|
"firmware": "BIOS",
|
|
"vcpu": 1,
|
|
"vcpu_mode": "host-model",
|
|
"memory": 128,
|
|
"hdd_size": 1,
|
|
"storage": livetest.POOL,
|
|
"mac": "52:54:00:aa:03:01",
|
|
"networks": "default",
|
|
"nwfilter": "",
|
|
"net_model": "virtio",
|
|
"cache_mode": "none",
|
|
"video": "vga",
|
|
"listener_addr": "0.0.0.0",
|
|
"console_pass": "",
|
|
"add_cdrom": "None",
|
|
"add_input": "None",
|
|
"create": True,
|
|
}
|
|
data.update(overrides)
|
|
return self.client.post(
|
|
reverse("instances:create_instance", args=[self.compute.id, "x86_64", "q35"]), data
|
|
)
|
|
|
|
def volumes(self):
|
|
pool = self.conn.storagePoolLookupByName(livetest.POOL)
|
|
pool.refresh(0)
|
|
return set(pool.listVolumes())
|
|
|
|
def domain_exists(self, name):
|
|
try:
|
|
self.conn.lookupByName(name)
|
|
return True
|
|
except libvirt.libvirtError:
|
|
return False
|
|
|
|
def assert_nothing_left(self, name, response):
|
|
self.assertLess(response.status_code, 500)
|
|
self.assertEqual(self.volumes(), set(), "a volume was left behind")
|
|
self.assertFalse(self.domain_exists(P + name))
|
|
|
|
def test_invalid_cache_mode_allocates_nothing(self):
|
|
self.assert_nothing_left("cr-cache", self.create("cr-cache", cache_mode="bogus"))
|
|
|
|
def test_malformed_uefi_firmware_allocates_nothing(self):
|
|
self.assert_nothing_left("cr-uefi", self.create("cr-uefi", firmware="UEFI"))
|
|
|
|
def test_rejected_definition_removes_the_new_disk(self):
|
|
self.assert_nothing_left("cr-define", self.create("cr-define", video="no-such-model"))
|
|
|
|
def test_successful_creation(self):
|
|
response = self.create("cr-ok")
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertTrue(self.domain_exists(P + "cr-ok"))
|
|
self.assertEqual(len(self.volumes()), 1)
|
|
|
|
def test_offered_uefi_firmware_boots_on_q35(self):
|
|
page = self.client.get(reverse("instances:create_instance", args=[self.compute.id, "x86_64", "q35"]))
|
|
uefi = [f for f in page.context["firmwares"] if f.startswith("UEFI")]
|
|
self.assertTrue(uefi, "no UEFI firmware offered for q35")
|
|
|
|
self.create("cr-uefi-ok", firmware=uefi[0])
|
|
|
|
dom = self.conn.lookupByName(P + "cr-uefi-ok")
|
|
loader = etree.fromstring(dom.XMLDesc(0)).findtext("os/loader")
|
|
self.assertEqual(loader, uefi[0].split(":", 1)[1].strip())
|
|
# Listed by libvirt for q35, so the domain starts with it
|
|
caps = etree.fromstring(self.conn.getDomainCapabilities(None, "x86_64", "q35", "kvm"))
|
|
self.assertIn(loader, caps.xpath("os/loader/value/text()"))
|
|
dom.create()
|
|
self.assertTrue(dom.isActive())
|
|
|
|
def test_secure_boot_loader_on_q35_gets_secure_boot(self):
|
|
caps = etree.fromstring(self.conn.getDomainCapabilities(None, "x86_64", "q35", "kvm"))
|
|
secboot = [p for p in caps.xpath("os/loader/value/text()") if "secboot" in p and "OVMF_CODE" in p]
|
|
if not secboot:
|
|
self.skipTest("the host has no secure boot loader for q35")
|
|
|
|
self.create("cr-secboot", firmware=f"UEFI x86_64: {secboot[0]}")
|
|
|
|
dom = self.conn.lookupByName(P + "cr-secboot")
|
|
tree = etree.fromstring(dom.XMLDesc(0))
|
|
self.assertEqual(tree.find("os/loader").get("secure"), "yes")
|
|
self.assertEqual(tree.find("features/smm").get("state"), "on")
|
|
dom.create()
|
|
self.assertTrue(dom.isActive())
|
|
|
|
def test_every_offered_cdrom_bus_creates_a_vm_that_starts(self):
|
|
page = self.client.get(reverse("instances:create_instance", args=[self.compute.id, "x86_64", "q35"]))
|
|
buses = page.context["cdrom_buses"]
|
|
self.assertTrue(buses)
|
|
for i, bus in enumerate(buses):
|
|
with self.subTest(bus=bus):
|
|
self.create(f"cr-cd-{bus}", add_cdrom=bus, mac=f"52:54:00:aa:03:1{i}")
|
|
dom = self.conn.lookupByName(P + f"cr-cd-{bus}")
|
|
self.assertEqual(etree.fromstring(dom.XMLDesc(0)).xpath("devices/disk[@device='cdrom']/target/@bus"), [bus])
|
|
dom.create()
|
|
self.assertTrue(dom.isActive())
|
|
|
|
def test_input_device_keeps_the_machine_usb_controller(self):
|
|
self.create("cr-input", add_input="usb")
|
|
tree = etree.fromstring(self.conn.lookupByName(P + "cr-input").XMLDesc(0))
|
|
self.assertEqual(tree.xpath("devices/controller[@type='usb']/@model"), ["qemu-xhci"])
|
|
self.assertTrue(tree.xpath("devices/input[@type='tablet' and @bus='usb']"))
|
|
|
|
def test_networks_without_a_mac_of_their_own(self):
|
|
self.create("cr-macs", networks="default,default")
|
|
macs = etree.fromstring(self.conn.lookupByName(P + "cr-macs").XMLDesc(0)).xpath("devices/interface/mac/@address")
|
|
self.assertEqual(len(macs), 2)
|
|
self.assertEqual(macs[0], "52:54:00:aa:03:01")
|
|
|
|
def test_api_rejected_definition_removes_the_new_disk(self):
|
|
response = self.client.post(
|
|
f"/api/v1/computes/{self.compute.id}/instances/create/x86_64/q35/",
|
|
{
|
|
"name": P + "cr-api",
|
|
"vcpu": 1,
|
|
"vcpu_mode": "host-model",
|
|
"memory": 128,
|
|
"networks": "default",
|
|
"mac": "52:54:00:aa:03:02",
|
|
"nwfilter": "",
|
|
"storage": livetest.POOL,
|
|
"hdd_size": 1,
|
|
"cache_mode": "none",
|
|
"meta_prealloc": False,
|
|
"virtio": True,
|
|
"qemu_ga": False,
|
|
"console_pass": "",
|
|
"listener_addr": "0.0.0.0",
|
|
"video": "no-such-model",
|
|
"add_cdrom": "None",
|
|
"add_input": "None",
|
|
},
|
|
content_type="application/json",
|
|
)
|
|
self.assertEqual(response.status_code, 400, response.content)
|
|
self.assertEqual(self.volumes(), set(), "a volume was left behind")
|
|
self.assertFalse(self.domain_exists(P + "cr-api"))
|
|
|