From 264e868d4931595140260c0f655a10b525be38f7 Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Mon, 12 Jan 2026 18:02:50 +0000 Subject: [PATCH] Support os-vif TAP pre-creation for OVS/OVN ports Add support for os-vif TAP device pre-creation when Neutron sets the 'ovs_create_tap' flag in vif_details. This reduces live migration downtime by ensuring the network is fully wired before the VM starts. Changes: - Add VIF_DETAILS_OVS_CREATE_TAP constant to model.py - Propagate create_tap from binding details to os-vif port profile in os_vif_util.py - Set managed='no' in libvirt XML when create_tap is enabled so libvirt uses the pre-created TAP device - Set multiqueue on port profile in _plug_os_vif based on instance flavor/image hw:vif_multiqueue_enabled property When checking oslo.versionedobjects fields for backward compat: - Use 'field in obj.fields' to check if field exists in schema - Use 'field in obj' to check if field value is set Depends-On: https://review.opendev.org/c/openstack/os-vif/+/971231 Generated-By: Cursor claude-opus-4.5 Closes-Bug: #2069718 Change-Id: I32343658b53e317696d1bd8b984793bfeeccd409 Signed-off-by: Sean Mooney --- nova/network/model.py | 5 + nova/network/os_vif_util.py | 12 ++ nova/tests/unit/network/test_os_vif_util.py | 61 +++++++- nova/tests/unit/virt/libvirt/test_vif.py | 141 ++++++++++++++++++ nova/virt/libvirt/vif.py | 48 +++++- ...ort-osvif-tap-creation-2069718-abc123.yaml | 15 ++ 6 files changed, 276 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/support-osvif-tap-creation-2069718-abc123.yaml diff --git a/nova/network/model.py b/nova/network/model.py index 16c925f6af..86d1d12893 100644 --- a/nova/network/model.py +++ b/nova/network/model.py @@ -95,6 +95,11 @@ VIF_DETAILS_TAP_MAC_ADDRESS = 'mac_address' VIF_DETAILS_OVS_DATAPATH_SYSTEM = 'system' VIF_DETAILS_OVS_DATAPATH_NETDEV = 'netdev' +# Specifies whether os-vif should create the TAP device. When True, os-vif +# will pre-create the TAP device before adding it to OVS, reducing live +# migration downtime. See bug #2069718. +VIF_DETAILS_OVS_CREATE_TAP = 'ovs_create_tap' + # Define supported virtual NIC types. VNIC_TYPE_DIRECT and VNIC_TYPE_MACVTAP # are used for SR-IOV ports VNIC_TYPE_NORMAL = 'normal' diff --git a/nova/network/os_vif_util.py b/nova/network/os_vif_util.py index 21d6f66b79..b16096375d 100644 --- a/nova/network/os_vif_util.py +++ b/nova/network/os_vif_util.py @@ -332,6 +332,18 @@ def _nova_to_osvif_vif_ovs(vif): interface_id=vif.get('ovs_interfaceid') or vif['id'], datapath_type=vif['details'].get( model.VIF_DETAILS_OVS_DATAPATH_TYPE)) + + # Set create_tap from Neutron binding details if supported by the + # os-vif version (check profile.fields for schema, not profile for set + # values) + create_tap = vif['details'].get( + model.VIF_DETAILS_OVS_CREATE_TAP, False) + if 'create_tap' in profile.fields: + profile.create_tap = create_tap + # NOTE: multiqueue is determined by Nova from hw:vif_multiqueue_enabled + # flavor extra spec or image property. It is set in _plug_os_vif() in + # nova/virt/libvirt/vif.py where the instance is available. + if vnic_type in (model.VNIC_TYPE_DIRECT, model.VNIC_TYPE_VDPA): obj = _get_vnic_direct_vif_instance( vif, diff --git a/nova/tests/unit/network/test_os_vif_util.py b/nova/tests/unit/network/test_os_vif_util.py index 338492aef0..cea1c87b58 100644 --- a/nova/tests/unit/network/test_os_vif_util.py +++ b/nova/tests/unit/network/test_os_vif_util.py @@ -473,7 +473,8 @@ class OSVIFUtilTestCase(test.NoDBTestCase): port_profile=osv_objects.vif.VIFPortProfileOpenVSwitch( interface_id="dc065497-3c8d-4f44-8fb4-e1d33c16a536", datapath_type=None, - create_port=False), + create_port=False, + create_tap=False), preserve_on_delete=False, vif_name="nicdc065497-3c", network=osv_objects.network.Network( @@ -604,7 +605,8 @@ class OSVIFUtilTestCase(test.NoDBTestCase): port_profile=osv_objects.vif.VIFPortProfileOpenVSwitch( interface_id="dc065497-3c8d-4f44-8fb4-e1d33c16a536", datapath_type=model.VIF_DETAILS_OVS_DATAPATH_SYSTEM, - create_port=True), + create_port=True, + create_tap=False), preserve_on_delete=False, vif_name="nicdc065497-3c", network=osv_objects.network.Network( @@ -644,7 +646,8 @@ class OSVIFUtilTestCase(test.NoDBTestCase): bridge_name="qbrdc065497-3c", port_profile=osv_objects.vif.VIFPortProfileOpenVSwitch( interface_id="dc065497-3c8d-4f44-8fb4-e1d33c16a536", - datapath_type="system"), + datapath_type="system", + create_tap=False), preserve_on_delete=False, vif_name="nicdc065497-3c", network=osv_objects.network.Network( @@ -1292,3 +1295,55 @@ class OSVIFUtilTestCase(test.NoDBTestCase): objects=[]))) self.assertObjEqual(expect, actual) + + def test_nova_to_osvif_vif_ovs_with_tap_creation(self): + """Test that ovs_create_tap is propagated to create_tap.""" + vif = model.VIF( + id="dc065497-3c8d-4f44-8fb4-e1d33c16a536", + type=model.VIF_TYPE_OVS, + address="22:52:25:62:e2:aa", + network=model.Network( + id="b82c1929-051e-481d-8110-4669916c7915", + label="Demo Net", + subnets=[]), + details={ + model.VIF_DETAILS_PORT_FILTER: True, + model.VIF_DETAILS_OVS_DATAPATH_TYPE: + model.VIF_DETAILS_OVS_DATAPATH_SYSTEM, + model.VIF_DETAILS_OVS_CREATE_TAP: True, + }, + ) + + actual = os_vif_util.nova_to_osvif_vif(vif) + + # Verify the port profile has create_tap set + self.assertIsInstance( + actual.port_profile, osv_objects.vif.VIFPortProfileOpenVSwitch) + # Check if the field exists in the schema (for backward compat) + if 'create_tap' in actual.port_profile.fields: + self.assertTrue(actual.port_profile.create_tap) + + def test_nova_to_osvif_vif_ovs_without_tap_creation(self): + """Test that create_tap defaults to False when not in details.""" + vif = model.VIF( + id="dc065497-3c8d-4f44-8fb4-e1d33c16a536", + type=model.VIF_TYPE_OVS, + address="22:52:25:62:e2:aa", + network=model.Network( + id="b82c1929-051e-481d-8110-4669916c7915", + label="Demo Net", + subnets=[]), + details={ + model.VIF_DETAILS_PORT_FILTER: True, + model.VIF_DETAILS_OVS_DATAPATH_TYPE: + model.VIF_DETAILS_OVS_DATAPATH_SYSTEM, + }, + ) + + actual = os_vif_util.nova_to_osvif_vif(vif) + + # Verify the port profile doesn't have create_tap set to True + self.assertIsInstance( + actual.port_profile, osv_objects.vif.VIFPortProfileOpenVSwitch) + if 'create_tap' in actual.port_profile.fields: + self.assertFalse(actual.port_profile.create_tap) diff --git a/nova/tests/unit/virt/libvirt/test_vif.py b/nova/tests/unit/virt/libvirt/test_vif.py index 8ea2f3e093..4215974b38 100644 --- a/nova/tests/unit/virt/libvirt/test_vif.py +++ b/nova/tests/unit/virt/libvirt/test_vif.py @@ -1547,6 +1547,107 @@ class LibvirtVifTestCase(test.NoDBTestCase): def test_osvif_plug_fail(self): self._test_osvif_plug(True) + @mock.patch("nova.network.os_vif_util.nova_to_osvif_instance") + @mock.patch("nova.network.os_vif_util.nova_to_osvif_vif") + @mock.patch.object(os_vif, "plug") + def test_osvif_plug_multiqueue_with_create_tap(self, mock_plug, + mock_convert_vif, + mock_convert_inst): + """Test that multiqueue is set on port profile when create_tap=True.""" + # Skip test if os-vif doesn't support create_tap/multiqueue fields + test_prof = osv_objects.vif.VIFPortProfileOpenVSwitch( + interface_id="07bd6cea-fb37-4594-b769-90fc51854ee9", + profile_id="fishfood") + if ('create_tap' not in test_prof.fields or + 'multiqueue' not in test_prof.fields): + self.skipTest("os-vif does not support create_tap/multiqueue") + + # Create a port profile with create_tap=True and multiqueue field + os_vif_ovs_tap_prof = osv_objects.vif.VIFPortProfileOpenVSwitch( + interface_id="07bd6cea-fb37-4594-b769-90fc51854ee9", + profile_id="fishfood", + create_tap=True, + multiqueue=False) # Will be set to True by _plug_os_vif + + os_vif_ovs_tap = osv_objects.vif.VIFOpenVSwitch( + id="dc065497-3c8d-4f44-8fb4-e1d33c16a536", + address="22:52:25:62:e2:aa", + vif_name="nicdc065497-3c", + bridge_name="br0", + port_profile=os_vif_ovs_tap_prof, + network=self.os_vif_network) + + mock_convert_vif.return_value = os_vif_ovs_tap + mock_convert_inst.return_value = self.os_vif_inst_info + + # Create an instance with multiqueue enabled via image property + ins = objects.Instance( + id=1, uuid='f0000000-0000-0000-0000-000000000001', + image_ref=uuids.image_ref, flavor=self.flavor_2vcpu, + project_id=723, + system_metadata={ + 'image_hw_vif_multiqueue_enabled': 'True' + }, + ) + + d = vif.LibvirtGenericVIFDriver() + d.plug(ins, self.vif_ovs) + + # Verify multiqueue was set to True on the port profile + self.assertTrue(os_vif_ovs_tap_prof.multiqueue) + mock_plug.assert_called_once() + + @mock.patch("nova.network.os_vif_util.nova_to_osvif_instance") + @mock.patch("nova.network.os_vif_util.nova_to_osvif_vif") + @mock.patch.object(os_vif, "plug") + def test_osvif_plug_multiqueue_without_create_tap(self, mock_plug, + mock_convert_vif, + mock_convert_inst): + """Test multiqueue is NOT set when create_tap=False.""" + # Skip test if os-vif doesn't support create_tap/multiqueue fields + test_prof = osv_objects.vif.VIFPortProfileOpenVSwitch( + interface_id="07bd6cea-fb37-4594-b769-90fc51854ee9", + profile_id="fishfood") + if ('create_tap' not in test_prof.fields or + 'multiqueue' not in test_prof.fields): + self.skipTest("os-vif does not support create_tap/multiqueue") + + # Create a profile with create_port=True but create_tap=False + os_vif_ovs_no_tap_prof = osv_objects.vif.VIFPortProfileOpenVSwitch( + interface_id="07bd6cea-fb37-4594-b769-90fc51854ee9", + profile_id="fishfood", + create_port=True, + create_tap=False, + multiqueue=False) + + os_vif_ovs_no_tap = osv_objects.vif.VIFOpenVSwitch( + id="dc065497-3c8d-4f44-8fb4-e1d33c16a536", + address="22:52:25:62:e2:aa", + vif_name="nicdc065497-3c", + bridge_name="br0", + port_profile=os_vif_ovs_no_tap_prof, + network=self.os_vif_network) + + mock_convert_vif.return_value = os_vif_ovs_no_tap + mock_convert_inst.return_value = self.os_vif_inst_info + + # Instance with multiqueue enabled + ins = objects.Instance( + id=1, uuid='f0000000-0000-0000-0000-000000000001', + image_ref=uuids.image_ref, flavor=self.flavor_2vcpu, + project_id=723, + system_metadata={ + 'image_hw_vif_multiqueue_enabled': 'True' + }, + ) + + d = vif.LibvirtGenericVIFDriver() + d.plug(ins, self.vif_ovs) + + # Verify multiqueue was NOT set (remains False) since create_tap=False + self.assertFalse(os_vif_ovs_no_tap_prof.multiqueue) + mock_plug.assert_called_once() + @mock.patch("nova.network.os_vif_util.nova_to_osvif_instance") @mock.patch("nova.network.os_vif_util.nova_to_osvif_vif") @mock.patch.object(os_vif, "unplug") @@ -1725,6 +1826,46 @@ class LibvirtVifTestCase(test.NoDBTestCase): self._test_config_os_vif(os_vif_type, vif_type, expected_xml) + def test_config_os_vif_ovs_with_create_tap(self): + """Test that create_tap=True results in managed='no' in XML.""" + # Skip test if os-vif doesn't support create_tap field + test_prof = osv_objects.vif.VIFPortProfileOpenVSwitch( + interface_id="07bd6cea-fb37-4594-b769-90fc51854ee9", + profile_id="fishfood") + if 'create_tap' not in test_prof.fields: + self.skipTest("os-vif does not support create_tap") + + # Create a port profile with create_tap=True + os_vif_ovs_tap_prof = osv_objects.vif.VIFPortProfileOpenVSwitch( + interface_id="07bd6cea-fb37-4594-b769-90fc51854ee9", + profile_id="fishfood", + create_tap=True) + + os_vif_type = osv_objects.vif.VIFOpenVSwitch( + id="dc065497-3c8d-4f44-8fb4-e1d33c16a536", + address="22:52:25:62:e2:aa", + vif_name="nicdc065497-3c", + bridge_name="br0", + port_profile=os_vif_ovs_tap_prof, + network=self.os_vif_network) + + vif_type = self.vif_ovs + + # Expected XML should have managed="no" on the target element + expected_xml = """ + + + + + + + + + + """ + + self._test_config_os_vif(os_vif_type, vif_type, expected_xml) + def test_config_os_vif_hostdevice_ethernet(self): os_vif_type = self.os_vif_hostdevice_ethernet vif_type = self.vif_bridge diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py index 2e5cfd7c80..9b9bcba21f 100644 --- a/nova/virt/libvirt/vif.py +++ b/nova/virt/libvirt/vif.py @@ -457,9 +457,33 @@ class LibvirtGenericVIFDriver(object): conf.target_dev = vif.vif_name def _set_config_VIFOpenVSwitch(self, instance, vif, conf): - # if delegating creation to os-vif, create an ethernet-type VIF and let - # os-vif do the actual wiring up - if 'create_port' in vif.port_profile and vif.port_profile.create_port: + # Check if os-vif will create the TAP device (with backward compat + # check for older os-vif versions that don't have create_tap field) + # NOTE: 'field in profile.fields' checks schema existence, + # 'field in profile' checks if the attribute is set + create_tap = ( + 'create_tap' in vif.port_profile.fields and + 'create_tap' in vif.port_profile and + vif.port_profile.create_tap + ) + # Check if delegating port creation to os-vif + create_port = ( + 'create_port' in vif.port_profile.fields and + 'create_port' in vif.port_profile and + vif.port_profile.create_port + ) + + # TODO(sean-k-mooney): we should always delegate to os-vif and have + # os-vif create the tap once we are sure all compute nodes are + # upgraded. Simplify this logic in 2026.2+ + if create_tap: + # os-vif will create the TAP device, so use ethernet-type VIF + # and set managed=no so libvirt uses the existing TAP device + self._set_config_VIFGeneric(instance, vif, conf) + conf.managed = "no" + elif create_port: + # Delegating creation to os-vif, create an ethernet-type VIF + # and let os-vif do the actual wiring up self._set_config_VIFGeneric(instance, vif, conf) else: conf.net_type = "bridge" @@ -706,6 +730,24 @@ class LibvirtGenericVIFDriver(object): def _plug_os_vif(self, instance, vif): instance_info = os_vif_util.nova_to_osvif_instance(instance) + # Set multiqueue on the port profile if create_tap is enabled and + # the instance has multiqueue enabled via flavor/image properties. + # This must be done here because nova_to_osvif_vif doesn't have + # access to the instance. + # NOTE: 'field in obj.fields' checks schema existence, + # 'field in obj' checks if the attribute is set on the instance + if ('port_profile' in vif.fields and + 'port_profile' in vif): + profile = vif.port_profile + if (profile is not None and + 'create_tap' in profile.fields and + 'create_tap' in profile and + profile.create_tap and + 'multiqueue' in profile.fields): + multiqueue = hardware.get_vif_multiqueue_constraint( + instance.flavor, instance.image_meta) + profile.multiqueue = multiqueue + try: os_vif.plug(vif, instance_info) except osv_exception.ExceptionBase as ex: diff --git a/releasenotes/notes/support-osvif-tap-creation-2069718-abc123.yaml b/releasenotes/notes/support-osvif-tap-creation-2069718-abc123.yaml new file mode 100644 index 0000000000..fb4518df8f --- /dev/null +++ b/releasenotes/notes/support-osvif-tap-creation-2069718-abc123.yaml @@ -0,0 +1,15 @@ +--- +fixes: + - | + Updated nova to support os-vif TAP device pre-creation for OVN ports. + When Neutron sets ``ovs_create_tap`` in vif_details (enabled via + the ``[ovn]/ovs_create_tap`` config option in the ML2 driver config), + Nova propagates this to os-vif which creates the TAP + device before libvirt starts the VM. Nova then configures libvirt with + ``managed="no"`` so it uses the pre-created TAP device instead of creating + a new one. This allows nova via os-vif to pre create the tap device in + pre live migration allowing ovn to install openflow rules before nova + starts the live migration. This reduces network connectivity downtime + when ovn is overloaded. + + See `bug 2069718 `_.