Merge "[OVN] Add support for external ports"
This commit is contained in:
commit
0f1e360a8c
|
@ -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')
|
||||
|
|
|
@ -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