[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:
parent
1f79ce8736
commit
4824a714bf
@ -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
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
11
releasenotes/notes/external-ports-03050eda7ffe13d5.yaml
Normal file
11
releasenotes/notes/external-ports-03050eda7ffe13d5.yaml
Normal 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.
|
Loading…
Reference in New Issue
Block a user