diff --git a/nova/exception.py b/nova/exception.py index befb5fb865f5..90b75e44bdd6 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -2460,6 +2460,11 @@ class FlavorImageConflict(NovaException): "(%(flavor_val)s) and the image (%(image_val)s).") +class MissingDomainCapabilityFeatureException(NovaException): + msg_fmt = _("Guest config could not be built without domain capabilities " + "including <%(feature)s> feature.") + + class HealPortAllocationException(NovaException): msg_fmt = _("Healing port allocation failed.") diff --git a/nova/tests/unit/virt/libvirt/fake_libvirt_data.py b/nova/tests/unit/virt/libvirt/fake_libvirt_data.py index 336ba2319789..a2ba4629ec62 100644 --- a/nova/tests/unit/virt/libvirt/fake_libvirt_data.py +++ b/nova/tests/unit/virt/libvirt/fake_libvirt_data.py @@ -87,13 +87,69 @@ def fake_kvm_guest(): obj.sysinfo.bios_vendor = "Acme" obj.sysinfo.system_version = "1.0.0" + # obj.devices[0] disk = config.LibvirtConfigGuestDisk() disk.source_type = "file" - disk.source_path = "/tmp/img" - disk.target_dev = "/dev/vda" + disk.source_path = "/tmp/disk-img" + disk.target_dev = "vda" disk.target_bus = "virtio" obj.add_device(disk) + # obj.devices[1] + disk = config.LibvirtConfigGuestDisk() + disk.source_device = "cdrom" + disk.source_type = "file" + disk.source_path = "/tmp/cdrom-img" + disk.target_dev = "sda" + disk.target_bus = "sata" + obj.add_device(disk) + + # obj.devices[2] + intf = config.LibvirtConfigGuestInterface() + intf.net_type = "network" + intf.mac_addr = "52:54:00:f6:35:8f" + intf.model = "virtio" + intf.source_dev = "virbr0" + obj.add_device(intf) + + # obj.devices[3] + balloon = config.LibvirtConfigMemoryBalloon() + balloon.model = 'virtio' + balloon.period = 11 + obj.add_device(balloon) + + # obj.devices[4] + mouse = config.LibvirtConfigGuestInput() + mouse.type = "mouse" + mouse.bus = "virtio" + obj.add_device(mouse) + + # obj.devices[5] + gfx = config.LibvirtConfigGuestGraphics() + gfx.type = "vnc" + gfx.autoport = True + gfx.keymap = "en_US" + gfx.listen = "127.0.0.1" + obj.add_device(gfx) + + # obj.devices[6] + video = config.LibvirtConfigGuestVideo() + video.type = 'qxl' + obj.add_device(video) + + # obj.devices[7] + serial = config.LibvirtConfigGuestSerial() + serial.type = "file" + serial.source_path = "/tmp/vm.log" + obj.add_device(serial) + + # obj.devices[8] + rng = config.LibvirtConfigGuestRng() + rng.backend = '/dev/urandom' + rng.rate_period = '12' + rng.rate_bytes = '34' + obj.add_device(rng) + return obj @@ -151,9 +207,33 @@ FAKE_KVM_GUEST = """ - - + + + + + + + + + + + + + + + + + + + + + + + /dev/urandom + 0x0033 diff --git a/nova/tests/unit/virt/libvirt/test_designer.py b/nova/tests/unit/virt/libvirt/test_designer.py index 8b91223796a0..720c47e38d85 100644 --- a/nova/tests/unit/virt/libvirt/test_designer.py +++ b/nova/tests/unit/virt/libvirt/test_designer.py @@ -17,6 +17,7 @@ import mock from nova.pci import utils as pci_utils from nova import test from nova.tests.unit import matchers +from nova.tests.unit.virt.libvirt import fake_libvirt_data from nova.virt.libvirt import config from nova.virt.libvirt import designer from nova.virt.libvirt import host @@ -224,3 +225,24 @@ class DesignerTestCase(test.NoDBTestCase): conf = config.LibvirtConfigGuestInterface() designer.set_vif_mtu_config(conf, 9000) self.assertEqual(9000, conf.mtu) + + def test_set_driver_iommu_for_sev(self): + conf = fake_libvirt_data.fake_kvm_guest() + designer.set_driver_iommu_for_sev(conf) + + # All disks/interfaces/memballoon are expected to be virtio, + # thus driver_iommu should be on + self.assertEqual(9, len(conf.devices)) + for i in (0, 2, 3, 8): + dev = conf.devices[i] + self.assertTrue( + dev.driver_iommu, + "expected device %d to have driver_iommu enabled\n%s" % + (i, dev.to_xml())) + + for i in (1, 4, 6): + dev = conf.devices[i] + self.assertFalse( + dev.driver_iommu, + "didn't expect device %i to have driver_iommu enabled\n%s" % + (i, dev.to_xml())) diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 16d00a2356fc..8b8d2aab87b2 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -14,6 +14,7 @@ # under the License. import binascii +from collections import defaultdict from collections import deque from collections import OrderedDict import contextlib @@ -87,7 +88,7 @@ from nova.tests.unit import fake_diagnostics from nova.tests.unit import fake_flavor from nova.tests.unit import fake_instance from nova.tests.unit import fake_network -import nova.tests.unit.image.fake +import nova.tests.unit.image.fake as fake_image from nova.tests.unit import matchers from nova.tests.unit.objects import test_diagnostics from nova.tests.unit.objects import test_pci_device @@ -105,10 +106,12 @@ from nova.virt.image import model as imgmodel from nova.virt import images from nova.virt.libvirt import blockinfo from nova.virt.libvirt import config as vconfig +from nova.virt.libvirt import designer from nova.virt.libvirt import driver as libvirt_driver from nova.virt.libvirt import firewall from nova.virt.libvirt import guest as libvirt_guest from nova.virt.libvirt import host +from nova.virt.libvirt.host import SEV_KERNEL_PARAM_FILE from nova.virt.libvirt import imagebackend from nova.virt.libvirt import imagecache from nova.virt.libvirt import migration as libvirt_migrate @@ -2531,11 +2534,15 @@ class LibvirtConnTestCase(test.NoDBTestCase, def _test_get_guest_memory_backing_config( self, host_topology, inst_topology, numatune): drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + flavor = objects.Flavor(memory_mb=4096, vcpus=4, root_gb=496, + ephemeral_gb=8128, swap=33550336, name='fake', + extra_specs={}) + image_meta = objects.ImageMeta.from_dict(self.test_image_meta) with mock.patch.object( drvr, "_get_host_numa_topology", return_value=host_topology): return drvr._get_guest_memory_backing_config( - inst_topology, numatune, {}) + inst_topology, numatune, flavor, image_meta) @mock.patch.object(host.Host, 'has_min_version', return_value=True) @@ -2596,16 +2603,212 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertIsNone(result) def test_get_guest_memory_backing_config_realtime(self): - flavor = {"extra_specs": { + extra_specs = { "hw:cpu_realtime": "yes", "hw:cpu_policy": "dedicated" - }} + } + flavor = objects.Flavor(name='m1.small', + memory_mb=6, + vcpus=28, + root_gb=496, + ephemeral_gb=8128, + swap=33550336, + extra_specs=extra_specs) + image_meta = objects.ImageMeta.from_dict(self.test_image_meta) drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) membacking = drvr._get_guest_memory_backing_config( - None, None, flavor) + None, None, flavor, image_meta) self.assertTrue(membacking.locked) self.assertFalse(membacking.sharedpages) + def _test_sev_enabled(self, expected=None, host_sev_enabled=False, + enc_extra_spec=None, enc_image_prop=None, + hw_machine_type=None, hw_firmware_type=None): + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + drvr._host._supports_amd_sev = host_sev_enabled + + extra_specs = {} + if enc_extra_spec is not None: + extra_specs['hw:mem_encryption'] = enc_extra_spec + flavor = objects.Flavor(name='m1.fake') + flavor.extra_specs = extra_specs + + image_props = {} + if hw_machine_type is not None: + image_props['hw_machine_type'] = hw_machine_type + if hw_firmware_type is not None: + image_props['hw_firmware_type'] = hw_firmware_type + if enc_image_prop is not None: + image_props['hw_mem_encryption'] = enc_image_prop + + image_meta = fake_image.fake_image_obj( + {'id': '150d530b-1c57-4367-b754-1f1b5237923d'}, + {}, image_props) + + enabled = drvr._sev_enabled(flavor, image_meta) + + if expected is None: + self.fail("_test_sev_enabled called without an expected " + "return value. Maybe you expected an exception?") + + self.assertEqual(expected, enabled) + + def test_sev_enabled_no_host_support(self): + self._test_sev_enabled(False) + + def test_sev_enabled_host_support_no_flavor_image(self): + self._test_sev_enabled(False, host_sev_enabled=True) + + def test_sev_enabled_no_host_support_flavor_requested(self): + self._test_sev_enabled(False, enc_extra_spec=True) + + def test_sev_enabled_no_host_support_image_requested(self): + self._test_sev_enabled(False, enc_image_prop=True) + + def test_sev_enabled_host_support_flavor_requested(self): + self._test_sev_enabled(True, host_sev_enabled=True, + enc_extra_spec=True, hw_firmware_type='uefi', + hw_machine_type='q35') + + def test_sev_enabled_host_support_image_requested(self): + self._test_sev_enabled(True, host_sev_enabled=True, + enc_image_prop=True, hw_firmware_type='uefi', + hw_machine_type='q35') + + # The cases where the flavor and image requests contradict each other + # are already covered by test_hardware.MemEncryptionConflictTestCase + # so we don't need to test them in great detail here. + def test_sev_enabled_host_extra_spec_image_conflict(self): + exc = self.assertRaises(exception.FlavorImageConflict, + self._test_sev_enabled, + host_sev_enabled=True, enc_extra_spec=False, + enc_image_prop=True) + self.assertEqual( + "Flavor m1.fake has hw:mem_encryption extra spec explicitly set " + "to False, conflicting with image fake_image which has " + "hw_mem_encryption property explicitly set to True", str(exc)) + + def test_sev_enabled_host_extra_spec_no_uefi(self): + exc = self.assertRaises(exception.FlavorImageConflict, + self._test_sev_enabled, + host_sev_enabled=True, enc_extra_spec=True) + self.assertEqual( + "Memory encryption requested by hw:mem_encryption extra spec in " + "m1.fake flavor but image fake_image doesn't have " + "'hw_firmware_type' property set to 'uefi'", str(exc)) + + def test_sev_enabled_host_extra_spec_no_machine_type(self): + exc = self.assertRaises(exception.InvalidMachineType, + self._test_sev_enabled, + host_sev_enabled=True, enc_extra_spec=True, + hw_firmware_type='uefi') + self.assertEqual( + "Machine type 'pc' is not compatible with image fake_image " + "(150d530b-1c57-4367-b754-1f1b5237923d): q35 type is required " + "for SEV to work", str(exc)) + + def test_sev_enabled_host_extra_spec_pc(self): + exc = self.assertRaises(exception.InvalidMachineType, + self._test_sev_enabled, + host_sev_enabled=True, enc_extra_spec=True, + hw_firmware_type='uefi', hw_machine_type='pc') + self.assertEqual( + "Machine type 'pc' is not compatible with image fake_image " + "(150d530b-1c57-4367-b754-1f1b5237923d): q35 type is required " + "for SEV to work", str(exc)) + + def _setup_fake_domain_caps(self, fake_domain_caps): + sev_feature = vconfig.LibvirtConfigDomainCapsFeatureSev() + sev_feature.cbitpos = 47 + sev_feature.reduced_phys_bits = 1 + domain_caps = vconfig.LibvirtConfigDomainCaps() + domain_caps._features = vconfig.LibvirtConfigDomainCapsFeatures() + domain_caps._features.features = [sev_feature] + fake_domain_caps.return_value = defaultdict( + dict, {'x86_64': {'q35': domain_caps}}) + + @mock.patch.object(host.Host, 'get_domain_capabilities') + def test_find_sev_feature_missing_arch(self, fake_domain_caps): + self._setup_fake_domain_caps(fake_domain_caps) + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + self.assertIsNone(drvr._find_sev_feature('arm1', 'q35')) + + @mock.patch.object(host.Host, 'get_domain_capabilities') + def test_find_sev_feature_missing_mach_type(self, fake_domain_caps): + self._setup_fake_domain_caps(fake_domain_caps) + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + self.assertIsNone(drvr._find_sev_feature('x86_64', 'g3beige')) + + @mock.patch.object(host.Host, 'get_domain_capabilities') + def test_find_sev_feature(self, fake_domain_caps): + self._setup_fake_domain_caps(fake_domain_caps) + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + feature = drvr._find_sev_feature('x86_64', 'q35') + self.assertIsInstance(feature, + vconfig.LibvirtConfigDomainCapsFeatureSev) + self.assertEqual(47, feature.cbitpos) + self.assertEqual(1, feature.reduced_phys_bits) + + @mock.patch.object(libvirt_driver.LibvirtDriver, + "_has_uefi_support", new=mock.Mock(return_value=True)) + def _setup_sev_guest(self): + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + drvr._host._supports_amd_sev = True + + ctxt = context.RequestContext(project_id=123, + project_name="aubergine", + user_id=456, + user_name="pie") + + extra_specs = { + "hw:mem_encryption": True, + } + flavor = objects.Flavor(name='m1.small', + memory_mb=6, + vcpus=28, + root_gb=496, + ephemeral_gb=8128, + swap=33550336, + extra_specs=extra_specs) + + instance_ref = objects.Instance(**self.test_instance) + instance_ref.flavor = flavor + image_meta = objects.ImageMeta.from_dict({ + 'id': 'd9c6aeee-8258-4bdb-bca4-39940461b182', + 'name': 'fakeimage', + 'disk_format': 'raw', + 'properties': {'hw_firmware_type': 'uefi', + 'hw_machine_type': 'q35'} + }) + + disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type, + instance_ref, + image_meta) + + return drvr._get_guest_config(instance_ref, + _fake_network_info(self, 1), + image_meta, disk_info, + context=ctxt) + + def test_get_guest_config_sev_no_feature(self): + self.assertRaises(exception.MissingDomainCapabilityFeatureException, + self._setup_sev_guest) + + @mock.patch.object(host.Host, 'get_domain_capabilities') + @mock.patch.object(designer, 'set_driver_iommu_for_sev') + def test_get_guest_config_sev(self, mock_designer, fake_domain_caps): + self._setup_fake_domain_caps(fake_domain_caps) + cfg = self._setup_sev_guest() + + # SEV-related tag should be set + self.assertIsInstance(cfg.launch_security, + vconfig.LibvirtConfigGuestSEVLaunchSecurity) + self.assertIsInstance(cfg.membacking, + vconfig.LibvirtConfigGuestMemoryBacking) + self.assertTrue(cfg.membacking.locked) + + mock_designer.assert_called_once_with(cfg) + def test_get_guest_memory_backing_config_file_backed(self): self.flags(file_backed_memory=1024, group="libvirt") @@ -5822,6 +6025,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertEqual(cfg.devices[5].rate_period, 2) @mock.patch('nova.virt.libvirt.driver.os.path.exists') + @test.patch_exists(SEV_KERNEL_PARAM_FILE, False) def test_get_guest_config_with_rng_backend(self, mock_path): self.flags(virt_type='kvm', rng_dev_path='/dev/hw_rng', @@ -6500,6 +6704,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, "_get_guest_storage_config") @mock.patch.object(libvirt_driver.LibvirtDriver, "_has_numa_support") @mock.patch('os.path.exists', return_value=True) + @test.patch_exists(SEV_KERNEL_PARAM_FILE, False) def test_get_guest_config_aarch64(self, mock_path_exists, mock_numa, mock_storage, mock_get_arch): def get_host_capabilities_stub(self): @@ -6554,6 +6759,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, "_get_guest_storage_config") @mock.patch.object(libvirt_driver.LibvirtDriver, "_has_numa_support") @mock.patch('os.path.exists', return_value=True) + @test.patch_exists(SEV_KERNEL_PARAM_FILE, False) def test_get_guest_config_aarch64_with_graphics(self, mock_path_exists, mock_numa, mock_storage, mock_get_arch): @@ -6592,26 +6798,33 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertTrue(usbhost_exists) self.assertTrue(keyboard_exists) - def test_get_guest_config_machine_type_through_image_meta(self): - self.flags(virt_type="kvm", - group='libvirt') + def _get_guest_config_machine_type_through_image_meta(self, mach_type): + self.flags(virt_type="kvm", group='libvirt') drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) instance_ref = objects.Instance(**self.test_instance) image_meta = objects.ImageMeta.from_dict({ "disk_format": "raw", - "properties": {"hw_machine_type": - "fake_machine_type"}}) + "properties": {"hw_machine_type": mach_type}}) disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type, instance_ref, image_meta) - cfg = drvr._get_guest_config(instance_ref, - _fake_network_info(self, 1), - image_meta, disk_info) + return drvr._get_guest_config(instance_ref, + _fake_network_info(self, 1), + image_meta, disk_info) + + def test_get_guest_config_machine_type_through_image_meta(self): + cfg = self._get_guest_config_machine_type_through_image_meta( + "fake_machine_type") self.assertEqual(cfg.os_mach_type, "fake_machine_type") + def test_get_guest_config_machine_type_through_image_meta_sev(self): + fake_q35 = "fake-q35-2.11" + cfg = self._get_guest_config_machine_type_through_image_meta(fake_q35) + self.assertEqual(cfg.os_mach_type, fake_q35) + def test_get_guest_config_machine_type_from_config(self): self.flags(virt_type='kvm', group='libvirt') self.flags(hw_machine_type=['x86_64=fake_machine_type'], diff --git a/nova/tests/unit/virt/libvirt/test_utils.py b/nova/tests/unit/virt/libvirt/test_utils.py index fe475723a99d..51badaa249dc 100644 --- a/nova/tests/unit/virt/libvirt/test_utils.py +++ b/nova/tests/unit/virt/libvirt/test_utils.py @@ -993,3 +993,10 @@ sunrpc /var/lib/nfs/rpc_pipefs rpc_pipefs rw,relatime 0 0 group="libvirt", hw_machine_type=['x86_64=q35', 'foo'])) self.assertEqual('q35', libvirt_utils.get_default_machine_type('x86_64')) + + def test_get_machine_type_from_image(self): + image_meta = objects.ImageMeta.from_dict({ + "disk_format": "raw", "properties": {"hw_machine_type": "q35"} + }) + os_mach_type = libvirt_utils.get_machine_type(image_meta) + self.assertEqual('q35', os_mach_type) diff --git a/nova/tests/unit/virt/libvirt/test_vif.py b/nova/tests/unit/virt/libvirt/test_vif.py index bff9e1a60453..841a3de3f5c8 100644 --- a/nova/tests/unit/virt/libvirt/test_vif.py +++ b/nova/tests/unit/virt/libvirt/test_vif.py @@ -596,6 +596,9 @@ class LibvirtVifTestCase(test.NoDBTestCase): is_public=True, vcpu_weight=None, id=2, disabled=False, rxtx_factor=1.0) + if image_meta is None: + image_meta = objects.ImageMeta.from_dict({}) + conf = self._get_conf() hostimpl = host.Host("qemu:///system") with mock.patch.object(hostimpl, 'has_min_version', diff --git a/nova/virt/hardware.py b/nova/virt/hardware.py index 3f4513fe1284..fd8dcec2a514 100644 --- a/nova/virt/hardware.py +++ b/nova/virt/hardware.py @@ -1137,7 +1137,7 @@ def _get_flavor_image_meta(key, flavor, image_meta, default=None): return flavor_policy, image_policy -def get_mem_encryption_constraint(flavor, image_meta): +def get_mem_encryption_constraint(flavor, image_meta, machine_type=None): """Return a boolean indicating whether encryption of guest memory was requested, either via the hw:mem_encryption extra spec or the hw_mem_encryption image property (or both). @@ -1156,12 +1156,16 @@ def get_mem_encryption_constraint(flavor, image_meta): 3) the flavor and/or image request memory encryption, but the machine type is set to a value which does not contain 'q35' - This is called from the API layer, so get_machine_type() cannot be - called since it relies on being run from the compute node in order - to retrieve CONF.libvirt.hw_machine_type. + This can be called from the libvirt driver on the compute node, in + which case the driver should pass the result of + nova.virt.libvirt.utils.get_machine_type() as the machine_type + parameter, or from the API layer, in which case get_machine_type() + cannot be called since it relies on being run from the compute + node in order to retrieve CONF.libvirt.hw_machine_type. :param instance_type: Flavor object :param image: an ImageMeta object + :param machine_type: a string representing the machine type (optional) :raises: nova.exception.FlavorImageConflict :raises: nova.exception.InvalidMachineType :returns: boolean indicating whether encryption of guest memory @@ -1196,7 +1200,7 @@ def get_mem_encryption_constraint(flavor, image_meta): image_meta.name) _check_mem_encryption_uses_uefi_image(requesters, image_meta) - _check_mem_encryption_machine_type(image_meta) + _check_mem_encryption_machine_type(image_meta, machine_type) LOG.debug("Memory encryption requested by %s", " and ".join(requesters)) return True @@ -1236,7 +1240,7 @@ def _check_mem_encryption_uses_uefi_image(requesters, image_meta): raise exception.FlavorImageConflict(emsg % data) -def _check_mem_encryption_machine_type(image_meta): +def _check_mem_encryption_machine_type(image_meta, machine_type=None): # NOTE(aspiers): As explained in the SEV spec, SEV needs a q35 # machine type in order to bind all the virtio devices to the PCIe # bridge so that they use virtio 1.0 and not virtio 0.9, since @@ -1247,10 +1251,12 @@ def _check_mem_encryption_machine_type(image_meta): # So if the image explicitly requests a machine type which is not # in the q35 family, raise an exception. # - # Note that this check occurs at API-level, therefore we can't - # check here what value of CONF.libvirt.hw_machine_type may have - # been configured on the compute node. - mach_type = image_meta.properties.get('hw_machine_type') + # This check can be triggered both at API-level, at which point we + # can't check here what value of CONF.libvirt.hw_machine_type may + # have been configured on the compute node, and by the libvirt + # driver, in which case the driver can check that config option + # and will pass the machine_type parameter. + mach_type = machine_type or image_meta.properties.get('hw_machine_type') # If hw_machine_type is not specified on the image and is not # configured correctly on SEV compute nodes, then a separate check diff --git a/nova/virt/libvirt/config.py b/nova/virt/libvirt/config.py index 814a42288670..cb699e472130 100644 --- a/nova/virt/libvirt/config.py +++ b/nova/virt/libvirt/config.py @@ -2991,6 +2991,7 @@ class LibvirtConfigGuestRng(LibvirtConfigGuestDevice): super(LibvirtConfigGuestRng, self).__init__(root_name="rng", **kwargs) + self.device_model = 'virtio' self.model = 'random' self.backend = None self.rate_period = None @@ -2999,7 +3000,7 @@ class LibvirtConfigGuestRng(LibvirtConfigGuestDevice): def format_dom(self): dev = super(LibvirtConfigGuestRng, self).format_dom() - dev.set('model', 'virtio') + dev.set('model', self.device_model) backend = etree.Element("backend") backend.set("model", self.model) diff --git a/nova/virt/libvirt/designer.py b/nova/virt/libvirt/designer.py index 2c8326edec0d..793f1f17cdb1 100644 --- a/nova/virt/libvirt/designer.py +++ b/nova/virt/libvirt/designer.py @@ -21,6 +21,7 @@ classes based on common operational needs / policies from nova.pci import utils as pci_utils +from nova.virt.libvirt import config MIN_LIBVIRT_ETHERNET_SCRIPT_PATH_NONE = (1, 3, 3) @@ -196,3 +197,17 @@ def set_vcpu_realtime_scheduler(conf, vcpus_rt, priority): conf.vcpus = vcpus_rt conf.scheduler = "fifo" conf.priority = priority + + +def set_driver_iommu_for_sev(conf): + virtio_attrs = { + config.LibvirtConfigGuestDisk: 'target_bus', + config.LibvirtConfigGuestInterface: 'model', + config.LibvirtConfigGuestRng: 'device_model', + config.LibvirtConfigMemoryBalloon: 'model', + } + + for dev in conf.devices: + virtio_attr = virtio_attrs.get(dev.__class__) + if virtio_attr and getattr(dev, virtio_attr) == 'virtio': + dev.driver_iommu = True diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index a2f0c113465f..95c01b42ffc4 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -4888,7 +4888,7 @@ class LibvirtDriver(driver.ComputeDriver): self._add_rng_device(guest, flavor) def _get_guest_memory_backing_config( - self, inst_topology, numatune, flavor): + self, inst_topology, numatune, flavor, image_meta): wantsmempages = False if inst_topology: for cell in inst_topology.cells: @@ -4928,6 +4928,10 @@ class LibvirtDriver(driver.ComputeDriver): MIN_LIBVIRT_FILE_BACKED_DISCARD_VERSION, MIN_QEMU_FILE_BACKED_DISCARD_VERSION): membacking.discard = True + if self._sev_enabled(flavor, image_meta): + if not membacking: + membacking = vconfig.LibvirtConfigGuestMemoryBacking() + membacking.locked = True return membacking @@ -5034,7 +5038,8 @@ class LibvirtDriver(driver.ComputeDriver): return True def _configure_guest_by_virt_type(self, guest, virt_type, caps, instance, - image_meta, flavor, root_device_name): + image_meta, flavor, root_device_name, + sev_enabled): if virt_type == "xen": if guest.os_type == fields.VMMode.HVM: guest.os_loader = CONF.libvirt.xen_hvmloader_path @@ -5367,7 +5372,7 @@ class LibvirtDriver(driver.ComputeDriver): guest.membacking = self._get_guest_memory_backing_config( instance.numa_topology, guest_numa_config.numatune, - flavor) + flavor, image_meta) guest.metadata.append(self._get_guest_config_meta(instance)) guest.idmaps = self._get_guest_idmaps() @@ -5399,9 +5404,11 @@ class LibvirtDriver(driver.ComputeDriver): self._get_guest_os_type(virt_type)) caps = self._host.get_capabilities() + sev_enabled = self._sev_enabled(flavor, image_meta) + self._configure_guest_by_virt_type(guest, virt_type, caps, instance, image_meta, flavor, - root_device_name) + root_device_name, sev_enabled) if virt_type not in ('lxc', 'uml'): self._conf_non_lxc_uml(virt_type, guest, root_device_name, rescue, instance, inst_path, image_meta, disk_info) @@ -5457,8 +5464,92 @@ class LibvirtDriver(driver.ComputeDriver): if mdevs: self._guest_add_mdevs(guest, mdevs) + if sev_enabled: + self._guest_configure_sev(guest, caps.host.cpu.arch, + guest.os_mach_type) + return guest + def _sev_enabled(self, flavor, image_meta): + """To enable AMD SEV, the following should be true: + + a) the supports_amd_sev instance variable in the host is + true, + b) the instance extra specs and/or image properties request + memory encryption to be enabled, and + c) there are no conflicts between extra specs, image properties + and machine type selection. + + Most potential conflicts in c) should already be caught in the + API layer. However there is still one remaining case which + needs to be handled here: when the image does not contain an + hw_machine_type property, the machine type will be chosen from + CONF.libvirt.hw_machine_type if configured, otherwise falling + back to the hardcoded value which is currently 'pc'. If it + ends up being 'pc' or another value not in the q35 family, we + need to raise an exception. So calculate the machine type and + pass it to be checked alongside the other sanity checks which + are run while determining whether SEV is selected. + """ + if not self._host.supports_amd_sev: + return False + + mach_type = libvirt_utils.get_machine_type(image_meta) + return hardware.get_mem_encryption_constraint(flavor, image_meta, + mach_type) + + def _guest_configure_sev(self, guest, arch, mach_type): + sev = self._find_sev_feature(arch, mach_type) + if sev is None: + # In theory this should never happen because it should + # only get called if SEV was requested, in which case the + # guest should only get scheduled on this host if it + # supports SEV, and SEV support is dependent on the + # presence of this feature. That said, it's + # conceivable that something could get messed up along the + # way, e.g. a mismatch in the choice of machine type. So + # make sure that if it ever does happen, we at least get a + # helpful error rather than something cryptic like + # "AttributeError: 'NoneType' object has no attribute 'cbitpos' + raise exception.MissingDomainCapabilityFeatureException( + feature='sev') + + designer.set_driver_iommu_for_sev(guest) + self._guest_add_launch_security(guest, sev) + + def _guest_add_launch_security(self, guest, sev): + launch_security = vconfig.LibvirtConfigGuestSEVLaunchSecurity() + launch_security.cbitpos = sev.cbitpos + launch_security.reduced_phys_bits = sev.reduced_phys_bits + guest.launch_security = launch_security + + def _find_sev_feature(self, arch, mach_type): + """Search domain capabilities for the given arch and machine type + for the element under , and return it if found. + """ + domain_caps = self._host.get_domain_capabilities() + if arch not in domain_caps: + LOG.warning( + "Wanted to add SEV to config for guest with arch %(arch)s " + "but only had domain capabilities for: %(archs)s", + {'arch': arch, 'archs': ' '.join(domain_caps)}) + return None + + if mach_type not in domain_caps[arch]: + LOG.warning( + "Wanted to add SEV to config for guest with machine type " + "%(mtype)s but for arch %(arch)s only had domain capabilities " + "for machine types: %(mtypes)s", + {'mtype': mach_type, 'arch': arch, + 'mtypes': ' '.join(domain_caps[arch])}) + return None + + for feature in domain_caps[arch][mach_type].features: + if feature.root_name == 'sev': + return feature + + return None + def _guest_add_mdevs(self, guest, chosen_mdevs): for chosen_mdev in chosen_mdevs: mdev = vconfig.LibvirtConfigGuestHostdevMDEV()