From 3169e7cebd1c0da6572ab593f07837e0d8efd582 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Tue, 20 Aug 2024 09:19:13 +0900 Subject: [PATCH] libvirt: Launch instances with stateless firmware This change implements the actual functionality to allow users to launch instances with stateless firmware (read-only firmware image + no NVRAM). Note that this feature is supported by the libvirt virt driver, and also requires libvirt >= 8.6.0. Implements: blueprint libvirt-stateless-firmware Change-Id: I7219bfa11ae98e65c326bec1a99c49d3e245cb9a --- doc/source/user/support-matrix.ini | 21 ++ nova/scheduler/utils.py | 7 + nova/tests/fixtures/libvirt.py | 11 + .../libvirt/test_stateless_firmware.py | 298 ++++++++++++++++++ nova/tests/unit/scheduler/test_utils.py | 24 ++ nova/tests/unit/virt/libvirt/test_config.py | 32 +- nova/tests/unit/virt/libvirt/test_driver.py | 36 ++- nova/tests/unit/virt/test_hardware.py | 34 ++ nova/virt/hardware.py | 21 ++ nova/virt/libvirt/config.py | 7 +- nova/virt/libvirt/driver.py | 17 +- ...t-stateless-firmware-1f1758c4df7c2d12.yaml | 8 + 12 files changed, 507 insertions(+), 9 deletions(-) create mode 100644 nova/tests/functional/libvirt/test_stateless_firmware.py create mode 100644 releasenotes/notes/libvirt-stateless-firmware-1f1758c4df7c2d12.yaml diff --git a/doc/source/user/support-matrix.ini b/doc/source/user/support-matrix.ini index 84975deac094..ec9fac402183 100644 --- a/doc/source/user/support-matrix.ini +++ b/doc/source/user/support-matrix.ini @@ -1413,3 +1413,24 @@ driver.ironic=missing driver.libvirt-vz-vm=missing driver.libvirt-vz-ct=missing driver.zvm=missing + +[operation.boot-stateless-firmware] +title=Boot instance with stateless firmware +status=optional +notes=The feature allows VMs to be booted with read-only firmware image without + NVRAM file. This feature is especially useful for confidential computing use + case because it allows more complete measurement of elements involved in + the boot chain and disables the potential attack serface from hypervisors. +cli=openstack server create +driver.libvirt-kvm-x86=partial +driver-notes.libvirt-kvm-x86=This feature is supported only with UEFI firmware +driver.libvirt-kvm-aarch64=missing +driver.libvirt-kvm-ppc64=missing +driver.libvirt-kvm-s390x=missing +driver.libvirt-qemu-x86=missing +driver.libvirt-lxc=missing +driver.vmware=missing +driver.ironic=missing +driver.libvirt-vz-vm=missing +driver.libvirt-vz-ct=missing +driver.zvm=missing diff --git a/nova/scheduler/utils.py b/nova/scheduler/utils.py index 961ef93e3060..64f714438b7f 100644 --- a/nova/scheduler/utils.py +++ b/nova/scheduler/utils.py @@ -196,6 +196,8 @@ class ResourceRequest(object): res_req._translate_maxphysaddr_request(request_spec.flavor, image) + res_req._translate_stateless_firmware_request(image) + res_req.strip_zeros() return res_req @@ -290,6 +292,11 @@ class ResourceRequest(object): self._add_trait(trait, 'required') LOG.debug("Requiring maxphysaddr support via trait %s.", trait) + def _translate_stateless_firmware_request(self, image): + if hardware.get_stateless_firmware_constraint(image): + self._add_trait(os_traits.COMPUTE_SECURITY_STATELESS_FIRMWARE, + 'required') + def _translate_vtpm_request(self, flavor, image): vtpm_config = hardware.get_vtpm_constraint(flavor, image) if not vtpm_config: diff --git a/nova/tests/fixtures/libvirt.py b/nova/tests/fixtures/libvirt.py index 099ea3945082..ffae5ab0e014 100644 --- a/nova/tests/fixtures/libvirt.py +++ b/nova/tests/fixtures/libvirt.py @@ -1124,6 +1124,10 @@ class Domain(object): os['type'] = os_type.text os['arch'] = os_type.get('arch', self._connection.host_info.arch) + os_loader = tree.find('./os/loader') + if os_loader is not None: + os['loader_stateless'] = os_loader.get('stateless') + os_kernel = tree.find('./os/kernel') if os_kernel is not None: os['kernel'] = os_kernel.text @@ -1431,6 +1435,11 @@ class Domain(object): pass def XMLDesc(self, flags): + loader = '' + if self._def['os'].get('loader_stateless'): + loader = ('' % + self._def['os'].get('loader_stateless')) + disks = '' for disk in self._def['devices']['disks']: if disk['type'] == 'file': @@ -1570,6 +1579,7 @@ class Domain(object): %(vcpu)s hvm + %(loader)s @@ -1618,6 +1628,7 @@ class Domain(object): 'vcpuset': vcpuset, 'vcpu': self._def['vcpu']['number'], 'arch': self._def['os']['arch'], + 'loader': loader, 'disks': disks, 'nics': nics, 'hostdevs': hostdevs, diff --git a/nova/tests/functional/libvirt/test_stateless_firmware.py b/nova/tests/functional/libvirt/test_stateless_firmware.py new file mode 100644 index 000000000000..84d6093c72a8 --- /dev/null +++ b/nova/tests/functional/libvirt/test_stateless_firmware.py @@ -0,0 +1,298 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import fixtures +from unittest import mock + +from lxml import etree +from oslo_utils.fixture import uuidsentinel +from oslo_utils import versionutils + +from nova import conf +from nova import context as nova_context +from nova import objects +from nova.tests.functional.libvirt import base +from nova.virt.libvirt import driver as libvirt_driver +from nova.virt.libvirt import migration as libvirt_migration + +CONF = conf.CONF + + +class LibvirtStatelessFirmwareTest( + base.LibvirtMigrationMixin, + base.ServersTestBase, +): + + # many move operations are admin-only + ADMIN_API = True + + microversion = 'latest' + + def setUp(self): + super().setUp() + self.context = nova_context.get_admin_context() + # Add the stateless firmware image to the glance fixture + hw_fw_stateless_image = copy.deepcopy(self.glance.image1) + hw_fw_stateless_image['id'] = uuidsentinel.fw_stateless_image_id + hw_fw_stateless_image['properties']['hw_machine_type'] = 'q35' + hw_fw_stateless_image['properties']['hw_firmware_type'] = 'uefi' + hw_fw_stateless_image['properties']['hw_firmware_stateless'] = True + self.glance.create(self.context, hw_fw_stateless_image) + + self._start_compute() + + self.guest_configs = {} + orig_get_config = self.computes['compute1'].driver._get_guest_config + + def _get_guest_config(_self, *args, **kwargs): + guest_config = orig_get_config(*args, **kwargs) + instance = args[0] + self.guest_configs[instance.uuid] = guest_config + return self.guest_configs[instance.uuid] + + self.useFixture(fixtures.MonkeyPatch( + 'nova.virt.libvirt.LibvirtDriver._get_guest_config', + _get_guest_config)) + + # disk.rescue image_create ignoring privsep + self.useFixture(fixtures.MockPatch( + 'nova.virt.libvirt.imagebackend._update_utime_ignore_eacces')) + + # dir to create 'unrescue.xml' + def fake_path(_self, *args, **kwargs): + return CONF.instances_path + + self.useFixture(fixtures.MonkeyPatch( + 'nova.virt.libvirt.utils.get_instance_path', fake_path)) + + def _start_compute(self, hostname='compute1'): + self.start_compute( + hostname, + libvirt_version=versionutils.convert_version_to_int( + libvirt_driver.MIN_LIBVIRT_STATELESS_FIRMWARE + )) + caps = self.computes[hostname].driver.capabilities + self.assertTrue(caps['supports_stateless_firmware']) + + def _create_server_with_stateless_firmware(self): + server = self._create_server( + image_uuid=uuidsentinel.fw_stateless_image_id, + networks='none', + ) + self.addCleanup(self._delete_server, server) + return server + + def _create_server_without_stateless_firmware(self): + server = self._create_server( + image_uuid=self.glance.image1['id'], + networks='none', + ) + self.addCleanup(self._delete_server, server) + return server + + def _assert_server_has_stateless_firmware(self, server_id): + instance = objects.Instance.get_by_uuid(self.context, server_id) + self.assertTrue( + instance.image_meta.properties.hw_firmware_stateless + ) + self.assertTrue( + self.guest_configs[server_id].os_loader_stateless + ) + del self.guest_configs[server_id] + + def _assert_server_has_no_stateless_firmware(self, server_id): + instance = objects.Instance.get_by_uuid(self.context, server_id) + self.assertIsNone( + instance.image_meta.properties.get('hw_firmware_stateless') + ) + self.assertIsNone( + self.guest_configs[server_id].os_loader_stateless + ) + del self.guest_configs[server_id] + + def test_create_server(self): + """Assert new instance is created with stateless firmware only when + the image has the required properties + """ + server_with = self._create_server_with_stateless_firmware() + self._assert_server_has_stateless_firmware(server_with['id']) + + server_without = self._create_server_without_stateless_firmware() + self._assert_server_has_no_stateless_firmware(server_without['id']) + + def test_live_migrate_server(self): + # create a server with stateless firmware + self.server = self._create_server_with_stateless_firmware() + self._assert_server_has_stateless_firmware(self.server['id']) + + self._start_compute('compute2') + self.src = self.computes['compute1'] + self.dest = self.computes['compute2'] + + self.migration_xml = None + orig_get_updated_guest_xml = libvirt_migration.get_updated_guest_xml + + def migration_xml_wrapper(*args, **kwargs): + self.migration_xml = orig_get_updated_guest_xml(*args, **kwargs) + return self.migration_xml + + with mock.patch( + 'nova.virt.libvirt.migration.get_updated_guest_xml', + side_effect=migration_xml_wrapper + ) as fake_get_updated_guest_xml: + self._live_migrate(self.server) + + fake_get_updated_guest_xml.assert_called_once() + server = etree.fromstring(self.migration_xml) + self.assertEqual('yes', server.find('./os/loader').get('stateless')) + + def test_migrate_server(self): + self._start_compute('compute2') + + # create a server with stateless firmware + server = self._create_server_with_stateless_firmware() + self._assert_server_has_stateless_firmware(server['id']) + + # TODO(stephenfin): The mock of 'migrate_disk_and_power_off' should + # probably be less...dumb + with mock.patch( + 'nova.virt.libvirt.driver.LibvirtDriver' + '.migrate_disk_and_power_off', return_value='{}', + ): + # cold migrate the server + self._migrate_server(server) + + self._assert_server_has_stateless_firmware(server['id']) + + server = self._confirm_resize(server) + + def test_migrate_server_revert(self): + self._start_compute('compute2') + + # create a server with stateless firmware + server = self._create_server_with_stateless_firmware() + self._assert_server_has_stateless_firmware(server['id']) + + # TODO(stephenfin): The mock of 'migrate_disk_and_power_off' should + # probably be less...dumb + with mock.patch( + 'nova.virt.libvirt.driver.LibvirtDriver' + '.migrate_disk_and_power_off', return_value='{}', + ): + # cold migrate the server + self._migrate_server(server) + + self._assert_server_has_stateless_firmware(server['id']) + + server = self._revert_resize(server) + self._assert_server_has_stateless_firmware(server['id']) + + def test_shelve_and_unshelve_to_same_host(self): + # create a server with stateless firmware + server = self._create_server_with_stateless_firmware() + self._assert_server_has_stateless_firmware(server['id']) + + # shelve the server + server = self._shelve_server(server) + + # uneshelve the server. the server is started at the same compute + server = self._unshelve_server(server) + self._assert_server_has_stateless_firmware(server['id']) + + def test_shelve_and_unshelve_to_different_host(self): + # create a server with stateless firmware + server = self._create_server_with_stateless_firmware() + self._assert_server_has_stateless_firmware(server['id']) + + # shelve the server + server = self._shelve_server(server) + + # force down the compute node + source_compute_id = self.admin_api.get_services( + host='compute1', binary='nova-compute')[0]['id'] + self.computes['compute1'].stop() + self.admin_api.put_service( + source_compute_id, {'forced_down': 'true'}) + + # start a new compute node and unshelve the server + self._start_compute('compute2') + server = self._unshelve_server(server) + self.assertEqual( + 'compute2', server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + self._assert_server_has_stateless_firmware(server['id']) + + def test_evacuate_server(self): + # create a server with stateless firmware + server = self._create_server_with_stateless_firmware() + self.assertEqual( + 'compute1', server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + self._assert_server_has_stateless_firmware(server['id']) + + # force down the compute node + source_compute_id = self.admin_api.get_services( + host='compute1', binary='nova-compute')[0]['id'] + self.computes['compute1'].stop() + self.admin_api.put_service( + source_compute_id, {'forced_down': 'true'}) + + # start a new compute node and evecuate the server + self._start_compute('compute2') + server = self._evacuate_server(server, expected_host='compute2') + self.assertEqual( + 'compute2', server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + self._assert_server_has_stateless_firmware(server['id']) + + def test_rescue_unrescue_server(self): + # create a server without stateless firmware + server = self._create_server_with_stateless_firmware() + self._assert_server_has_stateless_firmware(server['id']) + + self.api.post_server_action(server['id'], { + "rescue": { + "rescue_image_ref": self.glance.image1['id'] + } + }) + server = self._wait_for_state_change(server, 'RESCUE') + + # The instance object should still expect stateless firmware + instance = objects.Instance.get_by_uuid(self.context, server['id']) + self.assertTrue( + instance.image_meta.properties.hw_firmware_stateless + ) + + # but the actual xml config should not + self.assertIsNone( + self.guest_configs[server['id']].os_loader_stateless + ) + del self.guest_configs[server['id']] + + self.api.post_server_action(server['id'], { + "unrescue": None + }) + server = self._wait_for_state_change(server, 'ACTIVE') + # NOTE(tkajinam): Unrescue restores the original xml file so skip + # asserting its content. + + def test_rebuild_server(self): + # create a server without stateless firmware + server = self._create_server_without_stateless_firmware() + self._assert_server_has_no_stateless_firmware(server['id']) + + # rebuild using the image with stateless firmware + server = self._rebuild_server( + server, uuidsentinel.fw_stateless_image_id) + self._assert_server_has_stateless_firmware(server['id']) + + # rebuild using the image without stateless firmware + server = self._rebuild_server(server, self.glance.image1['id']) + self._assert_server_has_no_stateless_firmware(server['id']) diff --git a/nova/tests/unit/scheduler/test_utils.py b/nova/tests/unit/scheduler/test_utils.py index d7382228e889..fd32f6e938f9 100644 --- a/nova/tests/unit/scheduler/test_utils.py +++ b/nova/tests/unit/scheduler/test_utils.py @@ -1289,6 +1289,30 @@ class TestUtils(TestUtilsBase): rr = utils.ResourceRequest.from_request_spec(rs) self.assertResourceRequestsEqual(expected, rr) + def test_resource_request_from_request_spec_with_stateless_firmware(self): + flavor = objects.Flavor( + vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0, + ) + image = objects.ImageMeta( + properties=objects.ImageMetaProps( + hw_firmware_type = 'uefi', + hw_firmware_stateless = True + ) + ) + expected = FakeResourceRequest() + expected._rg_by_id[None] = objects.RequestGroup( + use_same_provider=False, + required_traits={'COMPUTE_SECURITY_STATELESS_FIRMWARE'}, + resources={ + 'VCPU': 1, + 'MEMORY_MB': 1024, + 'DISK_GB': 15, + }, + ) + rs = objects.RequestSpec(flavor=flavor, image=image, is_bfv=False) + rr = utils.ResourceRequest.from_request_spec(rs) + self.assertResourceRequestsEqual(expected, rr) + def test_resource_request_from_request_spec_with_secure_boot(self): flavor = objects.Flavor( vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0, diff --git a/nova/tests/unit/virt/libvirt/test_config.py b/nova/tests/unit/virt/libvirt/test_config.py index 2c96bd2a67f2..d7172ef29b04 100644 --- a/nova/tests/unit/virt/libvirt/test_config.py +++ b/nova/tests/unit/virt/libvirt/test_config.py @@ -2717,8 +2717,8 @@ class LibvirtConfigGuestTest(LibvirtConfigBaseTest): xml = obj.to_xml() self.assertXmlEqual(fake_libvirt_data.FAKE_KVM_GUEST, xml) - def test_config_uefi(self): - obj = config.LibvirtConfigGuest() + def _test_config_uefi(self): + obj = config.libvirtconfigguest() obj.virt_type = "kvm" obj.memory = 100 * units.Mi obj.vcpus = 1 @@ -2729,9 +2729,10 @@ class LibvirtConfigGuestTest(LibvirtConfigBaseTest): obj.os_loader = '/tmp/OVMF_CODE.secboot.fd' obj.os_loader_type = 'pflash' obj.os_loader_secure = True + obj.os_loader_stateless = True xml = obj.to_xml() - self.assertXmlEqual( + self.assertxmlequal( """ f01cf68d-515c-4daf-b85f-ef1424d93bfc @@ -2740,13 +2741,13 @@ class LibvirtConfigGuestTest(LibvirtConfigBaseTest): 1 hvm - /tmp/OVMF_CODE.secboot.fd + /tmp/OVMF_CODE.secboot.fd """, # noqa: E501 xml, ) - def _test_config_uefi_autoconfigure(self, secure): + def _test_config_uefi_autoconfigure(self, secure=False, stateless=None): obj = config.LibvirtConfigGuest() obj.virt_type = "kvm" obj.memory = 100 * units.Mi @@ -2757,10 +2758,11 @@ class LibvirtConfigGuestTest(LibvirtConfigBaseTest): obj.os_firmware = "efi" obj.os_mach_type = "pc-q35-5.1" obj.os_loader_secure = secure + obj.os_loader_stateless = stateless return obj.to_xml() def test_config_uefi_autoconfigure(self): - xml = self._test_config_uefi_autoconfigure(secure=False) + xml = self._test_config_uefi_autoconfigure() self.assertXmlEqual( xml, @@ -2795,6 +2797,24 @@ class LibvirtConfigGuestTest(LibvirtConfigBaseTest): """, ) + def test_config_uefi_autoconfigure_stateless(self): + xml = self._test_config_uefi_autoconfigure(stateless=True) + + self.assertXmlEqual( + xml, + """ + + f01cf68d-515c-4daf-b85f-ef1424d93bfc + uefi + 104857600 + 1 + + hvm + + + """, + ) + def test_config_boot_menu(self): obj = config.LibvirtConfigGuest() obj.virt_type = "kvm" diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 68a2851d7d4b..dda95794aca7 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -5294,6 +5294,27 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertEqual('/usr/share/OVMF/OVMF_CODE.fd', cfg.os_loader) self.assertEqual('/usr/share/OVMF/OVMF_VARS.fd', cfg.os_nvram_template) + def test_get_guest_config_with_uefi_and_stateless_firmware(self): + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + + image_meta = objects.ImageMeta.from_dict({ + "disk_format": "raw", + "properties": { + "hw_firmware_type": "uefi", + "hw_firmware_stateless": True + } + }) + instance_ref = objects.Instance(**self.test_instance) + + disk_info = blockinfo.get_disk_info( + CONF.libvirt.virt_type, instance_ref, image_meta) + cfg = drvr._get_guest_config( + instance_ref, [], image_meta, disk_info) + # these paths are derived from the FakeLibvirtFixture + self.assertEqual('/usr/share/OVMF/OVMF_CODE.fd', cfg.os_loader) + self.assertTrue(cfg.os_loader_stateless) + self.assertIsNone(cfg.os_nvram_template) + def test_get_guest_config_with_secure_boot_and_smm_required(self): drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) # uefi only used with secure boot @@ -21829,13 +21850,26 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertTrue( driver.capabilities.get('supports_address_space_emulated')) + @mock.patch.object(fakelibvirt.Connection, 'getLibVersion', + return_value=versionutils.convert_version_to_int( + libvirt_driver.MIN_LIBVIRT_STATELESS_FIRMWARE) - 1) def test_update_host_specific_capabilities_without_stateless_firmware( - self): + self, mock_get_version): driver = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI()) driver._update_host_specific_capabilities() self.assertFalse( driver.capabilities.get('supports_stateless_firmware')) + @mock.patch.object(fakelibvirt.Connection, 'getLibVersion', + return_value=versionutils.convert_version_to_int( + libvirt_driver.MIN_LIBVIRT_STATELESS_FIRMWARE)) + def test_update_host_specific_capabilities_with_stateless_firmware( + self, mock_get_version): + driver = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI()) + driver._update_host_specific_capabilities() + self.assertTrue( + driver.capabilities.get('supports_stateless_firmware')) + @mock.patch.object(fakelibvirt.Connection, 'getLibVersion', return_value=versionutils.convert_version_to_int( libvirt_driver.MIN_LIBVIRT_MAXPHYSADDR)) diff --git a/nova/tests/unit/virt/test_hardware.py b/nova/tests/unit/virt/test_hardware.py index a923a8feb97a..980b8703d2bf 100644 --- a/nova/tests/unit/virt/test_hardware.py +++ b/nova/tests/unit/virt/test_hardware.py @@ -6018,6 +6018,40 @@ class MaxphysaddrModeTest(test.NoDBTestCase): ) +@ddt.ddt +class StatelessFirmwareConstraintTest(test.NoDBTestCase): + @ddt.unpack + @ddt.data( + # pass: no configuration + (None, None, False), + # pass: uefi with stateless firmware + (True, 'uefi', True), + # pass: non-uefi with non-stateless firmware + (False, None, False), + # fail: non-uefi with stateless fiemware + (True, None, exception.Invalid), + ) + def test_get_stateless_firmware_constraint( + self, firmware_stateless, firmware_type, expected, + ): + image_meta_props = {} + if firmware_stateless is not None: + image_meta_props['hw_firmware_stateless'] = firmware_stateless + if firmware_type is not None: + image_meta_props['hw_firmware_type'] = firmware_type + image_meta = objects.ImageMeta.from_dict( + {'name': 'bar', 'properties': image_meta_props}) + + if isinstance(expected, type) and issubclass(expected, Exception): + self.assertRaises( + expected, hw.get_stateless_firmware_constraint, image_meta, + ) + else: + self.assertEqual( + expected, hw.get_stateless_firmware_constraint(image_meta), + ) + + @ddt.ddt class RescuePropertyTestCase(test.NoDBTestCase): diff --git a/nova/virt/hardware.py b/nova/virt/hardware.py index 97f514631ef5..af062d708bba 100644 --- a/nova/virt/hardware.py +++ b/nova/virt/hardware.py @@ -2083,6 +2083,27 @@ def get_secure_boot_constraint( return policy +def get_stateless_firmware_constraint( + image_meta: 'objects.ImageMeta', +) -> bool: + """Validate and return the requested statless firmware policy. + + :param flavor: ``nova.objects.Flavor`` instance + :param image_meta: ``nova.objects.ImageMeta`` instance + :raises: nova.exception.Invalid if a value or combination of values is + invalid + """ + if not image_meta.properties.get('hw_firmware_stateless', False): + return False + + if image_meta.properties.get('hw_firmware_type') != 'uefi': + raise exception.Invalid(_( + 'Stateless firmware is supported only when UEFI firmware type is ' + 'used.' + )) + return True + + def numa_get_constraints(flavor, image_meta): """Return topology related to input request. diff --git a/nova/virt/libvirt/config.py b/nova/virt/libvirt/config.py index 0f9d02498a31..505bad6bf325 100644 --- a/nova/virt/libvirt/config.py +++ b/nova/virt/libvirt/config.py @@ -3024,6 +3024,7 @@ class LibvirtConfigGuest(LibvirtConfigObject): self.os_firmware = None self.os_loader_type = None self.os_loader_secure = None + self.os_loader_stateless = None self.os_nvram = None self.os_nvram_template = None self.os_kernel = None @@ -3089,7 +3090,8 @@ class LibvirtConfigGuest(LibvirtConfigObject): if ( self.os_loader is not None or self.os_loader_type is not None or - self.os_loader_secure is not None + self.os_loader_secure is not None or + self.os_loader_stateless is not None ): loader = self._text_node("loader", self.os_loader) if self.os_loader_type is not None: @@ -3098,6 +3100,9 @@ class LibvirtConfigGuest(LibvirtConfigObject): if self.os_loader_secure is not None: loader.set( "secure", self.get_yes_no_str(self.os_loader_secure)) + if self.os_loader_stateless is not None: + loader.set( + "stateless", self.get_yes_no_str(self.os_loader_stateless)) os.append(loader) if ( diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 37613eb2c61f..511a850e0315 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -257,6 +257,9 @@ LIBVIRT_PERF_EVENT_PREFIX = 'VIR_PERF_PARAM_' MIN_LIBVIRT_MAXPHYSADDR = (8, 7, 0) MIN_QEMU_MAXPHYSADDR = (2, 7, 0) +# stateless firmware support +MIN_LIBVIRT_STATELESS_FIRMWARE = (8, 6, 0) + REGISTER_IMAGE_PROPERTY_DEFAULTS = [ 'hw_machine_type', 'hw_cdrom_bus', @@ -901,6 +904,13 @@ class LibvirtDriver(driver.ComputeDriver): 'supports_address_space_emulated': supports_maxphysaddr, }) + supports_stateless_firmware = self._host.has_min_version( + lv_ver=MIN_LIBVIRT_STATELESS_FIRMWARE, + ) + self.capabilities.update({ + 'supports_stateless_firmware': supports_stateless_firmware, + }) + def _register_all_undefined_instance_details(self) -> None: """Register the default image properties of instances on this host @@ -6929,6 +6939,8 @@ class LibvirtDriver(driver.ComputeDriver): guest.os_mach_type = mach_type hw_firmware_type = image_meta.properties.get('hw_firmware_type') + hw_firmware_stateless = hardware.get_stateless_firmware_constraint( + image_meta) if arch == fields.Architecture.AARCH64: if not hw_firmware_type: @@ -6986,7 +6998,10 @@ class LibvirtDriver(driver.ComputeDriver): guest.os_loader = loader guest.os_loader_type = 'pflash' - guest.os_nvram_template = nvram_template + if hw_firmware_stateless: + guest.os_loader_stateless = True + else: + guest.os_nvram_template = nvram_template # if the feature set says we need SMM then enable it if requires_smm: diff --git a/releasenotes/notes/libvirt-stateless-firmware-1f1758c4df7c2d12.yaml b/releasenotes/notes/libvirt-stateless-firmware-1f1758c4df7c2d12.yaml new file mode 100644 index 000000000000..2954a940af22 --- /dev/null +++ b/releasenotes/notes/libvirt-stateless-firmware-1f1758c4df7c2d12.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Libvirt virt driver now supports launching instances with stateless + firmware. The new ``hw_firmware_stateless`` image property can be used to + enable this feature. Note that the feature can be used only for + the instances with UEFI firmware. This feature requires libvirt v8.6.0 or + later.