diff --git a/neutron/common/ovn/constants.py b/neutron/common/ovn/constants.py index 8f61eb3f094..d21e95fa820 100644 --- a/neutron/common/ovn/constants.py +++ b/neutron/common/ovn/constants.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import re + from neutron_lib.api.definitions import portbindings from neutron_lib import constants as const import six @@ -163,6 +165,7 @@ FIP_ACTION_DISASSOCIATE = 'fip_disassociate' # Loadbalancer constants LRP_PREFIX = "lrp-" +RE_PORT_FROM_GWC = re.compile(r'(%s)([\w-]+)_([\w-]+)' % LRP_PREFIX) LB_VIP_PORT_PREFIX = "ovn-lb-vip-" LB_EXT_IDS_LS_REFS_KEY = 'ls_refs' LB_EXT_IDS_LR_REF_KEY = 'lr_ref' diff --git a/neutron/common/ovn/utils.py b/neutron/common/ovn/utils.py index e3ae0e3b413..0f33b29821c 100644 --- a/neutron/common/ovn/utils.py +++ b/neutron/common/ovn/utils.py @@ -458,3 +458,17 @@ def is_gateway_chassis(chassis): def get_port_capabilities(port): """Return a list of port's capabilities""" return port.get(portbindings.PROFILE, {}).get('capabilities', []) + + +def get_port_id_from_gwc_row(row): + """Return a port_id from gwc row + + The Gateway_Chassis row stores router port_id in + the row name attribute: + + -_ + + :param row: A Gateway_Chassis table row. + :returns: String containing router port_id. + """ + return constants.RE_PORT_FROM_GWC.search(row.name).group(2) diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py index cb79fd5ffee..996cf2bb9f5 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py @@ -434,22 +434,46 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend): except idlutils.RowNotFound: return [] - def get_unhosted_gateways(self, port_physnet_dict, chassis_physnets, - gw_chassis): - unhosted_gateways = [] - for lrp in self._tables['Logical_Router_Port'].rows.values(): - if not lrp.name.startswith('lrp-'): + def get_chassis_gateways(self, chassis_name): + gw_chassis = self.db_find_rows( + 'Gateway_Chassis', ('chassis_name', '=', chassis_name)) + return gw_chassis.execute(check_error=True) + + def get_unhosted_gateways(self, port_physnet_dict, chassis_with_physnets, + all_gw_chassis): + unhosted_gateways = set() + for port, physnet in port_physnet_dict.items(): + lrp_name = '%s%s' % (ovn_const.LRP_PREFIX, port) + original_state = self.get_gateway_chassis_binding(lrp_name) + + # Filter out chassis that lost physnet, the cms option, + # or has been deleted. + actual_gw_chassis = [ + chassis for chassis in original_state + if not utils.is_gateway_chassis_invalid( + chassis, all_gw_chassis, physnet, chassis_with_physnets)] + + # Check if gw ports are fully scheduled. + if len(actual_gw_chassis) >= ovn_const.MAX_GW_CHASSIS: continue - physnet = port_physnet_dict.get(lrp.name[len('lrp-'):]) - chassis_list = self._get_logical_router_port_gateway_chassis(lrp) - is_max_gw_reached = len(chassis_list) < ovn_const.MAX_GW_CHASSIS - for chassis_name, prio in chassis_list: - # TODO(azbiswas): Handle the case when a chassis is no - # longer valid. This may involve moving conntrack states, - # so it needs to discussed in the OVN community first. - if is_max_gw_reached or utils.is_gateway_chassis_invalid( - chassis_name, gw_chassis, physnet, chassis_physnets): - unhosted_gateways.append(lrp.name) + + # If there are no gateways with 'enable-chassis-as-gw' cms option + # then try to schedule on all gateways with physnets connected, + # and filter required physnet. + available_chassis = { + c for c in all_gw_chassis or chassis_with_physnets.keys() + if not utils.is_gateway_chassis_invalid( + c, all_gw_chassis, physnet, chassis_with_physnets)} + + if available_chassis == set(original_state): + # The same situation as was before. Nothing + # to be rescheduled. + continue + if not available_chassis: + # There is no chassis that could host + # this gateway. + continue + unhosted_gateways.add(lrp_name) return unhosted_gateways def add_dhcp_options(self, subnet_id, port_id=None, may_exist=True, 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 d43973e134d..5bd2928309c 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 @@ -82,11 +82,6 @@ class ChassisEvent(row_event.RowEvent): if not self.driver._ovn_client.is_external_ports_supported(): return - # NOTE(lucasgomes): If the external_ids column wasn't updated - # (meaning, Chassis "gateway" status didn't change) just returns - if not hasattr(old, 'external_ids') and event == self.ROW_UPDATE: - 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: @@ -123,6 +118,19 @@ class ChassisEvent(row_event.RowEvent): ovn_const.HA_CHASSIS_GROUP_DEFAULT_NAME, row.name, if_exists=True).execute(check_error=True) + def match_fn(self, event, row, old): + if event != self.ROW_UPDATE: + return True + # NOTE(lucasgomes): If the external_ids column wasn't updated + # (meaning, Chassis "gateway" status didn't change) just returns + if not hasattr(old, 'external_ids') and event == self.ROW_UPDATE: + return + if (old.external_ids.get('ovn-bridge-mappings') != + row.external_ids.get('ovn-bridge-mappings')): + return True + f = utils.is_gateway_chassis + return f(old) != f(row) + def run(self, event, row, old): host = row.hostname phy_nets = [] @@ -133,8 +141,33 @@ class ChassisEvent(row_event.RowEvent): phy_nets = list(mapping_dict) self.driver.update_segment_host_mapping(host, phy_nets) + if utils.is_ovn_l3(self.l3_plugin): - self.l3_plugin.schedule_unhosted_gateways() + # If chassis lost physnet or has been + # deleted we can limit the scope and + # reschedule only ports from this chassis. + # In other cases we need to reschedule all gw ports. + kwargs = {'event_from_chassis': None} + if event == self.ROW_DELETE: + kwargs['event_from_chassis'] = row.name + elif event == self.ROW_UPDATE: + old_mappings = old.external_ids.get('ovn-bridge-mappings', + set()) or set() + new_mappings = row.external_ids.get('ovn-bridge-mappings', + set()) or set() + if old_mappings: + old_mappings = set(old_mappings.split(',')) + if new_mappings: + new_mappings = set(new_mappings.split(',')) + + mappings_removed = old_mappings - new_mappings + mappings_added = new_mappings - old_mappings + if mappings_removed and not mappings_added: + # Mapping has been only removed. So we can + # limit scope of rescheduling only to impacted + # gateway chassis. + kwargs['event_from_chassis'] = row.name + self.l3_plugin.schedule_unhosted_gateways(**kwargs) self.handle_ha_chassis_group_changes(event, row, old) diff --git a/neutron/services/ovn_l3/plugin.py b/neutron/services/ovn_l3/plugin.py index bf3838f86cd..d784f09d17d 100644 --- a/neutron/services/ovn_l3/plugin.py +++ b/neutron/services/ovn_l3/plugin.py @@ -314,24 +314,48 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase, if port['status'] != status: self._plugin.update_port_status(context, port['id'], status) - def schedule_unhosted_gateways(self): + def schedule_unhosted_gateways(self, event_from_chassis=None): + # GW ports and its physnets. port_physnet_dict = self._get_gateway_port_physnet_mapping() - chassis_physnets = self._sb_ovn.get_chassis_and_physnets() - cms = self._sb_ovn.get_gateway_chassis_from_cms_options() + # Filter out unwanted ports in case of event. + if event_from_chassis: + gw_chassis = self._ovn.get_chassis_gateways( + chassis_name=event_from_chassis) + if not gw_chassis: + return + ports_impacted = [] + for gwc in gw_chassis: + try: + ports_impacted.append(utils.get_port_id_from_gwc_row(gwc)) + except AttributeError: + # Malformed GWC format. + pass + port_physnet_dict = { + k: v + for k, v in port_physnet_dict.items() + if k in ports_impacted} + if not port_physnet_dict: + return + # All chassis with physnets configured. + chassis_with_physnets = self._sb_ovn.get_chassis_and_physnets() + # All chassis with enable_as_gw_chassis set + all_gw_chassis = self._sb_ovn.get_gateway_chassis_from_cms_options() unhosted_gateways = self._ovn.get_unhosted_gateways( - port_physnet_dict, chassis_physnets, cms) + port_physnet_dict, chassis_with_physnets, + all_gw_chassis) for g_name in unhosted_gateways: - physnet = port_physnet_dict.get(g_name[len('lrp-'):]) + physnet = port_physnet_dict.get(g_name[len(ovn_const.LRP_PREFIX):]) # Remove any invalid gateway chassis from the list, otherwise # we can have a situation where all existing_chassis are invalid existing_chassis = self._ovn.get_gateway_chassis_binding(g_name) master = existing_chassis[0] if existing_chassis else None existing_chassis = self.scheduler.filter_existing_chassis( - nb_idl=self._ovn, gw_chassis=cms, - physnet=physnet, chassis_physnets=chassis_physnets, + nb_idl=self._ovn, gw_chassis=all_gw_chassis, + physnet=physnet, chassis_physnets=chassis_with_physnets, existing_chassis=existing_chassis) candidates = self._ovn_client.get_candidates_for_scheduling( - physnet, cms=cms, chassis_physnets=chassis_physnets) + physnet, cms=all_gw_chassis, + chassis_physnets=chassis_with_physnets) chassis = self.scheduler.select( self._ovn, self._sb_ovn, g_name, candidates=candidates, existing_chassis=existing_chassis) diff --git a/neutron/tests/functional/services/ovn_l3/test_plugin.py b/neutron/tests/functional/services/ovn_l3/test_plugin.py index 99a38ae7ba6..780800f97d5 100644 --- a/neutron/tests/functional/services/ovn_l3/test_plugin.py +++ b/neutron/tests/functional/services/ovn_l3/test_plugin.py @@ -135,11 +135,28 @@ class TestRouter(base.TestOVNFunctionalBase): def test_gateway_chassis_with_cms_and_no_bridge_mappings(self): # chassis1 is having proper bridge mappings. # chassis3 is having enable-chassis-as-gw, but no bridge mappings. - # Test if chassis1 is selected as candidate or not. self.chassis3 = self.add_fake_chassis( 'ovs-host3', external_ids={'ovn-cms-options': 'enable-chassis-as-gw'}) - self._check_gateway_chassis_candidates([self.chassis1]) + ovn_client = self.l3_plugin._ovn_client + ext1 = self._create_ext_network( + 'ext1', 'vlan', 'physnet1', 1, "10.0.0.1", "10.0.0.0/24") + # As we have 'gateways' in the system, but without required + # chassis we should not schedule gw in that case at all. + self._set_redirect_chassis_to_invalid_chassis(ovn_client) + with mock.patch.object(ovn_client._ovn_scheduler, 'select', + return_value=[self.chassis1]), \ + mock.patch.object(self.l3_plugin.scheduler, 'select', + side_effect=[self.chassis1]): + gw_info = {'network_id': ext1['network']['id']} + self._create_router('router1', gw_info=gw_info) + + with mock.patch.object( + ovn_client._nb_idl, 'update_lrouter_port') as ulrp: + self.l3_plugin.schedule_unhosted_gateways() + # Make sure that we don't schedule on chassis3 + # and do not updated the lrp port. + ulrp.assert_not_called() def test_gateway_chassis_with_bridge_mappings_and_no_cms(self): # chassis1 is configured with proper bridge mappings, diff --git a/neutron/tests/unit/fake_resources.py b/neutron/tests/unit/fake_resources.py index 442d457e08b..2a53d1a2b43 100644 --- a/neutron/tests/unit/fake_resources.py +++ b/neutron/tests/unit/fake_resources.py @@ -76,6 +76,7 @@ class FakeOvsdbNbOvnIdl(object): self.delete_address_set = mock.Mock() self.update_address_set = mock.Mock() self.get_all_chassis_gateway_bindings = mock.Mock() + self.get_chassis_gateways = mock.Mock() self.get_gateway_chassis_binding = mock.Mock() self.get_unhosted_gateways = mock.Mock() self.add_dhcp_options = mock.Mock() diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl_ovn.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl_ovn.py index ed57baf9933..d3fbdd9b2f0 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl_ovn.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl_ovn.py @@ -580,44 +580,97 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn): def test_get_unhosted_gateways(self): self._load_nb_db() - # Test only host-1 in the valid list + # Port physnet-dict + port_physnet_dict = { + 'orp-id-a1': 'physnet1', # scheduled + 'orp-id-a2': 'physnet1', # scheduled + 'orp-id-a3': 'physnet1', # not scheduled + 'orp-id-b6': 'physnet2'} # not scheduled + # Test only that orp-id-a3 is to be scheduled. + # Rest ports don't have required chassis (physnet2) + # or are already scheduled. unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( - {}, {'host-1': 'physnet1'}, []) - expected = ['lrp-orp-id-a1', 'lrp-orp-id-a2', - 'lrp-orp-id-a3', 'lrp-orp-id-b2'] + port_physnet_dict, {'host-1': 'physnet1', 'host-2': 'physnet3'}, + ['host-1', 'host-2']) + expected = ['lrp-orp-id-a3'] self.assertItemsEqual(unhosted_gateways, expected) # Test both host-1, host-2 in valid list unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( - {}, {'host-1': 'physnet1', 'host-2': 'physnet2'}, []) - self.assertItemsEqual(unhosted_gateways, expected) - # Schedule unhosted_gateways on host-2 - for unhosted_gateway in unhosted_gateways: - router_row = self._find_ovsdb_fake_row(self.lrp_table, - 'name', unhosted_gateway) - setattr(router_row, 'options', { - ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-2'}) - unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( - {}, {'host-1': 'physnet1', 'host-2': 'physnet2'}, []) + port_physnet_dict, {'host-1': 'physnet1', 'host-2': 'physnet2'}, + ['host-1', 'host-2']) + expected = ['lrp-orp-id-a3', 'lrp-orp-id-b6'] self.assertItemsEqual(unhosted_gateways, expected) - def test_unhosted_gateway_max_chassis(self): + def test_get_unhosted_gateways_deleted_physnet(self): + self._load_nb_db() + # The LRP is on host-2 now + router_row = self._find_ovsdb_fake_row(self.lrp_table, + 'name', 'lrp-orp-id-a1') + setattr(router_row, 'options', { + ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-2'}) + port_physnet_dict = {'orp-id-a1': 'physnet1'} + # Lets spoof that physnet1 is deleted from host-2. + unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( + port_physnet_dict, {'host-1': 'physnet1', 'host-2': 'physnet3'}, + ['host-1', 'host-2']) + # Make sure that lrp is rescheduled, because host-1 has physet1 + expected = ['lrp-orp-id-a1'] + self.assertItemsEqual(unhosted_gateways, expected) + # Spoof that there is no valid host with required physnet. + unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( + port_physnet_dict, {'host-1': 'physnet4', 'host-2': 'physnet3'}, + ['host-1', 'host-2']) + self.assertItemsEqual(unhosted_gateways, []) + + def _test_get_unhosted_gateway_max_chassis(self, r): gw_chassis_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() self._tables['Gateway_Chassis'] = gw_chassis_table gw_chassis = collections.namedtuple('gw_chassis', 'chassis_name priority') TestNBImplIdlOvn.fake_set['lrouter_ports'][0]['gateway_chassis'] = [ gw_chassis(chassis_name='host-%s' % x, - priority=x) for x in range(1, 6)] - for port in TestNBImplIdlOvn.fake_set['lrouter_ports'][1:]: - port['gateway_chassis'] = [] + priority=x) for x in r] self._load_nb_db() + self.port_physnet_dict = {'orp-id-a1': 'physnet1'} + + def test_get_unhosted_gateway_max_chassis_lack_of_chassis(self): + self._test_get_unhosted_gateway_max_chassis(r=(1, 3, 5)) unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( - {}, {'host-1': 'physnet1', 'host-2': 'physnet2', - 'host-3': 'physnet1', 'host-4': 'physnet2', - 'host-5': 'physnet1', 'host-6': 'physnet2'}, []) + self.port_physnet_dict, + {'host-1': 'physnet1', 'host-2': 'physnet2', + 'host-3': 'physnet1', 'host-4': 'physnet2', + 'host-5': 'physnet1', 'host-6': 'physnet2'}, + ['host-%s' % x for x in range(1, 7)]) + # We don't have required number of chassis expected = [] self.assertItemsEqual(unhosted_gateways, expected) + def test_get_unhosted_gateway_max_chassis(self): + # We have required number of chassis, and lrp + # is hosted everywhere. + self._test_get_unhosted_gateway_max_chassis(r=range(1, 6)) + unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( + self.port_physnet_dict, + {'host-1': 'physnet1', 'host-2': 'physnet1', + 'host-3': 'physnet1', 'host-4': 'physnet1', + 'host-5': 'physnet1', 'host-6': 'physnet1'}, + ['host-%s' % x for x in range(1, 7)]) + expected = [] + self.assertItemsEqual(unhosted_gateways, expected) + + def test_get_unhosed_gateway_schedule_to_max(self): + # The LRP is not yet scheduled on all chassis + # but we can schedule on new chassis now. + self._test_get_unhosted_gateway_max_chassis(r=range(1, 4)) + unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( + self.port_physnet_dict, + {'host-1': 'physnet1', 'host-2': 'physnet1', + 'host-3': 'physnet1', 'host-4': 'physnet1', + 'host-5': 'physnet1', 'host-6': 'physnet1'}, + ['host-%s' % x for x in range(1, 7)]) + expected = ['lrp-orp-id-a1'] + self.assertItemsEqual(unhosted_gateways, expected) + def test_get_subnet_dhcp_options(self): self._load_nb_db() subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( 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 3d21d458cec..53be4de4e45 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 @@ -459,17 +459,15 @@ class TestOvnSbIdlNotifyHandler(test_mech_driver.OVNMechanismDriverTestCase): self._test_chassis_helper('create', self.row_json) self.driver.update_segment_host_mapping.assert_called_once_with( 'fake-hostname', ['fake-phynet1']) - self.assertEqual( - 1, - self.l3_plugin.schedule_unhosted_gateways.call_count) + self.l3_plugin.schedule_unhosted_gateways.assert_called_once_with( + event_from_chassis=None) def test_chassis_delete_event(self): self._test_chassis_helper('delete', self.row_json) self.driver.update_segment_host_mapping.assert_called_once_with( 'fake-hostname', []) - self.assertEqual( - 1, - self.l3_plugin.schedule_unhosted_gateways.call_count) + self.l3_plugin.schedule_unhosted_gateways.assert_called_once_with( + event_from_chassis='fake-name') def test_chassis_update_event(self): old_row_json = copy.deepcopy(self.row_json) @@ -478,9 +476,54 @@ class TestOvnSbIdlNotifyHandler(test_mech_driver.OVNMechanismDriverTestCase): self._test_chassis_helper('update', self.row_json, old_row_json) self.driver.update_segment_host_mapping.assert_called_once_with( 'fake-hostname', ['fake-phynet1']) - self.assertEqual( - 1, - self.l3_plugin.schedule_unhosted_gateways.call_count) + self.l3_plugin.schedule_unhosted_gateways.assert_called_once_with( + event_from_chassis=None) + + def test_chassis_update_event_reschedule_not_needed(self): + self.row_json['external_ids'][1].append(['foo_field', 'foo_value_new']) + old_row_json = copy.deepcopy(self.row_json) + old_row_json['external_ids'][1][1][1] = ( + "foo_value") + self._test_chassis_helper('update', self.row_json, old_row_json) + self.driver.update_segment_host_mapping.assert_not_called() + self.l3_plugin.schedule_unhosted_gateways.assert_not_called() + + def test_chassis_update_event_reschedule_lost_physnet(self): + old_row_json = copy.deepcopy(self.row_json) + self.row_json['external_ids'][1][0][1] = '' + self._test_chassis_helper('update', self.row_json, old_row_json) + self.l3_plugin.schedule_unhosted_gateways.assert_called_once_with( + event_from_chassis='fake-name') + + def test_chassis_update_event_reschedule_add_physnet(self): + old_row_json = copy.deepcopy(self.row_json) + self.row_json['external_ids'][1][0][1] += ',foo_physnet:foo_br' + self._test_chassis_helper('update', self.row_json, old_row_json) + self.driver.update_segment_host_mapping.assert_called_once_with( + 'fake-hostname', ['fake-phynet1', 'foo_physnet']) + self.l3_plugin.schedule_unhosted_gateways.assert_called_once_with( + event_from_chassis=None) + + def test_chassis_update_event_reschedule_add_and_remove_physnet(self): + old_row_json = copy.deepcopy(self.row_json) + self.row_json['external_ids'][1][0][1] = 'foo_physnet:foo_br' + self._test_chassis_helper('update', self.row_json, old_row_json) + self.driver.update_segment_host_mapping.assert_called_once_with( + 'fake-hostname', ['foo_physnet']) + self.l3_plugin.schedule_unhosted_gateways.assert_called_once_with( + event_from_chassis=None) + + def test_chassis_update_empty_no_external_ids(self): + old_row_json = copy.deepcopy(self.row_json) + old_row_json.pop('external_ids') + with mock.patch( + 'neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.' + 'ovsdb_monitor.ChassisEvent.' + 'handle_ha_chassis_group_changes') as mock_ha: + self._test_chassis_helper('update', self.row_json, old_row_json) + self.driver.update_segment_host_mapping.assert_not_called() + self.l3_plugin.schedule_unhosted_gateways.assert_not_called() + mock_ha.assert_not_called() class TestChassisEvent(base.BaseTestCase): @@ -558,12 +601,3 @@ class TestChassisEvent(base.BaseTestCase): # after it became a Gateway chassis self._test_handle_ha_chassis_group_changes_create( self.event.ROW_UPDATE) - - def test_handle_ha_chassis_group_changes_update_ext_id_not_found(self): - self.is_gw_ch_mock.side_effect = (True, False) - old = fakes.FakeOvsdbTable.create_one_ovsdb_table( - attrs={'name': 'SpongeBob'}) - self.assertIsNone(self.event.handle_ha_chassis_group_changes( - self.event.ROW_UPDATE, mock.Mock(), old)) - self.assertFalse(self.nb_ovn.ha_chassis_group_add_chassis.called) - self.assertFalse(self.nb_ovn.ha_chassis_group_del_chassis.called) diff --git a/neutron/tests/unit/services/ovn_l3/test_plugin.py b/neutron/tests/unit/services/ovn_l3/test_plugin.py index 3540778664f..c9aa8f3c445 100644 --- a/neutron/tests/unit/services/ovn_l3/test_plugin.py +++ b/neutron/tests/unit/services/ovn_l3/test_plugin.py @@ -1354,16 +1354,14 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): @mock.patch('neutron.services.ovn_l3.plugin.OVNL3RouterPlugin.' '_get_gateway_port_physnet_mapping') def test_schedule_unhosted_gateways(self, get_gppm): - physnet_dict = {'foo-1': 'physnet1', - 'foo-2': 'physnet1', - 'foo-3': 'physnet1'} unhosted_gws = ['lrp-foo-1', 'lrp-foo-2', 'lrp-foo-3'] + get_gppm.return_value = {k[len(ovn_const.LRP_PREFIX):]: 'physnet1' + for k in unhosted_gws} chassis_mappings = { 'chassis1': ['physnet1'], 'chassis2': ['physnet1'], 'chassis3': ['physnet1']} chassis = ['chassis1', 'chassis2', 'chassis3'] - get_gppm.return_value = physnet_dict self.sb_idl().get_chassis_and_physnets.return_value = ( chassis_mappings) self.sb_idl().get_gateway_chassis_from_cms_options.return_value = ( @@ -1408,6 +1406,34 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): mock.call('lrp-foo-3', gateway_chassis=['chassis3', 'chassis2', 'chassis1'])]) + @mock.patch('neutron.services.ovn_l3.plugin.OVNL3RouterPlugin.' + '_get_gateway_port_physnet_mapping') + def test_schedule_unhosted_gateways_on_event_no_gw_chassis(self, get_gppm): + unhosted_gws = ['lrp-foo-1', 'lrp-foo-2', 'lrp-foo-3'] + get_gppm.return_value = {k[len(ovn_const.LRP_PREFIX):]: 'physnet1' + for k in unhosted_gws} + self.nb_idl().get_chassis_gateways.return_value = [] + self.l3_inst.schedule_unhosted_gateways(event_from_chassis='chassis4') + self.nb_idl().get_unhosted_gateways.assert_not_called() + + @mock.patch('neutron.services.ovn_l3.plugin.OVNL3RouterPlugin.' + '_get_gateway_port_physnet_mapping') + def test_schedule_unhosted_gateways_on_event(self, get_gppm): + unhosted_gws = ['lrp-foo-1', 'lrp-foo-2', 'lrp-foo-3'] + get_gppm.return_value = {k[len(ovn_const.LRP_PREFIX):]: 'physnet1' + for k in unhosted_gws} + foo_gw = fake_resources.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'name': 'lrp-foo-1_chassis1', + 'chassis_name': 'chassis1'}) + self.nb_idl().get_chassis_gateways.return_value = [ + foo_gw] + self.nb_idl().get_unhosted_gateways.return_value = [] + # Fake that rescheduling is executed on chassis event + self.l3_inst.schedule_unhosted_gateways(event_from_chassis='chassis1') + # Validate that only foo-1 port is beign rescheduled. + self.nb_idl().get_unhosted_gateways.assert_called_once_with( + {'foo-1': 'physnet1'}, mock.ANY, mock.ANY) + @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_network') @mock.patch('neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.' 'ovn_client.OVNClient._get_router_ports')