mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 03:36:48 +00:00
create_instance set secure='yes' (and with it SMM) only while switching a non-q35 machine to q35. With q35 already chosen, a secure boot loader was defined with secure='no' and without SMM. Secure boot is now requested whenever the loader is a secure boot build; the switch to q35 is unchanged. libvirt 11 on RHEL fills in secure='yes' and SMM from the firmware descriptor of a known loader, so there the domain was already correct; without such a descriptor (older libvirt, custom loader) it was not. A unit test checks what WebVirtCloud requests (fails on the previous code); a live test creates a q35 VM with the host's secure boot loader and starts it.
174 lines
6.5 KiB
Python
174 lines
6.5 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_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"))
|
|
|