[OVN] Add support for external ports

This patch is adding support for a new port type called "external" in
core OVN.

Prior to this work, when a VM had a SR-IOV port attached to it, OVN itself
wasn't able to reply to things such as DHCP requests packets since the
OVS port was skipped. Core OVN then introduced the concept of "external"
ports which are ports deployed on a different node than the one that the
VM is running and is able to reply to such requests on behalf of the VM.

With this patch, when a port with the VNIC type "direct" and no
"switchdev" capability is created, ovn driver will then create a
logical port with the type "external" for it and add it to a default
HA Chassis Group. The port will then get bound to the "master" (higher
priority) chassis of that group.

Please note that, as a first step, this patch is creating only one HA
Chassis Group which *all* external ports will belong to. That means that
all external ports will be *scheduled onto the same node* (but it's
HA nevertheless). In the future we should enhance this behavior.

Change-Id: Ic6c4bb6c584682169f3ebd73105a847b05dddc76
Closes-Bug: #1841154
Signed-off-by: Lucas Alvares Gomes <lucasagomes@gmail.com>
This commit is contained in:
Lucas Alvares Gomes 2020-01-20 11:20:29 +00:00
parent 1f79ce8736
commit 4824a714bf
11 changed files with 450 additions and 7 deletions

View File

@ -673,12 +673,9 @@ class OVNMechanismDriver(api.MechanismDriver):
{'port_id': port['id'], 'vnic_type': vnic_type})
return
profile = port.get(portbindings.PROFILE)
capabilities = []
if profile:
capabilities = profile.get('capabilities', [])
capabilities = ovn_utils.get_port_capabilities(port)
if (vnic_type == portbindings.VNIC_DIRECT and
'switchdev' not in capabilities):
ovn_const.PORT_CAP_SWITCHDEV not in capabilities):
LOG.debug("Refusing to bind port due to unsupported vnic_type: %s "
"with no switchdev capability", portbindings.VNIC_DIRECT)
return

View File

@ -76,6 +76,7 @@ class DBInconsistenciesPeriodics(object):
# attributes like that, perhaps we should extend the OVNClient
# class and create an interface for the locks ?
self._nb_idl = self._ovn_client._nb_idl
self._sb_idl = self._ovn_client._sb_idl
self._idl = self._nb_idl.idl
self._idl.set_lock('ovn_db_inconsistencies_periodics')
self._sync_timer = timeutils.StopWatch()
@ -480,6 +481,58 @@ class DBInconsistenciesPeriodics(object):
raise periodics.NeverAgain()
# A static spacing value is used here, but this method will only run
# once per lock due to the use of periodics.NeverAgain().
@periodics.periodic(spacing=600, run_immediately=True)
def check_for_ha_chassis_group_address(self):
# If external ports is not supported stop running
# this periodic task
if not self._ovn_client.is_external_ports_supported():
raise periodics.NeverAgain()
if not self.has_lock:
return
default_ch_grp = self._nb_idl.ha_chassis_group_add(
ovn_const.HA_CHASSIS_GROUP_DEFAULT_NAME, may_exist=True).execute(
check_error=True)
# NOTE(lucasagomes): Find the existing chassis with the highest
# priority and keep it as being the highest to avoid moving
# things around
high_prio_ch = max(default_ch_grp.ha_chassis, key=lambda x: x.priority)
all_ch = self._sb_idl.get_all_chassis()
gw_ch = self._sb_idl.get_gateway_chassis_from_cms_options()
ch_to_del = set(all_ch) - set(gw_ch)
with self._nb_idl.transaction(check_error=True) as txn:
for ch in ch_to_del:
txn.add(self._nb_idl.ha_chassis_group_del_chassis(
ovn_const.HA_CHASSIS_GROUP_DEFAULT_NAME, ch,
if_exists=True))
# NOTE(lucasagomes): If the high priority chassis is in
# the list of chassis to be added/updated. Add it first with
# the highest priority number possible and then add the rest
# (the priority of the rest of the chassis does not matter
# since only the highest one is active)
priority = ovn_const.HA_CHASSIS_GROUP_HIGHEST_PRIORITY
if high_prio_ch and high_prio_ch.chassis_name in gw_ch:
txn.add(self._nb_idl.ha_chassis_group_add_chassis(
ovn_const.HA_CHASSIS_GROUP_DEFAULT_NAME,
high_prio_ch.chassis_name, priority=priority))
gw_ch.remove(high_prio_ch.chassis_name)
priority -= 1
for ch in gw_ch:
txn.add(self._nb_idl.ha_chassis_group_add_chassis(
ovn_const.HA_CHASSIS_GROUP_DEFAULT_NAME,
ch, priority=priority))
priority -= 1
raise periodics.NeverAgain()
class HashRingHealthCheckPeriodics(object):

View File

@ -96,6 +96,10 @@ class OVNClient(object):
# "virtual" port type was added in the version 2.12 of OVN
return self._sb_idl.is_col_present('Port_Binding', 'virtual_parent')
def is_external_ports_supported(self):
return self._nb_idl.is_col_present(
'Logical_Switch_Port', 'ha_chassis_group')
def _get_allowed_addresses_from_port(self, port):
if not port.get(psec.PORTSECURITY):
return [], []
@ -253,6 +257,18 @@ class OVNClient(object):
not utils.is_neutron_dhcp_agent_port(port)):
port_type = 'localport'
capabilities = utils.get_port_capabilities(port)
vnic_type = port.get(portbindings.VNIC_TYPE,
portbindings.VNIC_NORMAL)
if (vnic_type == portbindings.VNIC_DIRECT and
ovn_const.PORT_CAP_SWITCHDEV not in capabilities):
if self.is_external_ports_supported():
port_type = ovn_const.LSP_TYPE_EXTERNAL
else:
LOG.warning('The version of OVN used does not support '
'the "external ports" feature used for '
'SR-IOV ports with OVN native DHCP')
# The "unknown" address should only be set for the normal LSP
# ports (the ones which type is empty)
if not port_security and not port_type:
@ -269,14 +285,23 @@ class OVNClient(object):
dhcpv4_options = self._get_port_dhcp_options(port, const.IP_VERSION_4)
dhcpv6_options = self._get_port_dhcp_options(port, const.IP_VERSION_6)
options.update({'requested-chassis':
port.get(portbindings.HOST_ID, '')})
# HA Chassis Group will bind the port to the highest
# priority Chassis
if port_type != ovn_const.LSP_TYPE_EXTERNAL:
options.update({'requested-chassis':
port.get(portbindings.HOST_ID, '')})
device_owner = port.get('device_owner', '')
sg_ids = ' '.join(utils.get_lsp_security_groups(port))
return OvnPortInfo(port_type, options, addresses, port_security,
parent_name, tag, dhcpv4_options, dhcpv6_options,
cidrs.strip(), device_owner, sg_ids)
def _get_default_ha_chassis_group(self):
return self._nb_idl.ha_chassis_group_get(
ovn_const.HA_CHASSIS_GROUP_DEFAULT_NAME).execute(
check_error=True).uuid
def create_port(self, port):
if utils.is_lsp_ignored(port):
return
@ -341,6 +366,11 @@ class OVNClient(object):
'dhcpv6_options': dhcpv6_options
}
if (self.is_external_ports_supported() and
port_info.type == ovn_const.LSP_TYPE_EXTERNAL):
kwargs['ha_chassis_group'] = (
self._get_default_ha_chassis_group())
# TODO(lucasgomes): Remove this workaround in the future,
# the core OVN version >= 2.12 supports the "virtual" port
# type which deals with these situations.
@ -505,6 +535,14 @@ class OVNClient(object):
portbindings.VIF_TYPE_UNBOUND):
columns_dict['addresses'] = []
if self.is_external_ports_supported():
if port_info.type == ovn_const.LSP_TYPE_EXTERNAL:
columns_dict['ha_chassis_group'] = (
self._get_default_ha_chassis_group())
else:
# Clear the ha_chassis_group field
columns_dict['ha_chassis_group'] = []
ovn_port = self._nb_idl.lookup('Logical_Switch_Port', port['id'])
addr_pairs_diff = utils.compute_address_pairs_diff(ovn_port, port)
@ -1446,6 +1484,10 @@ class OVNClient(object):
2) if no chassis is available from 1) then,
select chassis with proper bridge mappings
"""
# TODO(lucasagomes): Simplify the logic here, the CMS option has
# been introduced long ago and by now all gateway chassis should
# include it. This will match the logic in the is_gateway_chassis()
# (utils.py)
cms = cms or self._sb_idl.get_gateway_chassis_from_cms_options()
chassis_physnets = (chassis_physnets or
self._sb_idl.get_chassis_and_physnets())

View File

@ -73,6 +73,51 @@ class ChassisEvent(row_event.RowEvent):
super(ChassisEvent, self).__init__(events, table, None)
self.event_name = 'ChassisEvent'
def handle_ha_chassis_group_changes(self, event, row, old):
"""Handle HA Chassis Group changes.
This method handles the inclusion and removal of Chassis to/from
the default HA Chassis Group.
"""
if not self.driver._ovn_client.is_external_ports_supported():
return
is_gw_chassis = utils.is_gateway_chassis(row)
# If the Chassis being created is not a gateway, ignore it
if not is_gw_chassis and event == self.ROW_CREATE:
return
if event == self.ROW_UPDATE:
is_old_gw = utils.is_gateway_chassis(old)
if is_gw_chassis and is_old_gw:
return
elif not is_gw_chassis and is_old_gw:
# Chassis is not a gateway anymore, treat it as deletion
event = self.ROW_DELETE
elif is_gw_chassis and not is_old_gw:
# Chassis is now a gateway, treat it as creation
event = self.ROW_CREATE
if event == self.ROW_CREATE:
default_group = self.driver._nb_ovn.ha_chassis_group_get(
ovn_const.HA_CHASSIS_GROUP_DEFAULT_NAME).execute(
check_error=True)
# Find what's the lowest priority number current in the group
# and add the new chassis as the new lowest
min_priority = min(
[ch.priority for ch in default_group.ha_chassis],
default=ovn_const.HA_CHASSIS_GROUP_HIGHEST_PRIORITY)
self.driver._nb_ovn.ha_chassis_group_add_chassis(
ovn_const.HA_CHASSIS_GROUP_DEFAULT_NAME, row.name,
priority=min_priority - 1).execute(check_error=True)
elif event == self.ROW_DELETE:
self.driver._nb_ovn.ha_chassis_group_del_chassis(
ovn_const.HA_CHASSIS_GROUP_DEFAULT_NAME,
row.name, if_exists=True).execute(check_error=True)
def run(self, event, row, old):
host = row.hostname
phy_nets = []
@ -86,6 +131,8 @@ class ChassisEvent(row_event.RowEvent):
if utils.is_ovn_l3(self.l3_plugin):
self.l3_plugin.schedule_unhosted_gateways()
self.handle_ha_chassis_group_changes(event, row, old)
class PortBindingChassisUpdateEvent(row_event.RowEvent):
"""Event for matching a port moving chassis

View File

@ -15,6 +15,7 @@
import functools
import mock
from neutron_lib.api.definitions import portbindings
from oslo_config import cfg
from oslo_utils import uuidutils
@ -431,3 +432,122 @@ class TestVirtualPorts(base.TestOVNFunctionalBase):
ovn_vport.options)
self.assertNotIn(ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY,
ovn_vport.options)
class TestExternalPorts(base.TestOVNFunctionalBase):
def setUp(self):
super(TestExternalPorts, self).setUp()
self._ovn_client = self.mech_driver._ovn_client
self.n1 = self._make_network(self.fmt, 'n1', True)
res = self._create_subnet(self.fmt, self.n1['network']['id'],
'10.0.0.0/24')
self.sub = self.deserialize(self.fmt, res)
# The default group will be created by the maintenance task (
# which is disabled in the functional jobs). So let's add it
self.default_ch_grp = self.nb_api.ha_chassis_group_add(
ovn_const.HA_CHASSIS_GROUP_DEFAULT_NAME).execute(check_error=True)
def _find_port_row_by_name(self, name):
cmd = self.nb_api.db_find_rows(
'Logical_Switch_Port', ('name', '=', name))
rows = cmd.execute(check_error=True)
return rows[0] if rows else None
def test_external_port_create(self):
port_data = {
'port': {'network_id': self.n1['network']['id'],
'tenant_id': self._tenant_id,
portbindings.VNIC_TYPE: portbindings.VNIC_DIRECT}}
port_req = self.new_create_request('ports', port_data, self.fmt)
port_res = port_req.get_response(self.api)
port = self.deserialize(self.fmt, port_res)['port']
ovn_port = self._find_port_row_by_name(port['id'])
self.assertEqual(ovn_const.LSP_TYPE_EXTERNAL, ovn_port.type)
self.assertEqual(1, len(ovn_port.ha_chassis_group))
self.assertEqual(str(self.default_ch_grp.uuid),
str(ovn_port.ha_chassis_group[0].uuid))
def test_external_port_update(self):
port_data = {
'port': {'network_id': self.n1['network']['id'],
'tenant_id': self._tenant_id}}
port_req = self.new_create_request('ports', port_data, self.fmt)
port_res = port_req.get_response(self.api)
port = self.deserialize(self.fmt, port_res)['port']
ovn_port = self._find_port_row_by_name(port['id'])
self.assertEqual('', ovn_port.type)
self.assertEqual([], ovn_port.ha_chassis_group)
port_upt_data = {
'port': {portbindings.VNIC_TYPE: portbindings.VNIC_DIRECT}}
port_req = self.new_update_request(
'ports', port_upt_data, port['id'], self.fmt)
port_res = port_req.get_response(self.api)
port = self.deserialize(self.fmt, port_res)['port']
ovn_port = self._find_port_row_by_name(port['id'])
self.assertEqual(ovn_const.LSP_TYPE_EXTERNAL, ovn_port.type)
self.assertEqual(1, len(ovn_port.ha_chassis_group))
self.assertEqual(str(self.default_ch_grp.uuid),
str(ovn_port.ha_chassis_group[0].uuid))
def test_external_port_create_switchdev(self):
port_data = {
'port': {'network_id': self.n1['network']['id'],
'tenant_id': self._tenant_id,
portbindings.VNIC_TYPE: portbindings.VNIC_DIRECT,
ovn_const.OVN_PORT_BINDING_PROFILE: {
'capabilities': [ovn_const.PORT_CAP_SWITCHDEV]}}}
port_req = self.new_create_request('ports', port_data, self.fmt)
port_res = port_req.get_response(self.api)
port = self.deserialize(self.fmt, port_res)['port']
ovn_port = self._find_port_row_by_name(port['id'])
# When "switchdev" is set, we should treat it as a normal
# port instead of "external" type
self.assertEqual("", ovn_port.type)
# Assert the poer hasn't been added to any HA Chassis Group either
self.assertEqual(0, len(ovn_port.ha_chassis_group))
def test_external_port_update_switchdev(self):
port_data = {
'port': {'network_id': self.n1['network']['id'],
'tenant_id': self._tenant_id,
portbindings.VNIC_TYPE: portbindings.VNIC_DIRECT}}
# Create a VNIC_DIRECT type port without the "switchdev"
# capability and assert that it's an "external" port
port_req = self.new_create_request('ports', port_data, self.fmt)
port_res = port_req.get_response(self.api)
port = self.deserialize(self.fmt, port_res)['port']
ovn_port = self._find_port_row_by_name(port['id'])
self.assertEqual(ovn_const.LSP_TYPE_EXTERNAL, ovn_port.type)
self.assertEqual(1, len(ovn_port.ha_chassis_group))
self.assertEqual(str(self.default_ch_grp.uuid),
str(ovn_port.ha_chassis_group[0].uuid))
# Now, update the port to add a "switchdev" capability and make
# sure it's not treated as an "external" port anymore nor it's
# included in a HA Chassis Group
port_upt_data = {
'port': {ovn_const.OVN_PORT_BINDING_PROFILE: {
'capabilities': [ovn_const.PORT_CAP_SWITCHDEV]}}}
port_req = self.new_update_request(
'ports', port_upt_data, port['id'], self.fmt)
port_res = port_req.get_response(self.api)
port = self.deserialize(self.fmt, port_res)['port']
ovn_port = self._find_port_row_by_name(port['id'])
# When "switchdev" is set, we should treat it as a normal
# port instead of "external" type
self.assertEqual("", ovn_port.type)
# Assert the poer hasn't been added to any HA Chassis Group either
self.assertEqual(0, len(ovn_port.ha_chassis_group))

View File

@ -18,6 +18,7 @@ import fixtures
from neutron.common.ovn import constants
from neutron.common.ovn import utils
from neutron.tests import base
from neutron.tests.unit import fake_resources as fakes
RESOLV_CONF_TEMPLATE = """# TEST TEST TEST
# Geneated by OVN test
@ -43,6 +44,20 @@ class TestUtils(base.BaseTestCase):
resolver_file=resolver_file_name)
self.assertEqual(expected_dns_resolvers, observed_dns_resolvers)
def test_is_gateway_chassis(self):
chassis = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'external_ids': {'ovn-cms-options': 'enable-chassis-as-gw'}})
non_gw_chassis_0 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'external_ids': {'ovn-cms-options': ''}})
non_gw_chassis_1 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={})
non_gw_chassis_2 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'external_ids': {}})
self.assertTrue(utils.is_gateway_chassis(chassis))
self.assertFalse(utils.is_gateway_chassis(non_gw_chassis_0))
self.assertFalse(utils.is_gateway_chassis(non_gw_chassis_1))
self.assertFalse(utils.is_gateway_chassis(non_gw_chassis_2))
class TestGateWayChassisValidity(base.BaseTestCase):

View File

@ -146,6 +146,7 @@ class FakeOvsdbNbOvnIdl(object):
self.unset_lswitch_port_to_virtual_type = mock.Mock()
self.ls_get = mock.Mock()
self.check_liveness = mock.Mock()
self.ha_chassis_group_get = mock.Mock()
class FakeOvsdbSbOvnIdl(object):

View File

@ -307,3 +307,59 @@ class TestDBInconsistenciesPeriodics(testlib_api.SqlTestCaseLight,
constants.MCAST_FLOOD_UNREGISTERED: 'true'})),
]
nb_idl.db_set.assert_has_calls(expected_calls)
def test_check_for_ha_chassis_group_address_not_supported(self):
self.fake_ovn_client.is_external_ports_supported.return_value = False
self.assertRaises(periodics.NeverAgain,
self.periodic.check_for_ha_chassis_group_address)
self.assertFalse(
self.fake_ovn_client._nb_idl.ha_chassis_group_add.called)
def test_check_for_ha_chassis_group_address(self):
self.fake_ovn_client.is_external_ports_supported.return_value = True
nb_idl = self.fake_ovn_client._nb_idl
sb_idl = self.fake_ovn_client._sb_idl
gw_chassis_0 = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'priority': 1,
'name': 'gw_chassis_0',
'chassis_name': 'gw_chassis_0'})
gw_chassis_1 = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'priority': 2,
'name': 'gw_chassis_1',
'chassis_name': 'gw_chassis_1'})
non_gw_chassis_0 = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'name': 'non_gw_chassis_0'})
default_ha_group = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'ha_chassis': [gw_chassis_0, gw_chassis_1]})
nb_idl.ha_chassis_group_add.return_value.execute.return_value = (
default_ha_group)
sb_idl.get_all_chassis.return_value = [
non_gw_chassis_0.name, gw_chassis_0.name, gw_chassis_1.name]
sb_idl.get_gateway_chassis_from_cms_options.return_value = [
gw_chassis_0.name, gw_chassis_1.name]
# Invoke the periodic method, it meant to run only once at startup
# so NeverAgain will be raised at the end
self.assertRaises(periodics.NeverAgain,
self.periodic.check_for_ha_chassis_group_address)
# Make sure the non GW chassis has been removed from the
# default HA_CHASSIS_GROUP
nb_idl.ha_chassis_group_del_chassis.assert_called_once_with(
constants.HA_CHASSIS_GROUP_DEFAULT_NAME, non_gw_chassis_0.name,
if_exists=True)
# Assert the GW chassis are being added to the
# default HA_CHASSIS_GROUP
expected_calls = [
mock.call(constants.HA_CHASSIS_GROUP_DEFAULT_NAME,
gw_chassis_1.chassis_name,
priority=constants.HA_CHASSIS_GROUP_HIGHEST_PRIORITY),
# Note that the second chassis is getting priority -1
mock.call(constants.HA_CHASSIS_GROUP_DEFAULT_NAME,
gw_chassis_0.chassis_name,
priority=constants.HA_CHASSIS_GROUP_HIGHEST_PRIORITY - 1)
]
nb_idl.ha_chassis_group_add_chassis.assert_has_calls(expected_calls)

View File

@ -29,6 +29,7 @@ from ovsdbapp.backend.ovs_idl import idlutils
from neutron.common.ovn import constants as ovn_const
from neutron.common.ovn import hash_ring_manager
from neutron.common.ovn import utils
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
from neutron.db import ovn_hash_ring_db
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovsdb_monitor
@ -480,3 +481,80 @@ class TestOvnSbIdlNotifyHandler(test_mech_driver.OVNMechanismDriverTestCase):
self.assertEqual(
1,
self.l3_plugin.schedule_unhosted_gateways.call_count)
class TestChassisEvent(base.BaseTestCase):
def setUp(self):
super(TestChassisEvent, self).setUp()
self.driver = mock.Mock()
self.nb_ovn = self.driver._nb_ovn
self.driver._ovn_client.is_external_ports_supported.return_value = True
self.event = ovsdb_monitor.ChassisEvent(self.driver)
self.is_gw_ch_mock = mock.patch.object(
utils, 'is_gateway_chassis').start()
self.is_gw_ch_mock.return_value = True
def test_handle_ha_chassis_group_changes_create_not_gw(self):
self.is_gw_ch_mock.return_value = False
# Assert chassis is ignored because it's not a gateway chassis
self.assertIsNone(self.event.handle_ha_chassis_group_changes(
self.event.ROW_CREATE, mock.Mock(), mock.Mock()))
self.assertFalse(self.nb_ovn.ha_chassis_group_add_chassis.called)
self.assertFalse(self.nb_ovn.ha_chassis_group_del_chassis.called)
def _test_handle_ha_chassis_group_changes_create(self, event):
row = fakes.FakeOvsdbTable.create_one_ovsdb_table(
attrs={'name': 'SpongeBob'})
ch0 = fakes.FakeOvsdbTable.create_one_ovsdb_table(
attrs={'priority': 10})
ch1 = fakes.FakeOvsdbTable.create_one_ovsdb_table(
attrs={'priority': 9})
default_grp = fakes.FakeOvsdbTable.create_one_ovsdb_table(
attrs={'ha_chassis': [ch0, ch1]})
self.nb_ovn.ha_chassis_group_get.return_value.execute.return_value = (
default_grp)
self.event.handle_ha_chassis_group_changes(event, row, mock.Mock())
# Assert the new chassis has been added to the default
# group with the lowest priority
self.nb_ovn.ha_chassis_group_add_chassis.assert_called_once_with(
ovn_const.HA_CHASSIS_GROUP_DEFAULT_NAME, 'SpongeBob', priority=8)
def test_handle_ha_chassis_group_changes_create(self):
self._test_handle_ha_chassis_group_changes_create(
self.event.ROW_CREATE)
def _test_handle_ha_chassis_group_changes_delete(self, event):
row = fakes.FakeOvsdbTable.create_one_ovsdb_table(
attrs={'name': 'SpongeBob'})
self.event.handle_ha_chassis_group_changes(event, row, mock.Mock())
# Assert chassis was removed from the default group
self.nb_ovn.ha_chassis_group_del_chassis.assert_called_once_with(
ovn_const.HA_CHASSIS_GROUP_DEFAULT_NAME, 'SpongeBob',
if_exists=True)
def test_handle_ha_chassis_group_changes_delete(self):
self._test_handle_ha_chassis_group_changes_delete(
self.event.ROW_DELETE)
def test_handle_ha_chassis_group_changes_update_still_gw(self):
# Assert nothing was done because the update didn't
# change the gateway chassis status
self.assertIsNone(self.event.handle_ha_chassis_group_changes(
self.event.ROW_UPDATE, mock.Mock(), mock.Mock()))
self.assertFalse(self.nb_ovn.ha_chassis_group_add_chassis.called)
self.assertFalse(self.nb_ovn.ha_chassis_group_del_chassis.called)
def test_handle_ha_chassis_group_changes_update_no_longer_gw(self):
self.is_gw_ch_mock.side_effect = (False, True)
# Assert that the chassis was removed from the default group
# after it's no longer being a Gateway chassis
self._test_handle_ha_chassis_group_changes_delete(
self.event.ROW_UPDATE)
def test_handle_ha_chassis_group_changes_update_new_gw(self):
self.is_gw_ch_mock.side_effect = (True, False)
# Assert that the chassis was added to the default group
# after it became a Gateway chassis
self._test_handle_ha_chassis_group_changes_create(
self.event.ROW_UPDATE)

View File

@ -2598,6 +2598,29 @@ class TestOVNMechanismDriverSecurityGroup(
self.assertEqual(
5, self.mech_driver._nb_ovn.add_acl.call_count)
@mock.patch('neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.'
'ovn_client.OVNClient.is_external_ports_supported',
lambda *_: True)
def test_create_port_with_vnic_direct(self):
fake_grp = 'fake-default-ha-group-uuid'
row = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={'uuid': fake_grp})
self.mech_driver._nb_ovn.ha_chassis_group_get.return_value.\
execute.return_value = row
with self.network() as n, self.subnet(n):
self._create_port(
self.fmt, n['network']['id'],
arg_list=(portbindings.VNIC_TYPE,),
**{portbindings.VNIC_TYPE: portbindings.VNIC_DIRECT})
# Assert create_lswitch_port was called with the relevant
# parameters
_, kwargs = self.mech_driver._nb_ovn.create_lswitch_port.call_args
self.assertEqual(
1, self.mech_driver._nb_ovn.create_lswitch_port.call_count)
self.assertEqual(ovn_const.LSP_TYPE_EXTERNAL, kwargs['type'])
self.assertEqual(fake_grp, kwargs['ha_chassis_group'])
def test_update_port_with_sgs(self):
with self.network() as n, self.subnet(n):
sg1 = self._create_empty_sg('sg1')

View File

@ -0,0 +1,11 @@
---
features:
- |
The OVN driver now makes uses of the "external" ports concept
that was introduced by Core OVN. For example, with this work a VM
with a SR-IOV port attached (VNIC type "direct" and no "switchdev"
capability) will now be translated into an "external" port which is
able reply to packets (e.g DHCP) from another host that were bypassed
in the hypervisor before. Note that, for this first interaction all
external ports will belong to the same HA group and will be scheduled
onto the same node.