Browse Source

Merge "Support for Router Scheduling on addition/removal of chassis" into stable/rocky

changes/77/698877/2
Zuul 2 months ago
parent
commit
730a04afa7
13 changed files with 411 additions and 55 deletions
  1. +13
    -0
      doc/source/admin/routing.rst
  2. +3
    -0
      networking_ovn/common/constants.py
  3. +26
    -0
      networking_ovn/common/utils.py
  4. +26
    -1
      networking_ovn/l3/l3_ovn.py
  5. +38
    -16
      networking_ovn/l3/l3_ovn_scheduler.py
  6. +6
    -8
      networking_ovn/ovsdb/impl_idl_ovn.py
  7. +3
    -2
      networking_ovn/tests/functional/base.py
  8. +82
    -0
      networking_ovn/tests/functional/test_router.py
  9. +62
    -0
      networking_ovn/tests/unit/common/test_utils.py
  10. +2
    -0
      networking_ovn/tests/unit/fakes.py
  11. +60
    -4
      networking_ovn/tests/unit/l3/test_l3_ovn.py
  12. +68
    -13
      networking_ovn/tests/unit/l3/test_l3_ovn_scheduler.py
  13. +22
    -11
      networking_ovn/tests/unit/ovsdb/test_impl_idl_ovn.py

+ 13
- 0
doc/source/admin/routing.rst View File

@@ -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
- 0
networking_ovn/common/constants.py View File

@@ -144,3 +144,6 @@ MAINTENANCE_DELETE_TYPE_ORDER = {
# The addresses field to set in the logical switch port which has a
# peer router port (connecting to the logical router).
DEFAULT_ADDR_FOR_LSP_WITH_PEER = 'router'

# Maximum chassis count where a gateway port can be hosted
MAX_GW_CHASSIS = 5

+ 26
- 0
networking_ovn/common/utils.py View File

@@ -377,3 +377,29 @@ def get_method_class(method):
if not inspect.ismethod(method):
return
return method.__self__.__class__


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

+ 26
- 1
networking_ovn/l3/l3_ovn.py View File

@@ -321,10 +321,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:

+ 38
- 16
networking_ovn/l3/l3_ovn_scheduler.py View File

@@ -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):
@@ -46,23 +45,42 @@ class OVNGatewayScheduler(object):
"""
pass

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)
@@ -84,8 +102,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)
@@ -96,8 +116,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):

+ 6
- 8
networking_ovn/ovsdb/impl_idl_ovn.py View File

@@ -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


+ 3
- 2
networking_ovn/tests/functional/base.py View File

@@ -310,5 +310,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
- 0
networking_ovn/tests/functional/test_router.py View File

@@ -407,3 +407,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
- 0
networking_ovn/tests/unit/common/test_utils.py View File

@@ -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
- 0
networking_ovn/tests/unit/fakes.py View File

@@ -156,6 +156,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):

+ 60
- 4
networking_ovn/tests/unit/l3/test_l3_ovn.py View File

@@ -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'])
@@ -1134,6 +1134,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,

+ 68
- 13
networking_ovn/tests/unit/l3/test_l3_ovn_scheduler.py View File

@@ -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

+ 22
- 11
networking_ovn/tests/unit/ovsdb/test_impl_idl_ovn.py View File

@@ -11,7 +11,7 @@
# License for the specific language governing permissions and limitations
# under the License.
#
import collections
import copy
import uuid

@@ -583,19 +583,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:
@@ -605,7 +598,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