From 4824a714bfe36fb95e0881d61d8ae56e3422ddc1 Mon Sep 17 00:00:00 2001 From: Lucas Alvares Gomes Date: Mon, 20 Jan 2020 11:20:29 +0000 Subject: [PATCH] [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 --- .../drivers/ovn/mech_driver/mech_driver.py | 7 +- .../ovn/mech_driver/ovsdb/maintenance.py | 53 ++++++++ .../ovn/mech_driver/ovsdb/ovn_client.py | 46 ++++++- .../ovn/mech_driver/ovsdb/ovsdb_monitor.py | 47 +++++++ .../ovn/mech_driver/test_mech_driver.py | 120 ++++++++++++++++++ neutron/tests/unit/common/ovn/test_utils.py | 15 +++ neutron/tests/unit/fake_resources.py | 1 + .../ovn/mech_driver/ovsdb/test_maintenance.py | 56 ++++++++ .../mech_driver/ovsdb/test_ovsdb_monitor.py | 78 ++++++++++++ .../ovn/mech_driver/test_mech_driver.py | 23 ++++ .../external-ports-03050eda7ffe13d5.yaml | 11 ++ 11 files changed, 450 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/external-ports-03050eda7ffe13d5.yaml 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.