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 <work@seanmooney.info>
This commit is contained in:
Sean Mooney
2026-01-12 18:02:50 +00:00
parent a17b44f3eb
commit 264e868d49
6 changed files with 276 additions and 6 deletions
+5
View File
@@ -95,6 +95,11 @@ VIF_DETAILS_TAP_MAC_ADDRESS = 'mac_address'
VIF_DETAILS_OVS_DATAPATH_SYSTEM = 'system' VIF_DETAILS_OVS_DATAPATH_SYSTEM = 'system'
VIF_DETAILS_OVS_DATAPATH_NETDEV = 'netdev' 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 # Define supported virtual NIC types. VNIC_TYPE_DIRECT and VNIC_TYPE_MACVTAP
# are used for SR-IOV ports # are used for SR-IOV ports
VNIC_TYPE_NORMAL = 'normal' VNIC_TYPE_NORMAL = 'normal'
+12
View File
@@ -332,6 +332,18 @@ def _nova_to_osvif_vif_ovs(vif):
interface_id=vif.get('ovs_interfaceid') or vif['id'], interface_id=vif.get('ovs_interfaceid') or vif['id'],
datapath_type=vif['details'].get( datapath_type=vif['details'].get(
model.VIF_DETAILS_OVS_DATAPATH_TYPE)) 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): if vnic_type in (model.VNIC_TYPE_DIRECT, model.VNIC_TYPE_VDPA):
obj = _get_vnic_direct_vif_instance( obj = _get_vnic_direct_vif_instance(
vif, vif,
+58 -3
View File
@@ -473,7 +473,8 @@ class OSVIFUtilTestCase(test.NoDBTestCase):
port_profile=osv_objects.vif.VIFPortProfileOpenVSwitch( port_profile=osv_objects.vif.VIFPortProfileOpenVSwitch(
interface_id="dc065497-3c8d-4f44-8fb4-e1d33c16a536", interface_id="dc065497-3c8d-4f44-8fb4-e1d33c16a536",
datapath_type=None, datapath_type=None,
create_port=False), create_port=False,
create_tap=False),
preserve_on_delete=False, preserve_on_delete=False,
vif_name="nicdc065497-3c", vif_name="nicdc065497-3c",
network=osv_objects.network.Network( network=osv_objects.network.Network(
@@ -604,7 +605,8 @@ class OSVIFUtilTestCase(test.NoDBTestCase):
port_profile=osv_objects.vif.VIFPortProfileOpenVSwitch( port_profile=osv_objects.vif.VIFPortProfileOpenVSwitch(
interface_id="dc065497-3c8d-4f44-8fb4-e1d33c16a536", interface_id="dc065497-3c8d-4f44-8fb4-e1d33c16a536",
datapath_type=model.VIF_DETAILS_OVS_DATAPATH_SYSTEM, datapath_type=model.VIF_DETAILS_OVS_DATAPATH_SYSTEM,
create_port=True), create_port=True,
create_tap=False),
preserve_on_delete=False, preserve_on_delete=False,
vif_name="nicdc065497-3c", vif_name="nicdc065497-3c",
network=osv_objects.network.Network( network=osv_objects.network.Network(
@@ -644,7 +646,8 @@ class OSVIFUtilTestCase(test.NoDBTestCase):
bridge_name="qbrdc065497-3c", bridge_name="qbrdc065497-3c",
port_profile=osv_objects.vif.VIFPortProfileOpenVSwitch( port_profile=osv_objects.vif.VIFPortProfileOpenVSwitch(
interface_id="dc065497-3c8d-4f44-8fb4-e1d33c16a536", interface_id="dc065497-3c8d-4f44-8fb4-e1d33c16a536",
datapath_type="system"), datapath_type="system",
create_tap=False),
preserve_on_delete=False, preserve_on_delete=False,
vif_name="nicdc065497-3c", vif_name="nicdc065497-3c",
network=osv_objects.network.Network( network=osv_objects.network.Network(
@@ -1292,3 +1295,55 @@ class OSVIFUtilTestCase(test.NoDBTestCase):
objects=[]))) objects=[])))
self.assertObjEqual(expect, actual) 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)
+141
View File
@@ -1547,6 +1547,107 @@ class LibvirtVifTestCase(test.NoDBTestCase):
def test_osvif_plug_fail(self): def test_osvif_plug_fail(self):
self._test_osvif_plug(True) 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_instance")
@mock.patch("nova.network.os_vif_util.nova_to_osvif_vif") @mock.patch("nova.network.os_vif_util.nova_to_osvif_vif")
@mock.patch.object(os_vif, "unplug") @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) 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 = """
<interface type="ethernet">
<mac address="22:52:25:62:e2:aa"/>
<model type="virtio"/>
<mtu size="9000"/>
<target dev="nicdc065497-3c" managed="no"/>
<bandwidth>
<inbound average="100" peak="200" burst="300"/>
<outbound average="10" peak="20" burst="30"/>
</bandwidth>
</interface>"""
self._test_config_os_vif(os_vif_type, vif_type, expected_xml)
def test_config_os_vif_hostdevice_ethernet(self): def test_config_os_vif_hostdevice_ethernet(self):
os_vif_type = self.os_vif_hostdevice_ethernet os_vif_type = self.os_vif_hostdevice_ethernet
vif_type = self.vif_bridge vif_type = self.vif_bridge
+45 -3
View File
@@ -457,9 +457,33 @@ class LibvirtGenericVIFDriver(object):
conf.target_dev = vif.vif_name conf.target_dev = vif.vif_name
def _set_config_VIFOpenVSwitch(self, instance, vif, conf): def _set_config_VIFOpenVSwitch(self, instance, vif, conf):
# if delegating creation to os-vif, create an ethernet-type VIF and let # Check if os-vif will create the TAP device (with backward compat
# os-vif do the actual wiring up # check for older os-vif versions that don't have create_tap field)
if 'create_port' in vif.port_profile and vif.port_profile.create_port: # 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) self._set_config_VIFGeneric(instance, vif, conf)
else: else:
conf.net_type = "bridge" conf.net_type = "bridge"
@@ -706,6 +730,24 @@ class LibvirtGenericVIFDriver(object):
def _plug_os_vif(self, instance, vif): def _plug_os_vif(self, instance, vif):
instance_info = os_vif_util.nova_to_osvif_instance(instance) 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: try:
os_vif.plug(vif, instance_info) os_vif.plug(vif, instance_info)
except osv_exception.ExceptionBase as ex: except osv_exception.ExceptionBase as ex:
@@ -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 <https://bugs.launchpad.net/neutron/+bug/2069718>`_.