From eb8519d811f1c08bc964146f484367c858b8f0c2 Mon Sep 17 00:00:00 2001 From: Danylo Vodopianov Date: Thu, 2 Mar 2023 13:44:02 +0200 Subject: [PATCH] Packed virtqueue support was added. 1) Extend flavor/image extra spec. 2) New xml parameter for qemu command was added. 3) New request filter added for scheduler. 4) Unit and Functional tests were updated 5) Requirments was updated ( os-traits = 3.0.0 ) 6) Releasnote was added Nova spec: https://review.opendev.org/c/openstack/nova-specs/+/868377 Depends-On: https://review.opendev.org/c/openstack/os-traits/+/876069 Change-Id: I789eeae86947e9a3cbd7d5fcc58d2aabe3b8b84c --- .../ImageMetaPropsPayload.json | 2 +- nova/api/validation/extra_specs/hw.py | 12 ++++ nova/compute/api.py | 3 + nova/notifications/objects/image.py | 3 +- nova/objects/image_meta.py | 8 ++- nova/scheduler/request_filter.py | 17 +++++ nova/tests/functional/integrated_helpers.py | 27 +++++++ .../test_instance.py | 4 +- nova/tests/functional/test_servers.py | 71 +++++++++++++++++++ .../objects/test_notification.py | 2 +- nova/tests/unit/objects/test_objects.py | 2 +- .../unit/scheduler/test_request_filter.py | 35 +++++++++ nova/tests/unit/virt/libvirt/test_config.py | 20 ++++++ nova/tests/unit/virt/libvirt/test_designer.py | 4 +- nova/tests/unit/virt/libvirt/test_driver.py | 4 +- nova/tests/unit/virt/libvirt/test_vif.py | 38 +++++++++- nova/tests/unit/virt/test_hardware.py | 50 ++++++++++++- nova/virt/hardware.py | 50 +++++++++++++ nova/virt/libvirt/config.py | 8 ++- nova/virt/libvirt/designer.py | 4 +- nova/virt/libvirt/driver.py | 13 ++-- nova/virt/libvirt/vif.py | 15 +++- ...ked-virtqueue-filter-43a376674cb5b345.yaml | 19 +++++ requirements.txt | 2 +- 24 files changed, 388 insertions(+), 25 deletions(-) create mode 100644 releasenotes/notes/packed-virtqueue-filter-43a376674cb5b345.yaml diff --git a/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json b/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json index cdde7d3097af..913c75c02744 100644 --- a/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json +++ b/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json @@ -4,5 +4,5 @@ "hw_architecture": "x86_64" }, "nova_object.name": "ImageMetaPropsPayload", - "nova_object.version": "1.12" + "nova_object.version": "1.13" } diff --git a/nova/api/validation/extra_specs/hw.py b/nova/api/validation/extra_specs/hw.py index c0c8f02809ed..a930ec407853 100644 --- a/nova/api/validation/extra_specs/hw.py +++ b/nova/api/validation/extra_specs/hw.py @@ -527,6 +527,18 @@ feature_flag_validators = [ 'description': 'model for vIOMMU', }, ), + base.ExtraSpecValidator( + name='hw:virtio_packed_ring', + description=( + 'Permit guests to negotiate the virtio packed ring format. ' + 'This requires guest support and is only supported by ' + 'the libvirt driver.' + ), + value={ + 'type': bool, + 'description': 'Whether to enable packed virtqueue', + }, + ), ] ephemeral_encryption_validators = [ diff --git a/nova/compute/api.py b/nova/compute/api.py index 7eb409a25e5b..4b178a37a77a 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -751,6 +751,9 @@ class API: if flavor['memory_mb'] < int(image.get('min_ram') or 0): raise exception.FlavorMemoryTooSmall() + # Verify flavor/image Virtio Packed Ring configuration conflict. + hardware.get_packed_virtqueue_constraint(flavor, image) + # Image min_disk is in gb, size is in bytes. For sanity, have them both # in bytes. image_min_disk = int(image.get('min_disk') or 0) * units.Gi diff --git a/nova/notifications/objects/image.py b/nova/notifications/objects/image.py index 01c86d1cb04e..d13ff6845f73 100644 --- a/nova/notifications/objects/image.py +++ b/nova/notifications/objects/image.py @@ -130,7 +130,8 @@ class ImageMetaPropsPayload(base.NotificationPayloadBase): # 'hw_ephemeral_encryption_format' fields # Version 1.11: Added 'hw_locked_memory' field # Version 1.12: Added 'hw_viommu_model' field - VERSION = '1.12' + # Version 1.13: Added 'hw_virtio_packed_ring' field + VERSION = '1.13' SCHEMA = { k: ('image_meta_props', k) for k in image_meta.ImageMetaProps.fields} diff --git a/nova/objects/image_meta.py b/nova/objects/image_meta.py index 7927ad257526..903266982221 100644 --- a/nova/objects/image_meta.py +++ b/nova/objects/image_meta.py @@ -192,14 +192,17 @@ class ImageMetaProps(base.NovaObject): # 'hw_ephemeral_encryption_format' fields # 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 # NOTE(efried): When bumping this version, the version of # ImageMetaPropsPayload must also be bumped. See its docstring for details. - VERSION = '1.34' + VERSION = '1.35' 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, 35): + primitive.pop('hw_virtio_packed_ring', None) if target_version < (1, 34): primitive.pop('hw_viommu_model', None) if target_version < (1, 33): @@ -473,6 +476,9 @@ class ImageMetaProps(base.NovaObject): 'hw_ephemeral_encryption_format': fields.BlockDeviceEncryptionFormatTypeField(), + # boolean - If true, this will enable the virtio packed ring feature + 'hw_virtio_packed_ring': fields.FlexibleBooleanField(), + # if true download using bittorrent 'img_bittorrent': fields.FlexibleBooleanField(), diff --git a/nova/scheduler/request_filter.py b/nova/scheduler/request_filter.py index 31b7f2c0ac88..6f61e861cc0f 100644 --- a/nova/scheduler/request_filter.py +++ b/nova/scheduler/request_filter.py @@ -271,6 +271,22 @@ def accelerators_filter(ctxt, request_spec): return True +@trace_request_filter +def packed_virtqueue_filter(ctxt, request_spec): + """Allow only compute nodes with Packed virtqueue. + + This filter retains only nodes whose compute manager published the + COMPUTE_NET_VIRTIO_PACKED trait, thus indicates virtqueue packed feature. + """ + trait_name = os_traits.COMPUTE_NET_VIRTIO_PACKED + if (hardware.get_packed_virtqueue_constraint(request_spec.flavor, + request_spec.image)): + request_spec.root_required.add(trait_name) + LOG.debug('virtqueue_filter request filter added required ' + 'trait %s', trait_name) + return True + + @trace_request_filter def routed_networks_filter( ctxt: nova_context.RequestContext, @@ -436,6 +452,7 @@ ALL_REQUEST_FILTERS = [ isolate_aggregates, transform_image_metadata, accelerators_filter, + packed_virtqueue_filter, routed_networks_filter, remote_managed_ports_filter, ephemeral_encryption_filter, diff --git a/nova/tests/functional/integrated_helpers.py b/nova/tests/functional/integrated_helpers.py index 0d592fbdbeab..02a786d8b349 100644 --- a/nova/tests/functional/integrated_helpers.py +++ b/nova/tests/functional/integrated_helpers.py @@ -18,6 +18,7 @@ Provides common functionality for integrated unit tests """ import collections +import datetime import random import re import string @@ -394,6 +395,32 @@ class InstanceHelperMixin: return flavor['id'] + def _create_image(self, metadata): + image = { + 'id': 'c456eb30-91d7-4f43-8f46-2efd9eccd744', + 'name': 'fake-image-custom-property', + 'created_at': datetime.datetime(2011, 1, 1, 1, 2, 3), + 'updated_at': datetime.datetime(2011, 1, 1, 1, 2, 3), + 'deleted_at': None, + 'deleted': False, + 'status': 'active', + 'is_public': False, + 'container_format': 'raw', + 'disk_format': 'raw', + 'size': '25165824', + 'min_ram': 0, + 'min_disk': 0, + 'protected': False, + 'visibility': 'public', + 'tags': ['tag1', 'tag2'], + 'properties': { + 'kernel_id': 'nokernel', + 'ramdisk_id': 'nokernel', + }, + } + image['properties'].update(metadata) + return self.glance.create(None, image) + def _build_server(self, name=None, image_uuid=None, flavor_id=None, networks=None, az=None, host=None): """Build a request for the server create API. diff --git a/nova/tests/functional/notification_sample_tests/test_instance.py b/nova/tests/functional/notification_sample_tests/test_instance.py index 1db77b91ddf2..1f6d8d2efeff 100644 --- a/nova/tests/functional/notification_sample_tests/test_instance.py +++ b/nova/tests/functional/notification_sample_tests/test_instance.py @@ -1238,7 +1238,7 @@ class TestInstanceNotificationSample( 'nova_object.data': {}, 'nova_object.name': 'ImageMetaPropsPayload', 'nova_object.namespace': 'nova', - 'nova_object.version': '1.12', + 'nova_object.version': '1.13', }, 'image.size': 58145823, 'image.tags': [], @@ -1334,7 +1334,7 @@ class TestInstanceNotificationSample( 'nova_object.data': {}, 'nova_object.name': 'ImageMetaPropsPayload', 'nova_object.namespace': 'nova', - 'nova_object.version': '1.12', + 'nova_object.version': '1.13', }, 'image.size': 58145823, 'image.tags': [], diff --git a/nova/tests/functional/test_servers.py b/nova/tests/functional/test_servers.py index a9c3d9acbf3e..f08dac00ab30 100644 --- a/nova/tests/functional/test_servers.py +++ b/nova/tests/functional/test_servers.py @@ -2188,6 +2188,77 @@ class ServerMovingTests(integrated_helpers.ProviderUsageBaseTestCase): self.assert_hypervisor_usage( dest_rp_uuid, self.flavor2, volume_backed=False) + def test_resize_server_conflict(self): + + # Set appropriate traits for Resource Provider + rp_uuid1 = self._get_provider_uuid_by_host(self.compute1.host) + self._set_provider_traits(rp_uuid1, ['COMPUTE_NET_VIRTIO_PACKED']) + + # Create image + image = self._create_image(metadata={'hw_virtio_packed_ring': 'true'}) + + # Create server + server = self._build_server(image_uuid=image['id'], networks='none') + created_server = self.api.post_server({"server": server}) + created_server_id = created_server['id'] + found_server = self._wait_for_state_change(created_server, 'ACTIVE') + + # Create a flavor with conflict in relation to the image configuration + flavor_id = self._create_flavor( + extra_spec={'hw:virtio_packed_ring': 'false'}) + + # Resize server(flavorRef: 1 -> 2) + post = {'resize': {"flavorRef": flavor_id}} + + ex = self.assertRaises(client.OpenStackApiException, + self.api.post_server_action, + created_server_id, post) + + # By returning 400, We want to confirm that the RESIZE server + # does not cause unexpected behavior. + self.assertEqual(400, ex.response.status_code) + + # Verify that the instance is still in the Active state + self.assertEqual('ACTIVE', found_server['status']) + + # Cleanup + self._delete_server(found_server) + + def test_rebuild_server_conflict(self): + + # Set appropriate traits for Resource Provider + rp_uuid1 = self._get_provider_uuid_by_host(self.compute1.host) + self._set_provider_traits(rp_uuid1, ['COMPUTE_NET_VIRTIO_PACKED']) + + # Create flavor + flavor_id = self._create_flavor( + extra_spec={'hw:virtio_packed_ring': 'true'}) + + # Create server + server = self._build_server(flavor_id=flavor_id, networks='none') + created_server = self.api.post_server({"server": server}) + created_server_id = created_server['id'] + found_server = self._wait_for_state_change(created_server, 'ACTIVE') + + # Create an image with conflict in relation to the flavor configuration + image = self._create_image(metadata={'hw_virtio_packed_ring': 'false'}) + + # Now rebuild the server with a different image + post = {'rebuild': {'imageRef': image['id']}} + ex = self.assertRaises(client.OpenStackApiException, + self.api.post_server_action, + created_server_id, post) + + # By returning 400, We want to confirm that the RESIZE server + # does not cause unexpected behavior. + self.assertEqual(400, ex.response.status_code) + + # Verify that the instance is still in the Active state + self.assertEqual('ACTIVE', found_server['status']) + + # Cleanup + self._delete_server(found_server) + def test_evacuate_with_no_compute(self): source_hostname = self.compute1.host dest_hostname = self.compute2.host diff --git a/nova/tests/unit/notifications/objects/test_notification.py b/nova/tests/unit/notifications/objects/test_notification.py index de9e6f276232..6e4e5935e2eb 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.12-b9c64832d7772c1973e913bacbe0e8f9', + 'ImageMetaPropsPayload': '1.13-24345c28a6463e85e12902d43af0ecf2', 'InstanceActionNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceActionPayload': '1.8-4fa3da9cbf0761f1f700ae578f36dc2f', 'InstanceActionRebuildNotification': diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index 5ccb32e6133c..2568e099fc76 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.34-29b3a6b7fe703f36bfd240d914f16c21', + 'ImageMetaProps': '1.35-66ec4135a4c08d6e67e39cb0400b059e', 'Instance': '2.8-2727dba5e4a078e6cc848c1f94f7eb24', 'InstanceAction': '1.2-9a5abc87fdd3af46f45731960651efb5', 'InstanceActionEvent': '1.4-5b1f361bd81989f8bb2c20bb7e8a4cb4', diff --git a/nova/tests/unit/scheduler/test_request_filter.py b/nova/tests/unit/scheduler/test_request_filter.py index cedfd5066499..4a15f6b56ae6 100644 --- a/nova/tests/unit/scheduler/test_request_filter.py +++ b/nova/tests/unit/scheduler/test_request_filter.py @@ -499,6 +499,41 @@ class TestRequestFilter(test.NoDBTestCase): # Assert about logging mock_log.assert_not_called() + @mock.patch.object(request_filter, 'LOG') + def test_virtio_filter_with_packed_ring_in_flavor(self, mock_log): + # First ensure that packed_virtqueue_filter is included + self.assertIn(request_filter.packed_virtqueue_filter, + request_filter.ALL_REQUEST_FILTERS) + + es = {'hw:virtio_packed_ring': 'true'} + reqspec = objects.RequestSpec( + flavor=objects.Flavor(extra_specs=es), + image=objects.ImageMeta(properties=objects.ImageMetaProps())) + self.assertEqual(set(), reqspec.root_required) + self.assertEqual(set(), reqspec.root_forbidden) + + # Request filter puts the trait into the request spec + request_filter.packed_virtqueue_filter(self.context, reqspec) + self.assertEqual({ot.COMPUTE_NET_VIRTIO_PACKED}, reqspec.root_required) + self.assertEqual(set(), reqspec.root_forbidden) + + @mock.patch.object(request_filter, 'LOG') + def test_virtio_filter_with_packed_ring_in_image(self, mock_log): + # First ensure that packed_virtqueue_filter is included + self.assertIn(request_filter.packed_virtqueue_filter, + request_filter.ALL_REQUEST_FILTERS) + + reqspec = objects.RequestSpec(flavor=objects.Flavor(extra_specs={}), + image=objects.ImageMeta( + properties=objects.ImageMetaProps(hw_virtio_packed_ring=True))) + self.assertEqual(set(), reqspec.root_required) + self.assertEqual(set(), reqspec.root_forbidden) + + # Request filter puts the trait into the request spec + request_filter.packed_virtqueue_filter(self.context, reqspec) + self.assertEqual({ot.COMPUTE_NET_VIRTIO_PACKED}, reqspec.root_required) + self.assertEqual(set(), reqspec.root_forbidden) + def test_routed_networks_filter_not_enabled(self): self.assertIn(request_filter.routed_networks_filter, request_filter.ALL_REQUEST_FILTERS) diff --git a/nova/tests/unit/virt/libvirt/test_config.py b/nova/tests/unit/virt/libvirt/test_config.py index 7402a09647e7..3b04466e6c62 100644 --- a/nova/tests/unit/virt/libvirt/test_config.py +++ b/nova/tests/unit/virt/libvirt/test_config.py @@ -1923,6 +1923,26 @@ class LibvirtConfigGuestInterfaceTest(LibvirtConfigBaseTest): self.assertTrue(obj.uses_virtio) return obj + def test_config_driver_packed_options(self): + obj = self._get_virtio_interface() + obj.driver_name = "vhost" + obj.driver_packed = True + + xml = obj.to_xml() + self.assertXmlEqual(xml, """ + + + + + + """) + + # parse the xml from the first object into a new object and make sure + # they are the same + obj2 = config.LibvirtConfigGuestInterface() + obj2.parse_str(xml) + self.assertXmlEqual(xml, obj2.to_xml()) + def test_config_driver_options(self): obj = self._get_virtio_interface() obj.driver_name = "vhost" diff --git a/nova/tests/unit/virt/libvirt/test_designer.py b/nova/tests/unit/virt/libvirt/test_designer.py index cb435286e99d..78551a9e7ce3 100644 --- a/nova/tests/unit/virt/libvirt/test_designer.py +++ b/nova/tests/unit/virt/libvirt/test_designer.py @@ -40,7 +40,7 @@ class DesignerTestCase(test.NoDBTestCase): conf = config.LibvirtConfigGuestInterface() designer.set_vif_guest_frontend_config(conf, 'fake-mac', 'fake-model', 'fake-driver', - 'fake-queues', None) + 'fake-queues', None, None) self.assertEqual('fake-mac', conf.mac_addr) self.assertEqual('fake-model', conf.model) self.assertEqual('fake-driver', conf.driver_name) @@ -51,7 +51,7 @@ class DesignerTestCase(test.NoDBTestCase): conf = config.LibvirtConfigGuestInterface() designer.set_vif_guest_frontend_config(conf, 'fake-mac', 'fake-model', 'fake-driver', - 'fake-queues', 1024) + 'fake-queues', 1024, None) self.assertEqual('fake-mac', conf.mac_addr) self.assertEqual('fake-model', conf.model) self.assertEqual('fake-driver', conf.driver_name) diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index baabe14487b6..168a94950d77 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -992,6 +992,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, expected = { 'COMPUTE_GRAPHICS_MODEL_VGA': True, 'COMPUTE_NET_VIF_MODEL_VIRTIO': True, + 'COMPUTE_NET_VIRTIO_PACKED': True, 'COMPUTE_SECURITY_TPM_1_2': False, 'COMPUTE_SECURITY_TPM_2_0': False, 'COMPUTE_STORAGE_BUS_VIRTIO': True, @@ -1030,7 +1031,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, @mock.patch.object(libvirt_driver.LibvirtDriver, '_get_storage_bus_traits') @mock.patch.object(libvirt_driver.LibvirtDriver, '_get_video_model_traits') @mock.patch.object(libvirt_driver.LibvirtDriver, '_get_vif_model_traits') - def test_static_traits__invalid_trait( + def test_static_traits_invalid_trait( self, mock_vif_traits, mock_video_traits, mock_storage_traits, mock_cpu_traits, mock_log, ): @@ -1043,6 +1044,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) expected = { 'COMPUTE_NET_VIF_MODEL_VIRTIO': True, + 'COMPUTE_NET_VIRTIO_PACKED': True, 'COMPUTE_SECURITY_TPM_1_2': False, 'COMPUTE_SECURITY_TPM_2_0': False, 'COMPUTE_VIOMMU_MODEL_AUTO': True, diff --git a/nova/tests/unit/virt/libvirt/test_vif.py b/nova/tests/unit/virt/libvirt/test_vif.py index 6d87ed727c83..efd1058ca520 100644 --- a/nova/tests/unit/virt/libvirt/test_vif.py +++ b/nova/tests/unit/virt/libvirt/test_vif.py @@ -725,6 +725,38 @@ class LibvirtVifTestCase(test.NoDBTestCase): self.assertEqual(4, conf.vhost_queues) self.assertIsNone(conf.driver_name) + def _test_virtio_packed_config(self, image_meta, flavor): + d = vif.LibvirtGenericVIFDriver() + + xml = self._get_instance_xml(d, self.vif_bridge, + image_meta, flavor) + + node = self._get_node(xml) + packed = node.find("driver").get("packed") + self.assertEqual(packed, 'on') + + def test_image_packed_config(self): + extra_specs = {} + extra_specs['hw:virtio_packed_ring'] = True + + flavor = objects.Flavor( + name='foo', vcpus=2, memory_mb=1024, extra_specs=extra_specs) + image_meta = objects.ImageMeta.from_dict( + {'name': 'bar', 'properties': {}}) + + self._test_virtio_packed_config(image_meta, flavor) + + def test_flavor_packed_config(self): + image_meta_props = {} + image_meta_props['hw_virtio_packed_ring'] = True + + flavor = objects.Flavor( + name='foo', vcpus=2, memory_mb=1024, extra_specs={}) + image_meta = objects.ImageMeta.from_dict( + {'name': 'bar', 'properties': image_meta_props}) + + self._test_virtio_packed_config(image_meta, flavor) + def _test_virtio_config_queue_sizes( self, vnic_type=network_model.VNIC_TYPE_NORMAL): self.flags(rx_queue_size=512, group='libvirt') @@ -895,7 +927,7 @@ class LibvirtVifTestCase(test.NoDBTestCase): d.get_base_config(None, 'ca:fe:de:ad:be:ef', image_meta, flavor, 'kvm', 'normal') mock_set.assert_called_once_with(mock.ANY, 'ca:fe:de:ad:be:ef', - 'virtio', None, None, None) + 'virtio', None, None, None, False) @mock.patch.object(vif.designer, 'set_vif_guest_frontend_config', wraps=vif.designer.set_vif_guest_frontend_config) @@ -911,9 +943,9 @@ class LibvirtVifTestCase(test.NoDBTestCase): image_meta = objects.ImageMeta.from_dict( {'properties': {'hw_vif_model': 'virtio'}}) conf = d.get_base_config(None, 'ca:fe:de:ad:be:ef', image_meta, - None, 'kvm', vnic_type) + objects.Flavor(vcpus=2), 'kvm', vnic_type) mock_set.assert_called_once_with(mock.ANY, 'ca:fe:de:ad:be:ef', - None, None, None, None) + None, None, None, None, False) self.assertIsNone(conf.vhost_queues) self.assertIsNone(conf.driver_name) self.assertIsNone(conf.model) diff --git a/nova/tests/unit/virt/test_hardware.py b/nova/tests/unit/virt/test_hardware.py index 381213f9720d..11c8c7c748c2 100644 --- a/nova/tests/unit/virt/test_hardware.py +++ b/nova/tests/unit/virt/test_hardware.py @@ -5701,11 +5701,57 @@ class VIFMultiqueueEnabledTest(test.NoDBTestCase): if isinstance(expected, type) and issubclass(expected, Exception): self.assertRaises( - expected, hw.get_vif_multiqueue_constraint, flavor, image_meta, + expected, hw.get_vif_multiqueue_constraint, flavor, image_meta ) else: self.assertEqual( - expected, hw.get_vif_multiqueue_constraint(flavor, image_meta), + expected, hw.get_vif_multiqueue_constraint(flavor, image_meta) + ) + + +@ddt.ddt +class VIFVirtioEnabledTest(test.NoDBTestCase): + + @ddt.unpack + @ddt.data( + # pass: no configuration + (None, None, False), + # pass: flavor-only configuration + ('yes', None, True), + # pass: image-only configuration + (None, True, True), + # pass: identical image and flavor configuration + ('yes', True, True), + # fail: mismatched image and flavor configuration + ('no', True, exception.FlavorImageConflict), + ) + def test_get_vif_virtio_constraint( + self, flavor_policy, image_policy, expected, + ): + extra_specs = {} + + if flavor_policy: + extra_specs['hw:virtio_packed_ring'] = flavor_policy + + image_meta_props = {} + + if image_policy: + image_meta_props['hw_virtio_packed_ring'] = image_policy + + flavor = objects.Flavor( + name='foo', vcpus=2, 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_packed_virtqueue_constraint, + flavor, image_meta, + ) + else: + self.assertEqual( + expected, hw.get_packed_virtqueue_constraint( + flavor, image_meta), ) diff --git a/nova/virt/hardware.py b/nova/virt/hardware.py index 5ca0dda5967f..cdf6dd4086bb 100644 --- a/nova/virt/hardware.py +++ b/nova/virt/hardware.py @@ -1941,6 +1941,56 @@ def get_vif_multiqueue_constraint( return flavor_value or image_value or False +def get_packed_virtqueue_constraint( + flavor, + image_meta, +) -> bool: + """Validate and return the requested Packed virtqueue configuration. + + :param flavor: ``nova.objects.Flavor`` or dict instance + :param image_meta: ``nova.objects.ImageMeta`` or dict instance + :raises: nova.exception.FlavorImageConflict if a value is specified in both + the flavor and the image, but the values do not match + :returns: True if the Packed virtqueue must be enabled, else False. + """ + key_value = 'virtio_packed_ring' + + if type(image_meta) is dict: + flavor_key = ':'.join(['hw', key_value]) + image_key = '_'.join(['hw', key_value]) + flavor_value_str = flavor.get('extra_specs', {}).get(flavor_key, None) + image_value = image_meta.get('properties', {}).get(image_key, None) + else: + flavor_value_str, image_value = _get_flavor_image_meta( + key_value, flavor, image_meta) + + flavor_value = None + if flavor_value_str is not None: + flavor_value = strutils.bool_from_string(flavor_value_str) + + if ( + image_value is not None and + flavor_value is not None and + image_value != flavor_value + ): + msg = _( + "Flavor has %(prefix)s:%(key)s extra spec " + "explicitly set to %(flavor_val)s, conflicting with image " + "which has %(prefix)s_%(key)s explicitly set to " + "%(image_val)s." + ) + raise exception.FlavorImageConflict( + msg % { + 'prefix': 'hw', + 'key': key_value, + 'flavor_val': flavor_value, + 'image_val': image_value, + } + ) + + return flavor_value or image_value or False + + def get_vtpm_constraint( flavor: 'objects.Flavor', image_meta: 'objects.ImageMeta', diff --git a/nova/virt/libvirt/config.py b/nova/virt/libvirt/config.py index 2a1e703d31d2..c3c82f8c6ef1 100644 --- a/nova/virt/libvirt/config.py +++ b/nova/virt/libvirt/config.py @@ -1759,6 +1759,7 @@ class LibvirtConfigGuestInterface(LibvirtConfigGuestDevice): self.filterparams = [] self.driver_name = None self.driver_iommu = False + self.driver_packed = False self.vhostuser_mode = None self.vhostuser_path = None self.vhostuser_type = None @@ -1811,6 +1812,7 @@ class LibvirtConfigGuestInterface(LibvirtConfigGuestDevice): drv_elem = None if (self.driver_name or self.driver_iommu or + self.driver_packed or self.net_type == "vhostuser"): drv_elem = etree.Element("driver") @@ -1819,6 +1821,8 @@ class LibvirtConfigGuestInterface(LibvirtConfigGuestDevice): drv_elem.set("name", self.driver_name) if self.driver_iommu: drv_elem.set("iommu", "on") + if self.driver_packed: + drv_elem.set("packed", "on") if drv_elem is not None: if self.vhost_queues is not None: @@ -1831,7 +1835,8 @@ class LibvirtConfigGuestInterface(LibvirtConfigGuestDevice): if (drv_elem.get('name') or drv_elem.get('queues') or drv_elem.get('rx_queue_size') or drv_elem.get('tx_queue_size') or - drv_elem.get('iommu')): + drv_elem.get('iommu') or + drv_elem.get('packed')): # Append the driver element into the dom only if name # or queues or tx/rx or iommu attributes are set. dev.append(drv_elem) @@ -1931,6 +1936,7 @@ class LibvirtConfigGuestInterface(LibvirtConfigGuestDevice): elif c.tag == 'driver': self.driver_name = c.get('name') self.driver_iommu = (c.get('iommu', '') == 'on') + self.driver_packed = (c.get('packed', '') == 'on') self.vhost_queues = c.get('queues') self.vhost_rx_queue_size = c.get('rx_queue_size') self.vhost_tx_queue_size = c.get('tx_queue_size') diff --git a/nova/virt/libvirt/designer.py b/nova/virt/libvirt/designer.py index 7a29874eda8b..1d9e5fe56e3e 100644 --- a/nova/virt/libvirt/designer.py +++ b/nova/virt/libvirt/designer.py @@ -24,7 +24,7 @@ from nova.pci import utils as pci_utils def set_vif_guest_frontend_config(conf, mac, model, driver, queues, - rx_queue_size): + rx_queue_size, packed): """Populate a LibvirtConfigGuestInterface instance with guest frontend details. @@ -39,6 +39,8 @@ def set_vif_guest_frontend_config(conf, mac, model, driver, queues, conf.vhost_queues = queues if rx_queue_size: conf.vhost_rx_queue_size = rx_queue_size + if packed is not None: + conf.driver_packed = packed def set_vif_host_backend_ethernet_config(conf, tapname): diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 863ac60af097..5ff16c736e7a 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -224,17 +224,14 @@ NEXT_MIN_QEMU_VERSION = (6, 2, 0) # vIOMMU model value `virtio` minimal support version MIN_LIBVIRT_VIOMMU_VIRTIO_MODEL = (8, 3, 0) - MIN_LIBVIRT_TB_CACHE_SIZE = (8, 0, 0) # Virtuozzo driver support MIN_VIRTUOZZO_VERSION = (7, 0, 0) - # Names of the types that do not get compressed during migration NO_COMPRESSION_TYPES = ('qcow2',) - # number of serial console limit QEMU_MAX_SERIAL_PORTS = 4 # Qemu supports 4 serial consoles, we remove 1 because of the PTY one defined @@ -244,7 +241,6 @@ VGPU_RESOURCE_SEMAPHORE = 'vgpu_resources' LIBVIRT_PERF_EVENT_PREFIX = 'VIR_PERF_PARAM_' - # Maxphysaddr minimal support version. MIN_LIBVIRT_MAXPHYSADDR = (8, 7, 0) MIN_QEMU_MAXPHYSADDR = (2, 7, 0) @@ -9044,6 +9040,7 @@ class LibvirtDriver(driver.ComputeDriver): traits: ty.Dict[str, bool] = {} traits.update(self._get_cpu_traits()) + traits.update(self._get_packed_virtqueue_traits()) traits.update(self._get_storage_bus_traits()) traits.update(self._get_video_model_traits()) traits.update(self._get_vif_model_traits()) @@ -12426,6 +12423,14 @@ class LibvirtDriver(driver.ComputeDriver): in supported_models for model in all_models } + def _get_packed_virtqueue_traits(self) -> ty.Dict[str, bool]: + """Get Virtio Packed Ring traits to be set on the host's + resource provider. + + :return: A dict of trait names mapped to boolean values. + """ + return {ot.COMPUTE_NET_VIRTIO_PACKED: True} + def _get_cpu_traits(self) -> ty.Dict[str, bool]: """Get CPU-related traits to be set and unset on the host's resource provider. diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py index 6a7daa6b545f..6e9069fa50ff 100644 --- a/nova/virt/libvirt/vif.py +++ b/nova/virt/libvirt/vif.py @@ -192,11 +192,13 @@ class LibvirtGenericVIFDriver(object): vhost_queues = None rx_queue_size = None + packed = self._get_packed_virtqueue_settings( + image_meta, flavor) # NOTE(stephenfin): Skip most things here as only apply to virtio # devices if vnic_type in network_model.VNIC_TYPES_DIRECT_PASSTHROUGH: designer.set_vif_guest_frontend_config( - conf, mac, model, driver, vhost_queues, rx_queue_size) + conf, mac, model, driver, vhost_queues, rx_queue_size, packed) return conf rx_queue_size = CONF.libvirt.rx_queue_size @@ -211,7 +213,7 @@ class LibvirtGenericVIFDriver(object): # The rest of this only applies to virtio if model != network_model.VIF_MODEL_VIRTIO: designer.set_vif_guest_frontend_config( - conf, mac, model, driver, vhost_queues, rx_queue_size) + conf, mac, model, driver, vhost_queues, rx_queue_size, packed) return conf # Workaround libvirt bug, where it mistakenly enables vhost mode, even @@ -243,7 +245,7 @@ class LibvirtGenericVIFDriver(object): driver = 'vhost' designer.set_vif_guest_frontend_config( - conf, mac, model, driver, vhost_queues, rx_queue_size) + conf, mac, model, driver, vhost_queues, rx_queue_size, packed) return conf @@ -296,6 +298,13 @@ class LibvirtGenericVIFDriver(object): else: return None + def _get_packed_virtqueue_settings(self, image_meta, flavor): + """A method to check if Virtio Packed Ring was requested.""" + if not isinstance(image_meta, objects.ImageMeta): + image_meta = objects.ImageMeta.from_dict(image_meta) + + return hardware.get_packed_virtqueue_constraint(flavor, image_meta) + def get_bridge_name(self, vif): return vif['network']['bridge'] diff --git a/releasenotes/notes/packed-virtqueue-filter-43a376674cb5b345.yaml b/releasenotes/notes/packed-virtqueue-filter-43a376674cb5b345.yaml new file mode 100644 index 000000000000..08a6479facfa --- /dev/null +++ b/releasenotes/notes/packed-virtqueue-filter-43a376674cb5b345.yaml @@ -0,0 +1,19 @@ +--- +features: + - | + Handling packed virtqueue requests for an instance is now supported on + the nodes with Qemu v4.2 and Libvirt v6.3. + + VMs using virtio-net will see an increase in performance. The increase + can be anywhere between 10/20% (see DPDK Intel Vhost/virtio perf. reports) + and 75% (using Napatech SmartNICs). + + Packed Ring can be requested via image property or flavor extra spec. + hw_virtio_packed_ring=true|false (default false) + hw:virtio_packed_ring=true|false (default false) + + Useful references: + https://libvirt.org/formatdomain.html#virtio-related-options + https://docs.oasis-open.org/virtio/virtio/v1.1/csprd01/virtio-v1.1-csprd01.html + https://specs.openstack.org/openstack/nova-specs/specs/2023.2/approved/virtio_packedring_configuration_support.html + diff --git a/requirements.txt b/requirements.txt index 9be319ab7630..eb74909ffb4f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -52,7 +52,7 @@ psutil>=3.2.2 # BSD oslo.versionedobjects>=1.35.0 # Apache-2.0 os-brick>=5.2 # Apache-2.0 os-resource-classes>=1.1.0 # Apache-2.0 -os-traits>=2.10.0 # Apache-2.0 +os-traits>=3.0.0 # Apache-2.0 os-vif>=3.1.0 # Apache-2.0 castellan>=0.16.0 # Apache-2.0 microversion-parse>=0.2.1 # Apache-2.0