From 1038a633870b24b2409205cc3c5fe066442dd49f Mon Sep 17 00:00:00 2001 From: Nobuhiro MIKI Date: Fri, 15 Sep 2023 16:48:18 +0900 Subject: [PATCH] libvirt: Support maxphysaddr. With Libvirt v8.7.0+, the sub-element of the element specifies the number of vCPU physical address bits [1]. [1] https://libvirt.org/news.html#v8-7-0-2022-09-01 New flavor extra_specs and image properties are added to control the physical address bits of vCPUs in Libvirt guests. The nova-scheduler requests COMPUTE_ADDRESS_SPACE_* traits based on them. The traits are already defined in os-traits v2.10.0. Also numerical comparisons are performed at both compute capabilities filter and image props filter. blueprint: libvirt-maxphysaddr-support-caracal Change-Id: I98968f6ef1621c9fb4f682c119038e26d62ce381 Signed-off-by: Nobuhiro MIKI --- nova/objects/fields.py | 11 +++ nova/objects/image_meta.py | 13 ++- .../filters/compute_capabilities_filter.py | 10 +- nova/scheduler/filters/image_props_filter.py | 14 ++- nova/scheduler/utils.py | 19 ++++ .../objects/test_notification.py | 2 +- nova/tests/unit/objects/test_image_meta.py | 16 ++++ nova/tests/unit/objects/test_objects.py | 2 +- .../test_compute_capabilities_filters.py | 24 +++++ .../filters/test_image_props_filters.py | 34 +++++++ nova/tests/unit/scheduler/test_utils.py | 41 +++++++++ nova/tests/unit/virt/libvirt/test_config.py | 55 +++++++++++ nova/tests/unit/virt/libvirt/test_driver.py | 91 ++++++++++++++++++- nova/tests/unit/virt/test_hardware.py | 46 ++++++++++ nova/virt/hardware.py | 27 ++++++ nova/virt/libvirt/config.py | 43 +++++++++ nova/virt/libvirt/driver.py | 26 ++++++ ...maxphysaddr-support-7d03db9e0491515f9.yaml | 6 ++ 18 files changed, 473 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/bp-libvirt-maxphysaddr-support-7d03db9e0491515f9.yaml diff --git a/nova/objects/fields.py b/nova/objects/fields.py index d0ce37d8a759..332acf95a68b 100644 --- a/nova/objects/fields.py +++ b/nova/objects/fields.py @@ -560,6 +560,13 @@ class TPMVersion(BaseNovaEnum): ALL = (v1_2, v2_0) +class MaxPhyAddrMode(BaseNovaEnum): + PASSTHROUGH = "passthrough" + EMULATE = "emulate" + + ALL = (PASSTHROUGH, EMULATE) + + class SCSIModel(BaseNovaEnum): BUSLOGIC = "buslogic" @@ -1294,6 +1301,10 @@ class InputBusField(BaseEnumField): AUTO_TYPE = InputBus() +class MaxPhysAddrModeField(BaseEnumField): + AUTO_TYPE = MaxPhyAddrMode() + + class MigrationTypeField(BaseEnumField): AUTO_TYPE = MigrationType() diff --git a/nova/objects/image_meta.py b/nova/objects/image_meta.py index 903266982221..0afb0e31385b 100644 --- a/nova/objects/image_meta.py +++ b/nova/objects/image_meta.py @@ -193,14 +193,19 @@ class ImageMetaProps(base.NovaObject): # Version 1.33: Added 'hw_locked_memory' field # Version 1.34: Added 'hw_viommu_model' field # Version 1.35: Added 'hw_virtio_packed_ring' field + # Version 1.36: Added 'hw_maxphysaddr_mode' and + # 'hw_maxphysaddr_bits' field # NOTE(efried): When bumping this version, the version of # ImageMetaPropsPayload must also be bumped. See its docstring for details. - VERSION = '1.35' + VERSION = '1.36' def obj_make_compatible(self, primitive, target_version): super(ImageMetaProps, self).obj_make_compatible(primitive, target_version) target_version = versionutils.convert_version_to_tuple(target_version) + if target_version < (1, 36): + primitive.pop('hw_maxphysaddr_mode', None) + primitive.pop('hw_maxphysaddr_bits', None) if target_version < (1, 35): primitive.pop('hw_virtio_packed_ring', None) if target_version < (1, 34): @@ -479,6 +484,12 @@ class ImageMetaProps(base.NovaObject): # boolean - If true, this will enable the virtio packed ring feature 'hw_virtio_packed_ring': fields.FlexibleBooleanField(), + # Control mode for the physical memory address bit of Libvirt guests. + 'hw_maxphysaddr_mode': fields.MaxPhysAddrModeField(), + + # Control bits for the physical memory address bit of Libvirt guests. + 'hw_maxphysaddr_bits': fields.IntegerField(), + # if true download using bittorrent 'img_bittorrent': fields.FlexibleBooleanField(), diff --git a/nova/scheduler/filters/compute_capabilities_filter.py b/nova/scheduler/filters/compute_capabilities_filter.py index 981886761841..9b984c9e726d 100644 --- a/nova/scheduler/filters/compute_capabilities_filter.py +++ b/nova/scheduler/filters/compute_capabilities_filter.py @@ -72,7 +72,15 @@ class ComputeCapabilitiesFilter(filters.BaseHostFilter): if 'extra_specs' not in flavor: return True - for key, req in flavor.extra_specs.items(): + especs = flavor.extra_specs.copy() + + # Replace it with a capabilities filter specially. + bits = especs.get('hw:maxphysaddr_bits') + if bits is not None: + especs['capabilities:cpu_info:maxphysaddr:bits'] = '>= ' + bits + del especs['hw:maxphysaddr_bits'] + + for key, req in especs.items(): # Either not scope format, or in capabilities scope scope = key.split(':') # If key does not have a namespace, the scope's size is 1, check diff --git a/nova/scheduler/filters/image_props_filter.py b/nova/scheduler/filters/image_props_filter.py index e1084902372a..ab2c219396c0 100644 --- a/nova/scheduler/filters/image_props_filter.py +++ b/nova/scheduler/filters/image_props_filter.py @@ -89,10 +89,22 @@ class ImagePropertiesFilter(filters.BaseHostFilter): hyper_ver_str = versionutils.convert_version_to_str(hyper_version) return img_prop_predicate.satisfied_by(hyper_ver_str) + def _compare_maxphysaddr_bits(host_state, image_props): + bits_required = image_props.get('hw_maxphysaddr_bits') + if not bits_required: + return True + + bits = host_state.cpu_info.get('maxphysaddr', {}).get('bits') + if not bits: + return True + + return bits >= bits_required + for supp_inst in supp_instances: if _compare_props(checked_img_props, supp_inst): if _compare_product_version(hypervisor_version, image_props): - return True + if _compare_maxphysaddr_bits(host_state, image_props): + return True LOG.debug("Instance contains properties %(image_props)s " "that are not provided by the compute node " diff --git a/nova/scheduler/utils.py b/nova/scheduler/utils.py index 02c44093bdd0..961ef93e3060 100644 --- a/nova/scheduler/utils.py +++ b/nova/scheduler/utils.py @@ -194,6 +194,8 @@ class ResourceRequest(object): res_req._translate_secure_boot_request(request_spec.flavor, image) + res_req._translate_maxphysaddr_request(request_spec.flavor, image) + res_req.strip_zeros() return res_req @@ -271,6 +273,23 @@ class ResourceRequest(object): self._add_trait(trait, 'required') LOG.debug("Requiring secure boot support via trait %s.", trait) + def _translate_maxphysaddr_request(self, flavor, image): + mode = hardware.get_maxphysaddr_mode(flavor, image) + + if mode is None: + return + + trait = None + + if mode == obj_fields.MaxPhyAddrMode.PASSTHROUGH: + trait = os_traits.COMPUTE_ADDRESS_SPACE_PASSTHROUGH + elif mode == obj_fields.MaxPhyAddrMode.EMULATE: + trait = os_traits.COMPUTE_ADDRESS_SPACE_EMULATED + + if trait: + self._add_trait(trait, 'required') + LOG.debug("Requiring maxphysaddr support via trait %s.", trait) + def _translate_vtpm_request(self, flavor, image): vtpm_config = hardware.get_vtpm_constraint(flavor, image) if not vtpm_config: diff --git a/nova/tests/unit/notifications/objects/test_notification.py b/nova/tests/unit/notifications/objects/test_notification.py index 6e4e5935e2eb..8375631e9739 100644 --- a/nova/tests/unit/notifications/objects/test_notification.py +++ b/nova/tests/unit/notifications/objects/test_notification.py @@ -386,7 +386,7 @@ notification_object_data = { # ImageMetaProps, so when you see a fail here for that reason, you must # *also* bump the version of ImageMetaPropsPayload. See its docstring for # more information. - 'ImageMetaPropsPayload': '1.13-24345c28a6463e85e12902d43af0ecf2', + 'ImageMetaPropsPayload': '1.13-2859fbb81af5ad2f83a0e9be0b30ea60', 'InstanceActionNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceActionPayload': '1.8-4fa3da9cbf0761f1f700ae578f36dc2f', 'InstanceActionRebuildNotification': diff --git a/nova/tests/unit/objects/test_image_meta.py b/nova/tests/unit/objects/test_image_meta.py index 371f7b101a55..a5ac3f42c86b 100644 --- a/nova/tests/unit/objects/test_image_meta.py +++ b/nova/tests/unit/objects/test_image_meta.py @@ -358,6 +358,8 @@ class TestImageMetaProps(test.NoDBTestCase): 'img_hv_requested_version': '>= 1.0', 'os_require_quiesce': True, 'os_secure_boot': 'required', + 'hw_maxphysaddr_mode': 'passthrough', + 'hw_maxphysaddr_bits': 42, 'hw_rescue_bus': 'ide', 'hw_rescue_device': 'disk', 'hw_watchdog_action': fields.WatchdogAction.DISABLED, @@ -499,6 +501,20 @@ class TestImageMetaProps(test.NoDBTestCase): secure_props = objects.ImageMetaProps.from_dict(props) self.assertEqual("required", secure_props.os_secure_boot) + def test_set_hw_maxphysaddr_mode(self): + props = {'hw_maxphysaddr_mode': "passthrough"} + obj = objects.ImageMetaProps.from_dict(props) + self.assertEqual("passthrough", obj.hw_maxphysaddr_mode) + + def test_set_hw_maxphysaddr_mode_negative(self): + props = {'hw_maxphysaddr_mode': "blah"} + self.assertRaises(ValueError, objects.ImageMetaProps.from_dict, props) + + def test_set_hw_maxphysaddr_bits(self): + props = {'hw_maxphysaddr_bits': 42} + obj = objects.ImageMetaProps.from_dict(props) + self.assertEqual(42, obj.hw_maxphysaddr_bits) + def test_obj_make_compatible_img_hide_hypervisor_id(self): """Tests that checks if we pop img_hide_hypervisor_id.""" obj = objects.ImageMetaProps(img_hide_hypervisor_id=True) diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index 45c2941e1d41..9c177277cf18 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1105,7 +1105,7 @@ object_data = { 'HyperVLiveMigrateData': '1.4-e265780e6acfa631476c8170e8d6fce0', 'IDEDeviceBus': '1.0-29d4c9f27ac44197f01b6ac1b7e16502', 'ImageMeta': '1.8-642d1b2eb3e880a367f37d72dd76162d', - 'ImageMetaProps': '1.35-66ec4135a4c08d6e67e39cb0400b059e', + 'ImageMetaProps': '1.36-5d06cd527d8d32e6e2b457ce3b9369e0', 'Instance': '2.8-2727dba5e4a078e6cc848c1f94f7eb24', 'InstanceAction': '1.2-9a5abc87fdd3af46f45731960651efb5', 'InstanceActionEvent': '1.4-5b1f361bd81989f8bb2c20bb7e8a4cb4', diff --git a/nova/tests/unit/scheduler/filters/test_compute_capabilities_filters.py b/nova/tests/unit/scheduler/filters/test_compute_capabilities_filters.py index cbb8c3160169..321b8deb62b0 100644 --- a/nova/tests/unit/scheduler/filters/test_compute_capabilities_filters.py +++ b/nova/tests/unit/scheduler/filters/test_compute_capabilities_filters.py @@ -102,6 +102,30 @@ class TestComputeCapabilitiesFilter(test.NoDBTestCase): especs={'capabilities:cpu_info:vendor': 'Intel'}, passes=False) + def test_compute_filter_pass_maxphysaddr_bits(self): + cpu_info = '{"maxphysaddr": {"mode": "emulate", "bits": 42}}' + + self._do_test_compute_filter_extra_specs( + ecaps={'cpu_info': cpu_info}, + especs={'hw:maxphysaddr_bits': '42'}, + passes=True) + + def test_compute_filter_fails_maxphysaddr_bits(self): + cpu_info = '{"maxphysaddr": {"mode": "emulate", "bits": 20}}' + + self._do_test_compute_filter_extra_specs( + ecaps={'cpu_info': cpu_info}, + especs={'hw:maxphysaddr_bits': '42'}, + passes=False) + + def test_compute_filter_fails_without_maxphysaddr_bits(self): + cpu_info = '{ }' + + self._do_test_compute_filter_extra_specs( + ecaps={'cpu_info': cpu_info}, + especs={'hw:maxphysaddr_bits': '42'}, + passes=False) + def test_compute_filter_passes_extra_specs_simple(self): self._do_test_compute_filter_extra_specs( ecaps={'stats': {'opt1': 1, 'opt2': 2}}, diff --git a/nova/tests/unit/scheduler/filters/test_image_props_filters.py b/nova/tests/unit/scheduler/filters/test_image_props_filters.py index da7a3a8cd0be..10d4ac62b778 100644 --- a/nova/tests/unit/scheduler/filters/test_image_props_filters.py +++ b/nova/tests/unit/scheduler/filters/test_image_props_filters.py @@ -267,3 +267,37 @@ class TestImagePropsFilter(test.NoDBTestCase): 'hypervisor_version': hypervisor_version} host = fakes.FakeHostState('host1', 'node1', capabilities) self.assertTrue(self.filt_cls.host_passes(host, spec_obj)) + + def test_image_properties_filter_pass_maxphysaddr_bits(self): + img_props = objects.ImageMeta( + properties=objects.ImageMetaProps( + hw_architecture=obj_fields.Architecture.X86_64, + img_hv_type=obj_fields.HVType.KVM, + hw_vm_mode=obj_fields.VMMode.HVM, + hw_maxphysaddr_bits=42)) + spec_obj = objects.RequestSpec(image=img_props) + capabilities = { + 'supported_instances': [( + obj_fields.Architecture.X86_64, + obj_fields.HVType.KVM, + obj_fields.VMMode.HVM)], + 'cpu_info': {"maxphysaddr": {"mode": "emulate", "bits": 42}}} + host = fakes.FakeHostState('host1', 'node1', capabilities) + self.assertTrue(self.filt_cls.host_passes(host, spec_obj)) + + def test_image_properties_filter_fails_maxphysaddr_bits(self): + img_props = objects.ImageMeta( + properties=objects.ImageMetaProps( + hw_architecture=obj_fields.Architecture.X86_64, + img_hv_type=obj_fields.HVType.KVM, + hw_vm_mode=obj_fields.VMMode.HVM, + hw_maxphysaddr_bits=42)) + spec_obj = objects.RequestSpec(image=img_props) + capabilities = { + 'supported_instances': [( + obj_fields.Architecture.X86_64, + obj_fields.HVType.KVM, + obj_fields.VMMode.HVM)], + 'cpu_info': {"maxphysaddr": {"mode": "emulate", "bits": 20}}} + host = fakes.FakeHostState('host1', 'node1', capabilities) + self.assertFalse(self.filt_cls.host_passes(host, spec_obj)) diff --git a/nova/tests/unit/scheduler/test_utils.py b/nova/tests/unit/scheduler/test_utils.py index 55957f3d5518..d7382228e889 100644 --- a/nova/tests/unit/scheduler/test_utils.py +++ b/nova/tests/unit/scheduler/test_utils.py @@ -1358,6 +1358,47 @@ class TestUtils(TestUtilsBase): rr = utils.ResourceRequest.from_request_spec(rs) self.assertResourceRequestsEqual(expected, rr) + def test_resource_request_from_request_spec_with_maxphysaddr_passthrough( + self + ): + flavor = objects.Flavor( + vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0, + extra_specs={'hw:maxphysaddr_mode': 'passthrough'} + ) + expected = FakeResourceRequest() + expected._rg_by_id[None] = objects.RequestGroup( + use_same_provider=False, + required_traits={'COMPUTE_ADDRESS_SPACE_PASSTHROUGH'}, + resources={ + 'VCPU': 1, + 'MEMORY_MB': 1024, + 'DISK_GB': 15, + }, + ) + rs = objects.RequestSpec(flavor=flavor, is_bfv=False) + rr = utils.ResourceRequest.from_request_spec(rs) + self.assertResourceRequestsEqual(expected, rr) + + def test_resource_request_from_request_spec_with_maxphysaddr_emulate(self): + flavor = objects.Flavor( + vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0, + extra_specs={'hw:maxphysaddr_mode': 'emulate', + 'hw_maxphysaddr_bits': 42}, + ) + expected = FakeResourceRequest() + expected._rg_by_id[None] = objects.RequestGroup( + use_same_provider=False, + required_traits={'COMPUTE_ADDRESS_SPACE_EMULATED'}, + resources={ + 'VCPU': 1, + 'MEMORY_MB': 1024, + 'DISK_GB': 15, + }, + ) + rs = objects.RequestSpec(flavor=flavor, is_bfv=False) + rr = utils.ResourceRequest.from_request_spec(rs) + self.assertResourceRequestsEqual(expected, rr) + def test_resource_request_from_request_groups(self): rgs = objects.RequestGroup.from_extended_port_request( self.context, diff --git a/nova/tests/unit/virt/libvirt/test_config.py b/nova/tests/unit/virt/libvirt/test_config.py index 55d8b9b05b4d..58f1740683f9 100644 --- a/nova/tests/unit/virt/libvirt/test_config.py +++ b/nova/tests/unit/virt/libvirt/test_config.py @@ -520,6 +520,32 @@ class LibvirtConfigCPUTest(LibvirtConfigBaseTest): """) + def test_config_maxphysaddr(self): + obj = config.LibvirtConfigCPU() + obj.maxphysaddr = config.LibvirtConfigCPUMaxPhysAddr() + obj.maxphysaddr.mode = "emulate" + obj.maxphysaddr.bits = 42 + + xml = obj.to_xml() + self.assertXmlEqual(xml, """ + + + + """) + + def test_parse_dom(self): + xml = """ + + + + """ + xmldoc = etree.fromstring(xml) + obj = config.LibvirtConfigCPU() + obj.parse_dom(xmldoc) + + self.assertEqual("emulate", obj.maxphysaddr.mode) + self.assertEqual(42, obj.maxphysaddr.bits) + class LibvirtConfigGuestCPUTest(LibvirtConfigBaseTest): @@ -599,6 +625,35 @@ class LibvirtConfigGuestCPUTest(LibvirtConfigBaseTest): """) + def test_config_host_with_maxphysaddr_emulate(self): + obj = config.LibvirtConfigGuestCPU() + + m = config.LibvirtConfigGuestCPUMaxPhysAddr() + m.mode = "emulate" + m.bits = 42 + obj.maxphysaddr = m + + xml = obj.to_xml() + self.assertXmlEqual(xml, """ + + + + """) + + def test_config_host_with_maxphysaddr_passthrough(self): + obj = config.LibvirtConfigGuestCPU() + + m = config.LibvirtConfigGuestCPUMaxPhysAddr() + m.mode = "passthrough" + obj.maxphysaddr = m + + xml = obj.to_xml() + self.assertXmlEqual(xml, """ + + + + """) + class LibvirtConfigGuestSMBIOSTest(LibvirtConfigBaseTest): diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index fa351a648c45..f54d29bc521d 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -8614,6 +8614,86 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertEqual(conf.cpu.cores, 1) self.assertEqual(conf.cpu.threads, 1) + def test_get_guest_cpu_config_maxphysaddr_passthrough_flavor_espec(self): + self.flags(cpu_mode="none", group='libvirt') + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + instance_ref = objects.Instance(**self.test_instance) + instance_ref.flavor.extra_specs = { + 'hw:maxphysaddr_mode': 'passthrough'} + image_meta = objects.ImageMeta.from_dict(self.test_image_meta) + + disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type, + instance_ref, + image_meta) + conf = drvr._get_guest_config(instance_ref, + _fake_network_info(self), + image_meta, disk_info) + self.assertIsInstance(conf.cpu.maxphysaddr, + vconfig.LibvirtConfigGuestCPUMaxPhysAddr) + self.assertEqual(conf.cpu.maxphysaddr.mode, 'passthrough') + self.assertIsNone(conf.cpu.maxphysaddr.bits) + + def test_get_guest_cpu_config_maxphysaddr_emulate_flavor_espec(self): + self.flags(cpu_mode="none", group='libvirt') + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + instance_ref = objects.Instance(**self.test_instance) + instance_ref.flavor.extra_specs = { + 'hw:maxphysaddr_mode': 'emulate', + 'hw:maxphysaddr_bits': 42} + image_meta = objects.ImageMeta.from_dict(self.test_image_meta) + + disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type, + instance_ref, + image_meta) + conf = drvr._get_guest_config(instance_ref, + _fake_network_info(self), + image_meta, disk_info) + self.assertIsInstance(conf.cpu.maxphysaddr, + vconfig.LibvirtConfigGuestCPUMaxPhysAddr) + self.assertEqual(conf.cpu.maxphysaddr.mode, 'emulate') + self.assertEqual(conf.cpu.maxphysaddr.bits, 42) + + def test_get_guest_cpu_config_maxphysaddr_passthrough_image_meta(self): + self.flags(cpu_mode="none", 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_maxphysaddr_mode": "passthrough"}, + }) + + disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type, + instance_ref, + image_meta) + conf = drvr._get_guest_config(instance_ref, + _fake_network_info(self), + image_meta, disk_info) + self.assertIsInstance(conf.cpu.maxphysaddr, + vconfig.LibvirtConfigGuestCPUMaxPhysAddr) + self.assertEqual(conf.cpu.maxphysaddr.mode, 'passthrough') + self.assertIsNone(conf.cpu.maxphysaddr.bits) + + def test_get_guest_cpu_config_maxphysaddr_emulate_image_meta(self): + self.flags(cpu_mode="none", 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_maxphysaddr_mode": "emulate", + "hw_maxphysaddr_bits": 42}, + }) + + disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type, + instance_ref, + image_meta) + conf = drvr._get_guest_config(instance_ref, + _fake_network_info(self), + image_meta, disk_info) + self.assertIsInstance(conf.cpu.maxphysaddr, + vconfig.LibvirtConfigGuestCPUMaxPhysAddr) + self.assertEqual(conf.cpu.maxphysaddr.mode, 'emulate') + self.assertEqual(conf.cpu.maxphysaddr.bits, 42) + def test_get_guest_cpu_topology(self): instance_ref = objects.Instance(**self.test_instance) instance_ref.flavor.vcpus = 8 @@ -18515,6 +18595,10 @@ class LibvirtConnTestCase(test.NoDBTestCase, cpu.threads = 1 cpu.sockets = 4 + cpu.maxphysaddr = vconfig.LibvirtConfigCPUMaxPhysAddr() + cpu.maxphysaddr.mode = "emulate" + cpu.maxphysaddr.bits = 42 + cpu.add_feature(vconfig.LibvirtConfigCPUFeature("extapic")) cpu.add_feature(vconfig.LibvirtConfigCPUFeature("3dnow")) @@ -18541,6 +18625,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, want = {"vendor": "AMD", "features": set(["extapic", "3dnow"]), + "maxphysaddr": {"mode": "emulate", "bits": 42}, "model": "Opteron_G4", "arch": fields.Architecture.X86_64, "topology": {"cells": 1, "cores": 2, "threads": 1, @@ -21907,7 +21992,8 @@ class HostStateTestCase(test.NoDBTestCase): "features": ["ssse3", "monitor", "pni", "sse2", "sse", "fxsr", "clflush", "pse36", "pat", "cmov", "mca", "pge", "mtrr", "sep", "apic"], - "topology": {"cores": "1", "threads": "1", "sockets": "1"}} + "topology": {"cores": "1", "threads": "1", "sockets": "1"}, + "maxphysaddr": {"mode": "emulate", "bits": "42"}} instance_caps = [(fields.Architecture.X86_64, "kvm", "hvm"), (fields.Architecture.I686, "kvm", "hvm")] pci_devices = [{ @@ -22025,7 +22111,8 @@ class HostStateTestCase(test.NoDBTestCase): "features": ["ssse3", "monitor", "pni", "sse2", "sse", "fxsr", "clflush", "pse36", "pat", "cmov", "mca", "pge", "mtrr", "sep", "apic"], - "topology": {"cores": "1", "threads": "1", "sockets": "1"} + "topology": {"cores": "1", "threads": "1", "sockets": "1"}, + "maxphysaddr": {"mode": "emulate", "bits": "42"} }) self.assertEqual(stats["disk_available_least"], 80) self.assertEqual(jsonutils.loads(stats["pci_passthrough_devices"]), diff --git a/nova/tests/unit/virt/test_hardware.py b/nova/tests/unit/virt/test_hardware.py index ffecf4e38e97..be4ccb17ddc5 100644 --- a/nova/tests/unit/virt/test_hardware.py +++ b/nova/tests/unit/virt/test_hardware.py @@ -5860,6 +5860,52 @@ class SecureBootPolicyTest(test.NoDBTestCase): ) +@ddt.ddt +class MaxphysaddrModeTest(test.NoDBTestCase): + + @ddt.unpack + @ddt.data( + # pass: no configuration + (None, None, None), + # pass: flavor-only configuration + ('passthrough', None, 'passthrough'), + # pass: image-only configuration + (None, 'emulate', 'emulate'), + # pass: identical image and flavor configuration + ('passthrough', 'passthrough', 'passthrough'), + # fail: mismatched image and flavor configuration + ('passthrough', 'emulate', exception.FlavorImageConflict), + # fail: invalid value + ('foobar', None, exception.Invalid), + ) + def test_get_maxphysaddr_mode( + self, flavor_policy, image_policy, expected, + ): + extra_specs = {} + + if flavor_policy: + extra_specs['hw:maxphysaddr_mode'] = flavor_policy + + image_meta_props = {} + + if image_policy: + image_meta_props['hw_maxphysaddr_mode'] = image_policy + + flavor = objects.Flavor( + name='foo', vcpus=1, memory_mb=1024, extra_specs=extra_specs) + 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_maxphysaddr_mode, flavor, image_meta, + ) + else: + self.assertEqual( + expected, hw.get_maxphysaddr_mode(flavor, image_meta), + ) + + @ddt.ddt class RescuePropertyTestCase(test.NoDBTestCase): diff --git a/nova/virt/hardware.py b/nova/virt/hardware.py index 00c1b4af8bee..e869af876730 100644 --- a/nova/virt/hardware.py +++ b/nova/virt/hardware.py @@ -2735,6 +2735,33 @@ def get_vpmems(flavor): return formed_labels +def get_maxphysaddr_mode( + flavor: 'objects.Flavor', + image_meta: 'objects.ImageMeta', +) -> ty.Optional[str]: + """Return maxphysaddr mode. + + :param flavor: a flavor object to read extra specs from + :param image_meta: an objects.ImageMeta object + :raises: nova.exception.Invalid if a value is invalid + :returns: maxphysaddr mode if a value is valid, else None. + """ + mode = _get_unique_flavor_image_meta( + 'maxphysaddr_mode', flavor, image_meta, prefix='hw', + ) + + if mode is None: + return None + + if mode not in fields.MaxPhyAddrMode.ALL: + raise exception.Invalid( + "Invalid Maxphyaddr mode %(mode)r. Allowed values: %(valid)s." % + {'mode': mode, 'valid': ', '.join(fields.MaxPhyAddrMode.ALL)} + ) + + return mode + + def check_hw_rescue_props(image_meta): """Confirm that hw_rescue_* image properties are present. """ diff --git a/nova/virt/libvirt/config.py b/nova/virt/libvirt/config.py index 164af286dc4f..9d9f734f0135 100644 --- a/nova/virt/libvirt/config.py +++ b/nova/virt/libvirt/config.py @@ -793,6 +793,36 @@ class LibvirtConfigCPUFeature(LibvirtConfigObject): return hash(self.name) +class LibvirtConfigCPUMaxPhysAddr(LibvirtConfigObject): + + def __init__(self, **kwargs): + super(LibvirtConfigCPUMaxPhysAddr, self).__init__( + root_name='maxphysaddr', **kwargs) + + self.mode = None + self.bits = None + + def parse_dom(self, xmldoc): + super(LibvirtConfigCPUMaxPhysAddr, self).parse_dom(xmldoc) + + self.mode = xmldoc.get("mode") + self.bits = int(xmldoc.get("bits")) + + def format_dom(self): + m = super(LibvirtConfigCPUMaxPhysAddr, self).format_dom() + + m.set("mode", self.mode) + + if self.bits: + m.set("bits", str(self.bits)) + + return m + + +class LibvirtConfigGuestCPUMaxPhysAddr(LibvirtConfigCPUMaxPhysAddr): + pass + + class LibvirtConfigCPU(LibvirtConfigObject): def __init__(self, **kwargs): @@ -807,6 +837,8 @@ class LibvirtConfigCPU(LibvirtConfigObject): self.cores = None self.threads = None + self.maxphysaddr = None + self.features = set() def parse_dom(self, xmldoc): @@ -823,6 +855,9 @@ class LibvirtConfigCPU(LibvirtConfigObject): self.sockets = int(c.get("sockets")) self.cores = int(c.get("cores")) self.threads = int(c.get("threads")) + elif c.tag == "maxphysaddr": + self.maxphysaddr = LibvirtConfigCPUMaxPhysAddr() + self.maxphysaddr.parse_dom(c) elif c.tag == "feature": f = LibvirtConfigCPUFeature() f.parse_dom(c) @@ -848,6 +883,9 @@ class LibvirtConfigCPU(LibvirtConfigObject): top.set("threads", str(self.threads)) cpu.append(top) + if self.maxphysaddr is not None: + cpu.append(self.maxphysaddr.format_dom()) + # sorting the features to allow more predictable tests for f in sorted(self.features, key=lambda x: x.name): cpu.append(f.format_dom()) @@ -942,6 +980,7 @@ class LibvirtConfigGuestCPU(LibvirtConfigCPU): self.mode = None self.match = "exact" self.numa = None + self.maxphysaddr = None def parse_dom(self, xmldoc): super(LibvirtConfigGuestCPU, self).parse_dom(xmldoc) @@ -952,6 +991,10 @@ class LibvirtConfigGuestCPU(LibvirtConfigCPU): numa = LibvirtConfigGuestCPUNUMA() numa.parse_dom(child) self.numa = numa + elif child.tag == "maxphysaddr": + m = LibvirtConfigGuestCPUMaxPhysAddr() + m.parse_dom(child) + self.maxphysaddr = m def format_dom(self): cpu = super(LibvirtConfigGuestCPU, self).format_dom() diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 87ba7efc8616..906540887e9f 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -5475,6 +5475,23 @@ class LibvirtDriver(driver.ComputeDriver): cpu.add_feature(cpu_feature) return cpu + def _get_guest_cpu_config_maxphysaddr(self, flavor, image_meta): + mode = (flavor.extra_specs.get('hw:maxphysaddr_mode') or + image_meta.properties.get('hw_maxphysaddr_mode')) + bits = (flavor.extra_specs.get('hw:maxphysaddr_bits') or + image_meta.properties.get('hw_maxphysaddr_bits')) + + if not mode: + return None + + maxphysaddr = vconfig.LibvirtConfigGuestCPUMaxPhysAddr() + maxphysaddr.mode = mode + + if bits: + maxphysaddr.bits = int(bits) + + return maxphysaddr + def _match_cpu_model_by_flags(self, models, flags): for model in models: if flags.issubset(self.cpu_model_flag_mapping.get(model, set([]))): @@ -5509,6 +5526,9 @@ class LibvirtDriver(driver.ComputeDriver): cpu.threads = topology.threads cpu.numa = guest_cpu_numa_config + cpu.maxphysaddr = self._get_guest_cpu_config_maxphysaddr(flavor, + image_meta) + caps = self._host.get_capabilities() if arch != caps.host.cpu.arch: # Try emulating. Other arch configs will go here @@ -8234,6 +8254,12 @@ class LibvirtDriver(driver.ComputeDriver): topology['threads'] = caps.host.cpu.threads cpu_info['topology'] = topology + if caps.host.cpu.maxphysaddr: + maxphysaddr = dict() + maxphysaddr["mode"] = caps.host.cpu.maxphysaddr.mode + maxphysaddr["bits"] = caps.host.cpu.maxphysaddr.bits + cpu_info["maxphysaddr"] = maxphysaddr + features = set() for f in caps.host.cpu.features: features.add(f.name) diff --git a/releasenotes/notes/bp-libvirt-maxphysaddr-support-7d03db9e0491515f9.yaml b/releasenotes/notes/bp-libvirt-maxphysaddr-support-7d03db9e0491515f9.yaml new file mode 100644 index 000000000000..607cfabeda0f --- /dev/null +++ b/releasenotes/notes/bp-libvirt-maxphysaddr-support-7d03db9e0491515f9.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added new flavor extra_specs and image properties to control the physical + address bits of vCPUs in Libvirt guests. This option is used to boot a + guest with large RAM.