[OVN] Remove OVN_GATEWAY_INVALID_CHASSIS artifact

This artifact is no longer used in the "Logical_Router" registers (in
the "options" field) to mark this "Logical_Router" as unhosted. A
"Logical_Router" is considered as unhosted if the gateway
"Logical_Router_Ports" have no "chassis" set.

This artifact is also used to create a "Gateway_Chassis" register
pointing to a inexisting invalid chassis called
"neutron-ovn-invalid-chassis". Any "Logical_Router_Port" not bound
to a chassis will have no value in "gateway_chassis" (NOTE1).

NOTE1: this is valid now with the current two OVN L3 schedulers that
use "gateway_chassis" to schedule the "Logical_Router_Port" of a
router. In a future, we can consider using "ha_chassis_group" for
scheduling.

Partial-Bug: #2052821
Related-Bug: #2019217
Change-Id: I12717936fe2bc188545309bacb8a260981f14c88
This commit is contained in:
Rodolfo Alonso Hernandez 2024-02-16 19:24:28 +00:00 committed by Rodolfo Alonso
parent f73a2515cb
commit fa3223bb9d
14 changed files with 156 additions and 95 deletions

View File

@ -26,10 +26,9 @@ The maximum number of ``Gateway_Chassis`` that can be assigned to a
the highest priority a ``Gateway_Chassis`` will have is 5. the highest priority a ``Gateway_Chassis`` will have is 5.
If no gateway chassis are available during the ``Logical_Router_Port`` If no gateway chassis are available during the ``Logical_Router_Port``
scheduling, no ``Gateway_Chassis`` will be assigned and the value scheduling, no ``Gateway_Chassis`` will be assigned and no value will be set
"neutron-ovn-invalid-chassis" will be set in the "options" column of the in the "options" column of the ``Logical_Router`` register; that will be used
``Logical_Router`` register; that value will be used to detect an unhosted to detect an unhosted router gateway port.
router gateway port.
Types of schedulers Types of schedulers
@ -88,7 +87,7 @@ Both the ``OVNGatewayChanceScheduler`` and the
``OVNGatewayLeastLoadedScheduler`` schedulers have the Availability Zones (AZ) ``OVNGatewayLeastLoadedScheduler`` schedulers have the Availability Zones (AZ)
in consideration. If a router has any AZ defined, the schedulers will select in consideration. If a router has any AZ defined, the schedulers will select
only those chassis located in the AZs. If no chassis meets this condition, the only those chassis located in the AZs. If no chassis meets this condition, the
``Logical_Router_Port`` will be assigned to the "neutron-ovn-invalid-chassis". ``Logical_Router_Port`` won't be assigned to any chassis and won't be bound.
Once the list of candidate ``Chassis`` (depending on the scheduler selected) Once the list of candidate ``Chassis`` (depending on the scheduler selected)
is created, this list is reordered to prioritize these ``Chassis`` from is created, this list is reordered to prioritize these ``Chassis`` from

View File

@ -109,12 +109,6 @@ ACL_ACTION_ALLOW_RELATED = 'allow-related'
ACL_ACTION_ALLOW_STATELESS = 'allow-stateless' ACL_ACTION_ALLOW_STATELESS = 'allow-stateless'
ACL_ACTION_ALLOW = 'allow' ACL_ACTION_ALLOW = 'allow'
# When a OVN L3 gateway is created, it needs to be bound to a chassis. In
# case a chassis is not found OVN_GATEWAY_INVALID_CHASSIS will be set in
# the options column of the Logical Router. This value is used to detect
# unhosted router gateways to schedule.
OVN_GATEWAY_INVALID_CHASSIS = 'neutron-ovn-invalid-chassis'
# NOTE(lucasagomes): These options were last synced from # NOTE(lucasagomes): These options were last synced from
# https://github.com/ovn-org/ovn/blob/feb5d6e81d5a0290aa3618a229c860d01200422e/lib/ovn-l7.h # https://github.com/ovn-org/ovn/blob/feb5d6e81d5a0290aa3618a229c860d01200422e/lib/ovn-l7.h
# #

View File

@ -669,10 +669,7 @@ def is_gateway_chassis_invalid(chassis_name, gw_chassis,
@type chassis_with_azs: {} @type chassis_with_azs: {}
@return Boolean @return Boolean
""" """
if chassis_name not in chassis_physnets:
if chassis_name == constants.OVN_GATEWAY_INVALID_CHASSIS:
return True
elif chassis_name not in chassis_physnets:
return True return True
elif physnet and physnet not in chassis_physnets.get(chassis_name): elif physnet and physnet not in chassis_physnets.get(chassis_name):
return True return True

View File

@ -412,10 +412,9 @@ class ScheduleNewGatewayCommand(command.BaseCommand):
chassis = self.scheduler.select( chassis = self.scheduler.select(
self.api, self.sb_api, self.g_name, candidates=candidates, self.api, self.sb_api, self.g_name, candidates=candidates,
target_lrouter=lrouter) target_lrouter=lrouter)
if chassis:
setattr( setattr(lrouter_port,
lrouter_port, *_add_gateway_chassis(self.api, txn, self.g_name, chassis))
*_add_gateway_chassis(self.api, txn, self.g_name, chassis))
class AddLRouterPortCommand(command.BaseCommand): class AddLRouterPortCommand(command.BaseCommand):

View File

@ -551,8 +551,16 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
def get_unhosted_gateways(self, port_physnet_dict, chassis_with_physnets, def get_unhosted_gateways(self, port_physnet_dict, chassis_with_physnets,
all_gw_chassis, chassis_with_azs): all_gw_chassis, chassis_with_azs):
"""Return the GW LRPs with no chassis assigned
If the LRP belongs to a tunnelled network (physnet=None), it won't be
hosted to any chassis.
"""
unhosted_gateways = set() unhosted_gateways = set()
for port, physnet in port_physnet_dict.items(): for port, physnet in port_physnet_dict.items():
if not physnet:
continue
lrp_name = '%s%s' % (ovn_const.LRP_PREFIX, port) lrp_name = '%s%s' % (ovn_const.LRP_PREFIX, port)
original_state = self.get_gateway_chassis_binding(lrp_name) original_state = self.get_gateway_chassis_binding(lrp_name)
az_hints = self.get_gateway_chassis_az_hints(lrp_name) az_hints = self.get_gateway_chassis_az_hints(lrp_name)

View File

@ -29,6 +29,7 @@ from neutron_lib.exceptions import l3 as l3_exc
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import strutils
from oslo_utils import timeutils from oslo_utils import timeutils
from ovsdbapp.backend.ovs_idl import event as row_event from ovsdbapp.backend.ovs_idl import event as row_event
@ -1204,6 +1205,29 @@ class DBInconsistenciesPeriodics(SchemaAwarePeriodicsBase):
raise periodics.NeverAgain() raise periodics.NeverAgain()
# TODO(ralonsoh): Remove this method in the C+2 cycle (next SLURP release)
@has_lock_periodic(spacing=600, run_immediately=True)
def remove_invalid_gateway_chassis_from_unbound_lrp(self):
"""Removes all invalid 'Gateway_Chassis' from unbound LRPs"""
is_gw = ovn_const.OVN_ROUTER_IS_EXT_GW
lrp_list = []
for lr in self._nb_idl.lr_list().execute(check_error=True):
for lrp in self._nb_idl.lrp_list(lr.uuid).execute(
check_error=True):
if (is_gw in lrp.external_ids and
strutils.bool_from_string(lrp.external_ids[is_gw]) and
lrp.gateway_chassis and
lrp.gateway_chassis[0].chassis_name ==
'neutron-ovn-invalid-chassis'):
lrp_list.append(lrp)
with self._nb_idl.transaction(check_error=True) as txn:
for lrp in lrp_list:
txn.add(self._nb_idl.lrp_del_gateway_chassis(
lrp.uuid, 'neutron-ovn-invalid-chassis'))
raise periodics.NeverAgain()
class HashRingHealthCheckPeriodics(object): class HashRingHealthCheckPeriodics(object):

View File

@ -47,7 +47,7 @@ class OVNGatewayScheduler(object, metaclass=abc.ABCMeta):
physnet, chassis_physnets, physnet, chassis_physnets,
existing_chassis, az_hints, chassis_with_azs): existing_chassis, az_hints, chassis_with_azs):
chassis_list = copy.copy(existing_chassis) chassis_list = copy.copy(existing_chassis)
for chassis_name in existing_chassis: for chassis_name in existing_chassis or []:
if utils.is_gateway_chassis_invalid(chassis_name, gw_chassis, if utils.is_gateway_chassis_invalid(chassis_name, gw_chassis,
physnet, chassis_physnets, physnet, chassis_physnets,
az_hints, chassis_with_azs): az_hints, chassis_with_azs):
@ -73,7 +73,7 @@ class OVNGatewayScheduler(object, metaclass=abc.ABCMeta):
if not candidates: if not candidates:
LOG.warning('Gateway %s was not scheduled on any chassis, no ' LOG.warning('Gateway %s was not scheduled on any chassis, no '
'candidates are available', gateway_name) 'candidates are available', gateway_name)
return [ovn_const.OVN_GATEWAY_INVALID_CHASSIS] return
chassis_count = min( chassis_count = min(
ovn_const.MAX_GW_CHASSIS - len(existing_chassis), ovn_const.MAX_GW_CHASSIS - len(existing_chassis),
len(candidates) len(candidates)
@ -97,8 +97,7 @@ class OVNGatewayScheduler(object, metaclass=abc.ABCMeta):
azs = set() azs = set()
# Check if candidates list valid # Check if candidates list valid
if not candidates or ( if not candidates:
candidates == [ovn_const.OVN_GATEWAY_INVALID_CHASSIS]):
return candidates return candidates
chassis_with_azs = sb_idl.get_chassis_and_azs() chassis_with_azs = sb_idl.get_chassis_and_azs()

View File

@ -175,9 +175,9 @@ class _TestMaintenanceHelper(base.TestOVNFunctionalBase):
res = req.get_response(self.api) res = req.get_response(self.api)
return self.deserialize(self.fmt, res)['router'] return self.deserialize(self.fmt, res)['router']
def _update_router_name(self, net_id, new_name): def _update_router(self, router_id, router_dict):
data = {'router': {'name': new_name}} data = {'router': router_dict}
req = self.new_update_request('routers', data, net_id, self.fmt) req = self.new_update_request('routers', data, router_id, self.fmt)
res = req.get_response(self.api) res = req.get_response(self.api)
return self.deserialize(self.fmt, res)['router'] return self.deserialize(self.fmt, res)['router']
@ -591,8 +591,8 @@ class TestMaintenance(_TestMaintenanceHelper):
new_obj_name = 'routertest_updated' new_obj_name = 'routertest_updated'
with mock.patch.object(self._l3_ovn_client, 'update_router'): with mock.patch.object(self._l3_ovn_client, 'update_router'):
new_neutron_obj = self._update_router_name(neutron_obj['id'], new_neutron_obj = self._update_router(neutron_obj['id'],
new_obj_name) {'name': new_obj_name})
# Assert the revision numbers are out-of-sync # Assert the revision numbers are out-of-sync
ovn_obj = self._find_router_row_by_name(obj_name) ovn_obj = self._find_router_row_by_name(obj_name)
@ -1190,6 +1190,36 @@ class TestMaintenance(_TestMaintenanceHelper):
self.assertIn(router1['id'], pra_res_ids) self.assertIn(router1['id'], pra_res_ids)
self.assertIn(router2['id'], pra_res_ids) self.assertIn(router2['id'], pra_res_ids)
def test_remove_invalid_gateway_chassis_from_unbound_lrp(self):
net1 = self._create_network(uuidutils.generate_uuid(), external=True)
subnet1 = self._create_subnet(uuidutils.generate_uuid(), net1['id'])
external_gateway_info = {
'enable_snat': True, 'network_id': net1['id'],
'external_fixed_ips': [{'ip_address': '10.0.0.2',
'subnet_id': subnet1['id']}]}
router = self._create_router(
uuidutils.generate_uuid(),
external_gateway_info=external_gateway_info)
# Manually add the LRP.gateway_chassis with name
# 'neutron-ovn-invalid-chassis'
lr = self.nb_api.lookup('Logical_Router', utils.ovn_name(router['id']))
lrp = lr.ports[0]
self.nb_api.lrp_set_gateway_chassis(
lrp.uuid, 'neutron-ovn-invalid-chassis').execute(check_error=True)
gc = self.nb_api.db_find_rows(
'Gateway_Chassis',
('chassis_name', '=', 'neutron-ovn-invalid-chassis')).execute(
check_error=True)[0]
self.assertRaises(
periodics.NeverAgain,
self.maint.remove_invalid_gateway_chassis_from_unbound_lrp)
self.assertIsNone(self.nb_api.lookup('Gateway_Chassis', gc.uuid,
default=None))
lr = self.nb_api.lookup('Logical_Router', utils.ovn_name(router['id']))
self.assertEqual([], lr.ports[0].gateway_chassis)
class TestLogMaintenance(_TestMaintenanceHelper, class TestLogMaintenance(_TestMaintenanceHelper,
test_log_driver.LogApiTestCaseBase): test_log_driver.LogApiTestCaseBase):

View File

@ -20,6 +20,7 @@ import fixtures as og_fixtures
from neutron_lib.api.definitions import allowedaddresspairs from neutron_lib.api.definitions import allowedaddresspairs
from neutron_lib.api.definitions import external_net from neutron_lib.api.definitions import external_net
from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net
from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import constants as plugin_constants
from neutron_lib.plugins import directory from neutron_lib.plugins import directory
from oslo_concurrency import processutils from oslo_concurrency import processutils
@ -517,8 +518,10 @@ class TestSBDbMonitor(base.TestOVNFunctionalBase, test_l3.L3NatTestCaseMixin):
def setUp(self, **kwargs): def setUp(self, **kwargs):
super().setUp(**kwargs) super().setUp(**kwargs)
self.chassis = self.add_fake_chassis('ovs-host1', self.physnet = 'public'
enable_chassis_as_gw=True) self.chassis = self.add_fake_chassis(
'ovs-host1', physical_nets=[self.physnet],
enable_chassis_as_gw=True)
self.l3_plugin = directory.get_plugin(plugin_constants.L3) self.l3_plugin = directory.get_plugin(plugin_constants.L3)
ext_mgr = test_l3.L3TestExtensionManager() ext_mgr = test_l3.L3TestExtensionManager()
self.ext_api = test_extensions.setup_extensions_middleware(ext_mgr) self.ext_api = test_extensions.setup_extensions_middleware(ext_mgr)
@ -545,10 +548,12 @@ class TestSBDbMonitor(base.TestOVNFunctionalBase, test_l3.L3NatTestCaseMixin):
return True return True
return lrp.external_ids == pb.external_ids return lrp.external_ids == pb.external_ids
kwargs = {'arg_list': (external_net.EXTERNAL,), arg_dict = {external_net.EXTERNAL: True,
external_net.EXTERNAL: True} provider_net.NETWORK_TYPE: 'flat',
provider_net.PHYSICAL_NETWORK: self.physnet}
ext_net = self._make_network(self.fmt, 'ext_net', True, as_admin=True, ext_net = self._make_network(self.fmt, 'ext_net', True, as_admin=True,
**kwargs) arg_list=tuple(arg_dict.keys()),
**arg_dict)
self._make_subnet(self.fmt, ext_net, '10.251.0.1', '10.251.0.0/24', self._make_subnet(self.fmt, ext_net, '10.251.0.1', '10.251.0.0/24',
enable_dhcp=True) enable_dhcp=True)
router = self._make_router(self.fmt, self._tenant_id) router = self._make_router(self.fmt, self._tenant_id)

View File

@ -41,6 +41,7 @@ class TestRouter(base.TestOVNFunctionalBase):
self.chassis2 = self.add_fake_chassis( self.chassis2 = self.add_fake_chassis(
'ovs-host2', physical_nets=['physnet2', 'physnet3'], 'ovs-host2', physical_nets=['physnet2', 'physnet3'],
enable_chassis_as_gw=True, azs=[]) enable_chassis_as_gw=True, azs=[])
self.physnet_used = ['physnet1', 'physnet2', 'physnet3']
self.cr_lrp_pb_event = events.WaitForCrLrpPortBindingEvent() self.cr_lrp_pb_event = events.WaitForCrLrpPortBindingEvent()
self.sb_api.idl.notify_handler.watch_event(self.cr_lrp_pb_event) self.sb_api.idl.notify_handler.watch_event(self.cr_lrp_pb_event)
@ -86,13 +87,11 @@ class TestRouter(base.TestOVNFunctionalBase):
ip_version=n_consts.IP_VERSION_4) ip_version=n_consts.IP_VERSION_4)
return network return network
def _set_redirect_chassis_to_invalid_chassis(self, ovn_client): def _unset_lrp_gw_chassis(self, ovn_client):
with ovn_client._nb_idl.transaction(check_error=True) as txn: with ovn_client._nb_idl.transaction(check_error=True) as txn:
for lrp in self.nb_api.tables[ for lrp in self.nb_api.tables['Logical_Router_Port'].rows.values():
'Logical_Router_Port'].rows.values():
txn.add(ovn_client._nb_idl.update_lrouter_port( txn.add(ovn_client._nb_idl.update_lrouter_port(
lrp.name, lrp.name, gateway_chassis=[]))
gateway_chassis=[ovn_const.OVN_GATEWAY_INVALID_CHASSIS]))
def _get_gwc_dict(self): def _get_gwc_dict(self):
sched_info = {} sched_info = {}
@ -117,11 +116,11 @@ class TestRouter(base.TestOVNFunctionalBase):
# exception occasionally. # exception occasionally.
continue continue
gw_port_id = router.get('gw_port_id')
logical_port = 'cr-lrp-%s' % gw_port_id
self.assertTrue(self.cr_lrp_pb_event.wait(logical_port),
msg='lrp %s failed to bind' % logical_port)
if bind_chassis: if bind_chassis:
gw_port_id = router.get('gw_port_id')
logical_port = 'cr-lrp-%s' % gw_port_id
self.assertTrue(self.cr_lrp_pb_event.wait(logical_port),
msg='lrp %s failed to bind' % logical_port)
self.sb_api.lsp_bind(logical_port, bind_chassis, self.sb_api.lsp_bind(logical_port, bind_chassis,
may_exist=True).execute(check_error=True) may_exist=True).execute(check_error=True)
return routers return routers
@ -165,22 +164,24 @@ class TestRouter(base.TestOVNFunctionalBase):
def fake_select(*args, **kwargs): def fake_select(*args, **kwargs):
self.assertCountEqual(candidates, kwargs['candidates']) self.assertCountEqual(candidates, kwargs['candidates'])
# We are not interested in further processing, let us return # We are not interested in further processing, let us return
# INVALID_CHASSIS to avoid errors # a random chassis name to avoid errors. If there are no
return [ovn_const.OVN_GATEWAY_INVALID_CHASSIS] # candidates, this method returns None.
return ['a-random-chassis'] if candidates else None
with mock.patch.object(self.l3_plugin.scheduler, 'select', with mock.patch.object(self.l3_plugin.scheduler, 'select',
side_effect=fake_select) as plugin_select: side_effect=fake_select) as plugin_select:
gw_info = {'network_id': ext1['network']['id']} 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) az_hints=router_az_hints)
self.assertTrue(plugin_select.called) self.assertTrue(plugin_select.called)
plugin_select.reset_mock() plugin_select.reset_mock()
# set redirect-chassis to neutron-ovn-invalid-chassis, so # Unset the redirect-chassis so that schedule_unhosted_gateways
# that schedule_unhosted_gateways will try to schedule it # will try to schedule it.
self._set_redirect_chassis_to_invalid_chassis(ovn_client) self._unset_lrp_gw_chassis(ovn_client)
self.l3_plugin.schedule_unhosted_gateways() self.l3_plugin.schedule_unhosted_gateways()
self.assertTrue(plugin_select.called) check = self.assertTrue if candidates else self.assertFalse
check(plugin_select.called)
def test_gateway_chassis_with_cms_and_bridge_mappings(self): def test_gateway_chassis_with_cms_and_bridge_mappings(self):
# Both chassis1 and chassis3 are having proper bridge mappings, # Both chassis1 and chassis3 are having proper bridge mappings,
@ -201,7 +202,7 @@ class TestRouter(base.TestOVNFunctionalBase):
'ext1', 'vlan', 'physnet1', 1, "10.0.0.1", "10.0.0.0/24") 'ext1', 'vlan', 'physnet1', 1, "10.0.0.1", "10.0.0.0/24")
# As we have 'gateways' in the system, but without required # As we have 'gateways' in the system, but without required
# chassis we should not schedule gw in that case at all. # chassis we should not schedule gw in that case at all.
self._set_redirect_chassis_to_invalid_chassis(ovn_client) self._unset_lrp_gw_chassis(ovn_client)
with mock.patch.object(self.l3_plugin.scheduler, 'select', with mock.patch.object(self.l3_plugin.scheduler, 'select',
side_effect=[self.chassis1]): side_effect=[self.chassis1]):
gw_info = {'network_id': ext1['network']['id']} gw_info = {'network_id': ext1['network']['id']}
@ -242,7 +243,7 @@ class TestRouter(base.TestOVNFunctionalBase):
'ext1', 'vlan', 'physnet1', 1, "10.0.0.1", "10.0.0.0/24") 'ext1', 'vlan', 'physnet1', 1, "10.0.0.1", "10.0.0.0/24")
# As we have 'gateways' in the system, but without required # As we have 'gateways' in the system, but without required
# chassis we should not schedule gw in that case at all. # chassis we should not schedule gw in that case at all.
self._set_redirect_chassis_to_invalid_chassis(ovn_client) self._unset_lrp_gw_chassis(ovn_client)
with mock.patch.object(self.l3_plugin.scheduler, 'select', with mock.patch.object(self.l3_plugin.scheduler, 'select',
side_effect=[self.chassis1]): side_effect=[self.chassis1]):
gw_info = {'network_id': ext1['network']['id']} gw_info = {'network_id': ext1['network']['id']}
@ -264,7 +265,7 @@ class TestRouter(base.TestOVNFunctionalBase):
def test_gateway_chassis_no_physnet_tunnelled_network(self): def test_gateway_chassis_no_physnet_tunnelled_network(self):
# The GW network is tunnelled, no physnet defined --> no possible # The GW network is tunnelled, no physnet defined --> no possible
# candidates. # candidates.
self._check_gateway_chassis_candidates([], physnet=None) self._check_gateway_chassis_candidates(None, physnet=None)
def test_gateway_chassis_least_loaded_scheduler(self): def test_gateway_chassis_least_loaded_scheduler(self):
# This test will create 4 routers each with its own gateway. # This test will create 4 routers each with its own gateway.
@ -372,11 +373,8 @@ class TestRouter(base.TestOVNFunctionalBase):
Test cases when subnets are added to an external network after router Test cases when subnets are added to an external network after router
has been configured to use that network via "set --external-gateway" has been configured to use that network via "set --external-gateway"
""" """
with mock.patch.object(self.l3_plugin.scheduler, 'select', with mock.patch.object(self.l3_plugin.scheduler, 'select',
return_value=[ return_value=self.chassis1) as plugin_select:
ovn_const.OVN_GATEWAY_INVALID_CHASSIS
]) as plugin_select:
router1 = self._create_router('router1', gw_info=None) router1 = self._create_router('router1', gw_info=None)
router_id = router1['id'] router_id = router1['id']
self.assertIsNone(self._get_gw_port(router_id), self.assertIsNone(self._get_gw_port(router_id),
@ -490,8 +488,8 @@ class TestRouter(base.TestOVNFunctionalBase):
def fake_select(*args, **kwargs): def fake_select(*args, **kwargs):
self.assertCountEqual(self.candidates, kwargs['candidates']) self.assertCountEqual(self.candidates, kwargs['candidates'])
# We are not interested in further processing, let us return # We are not interested in further processing, let us return
# INVALID_CHASSIS to avoid errors # a random chassis name to avoid errors.
return [ovn_const.OVN_GATEWAY_INVALID_CHASSIS] return ['a-random-chassis']
with mock.patch.object(self.l3_plugin.scheduler, 'select', with mock.patch.object(self.l3_plugin.scheduler, 'select',
side_effect=fake_select) as plugin_select: side_effect=fake_select) as plugin_select:
@ -499,9 +497,9 @@ class TestRouter(base.TestOVNFunctionalBase):
gw_info = {'network_id': ext1['network']['id']} gw_info = {'network_id': ext1['network']['id']}
router1 = self._create_router('router1', gw_info=gw_info) router1 = self._create_router('router1', gw_info=gw_info)
# set redirect-chassis to neutron-ovn-invalid-chassis, so # Unset the redirect-chassis so that schedule_unhosted_gateways
# that schedule_unhosted_gateways will try to schedule it # will try to schedule it.
self._set_redirect_chassis_to_invalid_chassis(ovn_client) self._unset_lrp_gw_chassis(ovn_client)
self.l3_plugin.schedule_unhosted_gateways() self.l3_plugin.schedule_unhosted_gateways()
self.candidates = [self.chassis1, self.chassis2] self.candidates = [self.chassis1, self.chassis2]
@ -509,7 +507,7 @@ class TestRouter(base.TestOVNFunctionalBase):
self.l3_plugin.update_router( self.l3_plugin.update_router(
self.context, router1['id'], self.context, router1['id'],
{'router': {l3_apidef.EXTERNAL_GW_INFO: gw_info}}) {'router': {l3_apidef.EXTERNAL_GW_INFO: gw_info}})
self._set_redirect_chassis_to_invalid_chassis(ovn_client) self._unset_lrp_gw_chassis(ovn_client)
self.l3_plugin.schedule_unhosted_gateways() self.l3_plugin.schedule_unhosted_gateways()
self.candidates = [] self.candidates = []
@ -517,17 +515,17 @@ class TestRouter(base.TestOVNFunctionalBase):
self.l3_plugin.update_router( self.l3_plugin.update_router(
self.context, router1['id'], self.context, router1['id'],
{'router': {l3_apidef.EXTERNAL_GW_INFO: gw_info}}) {'router': {l3_apidef.EXTERNAL_GW_INFO: gw_info}})
self._set_redirect_chassis_to_invalid_chassis(ovn_client) self._unset_lrp_gw_chassis(ovn_client)
self.l3_plugin.schedule_unhosted_gateways() self.l3_plugin.schedule_unhosted_gateways()
# We can't test call_count for these mocks, as we have disabled # We can't test call_count for these mocks, as we have disabled
# maintenance_worker which will trigger chassis events # maintenance_worker which will trigger chassis events
# and eventually calling schedule_unhosted_gateways. # and eventually calling schedule_unhosted_gateways.
# However, we know for sure that these mocks must have been # The router is created with a gateway port and updated twice.
# called at least 3 times because that is the number of times # However, the "plugin_select" is called only twice because the
# this test invokes them: 1x create_router + 2x update_router; # third gateway network used is type "geneve" and the LRP are not
# and 3x schedule_unhosted_gateways for plugin_select mock. # hosted in any chassis.
self.assertGreaterEqual(plugin_select.call_count, 3) self.assertGreaterEqual(plugin_select.call_count, 2)
def test_router_gateway_port_binding_host_id(self): def test_router_gateway_port_binding_host_id(self):
# Test setting chassis on chassisredirect port in Port_Binding table, # Test setting chassis on chassisredirect port in Port_Binding table,
@ -613,7 +611,8 @@ class TestRouter(base.TestOVNFunctionalBase):
def test_create_delete_router_multiple_gw_ports(self): def test_create_delete_router_multiple_gw_ports(self):
ext4 = self._create_ext_network( ext4 = self._create_ext_network(
'ext4', 'flat', 'physnet4', None, "40.0.0.1", "40.0.0.0/24") 'ext4', 'flat', self.physnet_used[0], None, '40.0.0.1',
'40.0.0.0/24')
router = self._create_router('router4') router = self._create_router('router4')
gws = self._add_external_gateways( gws = self._add_external_gateways(
router['id'], router['id'],
@ -638,7 +637,8 @@ class TestRouter(base.TestOVNFunctionalBase):
def test_create_router_multiple_gw_ports_ecmp(self): def test_create_router_multiple_gw_ports_ecmp(self):
ext5 = self._create_ext_network( ext5 = self._create_ext_network(
'ext5', 'flat', 'physnet5', None, "10.0.50.1", "10.0.50.0/24") 'ext5', 'flat', self.physnet_used[1], None, '10.0.50.1',
'10.0.50.0/24')
router = self._create_router('router5', enable_ecmp=True) router = self._create_router('router5', enable_ecmp=True)
gws = self._add_external_gateways( gws = self._add_external_gateways(
router['id'], router['id'],
@ -740,12 +740,10 @@ class TestRouter(base.TestOVNFunctionalBase):
'ext1', 'flat', 'physnet6', None, "10.0.60.1", "10.0.60.0/24") 'ext1', 'flat', 'physnet6', None, "10.0.60.1", "10.0.60.0/24")
gw_info = {'network_id': ext1['network']['id']} gw_info = {'network_id': ext1['network']['id']}
# Attempt to add 4 routers, since there are no chassis all will be # Attempt to add 4 routers, since there are no chassis, none of them
# scheduled on the ovn_const.OVN_GATEWAY_INVALID_CHASSIS. # will be scheduled on any chassis.
num_routers = len(self._create_routers_wait_pb(1, 4, gw_info)) num_routers = len(self._create_routers_wait_pb(1, 4, gw_info=gw_info))
self.assertEqual( self.assertEqual({}, self._get_gwc_dict())
{ovn_const.OVN_GATEWAY_INVALID_CHASSIS: {1: num_routers}},
self._get_gwc_dict())
# Add 2 chassis and rebalance gateways. # Add 2 chassis and rebalance gateways.
# #
@ -758,9 +756,7 @@ class TestRouter(base.TestOVNFunctionalBase):
with mock.patch.object( with mock.patch.object(
self.l3_plugin, 'schedule_unhosted_gateways'): self.l3_plugin, 'schedule_unhosted_gateways'):
chassis_list.extend(self._add_chassis(1, 2, ['physnet6'])) chassis_list.extend(self._add_chassis(1, 2, ['physnet6']))
self.assertEqual( self.assertEqual({}, self._get_gwc_dict())
{ovn_const.OVN_GATEWAY_INVALID_CHASSIS: {1: num_routers}},
self._get_gwc_dict())
# Wrap `self.l3_plugin._nb_ovn.transaction` so that we can assert on # Wrap `self.l3_plugin._nb_ovn.transaction` so that we can assert on
# number of calls. # number of calls.

View File

@ -266,7 +266,7 @@ class TestGateWayChassisValidity(base.BaseTestCase):
def test_gateway_chassis_due_to_invalid_chassis_name(self): def test_gateway_chassis_due_to_invalid_chassis_name(self):
# Return True since chassis is invalid # Return True since chassis is invalid
self.chassis_name = constants.OVN_GATEWAY_INVALID_CHASSIS self.chassis_name = None
self.assertTrue(utils.is_gateway_chassis_invalid( self.assertTrue(utils.is_gateway_chassis_invalid(
self.chassis_name, self.gw_chassis, self.physnet, self.chassis_name, self.gw_chassis, self.physnet,
self.chassis_physnets, self.az_hints, self.chassis_azs)) self.chassis_physnets, self.az_hints, self.chassis_azs))

View File

@ -181,8 +181,7 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn):
'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'lr-id-a', 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'lr-id-a',
ovn_const.OVN_ROUTER_IS_EXT_GW: str(True)}, ovn_const.OVN_ROUTER_IS_EXT_GW: str(True)},
'networks': ['10.0.3.0/24'], 'networks': ['10.0.3.0/24'],
'options': {ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'options': {ovn_const.OVN_GATEWAY_CHASSIS_KEY: None}},
ovn_const.OVN_GATEWAY_INVALID_CHASSIS}},
{'name': 'xrp-id-b1', {'name': 'xrp-id-b1',
'external_ids': {}, 'networks': ['20.0.1.0/24']}, 'external_ids': {}, 'networks': ['20.0.1.0/24']},
{'name': utils.ovn_lrouter_port_name('orp-id-b2'), {'name': utils.ovn_lrouter_port_name('orp-id-b2'),
@ -627,8 +626,7 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn):
expected = {'host-1': [utils.ovn_lrouter_port_name('orp-id-a1'), expected = {'host-1': [utils.ovn_lrouter_port_name('orp-id-a1'),
utils.ovn_lrouter_port_name('orp-id-a2')], utils.ovn_lrouter_port_name('orp-id-a2')],
'host-2': [utils.ovn_lrouter_port_name('orp-id-b2')], 'host-2': [utils.ovn_lrouter_port_name('orp-id-b2')],
ovn_const.OVN_GATEWAY_INVALID_CHASSIS: [ }
utils.ovn_name('orp-id-a3')]}
self.assertCountEqual(bindings, expected) self.assertCountEqual(bindings, expected)
bindings = self.nb_ovn_idl.get_all_chassis_gateway_bindings([]) bindings = self.nb_ovn_idl.get_all_chassis_gateway_bindings([])
@ -654,7 +652,7 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn):
self.assertEqual(chassis, ['host-2']) self.assertEqual(chassis, ['host-2'])
chassis = self.nb_ovn_idl.get_gateway_chassis_binding( chassis = self.nb_ovn_idl.get_gateway_chassis_binding(
utils.ovn_lrouter_port_name('orp-id-a3')) utils.ovn_lrouter_port_name('orp-id-a3'))
self.assertEqual(chassis, ['neutron-ovn-invalid-chassis']) self.assertEqual([], chassis)
chassis = self.nb_ovn_idl.get_gateway_chassis_binding( chassis = self.nb_ovn_idl.get_gateway_chassis_binding(
utils.ovn_lrouter_port_name('orp-id-b3')) utils.ovn_lrouter_port_name('orp-id-b3'))
self.assertEqual([], chassis) self.assertEqual([], chassis)

View File

@ -50,8 +50,7 @@ class TestOVNGatewayScheduler(base.BaseTestCase):
self.new_gateway_name = 'lrp_new' self.new_gateway_name = 'lrp_new'
self.fake_chassis_gateway_mappings = { self.fake_chassis_gateway_mappings = {
'None': {'Chassis': [], 'None': {'Chassis': [],
'Gateways': { 'Gateways': {'g1': None}},
'g1': [ovn_const.OVN_GATEWAY_INVALID_CHASSIS]}},
'Multiple1': {'Chassis': ['hv1', 'hv2', 'hv3', 'hv4', 'hv5'], 'Multiple1': {'Chassis': ['hv1', 'hv2', 'hv3', 'hv4', 'hv5'],
'Gateways': { 'Gateways': {
'g1': ['hv1', 'hv2', 'hv4', 'hv3', 'hv5'], 'g1': ['hv1', 'hv2', 'hv4', 'hv3', 'hv5'],
@ -102,6 +101,7 @@ class TestOVNGatewayScheduler(base.BaseTestCase):
for chassis in details['Chassis']: for chassis in details['Chassis']:
details['Chassis_Bindings'].setdefault(chassis, []) details['Chassis_Bindings'].setdefault(chassis, [])
for gw, chassis_list in details['Gateways'].items(): for gw, chassis_list in details['Gateways'].items():
chassis_list = chassis_list or []
max_prio = len(chassis_list) max_prio = len(chassis_list)
for idx, chassis in enumerate(chassis_list): for idx, chassis in enumerate(chassis_list):
prio = max_prio - idx prio = max_prio - idx
@ -138,7 +138,7 @@ class OVNGatewayChanceScheduler(TestOVNGatewayScheduler):
gateway_name = random.choice(list(mapping['Gateways'].keys())) gateway_name = random.choice(list(mapping['Gateways'].keys()))
chassis = self.select(mapping, gateway_name, chassis_and_azs, chassis = self.select(mapping, gateway_name, chassis_and_azs,
candidates=mapping['Chassis']) candidates=mapping['Chassis'])
self.assertEqual([ovn_const.OVN_GATEWAY_INVALID_CHASSIS], chassis) self.assertIsNone(chassis)
def test_no_chassis_available_for_new_gateway(self): def test_no_chassis_available_for_new_gateway(self):
mapping = self.fake_chassis_gateway_mappings['None'] mapping = self.fake_chassis_gateway_mappings['None']
@ -146,7 +146,7 @@ class OVNGatewayChanceScheduler(TestOVNGatewayScheduler):
gateway_name = self.new_gateway_name gateway_name = self.new_gateway_name
chassis = self.select(mapping, gateway_name, chassis_and_azs, chassis = self.select(mapping, gateway_name, chassis_and_azs,
candidates=mapping['Chassis']) candidates=mapping['Chassis'])
self.assertEqual([ovn_const.OVN_GATEWAY_INVALID_CHASSIS], chassis) self.assertIsNone(chassis)
def test_random_chassis_available_for_new_gateway(self): def test_random_chassis_available_for_new_gateway(self):
mapping = self.fake_chassis_gateway_mappings['Multiple1'] mapping = self.fake_chassis_gateway_mappings['Multiple1']
@ -161,7 +161,7 @@ class OVNGatewayChanceScheduler(TestOVNGatewayScheduler):
chassis_and_azs = self.fake_chassis_and_azs['None'] chassis_and_azs = self.fake_chassis_and_azs['None']
gateway_name = self.new_gateway_name gateway_name = self.new_gateway_name
chassis = self.select(mapping, gateway_name, chassis_and_azs) chassis = self.select(mapping, gateway_name, chassis_and_azs)
self.assertEqual([ovn_const.OVN_GATEWAY_INVALID_CHASSIS], chassis) self.assertIsNone(chassis)
self.mock_log.warning.assert_called_once_with( self.mock_log.warning.assert_called_once_with(
'Gateway %s was not scheduled on any chassis, no candidates are ' 'Gateway %s was not scheduled on any chassis, no candidates are '
'available', gateway_name) 'available', gateway_name)
@ -179,15 +179,14 @@ class OVNGatewayChanceScheduler(TestOVNGatewayScheduler):
nb_idl=nb_idl, gw_chassis=["temp"], nb_idl=nb_idl, gw_chassis=["temp"],
physnet='phys-network-1', physnet='phys-network-1',
chassis_physnets=chassis_physnets, chassis_physnets=chassis_physnets,
existing_chassis=['temp', existing_chassis=['temp', None]))
ovn_const.OVN_GATEWAY_INVALID_CHASSIS]))
# Check if invalid is removed -II # Check if invalid is removed -II
self.assertFalse( self.assertFalse(
self.filter_existing_chassis( self.filter_existing_chassis(
nb_idl=nb_idl, gw_chassis=["temp"], nb_idl=nb_idl, gw_chassis=["temp"],
physnet='phys-network-1', physnet='phys-network-1',
chassis_physnets=chassis_physnets, chassis_physnets=chassis_physnets,
existing_chassis=[ovn_const.OVN_GATEWAY_INVALID_CHASSIS])) existing_chassis=None))
# Check if chassis removed when physnet doesnt exist # Check if chassis removed when physnet doesnt exist
self.assertFalse( self.assertFalse(
self.filter_existing_chassis( self.filter_existing_chassis(
@ -236,7 +235,7 @@ class OVNGatewayLeastLoadedScheduler(TestOVNGatewayScheduler):
gateway_name = random.choice(list(mapping['Gateways'].keys())) gateway_name = random.choice(list(mapping['Gateways'].keys()))
chassis = self.select(mapping, gateway_name, chassis_and_azs, chassis = self.select(mapping, gateway_name, chassis_and_azs,
candidates=mapping['Chassis']) candidates=mapping['Chassis'])
self.assertEqual([ovn_const.OVN_GATEWAY_INVALID_CHASSIS], chassis) self.assertIsNone(chassis)
def test_no_chassis_available_for_new_gateway(self): def test_no_chassis_available_for_new_gateway(self):
mapping = self.fake_chassis_gateway_mappings['None'] mapping = self.fake_chassis_gateway_mappings['None']
@ -244,7 +243,7 @@ class OVNGatewayLeastLoadedScheduler(TestOVNGatewayScheduler):
gateway_name = self.new_gateway_name gateway_name = self.new_gateway_name
chassis = self.select(mapping, gateway_name, chassis_and_azs, chassis = self.select(mapping, gateway_name, chassis_and_azs,
candidates=mapping['Chassis']) candidates=mapping['Chassis'])
self.assertEqual([ovn_const.OVN_GATEWAY_INVALID_CHASSIS], chassis) self.assertIsNone(chassis)
def test_least_loaded_chassis_available_for_new_gateway1(self): def test_least_loaded_chassis_available_for_new_gateway1(self):
mapping = self.fake_chassis_gateway_mappings['Multiple1'] mapping = self.fake_chassis_gateway_mappings['Multiple1']

View File

@ -0,0 +1,13 @@
---
other:
- |
The artifact of creating a gateway chassis called
"neutron-ovn-invalid-chassis" when a "Logical_Router_Port" cannot be
assigned to any chassis is removed. Now no gateway chassis is created and
the "Logical_Router_Port" field will be empty.
upgrade:
- |
Any "Logical_Router_Port" with a gateway chassis named
"neutron-ovn-invalid-chassis" will be updated and this chassis will be
deleted. An unhosted (unbound) "Logical_Router_Port" will have no gateway
assigned.