Skip to content

Commit 0a42111

Browse files
committed
[OVN] The L3 scheduler does not use all chassis by default
Any OVN scheduler, inheriting from ``OVNGatewayScheduler``, calls ``_schedule_gateway`` to make the decision of in what chassis the router gatweway should be located. Before this patch, if the list of candidates was empty, the scheduler used all available chassis as candidate list. This patch is removing this default behaviour. In a deployment, only those chassis marked explicitly with "ovn-cms-options=enable-chassis-as-gw" can be used as gateway chassis. After enabling this patch, any existing router gateway port will preserve the assigned chassis; any new router gateway will be scheduled only on the chassis configured as gateways. If a router gateway port cannot find any chassis to be scheduled, the "neutron-ovn-invalid-chassis" will be used instead and a warning message will be printed in the logs. Closes-Bug: #2019217 Change-Id: If0f843463edfd7edc5c897cc098de31444f9d81b (cherry picked from commit 413044f)
1 parent a8bf8cd commit 0a42111

File tree

6 files changed

+56
-43
lines changed

6 files changed

+56
-43
lines changed

neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1651,8 +1651,7 @@ def _create_lrouter_port(self, context, router, port, txn=None):
16511651
physnet, availability_zone_hints=common_utils.get_az_hints(
16521652
router))
16531653
selected_chassis = self._ovn_scheduler.select(
1654-
self._nb_idl, self._sb_idl, lrouter_port_name,
1655-
candidates=candidates)
1654+
self._nb_idl, lrouter_port_name, candidates=candidates)
16561655
if selected_chassis:
16571656
columns['gateway_chassis'] = selected_chassis
16581657

neutron/scheduler/l3_ovn_scheduler.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def __init__(self):
3535
pass
3636

3737
@abc.abstractmethod
38-
def select(self, nb_idl, sb_idl, gateway_name, candidates=None):
38+
def select(self, nb_idl, gateway_name, candidates=None):
3939
"""Schedule the gateway port of a router to an OVN chassis.
4040
4141
Schedule the gateway router port only if it is not already
@@ -57,10 +57,10 @@ def filter_existing_chassis(self, nb_idl, gw_chassis,
5757
chassis_list.remove(chassis_name)
5858
return chassis_list
5959

60-
def _schedule_gateway(self, nb_idl, sb_idl, gateway_name, candidates,
60+
def _schedule_gateway(self, nb_idl, gateway_name, candidates,
6161
existing_chassis):
6262
existing_chassis = existing_chassis or []
63-
candidates = candidates or self._get_chassis_candidates(sb_idl)
63+
candidates = candidates or []
6464
candidates = list(set(candidates) - set(existing_chassis))
6565
# If no candidates, or gateway scheduled on MAX_GATEWAY_CHASSIS nodes
6666
# or all candidates in existing_chassis, return existing_chassis.
@@ -70,6 +70,8 @@ def _schedule_gateway(self, nb_idl, sb_idl, gateway_name, candidates,
7070
len(existing_chassis) == ovn_const.MAX_GW_CHASSIS):
7171
return existing_chassis
7272
if not candidates:
73+
LOG.warning('Gateway %s was not scheduled on any chassis, no '
74+
'candidates are available', gateway_name)
7375
return [ovn_const.OVN_GATEWAY_INVALID_CHASSIS]
7476
chassis_count = ovn_const.MAX_GW_CHASSIS - len(existing_chassis)
7577
# The actual binding of the gateway to a chassis via the options
@@ -88,20 +90,13 @@ def _schedule_gateway(self, nb_idl, sb_idl, gateway_name, candidates,
8890
def _select_gateway_chassis(self, nb_idl, candidates):
8991
"""Choose a chassis from candidates based on a specific policy."""
9092

91-
def _get_chassis_candidates(self, sb_idl):
92-
# TODO(azbiswas): Allow selection of a specific type of chassis when
93-
# the upstream code merges.
94-
# return (sb_idl.get_all_chassis('gateway_router') or
95-
# sb_idl.get_all_chassis())
96-
return sb_idl.get_all_chassis()
97-
9893

9994
class OVNGatewayChanceScheduler(OVNGatewayScheduler):
10095
"""Randomly select an chassis for a gateway port of a router"""
10196

102-
def select(self, nb_idl, sb_idl, gateway_name, candidates=None,
97+
def select(self, nb_idl, gateway_name, candidates=None,
10398
existing_chassis=None):
104-
return self._schedule_gateway(nb_idl, sb_idl, gateway_name,
99+
return self._schedule_gateway(nb_idl, gateway_name,
105100
candidates, existing_chassis)
106101

107102
def _select_gateway_chassis(self, nb_idl, candidates):
@@ -113,9 +108,9 @@ def _select_gateway_chassis(self, nb_idl, candidates):
113108
class OVNGatewayLeastLoadedScheduler(OVNGatewayScheduler):
114109
"""Select the least loaded chassis for a gateway port of a router"""
115110

116-
def select(self, nb_idl, sb_idl, gateway_name, candidates=None,
111+
def select(self, nb_idl, gateway_name, candidates=None,
117112
existing_chassis=None):
118-
return self._schedule_gateway(nb_idl, sb_idl, gateway_name,
113+
return self._schedule_gateway(nb_idl, gateway_name,
119114
candidates, existing_chassis)
120115

121116
@staticmethod

neutron/services/ovn_l3/plugin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ def schedule_unhosted_gateways(self, event_from_chassis=None):
434434
chassis_physnets=chassis_with_physnets,
435435
availability_zone_hints=az_hints)
436436
chassis = self.scheduler.select(
437-
self._nb_ovn, self._sb_ovn, g_name, candidates=candidates,
437+
self._nb_ovn, g_name, candidates=candidates,
438438
existing_chassis=existing_chassis)
439439
if primary and primary != chassis[0]:
440440
if primary not in chassis:

neutron/tests/unit/scheduler/test_l3_ovn_scheduler.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,11 @@ def __init__(self, chassis_gateway_mapping, gateway):
3030
None))
3131

3232

33-
class FakeOVNGatewaySchedulerSbOvnIdl(object):
34-
def __init__(self, chassis_gateway_mapping):
35-
self.get_all_chassis = mock.Mock(
36-
return_value=chassis_gateway_mapping['Chassis'])
37-
38-
3933
class TestOVNGatewayScheduler(base.BaseTestCase):
4034

4135
def setUp(self):
4236
super(TestOVNGatewayScheduler, self).setUp()
37+
self.mock_log = mock.patch.object(l3_ovn_scheduler, 'LOG').start()
4338

4439
# Overwritten by derived classes
4540
self.l3_scheduler = None
@@ -82,12 +77,11 @@ def setUp(self):
8277
if chassis in details['Chassis_Bindings']:
8378
details['Chassis_Bindings'][chassis].append((gw, 0))
8479

85-
def select(self, chassis_gateway_mapping, gateway_name):
80+
def select(self, chassis_gateway_mapping, gateway_name, candidates=None):
8681
nb_idl = FakeOVNGatewaySchedulerNbOvnIdl(chassis_gateway_mapping,
8782
gateway_name)
88-
sb_idl = FakeOVNGatewaySchedulerSbOvnIdl(chassis_gateway_mapping)
89-
return self.l3_scheduler.select(nb_idl, sb_idl,
90-
gateway_name)
83+
return self.l3_scheduler.select(nb_idl, gateway_name,
84+
candidates=candidates)
9185

9286
def filter_existing_chassis(self, *args, **kwargs):
9387
return self.l3_scheduler.filter_existing_chassis(
@@ -108,21 +102,33 @@ def setUp(self):
108102
def test_no_chassis_available_for_existing_gateway(self):
109103
mapping = self.fake_chassis_gateway_mappings['None']
110104
gateway_name = random.choice(list(mapping['Gateways'].keys()))
111-
chassis = self.select(mapping, gateway_name)
105+
chassis = self.select(mapping, gateway_name,
106+
candidates=mapping['Chassis'])
112107
self.assertEqual([ovn_const.OVN_GATEWAY_INVALID_CHASSIS], chassis)
113108

114109
def test_no_chassis_available_for_new_gateway(self):
115110
mapping = self.fake_chassis_gateway_mappings['None']
116111
gateway_name = self.new_gateway_name
117-
chassis = self.select(mapping, gateway_name)
112+
chassis = self.select(mapping, gateway_name,
113+
candidates=mapping['Chassis'])
118114
self.assertEqual([ovn_const.OVN_GATEWAY_INVALID_CHASSIS], chassis)
119115

120116
def test_random_chassis_available_for_new_gateway(self):
121117
mapping = self.fake_chassis_gateway_mappings['Multiple1']
122118
gateway_name = self.new_gateway_name
123-
chassis = self.select(mapping, gateway_name)
119+
chassis = self.select(mapping, gateway_name,
120+
candidates=mapping['Chassis'])
124121
self.assertCountEqual(chassis, mapping.get('Chassis'))
125122

123+
def test_no_candidates_provided(self):
124+
mapping = self.fake_chassis_gateway_mappings['None']
125+
gateway_name = self.new_gateway_name
126+
chassis = self.select(mapping, gateway_name)
127+
self.assertEqual([ovn_const.OVN_GATEWAY_INVALID_CHASSIS], chassis)
128+
self.mock_log.warning.assert_called_once_with(
129+
'Gateway %s was not scheduled on any chassis, no candidates are '
130+
'available', gateway_name)
131+
126132
def test_filter_existing_chassis(self):
127133
# filter_existing_chassis is scheduler independent, but calling
128134
# it from Base class didnt seem right. Also, there is no need to have
@@ -171,19 +177,22 @@ def setUp(self):
171177
def test_no_chassis_available_for_existing_gateway(self):
172178
mapping = self.fake_chassis_gateway_mappings['None']
173179
gateway_name = random.choice(list(mapping['Gateways'].keys()))
174-
chassis = self.select(mapping, gateway_name)
180+
chassis = self.select(mapping, gateway_name,
181+
candidates=mapping['Chassis'])
175182
self.assertEqual([ovn_const.OVN_GATEWAY_INVALID_CHASSIS], chassis)
176183

177184
def test_no_chassis_available_for_new_gateway(self):
178185
mapping = self.fake_chassis_gateway_mappings['None']
179186
gateway_name = self.new_gateway_name
180-
chassis = self.select(mapping, gateway_name)
187+
chassis = self.select(mapping, gateway_name,
188+
candidates=mapping['Chassis'])
181189
self.assertEqual([ovn_const.OVN_GATEWAY_INVALID_CHASSIS], chassis)
182190

183191
def test_least_loaded_chassis_available_for_new_gateway1(self):
184192
mapping = self.fake_chassis_gateway_mappings['Multiple1']
185193
gateway_name = self.new_gateway_name
186-
chassis = self.select(mapping, gateway_name)
194+
chassis = self.select(mapping, gateway_name,
195+
candidates=mapping['Chassis'])
187196
self.assertCountEqual(chassis, mapping.get('Chassis'))
188197
# least loaded will be the first one in the list,
189198
# networking-ovn will assign highest priority to this first element
@@ -192,28 +201,32 @@ def test_least_loaded_chassis_available_for_new_gateway1(self):
192201
def test_least_loaded_chassis_available_for_new_gateway2(self):
193202
mapping = self.fake_chassis_gateway_mappings['Multiple2']
194203
gateway_name = self.new_gateway_name
195-
chassis = self.select(mapping, gateway_name)
204+
chassis = self.select(mapping, gateway_name,
205+
candidates=mapping['Chassis'])
196206
# hv1 will have least priority
197207
self.assertEqual(chassis[2], 'hv1')
198208

199209
def test_least_loaded_chassis_available_for_new_gateway3(self):
200210
mapping = self.fake_chassis_gateway_mappings['Multiple3']
201211
gateway_name = self.new_gateway_name
202-
chassis = self.select(mapping, gateway_name)
212+
chassis = self.select(mapping, gateway_name,
213+
candidates=mapping['Chassis'])
203214
# least loaded chassis will be in the front of the list
204215
self.assertEqual(['hv1', 'hv3', 'hv2'], chassis)
205216

206217
def test_least_loaded_chassis_with_rebalance(self):
207218
mapping = self.fake_chassis_gateway_mappings['Multiple4']
208219
gateway_name = self.new_gateway_name
209-
chassis = self.select(mapping, gateway_name)
220+
chassis = self.select(mapping, gateway_name,
221+
candidates=mapping['Chassis'])
210222
# least loaded chassis will be in the front of the list
211223
self.assertEqual(['hv2', 'hv1'], chassis)
212224

213225
def test_existing_chassis_available_for_existing_gateway(self):
214226
mapping = self.fake_chassis_gateway_mappings['Multiple1']
215227
gateway_name = random.choice(list(mapping['Gateways'].keys()))
216-
chassis = self.select(mapping, gateway_name)
228+
chassis = self.select(mapping, gateway_name,
229+
candidates=mapping['Chassis'])
217230
self.assertEqual(ovn_const.MAX_GW_CHASSIS, len(chassis))
218231

219232
def test__get_chassis_load_by_prios_several_ports(self):

neutron/tests/unit/services/ovn_l3/test_plugin.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from neutron.api.v2 import router as api_router
3535
from neutron.common.ovn import constants as ovn_const
3636
from neutron.common.ovn import utils
37+
from neutron.conf.plugins.ml2.drivers import driver_type as driver_type_conf
3738
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf as config
3839
from neutron.db import extraroute_db
3940
from neutron import manager as neutron_manager
@@ -65,6 +66,7 @@ def _start_mock(self, path, return_value, new_callable=None):
6566
def setUp(self):
6667
mock.patch('neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.'
6768
'impl_idl_ovn.Backend.schema_helper').start()
69+
driver_type_conf.register_ml2_drivers_geneve_opts(cfg=cfg.CONF)
6870
cfg.CONF.set_override('max_header_size', 38, group='ml2_type_geneve')
6971
super(TestOVNL3RouterPlugin, self).setUp()
7072
revision_plugin.RevisionPlugin()
@@ -1559,12 +1561,10 @@ def test_schedule_unhosted_gateways(self, get_gppm):
15591561
chassis_physnets=chassis_mappings,
15601562
cms=chassis, availability_zone_hints=[])] * 3)
15611563
self.mock_schedule.assert_has_calls([
1562-
mock.call(self.nb_idl(), self.sb_idl(),
1563-
'lrp-foo-1', [], ['chassis1', 'chassis2']),
1564-
mock.call(self.nb_idl(), self.sb_idl(),
1565-
'lrp-foo-2', [], ['chassis2']),
1566-
mock.call(self.nb_idl(), self.sb_idl(),
1567-
'lrp-foo-3', [], [])])
1564+
mock.call(self.nb_idl(), 'lrp-foo-1', [],
1565+
['chassis1', 'chassis2']),
1566+
mock.call(self.nb_idl(), 'lrp-foo-2', [], ['chassis2']),
1567+
mock.call(self.nb_idl(), 'lrp-foo-3', [], [])])
15681568
# make sure that for second port primary chassis stays untouched
15691569
self.nb_idl().update_lrouter_port.assert_has_calls([
15701570
mock.call('lrp-foo-1',
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
upgrade:
3+
- |
4+
In ML2/OVN, any new router gateway port (OVN logical router port) will be
5+
scheduled only on those chassis configured as gateway. Any existing router
6+
gateway port will preserve the current chassis assignation.

0 commit comments

Comments
 (0)