diff --git a/nova/api/validation/extra_specs/os.py b/nova/api/validation/extra_specs/os.py index 1fc9c76ad443..7709b6345fa9 100644 --- a/nova/api/validation/extra_specs/os.py +++ b/nova/api/validation/extra_specs/os.py @@ -23,8 +23,8 @@ EXTRA_SPEC_VALIDATORS = [ base.ExtraSpecValidator( name='os:secure_boot', description=( - 'Determine whether secure boot is enabled or not. Currently only ' - 'supported by the HyperV driver.' + 'Determine whether secure boot is enabled or not. Only supported ' + 'by the libvirt and HyperV drivers.' ), value={ 'type': str, diff --git a/nova/exception.py b/nova/exception.py index c198686da791..3b0887e34546 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1891,6 +1891,10 @@ class UEFINotSupported(Invalid): msg_fmt = _("UEFI is not supported") +class SecureBootNotSupported(Invalid): + msg_fmt = _("Secure Boot is not supported by host") + + class TriggerCrashDumpNotSupported(Invalid): msg_fmt = _("Triggering crash dump is not supported") diff --git a/nova/tests/unit/virt/libvirt/fakelibvirt.py b/nova/tests/unit/virt/libvirt/fakelibvirt.py index 6d556280b5be..718dad8b106a 100644 --- a/nova/tests/unit/virt/libvirt/fakelibvirt.py +++ b/nova/tests/unit/virt/libvirt/fakelibvirt.py @@ -1993,6 +1993,36 @@ class FakeLibvirtFixture(fixtures.Fixture): 'features': ['acpi-s3', 'amd-sev', 'verbose-dynamic'], 'tags': [], }, + { + 'description': 'UEFI firmware for x86_64, with SB+SMM', + 'interface-types': ['uefi'], + 'mapping': { + 'device': 'flash', + 'executable': { + 'filename': '/usr/share/OVMF/OVMF_CODE.secboot.fd', + 'format': 'raw', + }, + 'nvram-template': { + 'filename': '/usr/share/OVMF/OVMF_VARS.secboot.fd', + 'format': 'raw', + }, + }, + 'targets': [ + { + 'architecture': 'x86_64', + 'machines': ['pc-q35-*'], + }, + ], + 'features': [ + 'acpi-s3', + 'amd-sev', + 'enrolled-keys', + 'requires-smm', + 'secure-boot', + 'verbose-dynamic', + ], + 'tags': [], + }, { 'description': 'UEFI firmware for aarch64', 'interface-types': ['uefi'], @@ -2015,7 +2045,7 @@ class FakeLibvirtFixture(fixtures.Fixture): ], 'features': ['verbose-static'], "tags": [], - } + }, ] self.useFixture( fixtures.MockPatch( diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index b456ccc68e82..4b73fd6bdefd 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -5008,10 +5008,86 @@ class LibvirtConnTestCase(test.NoDBTestCase, CONF.libvirt.virt_type, instance_ref, image_meta) cfg = drvr._get_guest_config( instance_ref, [], image_meta, disk_info) - # these values are derived from the FakeLibvirtFixture + # these paths are derived from the FakeLibvirtFixture self.assertEqual('/usr/share/OVMF/OVMF_CODE.fd', cfg.os_loader) self.assertEqual('/usr/share/OVMF/OVMF_VARS.fd', cfg.os_nvram_template) + @ddt.data(True, False) + def test_get_guest_config_with_secure_boot_required( + self, host_has_support, + ): + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + drvr._host._supports_uefi = True + drvr._host._supports_secure_boot = host_has_support + + image_meta = objects.ImageMeta.from_dict({ + 'disk_format': 'raw', + # secure boot requires UEFI + 'properties': { + 'hw_firmware_type': 'uefi', + 'hw_machine_type': 'q35', + 'os_secure_boot': 'required', + }, + }) + instance_ref = objects.Instance(**self.test_instance) + + disk_info = blockinfo.get_disk_info( + CONF.libvirt.virt_type, instance_ref, image_meta) + if host_has_support: + # if the host supports it, we should get the feature + 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.secboot.fd', cfg.os_loader) + self.assertEqual( + '/usr/share/OVMF/OVMF_VARS.secboot.fd', cfg.os_nvram_template) + self.assertTrue(cfg.os_loader_secure) + else: + # if not, we should see an exception + self.assertRaises( + exception.SecureBootNotSupported, + drvr._get_guest_config, + instance_ref, [], image_meta, disk_info) + + @ddt.data(True, False) + def test_get_guest_config_with_secure_boot_optional( + self, host_has_support, + ): + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + drvr._host._supports_uefi = True + drvr._host._supports_secure_boot = host_has_support + + image_meta = objects.ImageMeta.from_dict({ + 'disk_format': 'raw', + # secure boot requires UEFI + 'properties': { + 'hw_firmware_type': 'uefi', + 'hw_machine_type': 'q35', + 'os_secure_boot': 'optional', + }, + }) + 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) + if host_has_support: + # if the host supports it we should get the feature + self.assertEqual( + '/usr/share/OVMF/OVMF_CODE.secboot.fd', cfg.os_loader) + self.assertEqual( + '/usr/share/OVMF/OVMF_VARS.secboot.fd', cfg.os_nvram_template) + self.assertTrue(cfg.os_loader_secure) + else: + # if not, silently ignore + self.assertEqual( + '/usr/share/OVMF/OVMF_CODE.fd', cfg.os_loader) + self.assertEqual( + '/usr/share/OVMF/OVMF_VARS.fd', cfg.os_nvram_template) + self.assertFalse(cfg.os_loader_secure) + def test_check_uefi_support_aarch64(self): self.mock_uname.return_value = fakelibvirt.os_uname( 'Linux', '', '5.4.0-0-generic', '', fields.Architecture.AARCH64) diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index c7f69e78c101..856499a25f63 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -5822,6 +5822,29 @@ class LibvirtDriver(driver.ComputeDriver): caps.host.cpu.arch == fields.Architecture.AARCH64 ) + def _check_secure_boot_support( + self, + arch: str, + machine_type: str, + firmware_type: str, + ) -> bool: + if not self._host.supports_secure_boot: + # secure boot requires host configuration + return False + + if firmware_type != fields.FirmwareType.UEFI: + # secure boot is only supported with UEFI + return False + + if ( + arch == fields.Architecture.X86_64 and + 'q35' not in machine_type + ): + # secure boot on x86_64 requires the Q35 machine type + return False + + return True + def _get_supported_perf_events(self): if not len(CONF.libvirt.enabled_perf_events): return [] @@ -5887,8 +5910,35 @@ class LibvirtDriver(driver.ComputeDriver): # architecture that we have no default machine type for raise exception.UEFINotSupported() - loader, nvram_template = self._host.get_loader( - arch, mach_type, has_secure_boot=False) + os_secure_boot = hardware.get_secure_boot_constraint( + flavor, image_meta) + if os_secure_boot == 'required': + # hard fail if we don't support secure boot and it's + # required + if not self._check_secure_boot_support( + arch, mach_type, hw_firmware_type, + ): + raise exception.SecureBootNotSupported() + + guest.os_loader_secure = True + elif os_secure_boot == 'optional': + # only enable it if the host is configured appropriately + guest.os_loader_secure = self._check_secure_boot_support( + arch, mach_type, hw_firmware_type, + ) + else: + guest.os_loader_secure = False + + try: + loader, nvram_template = self._host.get_loader( + arch, mach_type, + has_secure_boot=guest.os_loader_secure) + except exception.UEFINotSupported as exc: + if guest.os_loader_secure: + # we raise a specific exception if we requested secure + # boot and couldn't get that + raise exception.SecureBootNotSupported() from exc + raise guest.os_loader = loader guest.os_loader_type = 'pflash'