Browse Source

Support for Router Scheduling on addition/removal of chassis

The following patch provides L3HA Rescheduling of gateways when chassis
are added/removed. It reschedules the gateway ports when a new chassis
is added/old one removed. However, the number of chassis where a
gateway can be hosted is limited by the constant MAX_GW_CHASSIS.

Co-Authored-By: Maciej Józefczyk <mjozefcz@redhat.com>

Change-Id: I0d96efe4ceef4168039930738285c19d5c003951
Closes-Bug: #1762691
changes/61/591461/40
reedip 2 years ago
committed by Maciej Józefczyk
parent
commit
12070403db
  1. 13
      doc/source/admin/routing.rst
  2. 3
      networking_ovn/common/constants.py
  3. 26
      networking_ovn/common/utils.py
  4. 27
      networking_ovn/l3/l3_ovn.py
  5. 54
      networking_ovn/l3/l3_ovn_scheduler.py
  6. 14
      networking_ovn/ovsdb/impl_idl_ovn.py
  7. 5
      networking_ovn/tests/functional/base.py
  8. 82
      networking_ovn/tests/functional/test_router.py
  9. 62
      networking_ovn/tests/unit/common/test_utils.py
  10. 2
      networking_ovn/tests/unit/fakes.py
  11. 64
      networking_ovn/tests/unit/l3/test_l3_ovn.py
  12. 81
      networking_ovn/tests/unit/l3/test_l3_ovn_scheduler.py
  13. 33
      networking_ovn/tests/unit/ovsdb/test_impl_idl_ovn.py

13
doc/source/admin/routing.rst

@ -44,6 +44,19 @@ network attached to the router it will schedule the router gateway port
to multiple chassis, making use of the ``gateway_chassis`` column on OVN's
``Logical_Router_Port`` table.
In order to have external connectivity, either:
* Some gateway nodes have ``ovn-cms-options`` with the value
``enable-chassis-as-gw`` in Open_vSwitch table's external_ids column, or
* if no gateway node exists with the external ids column set with that
value, then all nodes would be eligible to host gateway chassis.
Example to how to enabled chassis to host gateways:
.. code-block:: console
$ ovs-vsctl set open . external-ids:ovn-cms-options="enable-chassis-as-gw"
At the low level, functionality is all implemented mostly by OpenFlow rules
with bundle active_passive outputs. The ARP responder and router
enablement/disablement is handled by ovn-controller. Gratuitous ARPs for FIPs

3
networking_ovn/common/constants.py

@ -163,3 +163,6 @@ HASH_RING_NODES_TIMEOUT = 60
HASH_RING_TOUCH_INTERVAL = 30
HASH_RING_CACHE_TIMEOUT = 30
HASH_RING_ML2_GROUP = 'mechanism_driver'
# Maximum chassis count where a gateway port can be hosted
MAX_GW_CHASSIS = 5

26
networking_ovn/common/utils.py

@ -388,3 +388,29 @@ def get_method_class(method):
def ovn_metadata_name(id_):
"""Return the OVN metadata name based on an id."""
return 'metadata-%s' % id_
def is_gateway_chassis_invalid(chassis_name, gw_chassis,
physnet, chassis_physnets):
"""Check if gateway chassis is invalid
@param chassis_name: gateway chassis name
@type chassis_name: string
@param gw_chassis: List of gateway chassis in the system
@type gw_chassis: []
@param physnet: physical network associated to chassis_name
@type physnet: string
@param chassis_physnets: Dictionary linking chassis with their physnets
@type chassis_physnets: {}
@return Boolean
"""
if chassis_name == constants.OVN_GATEWAY_INVALID_CHASSIS:
return True
elif chassis_name not in chassis_physnets:
return True
elif physnet and physnet not in chassis_physnets.get(chassis_name):
return True
elif gw_chassis and chassis_name not in gw_chassis:
return True
return False

27
networking_ovn/l3/l3_ovn.py

@ -318,10 +318,35 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
port_physnet_dict, chassis_physnets, cms)
for g_name in unhosted_gateways:
physnet = port_physnet_dict.get(g_name[len('lrp-'):])
# 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,
existing_chassis=existing_chassis)
candidates = self._ovn_client.get_candidates_for_scheduling(
physnet, cms=cms, chassis_physnets=chassis_physnets)
chassis = self.scheduler.select(
self._ovn, self._sb_ovn, g_name, candidates=candidates)
self._ovn, self._sb_ovn, g_name, candidates=candidates,
existing_chassis=existing_chassis)
if master and master != chassis[0]:
if master not in chassis:
LOG.debug("Master gateway chassis %(old)s "
"has been removed from the system. Moving "
"gateway %(gw)s to other chassis %(new)s.",
{'gw': g_name,
'old': master,
'new': chassis[0]})
else:
LOG.debug("Gateway %s is hosted at %s.", g_name, master)
# NOTE(mjozefcz): It means scheduler moved master chassis
# to other gw based on scheduling method. But we don't
# want network flap - so moving actual master to be on
# the top.
index = chassis.index(master)
chassis[0], chassis[index] = chassis[index], chassis[0]
# NOTE(dalvarez): Let's commit the changes in separate transactions
# as we will rely on those for scheduling subsequent gateways.
with self._ovn.transaction(check_error=True) as txn:

54
networking_ovn/l3/l3_ovn_scheduler.py

@ -21,6 +21,7 @@ import six
from networking_ovn.common import config as ovn_config
from networking_ovn.common import constants as ovn_const
from networking_ovn.common import utils
LOG = log.getLogger(__name__)
@ -28,8 +29,6 @@ LOG = log.getLogger(__name__)
OVN_SCHEDULER_CHANCE = 'chance'
OVN_SCHEDULER_LEAST_LOADED = 'leastloaded'
MAX_GW_CHASSIS = 5
@six.add_metaclass(abc.ABCMeta)
class OVNGatewayScheduler(object):
@ -45,23 +44,42 @@ class OVNGatewayScheduler(object):
scheduled.
"""
def _schedule_gateway(self, nb_idl, sb_idl, gateway_name, candidates):
existing_chassis = nb_idl.get_gateway_chassis_binding(gateway_name)
def filter_existing_chassis(self, nb_idl, gw_chassis,
physnet, chassis_physnets,
existing_chassis):
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):
LOG.debug("Chassis %(chassis)s is invalid for scheduling "
"router in physnet: %(physnet)s.",
{'chassis': chassis_name,
'physnet': physnet})
chassis_list.remove(chassis_name)
return chassis_list
def _schedule_gateway(self, nb_idl, sb_idl, gateway_name, candidates,
existing_chassis):
existing_chassis = existing_chassis or []
candidates = candidates or self._get_chassis_candidates(sb_idl)
# if no candidates or all chassis in existing_chassis also present
# in candidates, then return existing_chassis
# TODO(anilvenkata): If more candidates avaialable, then schedule
# on them also?
if existing_chassis and (not candidates or
not (set(existing_chassis) - set(candidates))):
return existing_chassis
candidates = list(set(candidates) - set(existing_chassis))
# If no candidates, or gateway scheduled on MAX_GATEWAY_CHASSIS nodes
# or all candidates in existing_chassis, return existing_chassis.
# Otherwise, if more candidates present, then schedule them.
if existing_chassis:
if not candidates or (
len(existing_chassis) == ovn_const.MAX_GW_CHASSIS):
return existing_chassis
if not candidates:
return [ovn_const.OVN_GATEWAY_INVALID_CHASSIS]
chassis_count = ovn_const.MAX_GW_CHASSIS - len(existing_chassis)
# The actual binding of the gateway to a chassis via the options
# column or gateway_chassis column in the OVN_Northbound is done
# by the caller
chassis = self._select_gateway_chassis(
nb_idl, candidates)[:MAX_GW_CHASSIS]
nb_idl, candidates)[:chassis_count]
# priority of existing chassis is higher than candidates
chassis = existing_chassis + chassis
LOG.debug("Gateway %s scheduled on chassis %s",
gateway_name, chassis)
@ -82,8 +100,10 @@ class OVNGatewayScheduler(object):
class OVNGatewayChanceScheduler(OVNGatewayScheduler):
"""Randomly select an chassis for a gateway port of a router"""
def select(self, nb_idl, sb_idl, gateway_name, candidates=None):
return self._schedule_gateway(nb_idl, sb_idl, gateway_name, candidates)
def select(self, nb_idl, sb_idl, gateway_name, candidates=None,
existing_chassis=None):
return self._schedule_gateway(nb_idl, sb_idl, gateway_name,
candidates, existing_chassis)
def _select_gateway_chassis(self, nb_idl, candidates):
candidates = copy.deepcopy(candidates)
@ -94,8 +114,10 @@ class OVNGatewayChanceScheduler(OVNGatewayScheduler):
class OVNGatewayLeastLoadedScheduler(OVNGatewayScheduler):
"""Select the least loaded chassis for a gateway port of a router"""
def select(self, nb_idl, sb_idl, gateway_name, candidates=None):
return self._schedule_gateway(nb_idl, sb_idl, gateway_name, candidates)
def select(self, nb_idl, sb_idl, gateway_name, candidates=None,
existing_chassis=None):
return self._schedule_gateway(nb_idl, sb_idl, gateway_name,
candidates, existing_chassis)
@staticmethod
def _get_chassis_load_by_prios(chassis_info):

14
networking_ovn/ovsdb/impl_idl_ovn.py

@ -394,7 +394,7 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
@param lrp: logical router port
@type lrp: Logical_Router_Port row
@return: List of tuples (chassis_name, priority)
@return: List of tuples (chassis_name, priority) sorted by priority
"""
# Try retrieving gateway_chassis with new schema. If new schema is not
# supported or user is using old schema, then use old schema for
@ -407,7 +407,8 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
rc = lrp.options.get(ovn_const.OVN_GATEWAY_CHASSIS_KEY)
if rc:
chassis.append((rc, 0))
return chassis
# make sure that chassis are sorted by priority
return sorted(chassis, reverse=True, key=lambda x: x[1])
def get_all_chassis_gateway_bindings(self,
chassis_candidate_list=None):
@ -438,21 +439,18 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
def get_unhosted_gateways(self, port_physnet_dict, chassis_physnets,
gw_chassis):
unhosted_gateways = []
valid_chassis_list = list(chassis_physnets)
for lrp in self._tables['Logical_Router_Port'].rows.values():
if not lrp.name.startswith('lrp-'):
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 (chassis_name == ovn_const.OVN_GATEWAY_INVALID_CHASSIS or
chassis_name not in valid_chassis_list or
(physnet and
physnet not in chassis_physnets.get(chassis_name)) or
(gw_chassis and chassis_name not in gw_chassis)):
if is_max_gw_reached or utils.is_gateway_chassis_invalid(
chassis_name, gw_chassis, physnet, chassis_physnets):
unhosted_gateways.append(lrp.name)
return unhosted_gateways

5
networking_ovn/tests/functional/base.py

@ -289,5 +289,6 @@ class TestOVNFunctionalBase(test_plugin.Ml2PluginV2TestCase):
external_ids=external_ids, hostname=host).execute(check_error=True)
return name
def del_fake_chassis(self, name):
self.sb_api.chassis_del(name).execute(check_error=True)
def del_fake_chassis(self, chassis, if_exists=True):
self.sb_api.chassis_del(
chassis, if_exists=if_exists).execute(check_error=True)

82
networking_ovn/tests/functional/test_router.py

@ -322,3 +322,85 @@ class TestRouter(base.TestOVNFunctionalBase):
def test_router_port_ipv6_ra_configs_ipv4(self):
self._test_router_port_ipv6_ra_configs_helper(
ip_version=4)
def test_gateway_chassis_rebalance(self):
def _get_result_dict():
sched_info = {}
for row in self.nb_api.tables[
'Logical_Router_Port'].rows.values():
for gwc in row.gateway_chassis:
chassis = sched_info.setdefault(gwc.chassis_name, {})
chassis[gwc.priority] = chassis.get(gwc.priority, 0) + 1
return sched_info
if not self._l3_ha_supported():
self.skipTest('L3 HA not supported')
ovn_client = self.l3_plugin._ovn_client
chassis4 = self.add_fake_chassis(
'ovs-host4', physical_nets=['physnet4'], external_ids={
'ovn-cms-options': 'enable-chassis-as-gw'})
ovn_client._ovn_scheduler = l3_sched.OVNGatewayLeastLoadedScheduler()
ext1 = self._create_ext_network(
'ext1', 'flat', 'physnet4', None, "30.0.0.1", "30.0.0.0/24")
gw_info = {'network_id': ext1['network']['id']}
# Create 20 routers with a gateway. Since we're using physnet4, the
# chassis candidates will be chassis4 initially.
for i in range(20):
router = self._create_router('router%d' % i, gw_info=gw_info)
gw_port_id = router.get('gw_port_id')
logical_port = 'cr-lrp-%s' % gw_port_id
self.sb_api.lsp_bind(logical_port, chassis4,
may_exist=True).execute(check_error=True)
self.l3_plugin.schedule_unhosted_gateways()
expected = {chassis4: {1: 20}}
self.assertEqual(expected, _get_result_dict())
# Add another chassis as a gateway chassis
chassis5 = self.add_fake_chassis(
'ovs-host5', physical_nets=['physnet4'], external_ids={
'ovn-cms-options': 'enable-chassis-as-gw'})
# Add a node as compute node. Compute node wont be
# used to schedule the router gateway ports therefore
# priority values wont be changed. Therefore chassis4 would
# still have priority 2
self.add_fake_chassis('ovs-host6', physical_nets=['physnet4'])
# Chassis4 should have all ports at Priority 2
self.l3_plugin.schedule_unhosted_gateways()
self.assertEqual({2: 20}, _get_result_dict()[chassis4])
# Chassis5 should have all ports at Priority 1
self.assertEqual({1: 20}, _get_result_dict()[chassis5])
# delete chassis that hosts all the gateways
self.del_fake_chassis(chassis4)
self.l3_plugin.schedule_unhosted_gateways()
# As Chassis4 has been removed so all gateways that were
# hosted there are now masters on chassis5 and have
# priority 1.
self.assertEqual({1: 20}, _get_result_dict()[chassis5])
def test_gateway_chassis_rebalance_max_chassis(self):
chassis_list = []
# spawn 6 chassis and check if port has MAX_CHASSIS candidates.
for i in range(0, ovn_const.MAX_GW_CHASSIS + 1):
chassis_list.append(
self.add_fake_chassis(
'ovs-host%s' % i, physical_nets=['physnet1'],
external_ids={
'ovn-cms-options': 'enable-chassis-as-gw'}))
ext1 = self._create_ext_network(
'ext1', 'vlan', 'physnet1', 1, "10.0.0.1", "10.0.0.0/24")
gw_info = {'network_id': ext1['network']['id']}
router = self._create_router('router', gw_info=gw_info)
gw_port_id = router.get('gw_port_id')
logical_port = 'cr-lrp-%s' % gw_port_id
self.sb_api.lsp_bind(logical_port, chassis_list[0],
may_exist=True).execute(check_error=True)
self.l3_plugin.schedule_unhosted_gateways()
for row in self.nb_api.tables[
'Logical_Router_Port'].rows.values():
self.assertEqual(ovn_const.MAX_GW_CHASSIS,
len(row.gateway_chassis))

62
networking_ovn/tests/unit/common/test_utils.py

@ -15,6 +15,7 @@
import fixtures
from networking_ovn.common import constants
from networking_ovn.common import utils
from networking_ovn.tests import base
@ -41,3 +42,64 @@ class TestUtils(base.TestCase):
observed_dns_resolvers = utils.get_system_dns_resolvers(
resolver_file=resolver_file_name)
self.assertEqual(expected_dns_resolvers, observed_dns_resolvers)
class TestGateWayChassisValidity(base.TestCase):
def setUp(self):
super(TestGateWayChassisValidity, self).setUp()
self.gw_chassis = ['host1', 'host2']
self.chassis_name = self.gw_chassis[0]
self.physnet = 'physical-nw-1'
self.chassis_physnets = {self.chassis_name: [self.physnet]}
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))
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))
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))
def test_gateway_chassis_for_undefined_physnet(self):
# Return True since physnet is not defined
self.chassis_name = 'host-1'
self.physnet = None
self.assertTrue(utils.is_gateway_chassis_invalid(
self.chassis_name, self.gw_chassis, self.physnet,
self.chassis_physnets))
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))
def test_gateway_chassis_for_gw_chassis_empty(self):
# Return False if gw_chassis is []
# This condition states that the chassis is valid, has valid
# physnets and there are no gw_chassis present in the system.
self.gw_chassis = []
self.assertFalse(utils.is_gateway_chassis_invalid(
self.chassis_name, self.gw_chassis, self.physnet,
self.chassis_physnets))
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))

2
networking_ovn/tests/unit/fakes.py

@ -157,6 +157,8 @@ class FakeOvsdbSbOvnIdl(object):
self.get_logical_port_chassis_and_datapath = mock.Mock()
self.get_logical_port_chassis_and_datapath.return_value = \
('fake', 'fake-dp')
self.get_chassis_and_physnets = mock.Mock()
self.get_gateway_chassis_from_cms_options = mock.Mock()
class FakeOvsdbTransaction(object):

64
networking_ovn/tests/unit/l3/test_l3_ovn.py

@ -151,11 +151,11 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name(
self.fake_floating_ip['router_id'])}}
self.l3_inst = directory.get_plugin(plugin_constants.L3)
self._start_mock(
self.nb_idl = self._start_mock(
'networking_ovn.l3.l3_ovn.OVNL3RouterPlugin._ovn',
new_callable=mock.PropertyMock,
return_value=fakes.FakeOvsdbNbOvnIdl())
self._start_mock(
self.sb_idl = self._start_mock(
'networking_ovn.l3.l3_ovn.OVNL3RouterPlugin._sb_ovn',
new_callable=mock.PropertyMock,
return_value=fakes.FakeOvsdbSbOvnIdl())
@ -183,11 +183,11 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
self._start_mock(
'neutron.db.l3_db.L3_NAT_dbonly_mixin.delete_router',
return_value={})
self._start_mock(
self.mock_candidates = self._start_mock(
'networking_ovn.common.ovn_client.'
'OVNClient.get_candidates_for_scheduling',
return_value=[])
self._start_mock(
self.mock_schedule = self._start_mock(
'networking_ovn.l3.l3_ovn_scheduler.'
'OVNGatewayLeastLoadedScheduler._schedule_gateway',
return_value=['hv1'])
@ -1218,6 +1218,62 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
mock_updt_status.assert_called_once_with(
mock.ANY, fake_port_id, constants.PORT_STATUS_DOWN)
def test_schedule_unhosted_gateways_no_gateways(self):
self.nb_idl().get_unhosted_gateways.return_value = []
self.l3_inst.schedule_unhosted_gateways()
self.nb_idl().update_lrouter_port.assert_not_called()
def test_schedule_unhosted_gateways(self):
unhosted_gws = ['lrp-foo-1', 'lrp-foo-2', 'lrp-foo-3']
chassis_mappings = {
'chassis1': ['physnet1'],
'chassis2': ['physnet1'],
'chassis3': ['physnet1']}
chassis = ['chassis1', 'chassis2', 'chassis3']
self.sb_idl().get_chassis_and_physnets.return_value = (
chassis_mappings)
self.sb_idl().get_gateway_chassis_from_cms_options.return_value = (
chassis)
self.nb_idl().get_unhosted_gateways.return_value = unhosted_gws
# 1. port has 2 gateway chassis
# 2. port has only chassis2
# 3. port is not bound
existing_port_bindings = [
['chassis1', 'chassis2'],
['chassis2'],
[]]
self.nb_idl().get_gateway_chassis_binding.side_effect = (
existing_port_bindings)
# for 1. port schedule untouched, add only 3'rd chassis
# for 2. port master scheduler somewhere else
# for 3. port schedule all
self.mock_schedule.side_effect = [
['chassis1', 'chassis2', 'chassis3'],
['chassis1', 'chassis2', 'chassis3'],
['chassis3', 'chassis2', 'chassis1']]
self.l3_inst.schedule_unhosted_gateways()
self.mock_candidates.assert_has_calls([
mock.call(mock.ANY,
chassis_physnets=chassis_mappings,
cms=chassis)] * 3)
self.mock_schedule.assert_has_calls([
mock.call(self.nb_idl(), self.sb_idl(),
'lrp-foo-1', [], ['chassis1', 'chassis2']),
mock.call(self.nb_idl(), self.sb_idl(),
'lrp-foo-2', [], ['chassis2']),
mock.call(self.nb_idl(), self.sb_idl(),
'lrp-foo-3', [], [])])
# make sure that for second port master chassis stays untouched
self.nb_idl().update_lrouter_port.assert_has_calls([
mock.call('lrp-foo-1',
gateway_chassis=['chassis1', 'chassis2', 'chassis3']),
mock.call('lrp-foo-2',
gateway_chassis=['chassis2', 'chassis1', 'chassis3']),
mock.call('lrp-foo-3',
gateway_chassis=['chassis3', 'chassis2', 'chassis1'])])
class OVNL3ExtrarouteTests(test_l3_gw.ExtGwModeIntTestCase,
test_l3.L3NatDBIntTestCase,

81
networking_ovn/tests/unit/l3/test_l3_ovn_scheduler.py

@ -50,10 +50,11 @@ class TestOVNGatewayScheduler(base.BaseTestCase):
'None': {'Chassis': [],
'Gateways': {
'g1': [ovn_const.OVN_GATEWAY_INVALID_CHASSIS]}},
'Multiple1': {'Chassis': ['hv1', 'hv2'],
'Gateways': {'g1': ['hv1'],
'g2': ['hv2'],
'g3': ['hv1']}},
'Multiple1': {'Chassis': ['hv1', 'hv2', 'hv3', 'hv4', 'hv5'],
'Gateways': {'g1': ['hv1', 'hv2', 'hv3', 'hv4'],
'g2': ['hv1', 'hv2', 'hv3'],
'g3': ['hv1', 'hv2'],
'g4': ['hv1']}},
'Multiple2': {'Chassis': ['hv1', 'hv2', 'hv3'],
'Gateways': {'g1': ['hv1'],
'g2': ['hv1'],
@ -61,7 +62,14 @@ class TestOVNGatewayScheduler(base.BaseTestCase):
'Multiple3': {'Chassis': ['hv1', 'hv2', 'hv3'],
'Gateways': {'g1': ['hv3'],
'g2': ['hv2'],
'g3': ['hv2']}}
'g3': ['hv2']}},
'Multiple4': {'Chassis': ['hv1', 'hv2'],
'Gateways': {'g1': ['hv1'],
'g2': ['hv1'],
'g3': ['hv1'],
'g4': ['hv1'],
'g5': ['hv1'],
'g6': ['hv1']}},
}
# Determine the chassis to gateway list bindings
@ -79,7 +87,15 @@ class TestOVNGatewayScheduler(base.BaseTestCase):
nb_idl = FakeOVNGatewaySchedulerNbOvnIdl(chassis_gateway_mapping,
gateway_name)
sb_idl = FakeOVNGatewaySchedulerSbOvnIdl(chassis_gateway_mapping)
return self.l3_scheduler.select(nb_idl, sb_idl, gateway_name)
return self.l3_scheduler.select(nb_idl, sb_idl,
gateway_name)
def filter_existing_chassis(self, *args, **kwargs):
return self.l3_scheduler.filter_existing_chassis(
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'))
class OVNGatewayChanceScheduler(TestOVNGatewayScheduler):
@ -106,11 +122,43 @@ class OVNGatewayChanceScheduler(TestOVNGatewayScheduler):
chassis = self.select(mapping, gateway_name)
self.assertItemsEqual(chassis, mapping.get('Chassis'))
def test_existing_chassis_available_for_existing_gateway(self):
mapping = self.fake_chassis_gateway_mappings['Multiple1']
gateway_name = random.choice(list(mapping['Gateways'].keys()))
chassis = self.select(mapping, gateway_name)
self.assertEqual(mapping['Gateways'][gateway_name], chassis)
def test_filter_existing_chassis(self):
# filter_existing_chassis is scheduler independent, but calling
# it from Base class didnt seem right. Also, there is no need to have
# another test in LeastLoadedScheduler.
chassis_physnets = {'temp': ['phys-network-0', 'phys-network-1']}
nb_idl = FakeOVNGatewaySchedulerNbOvnIdl(
self.fake_chassis_gateway_mappings['None'], 'g1')
# Check if invalid chassis is removed
self.assertEqual(
['temp'], self.filter_existing_chassis(
nb_idl=nb_idl, gw_chassis=["temp"],
physnet='phys-network-1',
chassis_physnets=chassis_physnets,
existing_chassis=['temp',
ovn_const.OVN_GATEWAY_INVALID_CHASSIS]))
# Check if invalid is removed -II
self.assertFalse(
self.filter_existing_chassis(
nb_idl=nb_idl, gw_chassis=["temp"],
physnet='phys-network-1',
chassis_physnets=chassis_physnets,
existing_chassis=[ovn_const.OVN_GATEWAY_INVALID_CHASSIS]))
# Check if chassis removed when physnet doesnt exist
self.assertFalse(
self.filter_existing_chassis(
nb_idl=nb_idl, gw_chassis=["temp"],
physnet='phys-network-2',
chassis_physnets=chassis_physnets,
existing_chassis=['temp']))
# Check if chassis removed when it doesnt exist in gw_chassis
# or in chassis_physnets
self.assertFalse(
self.filter_existing_chassis(
nb_idl=nb_idl, gw_chassis=["temp1"],
physnet='phys-network-2',
chassis_physnets=chassis_physnets,
existing_chassis=['temp']))
class OVNGatewayLeastLoadedScheduler(TestOVNGatewayScheduler):
@ -138,7 +186,7 @@ class OVNGatewayLeastLoadedScheduler(TestOVNGatewayScheduler):
self.assertItemsEqual(chassis, mapping.get('Chassis'))
# least loaded will be the first one in the list,
# networking-ovn will assign highest priority to this first element
self.assertEqual(['hv2', 'hv1'], chassis)
self.assertEqual(['hv5', 'hv4', 'hv3', 'hv2', 'hv1'], chassis)
def test_least_loaded_chassis_available_for_new_gateway2(self):
mapping = self.fake_chassis_gateway_mappings['Multiple2']
@ -154,11 +202,18 @@ class OVNGatewayLeastLoadedScheduler(TestOVNGatewayScheduler):
# least loaded chassis will be in the front of the list
self.assertEqual(['hv1', 'hv3', 'hv2'], chassis)
def test_least_loaded_chassis_with_rebalance(self):
mapping = self.fake_chassis_gateway_mappings['Multiple4']
gateway_name = self.new_gateway_name
chassis = self.select(mapping, gateway_name)
# least loaded chassis will be in the front of the list
self.assertEqual(['hv2', 'hv1'], chassis)
def test_existing_chassis_available_for_existing_gateway(self):
mapping = self.fake_chassis_gateway_mappings['Multiple1']
gateway_name = random.choice(list(mapping['Gateways'].keys()))
chassis = self.select(mapping, gateway_name)
self.assertEqual(mapping['Gateways'][gateway_name], chassis)
self.assertEqual(ovn_const.MAX_GW_CHASSIS, len(chassis))
def test__get_chassis_load_by_prios_several_ports(self):
# Adding 5 ports of prio 1 and 5 ports of prio 2

33
networking_ovn/tests/unit/ovsdb/test_impl_idl_ovn.py

@ -11,7 +11,7 @@
# License for the specific language governing permissions and limitations
# under the License.
#
import collections
import copy
import mock
@ -582,19 +582,12 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn):
# Test only host-1 in the valid list
unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways(
{}, {'host-1': 'physnet1'}, [])
expected = {
utils.ovn_lrouter_port_name('orp-id-b2'): {
ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-2'},
utils.ovn_lrouter_port_name('orp-id-a3'): {
ovn_const.OVN_GATEWAY_CHASSIS_KEY:
ovn_const.OVN_GATEWAY_INVALID_CHASSIS}}
expected = ['lrp-orp-id-a1', 'lrp-orp-id-a2',
'lrp-orp-id-a3', 'lrp-orp-id-b2']
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'}, [])
expected = {utils.ovn_lrouter_port_name('orp-id-a3'): {
ovn_const.OVN_GATEWAY_CHASSIS_KEY:
ovn_const.OVN_GATEWAY_INVALID_CHASSIS}}
self.assertItemsEqual(unhosted_gateways, expected)
# Schedule unhosted_gateways on host-2
for unhosted_gateway in unhosted_gateways:
@ -604,7 +597,25 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn):
ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-2'})
unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways(
{}, {'host-1': 'physnet1', 'host-2': 'physnet2'}, [])
self.assertItemsEqual(unhosted_gateways, {})
self.assertItemsEqual(unhosted_gateways, expected)
def test_unhosted_gateway_max_chassis(self):
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'] = []
self._load_nb_db()
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'}, [])
expected = []
self.assertItemsEqual(unhosted_gateways, expected)
def test_get_subnet_dhcp_options(self):
self._load_nb_db()

Loading…
Cancel
Save