diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py index 9437fdf6658..21786e82435 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py @@ -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 diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py index 276888c2dee..7d11e84d00e 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py @@ -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): diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py index 18afcf36578..7c71605766b 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py @@ -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()) diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py index 1d4dfcee997..28235b1ae2b 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovsdb_monitor.py @@ -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 diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py index f720cf1b471..2d480212fb6 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py @@ -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)) diff --git a/neutron/tests/unit/common/ovn/test_utils.py b/neutron/tests/unit/common/ovn/test_utils.py index ea5e3ad9a74..b63fc5e8e5c 100644 --- a/neutron/tests/unit/common/ovn/test_utils.py +++ b/neutron/tests/unit/common/ovn/test_utils.py @@ -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): diff --git a/neutron/tests/unit/fake_resources.py b/neutron/tests/unit/fake_resources.py index 3181790bafa..1363fb3eb48 100644 --- a/neutron/tests/unit/fake_resources.py +++ b/neutron/tests/unit/fake_resources.py @@ -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): diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py index 70d338647a8..0bd17a4f4a4 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py @@ -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) diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py index 6ca4ea83ea1..ac5ad29a8c1 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovsdb_monitor.py @@ -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) diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py index 97b9b3c8c41..4b86972d3a5 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py @@ -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') diff --git a/releasenotes/notes/external-ports-03050eda7ffe13d5.yaml b/releasenotes/notes/external-ports-03050eda7ffe13d5.yaml new file mode 100644 index 00000000000..95d7e5559b7 --- /dev/null +++ b/releasenotes/notes/external-ports-03050eda7ffe13d5.yaml @@ -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.