From b5253b224bcf6cec428f18a831798d0955c61797 Mon Sep 17 00:00:00 2001 From: zhouhenglc Date: Tue, 18 Jan 2022 17:25:06 +0800 Subject: [PATCH] [OVN] Reschedule router GW chassis when AZ updated when chassis's available zone updated, triger rescheduler router gateway chassis. Closes-bug: #1958225 Change-Id: If7cf55f2c12b388fc34fa942b6b0bf70cb9eb866 --- neutron/common/ovn/utils.py | 10 +++- .../ml2/drivers/ovn/mech_driver/ovsdb/api.py | 3 +- .../ovn/mech_driver/ovsdb/impl_idl_ovn.py | 33 +++++++++-- neutron/scheduler/l3_ovn_scheduler.py | 5 +- neutron/services/ovn_l3/plugin.py | 9 ++- neutron/tests/functional/base.py | 10 +++- .../functional/services/ovn_l3/test_plugin.py | 59 +++++++++++++++++-- neutron/tests/unit/common/ovn/test_utils.py | 30 +++++++--- neutron/tests/unit/fake_resources.py | 10 +++- .../mech_driver/ovsdb/test_impl_idl_ovn.py | 40 +++++++++---- .../unit/scheduler/test_l3_ovn_scheduler.py | 4 +- .../tests/unit/services/ovn_l3/test_plugin.py | 2 +- 12 files changed, 179 insertions(+), 36 deletions(-) diff --git a/neutron/common/ovn/utils.py b/neutron/common/ovn/utils.py index 623568a88c2..1d6a219e26f 100644 --- a/neutron/common/ovn/utils.py +++ b/neutron/common/ovn/utils.py @@ -498,7 +498,8 @@ def ovn_metadata_name(id_): def is_gateway_chassis_invalid(chassis_name, gw_chassis, - physnet, chassis_physnets): + physnet, chassis_physnets, + az_hints, chassis_with_azs): """Check if gateway chassis is invalid @param chassis_name: gateway chassis name @@ -509,6 +510,10 @@ def is_gateway_chassis_invalid(chassis_name, gw_chassis, @type physnet: string @param chassis_physnets: Dictionary linking chassis with their physnets @type chassis_physnets: {} + @param az_hints: available zone hints associated to chassis_name + @type az_hints: [] + @param chassis_with_azs: Dictionary linking chassis with their azs + @type chassis_with_azs: {} @return Boolean """ @@ -520,6 +525,9 @@ def is_gateway_chassis_invalid(chassis_name, gw_chassis, return True elif gw_chassis and chassis_name not in gw_chassis: return True + elif az_hints and not set(az_hints) & set(chassis_with_azs.get( + chassis_name, [])): + return True return False diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/api.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/api.py index 0d16d3f100d..a77d5baf411 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/api.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/api.py @@ -293,13 +293,14 @@ class API(api.API, metaclass=abc.ABCMeta): @abc.abstractmethod def get_unhosted_gateways(self, port_physnet_dict, chassis_physnets, - gw_chassis): + gw_chassis, chassis_with_azs): """Return a list of gateways not hosted on chassis :param port_physnet_dict: Dictionary of gateway ports and their physnet :param chassis_physnets: Dictionary of chassis and physnets :param gw_chassis: List of gateway chassis provided by admin through ovn-cms-options + :param chassis_with_azs: Dictionary of chassis and available zones :returns: List of gateways not hosted on a valid chassis """ 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 d2ed6b06413..452eb2aaca3 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 @@ -513,24 +513,42 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend): except idlutils.RowNotFound: return [] + def get_gateway_chassis_az_hints(self, gateway_name): + lrp = self.lookup('Logical_Router_Port', gateway_name, + default=None) + if not lrp: + return [] + router_id = lrp.external_ids.get( + ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY, "") + lrouter = self.lookup('Logical_Router', utils.ovn_name(router_id), + default=None) + if not lrouter: + return [] + az_string = lrouter.external_ids.get( + ovn_const.OVN_AZ_HINTS_EXT_ID_KEY, "") + if not az_string: + return [] + return az_string.split(",") + 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): + all_gw_chassis, chassis_with_azs): 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) - + az_hints = self.get_gateway_chassis_az_hints(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)] + chassis, all_gw_chassis, physnet, chassis_with_physnets, + az_hints, chassis_with_azs)] # Check if gw ports are fully scheduled. if len(actual_gw_chassis) >= ovn_const.MAX_GW_CHASSIS: @@ -542,7 +560,8 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend): 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)} + c, all_gw_chassis, physnet, chassis_with_physnets, + az_hints, chassis_with_azs)} if available_chassis == set(original_state): # The same situation as was before. Nothing @@ -853,6 +872,12 @@ class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend): chassis_info_dict[ch.name] = self._get_chassis_physnets(ch) return chassis_info_dict + def get_chassis_and_azs(self): + chassis_azs = {} + for ch in self.chassis_list().execute(check_error=True): + chassis_azs[ch.name] = utils.get_chassis_availability_zones(ch) + return chassis_azs + def get_all_chassis(self, chassis_type=None): # TODO(azbiswas): Use chassis_type as input once the compute type # preference patch (as part of external ids) merges. diff --git a/neutron/scheduler/l3_ovn_scheduler.py b/neutron/scheduler/l3_ovn_scheduler.py index c084ed31250..6bcb9e5d2e9 100644 --- a/neutron/scheduler/l3_ovn_scheduler.py +++ b/neutron/scheduler/l3_ovn_scheduler.py @@ -44,11 +44,12 @@ class OVNGatewayScheduler(object, metaclass=abc.ABCMeta): def filter_existing_chassis(self, nb_idl, gw_chassis, physnet, chassis_physnets, - existing_chassis): + existing_chassis, az_hints, chassis_with_azs): chassis_list = copy.copy(existing_chassis) for chassis_name in existing_chassis: if utils.is_gateway_chassis_invalid(chassis_name, gw_chassis, - physnet, chassis_physnets): + physnet, chassis_physnets, + az_hints, chassis_with_azs): LOG.debug("Chassis %(chassis)s is invalid for scheduling " "router in physnet: %(physnet)s.", {'chassis': chassis_name, diff --git a/neutron/services/ovn_l3/plugin.py b/neutron/services/ovn_l3/plugin.py index f4fe016f40f..1f0882c0d20 100644 --- a/neutron/services/ovn_l3/plugin.py +++ b/neutron/services/ovn_l3/plugin.py @@ -394,20 +394,23 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase, 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() + chassis_with_azs = self._sb_ovn.get_chassis_and_azs() unhosted_gateways = self._nb_ovn.get_unhosted_gateways( port_physnet_dict, chassis_with_physnets, - all_gw_chassis) + all_gw_chassis, chassis_with_azs) for g_name in unhosted_gateways: 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._nb_ovn.get_gateway_chassis_binding(g_name) primary = existing_chassis[0] if existing_chassis else None + az_hints = self._nb_ovn.get_gateway_chassis_az_hints(g_name) existing_chassis = self.scheduler.filter_existing_chassis( nb_idl=self._nb_ovn, gw_chassis=all_gw_chassis, physnet=physnet, chassis_physnets=chassis_with_physnets, - existing_chassis=existing_chassis) - az_hints = self._get_availability_zones_from_router_port(g_name) + existing_chassis=existing_chassis, az_hints=az_hints, + chassis_with_azs=chassis_with_azs) + candidates = self._ovn_client.get_candidates_for_scheduling( physnet, cms=all_gw_chassis, chassis_physnets=chassis_with_physnets, diff --git a/neutron/tests/functional/base.py b/neutron/tests/functional/base.py index 4b8b2d2c36f..5faf68628e8 100644 --- a/neutron/tests/functional/base.py +++ b/neutron/tests/functional/base.py @@ -393,9 +393,17 @@ class TestOVNFunctionalBase(test_plugin.Ml2PluginV2TestCase, self._start_ovn_northd() def add_fake_chassis(self, host, physical_nets=None, external_ids=None, - name=None): + name=None, azs=None): physical_nets = physical_nets or [] external_ids = external_ids or {} + if azs is None: + azs = ['ovn'] + if azs: + if 'ovn-cms-options' not in external_ids: + external_ids['ovn-cms-options'] = 'availability-zones=' + else: + external_ids['ovn-cms-options'] += ',availability-zones=' + external_ids['ovn-cms-options'] += ':'.join(azs) bridge_mapping = ",".join(["%s:br-provider%s" % (phys_net, i) for i, phys_net in enumerate(physical_nets)]) diff --git a/neutron/tests/functional/services/ovn_l3/test_plugin.py b/neutron/tests/functional/services/ovn_l3/test_plugin.py index be7fb6c3aef..f87630bd3c7 100644 --- a/neutron/tests/functional/services/ovn_l3/test_plugin.py +++ b/neutron/tests/functional/services/ovn_l3/test_plugin.py @@ -41,11 +41,13 @@ class TestRouter(base.TestOVNFunctionalBase): self.cr_lrp_pb_event = events.WaitForCrLrpPortBindingEvent() self.sb_api.idl.notify_handler.watch_event(self.cr_lrp_pb_event) - def _create_router(self, name, gw_info=None): + def _create_router(self, name, gw_info=None, az_hints=None): router = {'router': {'name': name, 'admin_state_up': True, 'tenant_id': self._tenant_id}} + if az_hints: + router['router']['availability_zone_hints'] = az_hints if gw_info: router['router']['external_gateway_info'] = gw_info return self.l3_plugin.create_router(self.context, router) @@ -92,7 +94,8 @@ class TestRouter(base.TestOVNFunctionalBase): rc = row.options.get(ovn_const.OVN_GATEWAY_CHASSIS_KEY) self.assertIn(rc, expected) - def _check_gateway_chassis_candidates(self, candidates): + def _check_gateway_chassis_candidates(self, candidates, + router_az_hints=None): # In this test, fake_select() is called once from _create_router() # and later from schedule_unhosted_gateways() ovn_client = self.l3_plugin._ovn_client @@ -104,7 +107,7 @@ class TestRouter(base.TestOVNFunctionalBase): def fake_select(*args, **kwargs): self.assertCountEqual(candidates, kwargs['candidates']) # We are not interested in further processing, let us return - # INVALID_CHASSIS to avoid erros + # INVALID_CHASSIS to avoid errors return [ovn_const.OVN_GATEWAY_INVALID_CHASSIS] with mock.patch.object(ovn_client._ovn_scheduler, 'select', @@ -112,7 +115,8 @@ class TestRouter(base.TestOVNFunctionalBase): mock.patch.object(self.l3_plugin.scheduler, 'select', side_effect=fake_select) as plugin_select: gw_info = {'network_id': ext1['network']['id']} - self._create_router('router1', gw_info=gw_info) + self._create_router('router1', gw_info=gw_info, + az_hints=router_az_hints) self.assertFalse(plugin_select.called) self.assertTrue(client_select.called) client_select.reset_mock() @@ -160,6 +164,53 @@ class TestRouter(base.TestOVNFunctionalBase): # and do not updated the lrp port. ulrp.assert_not_called() + def test_gateway_chassis_with_cms_and_azs(self): + # Both chassis3 and chassis4 are having azs, + # but only chassis3's azs is ['ovn']. + # Test if chassis3 is selected as candidate or not. + self.chassis3 = self.add_fake_chassis( + 'ovs-host3', physical_nets=['physnet1'], + external_ids={'ovn-cms-options': 'enable-chassis-as-gw'}, + azs=['ovn']) + self.chassis4 = self.add_fake_chassis( + 'ovs-host4', physical_nets=['physnet1'], + external_ids={'ovn-cms-options': 'enable-chassis-as-gw'}, + azs=['ovn2']) + self._check_gateway_chassis_candidates([self.chassis3], + router_az_hints=['ovn']) + + def test_gateway_chassis_with_cms_and_not_match_azs(self): + # add chassis3 is having azs [ovn], match router az_hints, + # if not add chassis3, create router will fail with + # AvailabilityZoneNotFound. after create will delete if. + # add chassis4 is having azs [ovn2], not match routers az_hints [ovn] + self.chassis3 = self.add_fake_chassis( + 'ovs-host3', physical_nets=['physnet1'], + external_ids={'ovn-cms-options': 'enable-chassis-as-gw'}) + self.chassis4 = self.add_fake_chassis( + 'ovs-host4', physical_nets=['physnet1'], + external_ids={'ovn-cms-options': 'enable-chassis-as-gw'}, + azs=['ovn2']) + 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, az_hints=['ovn']) + self.del_fake_chassis(self.chassis3) + 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, # but none of the chassis having enable-chassis-as-gw. diff --git a/neutron/tests/unit/common/ovn/test_utils.py b/neutron/tests/unit/common/ovn/test_utils.py index e4cfef36091..43cf62e1a8b 100644 --- a/neutron/tests/unit/common/ovn/test_utils.py +++ b/neutron/tests/unit/common/ovn/test_utils.py @@ -209,26 +209,28 @@ class TestGateWayChassisValidity(base.BaseTestCase): self.chassis_name = self.gw_chassis[0] self.physnet = 'physical-nw-1' self.chassis_physnets = {self.chassis_name: [self.physnet]} + self.az_hints = ['ovn', ] + self.chassis_azs = {self.chassis_name: self.az_hints} def test_gateway_chassis_valid(self): # Return False, since everything is valid self.assertFalse(utils.is_gateway_chassis_invalid( self.chassis_name, self.gw_chassis, self.physnet, - self.chassis_physnets)) + self.chassis_physnets, self.az_hints, self.chassis_azs)) def test_gateway_chassis_due_to_invalid_chassis_name(self): # Return True since chassis is invalid self.chassis_name = constants.OVN_GATEWAY_INVALID_CHASSIS self.assertTrue(utils.is_gateway_chassis_invalid( self.chassis_name, self.gw_chassis, self.physnet, - self.chassis_physnets)) + self.chassis_physnets, self.az_hints, self.chassis_azs)) def test_gateway_chassis_for_chassis_not_in_chassis_physnets(self): # Return True since chassis is not in chassis_physnets self.chassis_name = 'host-2' self.assertTrue(utils.is_gateway_chassis_invalid( self.chassis_name, self.gw_chassis, self.physnet, - self.chassis_physnets)) + self.chassis_physnets, self.az_hints, self.chassis_azs)) def test_gateway_chassis_for_undefined_physnet(self): # Return True since physnet is not defined @@ -236,14 +238,14 @@ class TestGateWayChassisValidity(base.BaseTestCase): self.physnet = None self.assertTrue(utils.is_gateway_chassis_invalid( self.chassis_name, self.gw_chassis, self.physnet, - self.chassis_physnets)) + self.chassis_physnets, self.az_hints, self.chassis_azs)) def test_gateway_chassis_for_physnet_not_in_chassis_physnets(self): # Return True since physnet is not in chassis_physnets self.physnet = 'physical-nw-2' self.assertTrue(utils.is_gateway_chassis_invalid( self.chassis_name, self.gw_chassis, self.physnet, - self.chassis_physnets)) + self.chassis_physnets, self.az_hints, self.chassis_azs)) def test_gateway_chassis_for_gw_chassis_empty(self): # Return False if gw_chassis is [] @@ -252,14 +254,28 @@ class TestGateWayChassisValidity(base.BaseTestCase): self.gw_chassis = [] self.assertFalse(utils.is_gateway_chassis_invalid( self.chassis_name, self.gw_chassis, self.physnet, - self.chassis_physnets)) + self.chassis_physnets, self.az_hints, self.chassis_azs)) def test_gateway_chassis_for_chassis_not_in_gw_chassis_list(self): # Return True since chassis_name not in gw_chassis self.gw_chassis = ['host-2'] self.assertTrue(utils.is_gateway_chassis_invalid( self.chassis_name, self.gw_chassis, self.physnet, - self.chassis_physnets)) + self.chassis_physnets, self.az_hints, self.chassis_azs)) + + def test_gateway_chassis_for_chassis_az_hints_empty(self): + # Return False since az_hints is [] + az_hints = [] + self.assertFalse(utils.is_gateway_chassis_invalid( + self.chassis_name, self.gw_chassis, self.physnet, + self.chassis_physnets, az_hints, self.chassis_azs)) + + def test_gateway_chassis_for_chassis_no_in_az_hints(self): + # Return True since az_hints not match chassis_azs + az_hints = ['ovs'] + self.assertTrue(utils.is_gateway_chassis_invalid( + self.chassis_name, self.gw_chassis, self.physnet, + self.chassis_physnets, az_hints, self.chassis_azs)) class TestDHCPUtils(base.BaseTestCase): diff --git a/neutron/tests/unit/fake_resources.py b/neutron/tests/unit/fake_resources.py index 492a3216dcf..8d8ab2cba11 100644 --- a/neutron/tests/unit/fake_resources.py +++ b/neutron/tests/unit/fake_resources.py @@ -14,6 +14,7 @@ import collections import copy +import sys from unittest import mock from neutron_lib.api.definitions import l3 @@ -77,6 +78,8 @@ class FakeOvsdbNbOvnIdl(object): self.get_all_chassis_gateway_bindings = mock.Mock() self.get_chassis_gateways = mock.Mock() self.get_gateway_chassis_binding = mock.Mock() + self.get_gateway_chassis_az_hints = mock.Mock() + self.get_gateway_chassis_az_hints.return_value = [] self.get_unhosted_gateways = mock.Mock() self.add_dhcp_options = mock.Mock() self.delete_dhcp_options = mock.Mock() @@ -168,6 +171,8 @@ class FakeOvsdbSbOvnIdl(object): self.chassis_exists.return_value = True self.get_chassis_hostname_and_physnets = mock.Mock() self.get_chassis_hostname_and_physnets.return_value = {} + self.get_chassis_and_azs = mock.Mock() + self.get_chassis_and_azs.return_value = {} self.get_all_chassis = mock.Mock() self.get_chassis_data_for_ml2_bind_port = mock.Mock() self.get_chassis_data_for_ml2_bind_port.return_value = \ @@ -431,11 +436,13 @@ class FakeOvsdbTable(FakeResource): """Fake one or more OVSDB tables.""" @staticmethod - def create_one_ovsdb_table(attrs=None): + def create_one_ovsdb_table(attrs=None, max_rows=sys.maxsize): """Create a fake OVSDB table. :param Dictionary attrs: A dictionary with all attributes + :param Int max_rows: + A num of max rows :return: A FakeResource object faking the OVSDB table """ @@ -446,6 +453,7 @@ class FakeOvsdbTable(FakeResource): 'rows': collections.UserDict(), 'columns': {}, 'indexes': [], + 'max_rows': max_rows, } # Overwrite default attributes. 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 8dba2de7be8..1b646c567a1 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 @@ -147,10 +147,12 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn): 'lrouters': [ {'name': utils.ovn_name('lr-id-a'), 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: - 'lr-name-a'}}, + 'lr-name-a', + ovn_const.OVN_AZ_HINTS_EXT_ID_KEY: 'az-a'}}, {'name': utils.ovn_name('lr-id-b'), 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: - 'lr-name-b'}}, + 'lr-name-b', + ovn_const.OVN_AZ_HINTS_EXT_ID_KEY: 'az-b'}}, {'name': utils.ovn_name('lr-id-c'), 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'lr-name-c'}}, @@ -162,7 +164,9 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn): 'lr-name-e'}}], 'lrouter_ports': [ {'name': utils.ovn_lrouter_port_name('orp-id-a1'), - 'external_ids': {}, 'networks': ['10.0.1.0/24'], + 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: + 'lr-id-a'}, + 'networks': ['10.0.1.0/24'], 'options': {ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-1'}}, {'name': utils.ovn_lrouter_port_name('orp-id-a2'), 'external_ids': {}, 'networks': ['10.0.2.0/24'], @@ -596,17 +600,29 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn): # Test only that orp-id-a3 is to be scheduled. # Rest ports don't have required chassis (physnet2) # or are already scheduled. + chassis_with_azs = {'host-1': ['az-a'], 'host-2': ['az-b']} unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( port_physnet_dict, {'host-1': 'physnet1', 'host-2': 'physnet3'}, - ['host-1', 'host-2']) + ['host-1', 'host-2'], chassis_with_azs) expected = ['lrp-orp-id-a3'] self.assertCountEqual(unhosted_gateways, expected) # Test both host-1, host-2 in valid list unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( port_physnet_dict, {'host-1': 'physnet1', 'host-2': 'physnet2'}, - ['host-1', 'host-2']) + ['host-1', 'host-2'], chassis_with_azs) expected = ['lrp-orp-id-a3', 'lrp-orp-id-b6'] self.assertCountEqual(unhosted_gateways, expected) + # Test lrp-orp-id-a1 az_hints not in host-1's azs + # lrp-orp-id-a2 not set az_hints, should schedule in host-1, host-3 + # lrp-orp-id-a3 not scheduled + chassis_with_azs = {'host-1': ['az-b'], 'host-2': ['az-b'], + 'host-3': ['az-a']} + unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( + port_physnet_dict, {'host-1': 'physnet1', 'host-2': 'physnet3', + 'host-3': 'physnet1'}, + ['host-1', 'host-2', 'host-3'], chassis_with_azs) + expected = ['lrp-orp-id-a1', 'lrp-orp-id-a2', 'lrp-orp-id-a3'] + self.assertCountEqual(unhosted_gateways, expected) def test_get_unhosted_gateways_deleted_physnet(self): self._load_nb_db() @@ -616,17 +632,18 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn): setattr(router_row, 'options', { ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-2'}) port_physnet_dict = {'orp-id-a1': 'physnet1'} + chassis_with_azs = {'host-1': ['az-a'], 'host-2': ['az-a']} # 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']) + ['host-1', 'host-2'], chassis_with_azs) # Make sure that lrp is rescheduled, because host-1 has physet1 expected = ['lrp-orp-id-a1'] self.assertCountEqual(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']) + ['host-1', 'host-2'], chassis_with_azs) self.assertCountEqual(unhosted_gateways, []) def _test_get_unhosted_gateway_max_chassis(self, r): @@ -647,7 +664,8 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn): {'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)]) + ['host-%s' % x for x in range(1, 7)], + {'host-%s' % x: ['az-a'] for x in range(1, 7)}) # We don't have required number of chassis expected = [] self.assertCountEqual(unhosted_gateways, expected) @@ -661,7 +679,8 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn): {'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)]) + ['host-%s' % x for x in range(1, 7)], + {'host-%s' % x: ['az-a'] for x in range(1, 7)}) expected = [] self.assertCountEqual(unhosted_gateways, expected) @@ -674,7 +693,8 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn): {'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)]) + ['host-%s' % x for x in range(1, 7)], + {'host-%s' % x: ['az-a'] for x in range(1, 7)}) expected = ['lrp-orp-id-a1'] self.assertCountEqual(unhosted_gateways, expected) diff --git a/neutron/tests/unit/scheduler/test_l3_ovn_scheduler.py b/neutron/tests/unit/scheduler/test_l3_ovn_scheduler.py index f345dcafbe6..3c10462b585 100644 --- a/neutron/tests/unit/scheduler/test_l3_ovn_scheduler.py +++ b/neutron/tests/unit/scheduler/test_l3_ovn_scheduler.py @@ -94,7 +94,9 @@ class TestOVNGatewayScheduler(base.BaseTestCase): nb_idl=kwargs.pop('nb_idl'), gw_chassis=kwargs.pop('gw_chassis'), physnet=kwargs.pop('physnet'), chassis_physnets=kwargs.pop('chassis_physnets'), - existing_chassis=kwargs.pop('existing_chassis')) + existing_chassis=kwargs.pop('existing_chassis'), + az_hints=kwargs.pop('az_hints', []), + chassis_with_azs=kwargs.pop('chassis_with_azs', {})) class OVNGatewayChanceScheduler(TestOVNGatewayScheduler): diff --git a/neutron/tests/unit/services/ovn_l3/test_plugin.py b/neutron/tests/unit/services/ovn_l3/test_plugin.py index c4c12f081dd..4faf3db72cc 100644 --- a/neutron/tests/unit/services/ovn_l3/test_plugin.py +++ b/neutron/tests/unit/services/ovn_l3/test_plugin.py @@ -1571,7 +1571,7 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): 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) + {'foo-1': 'physnet1'}, mock.ANY, mock.ANY, mock.ANY) @mock.patch('neutron.plugins.ml2.plugin.Ml2Plugin.get_network') @mock.patch('neutron.plugins.ml2.plugin.Ml2Plugin.get_networks')