232 lines
9.4 KiB
Python
232 lines
9.4 KiB
Python
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
#
|
|
|
|
import abc
|
|
import copy
|
|
import secrets
|
|
|
|
from oslo_log import log
|
|
|
|
from neutron.common.ovn import constants as ovn_const
|
|
from neutron.common.ovn import utils
|
|
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
|
|
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
OVN_SCHEDULER_CHANCE = 'chance'
|
|
OVN_SCHEDULER_LEAST_LOADED = 'leastloaded'
|
|
|
|
|
|
class OVNGatewayScheduler(object, metaclass=abc.ABCMeta):
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def select(self, nb_idl, sb_idl, gateway_name, candidates=None,
|
|
existing_chassis=None, target_lrouter=None):
|
|
"""Schedule the gateway port of a router to an OVN chassis.
|
|
|
|
Schedule the gateway router port only if it is not already
|
|
scheduled.
|
|
"""
|
|
|
|
def filter_existing_chassis(self, nb_idl, gw_chassis,
|
|
physnet, chassis_physnets,
|
|
existing_chassis, az_hints, chassis_with_azs):
|
|
chassis_list = copy.copy(existing_chassis)
|
|
for chassis_name in existing_chassis or []:
|
|
if utils.is_gateway_chassis_invalid(chassis_name, gw_chassis,
|
|
physnet, chassis_physnets,
|
|
az_hints, chassis_with_azs):
|
|
LOG.debug("Chassis %(chassis)s is invalid for scheduling "
|
|
"router in physnet: %(physnet)s.",
|
|
{'chassis': chassis_name,
|
|
'physnet': physnet})
|
|
chassis_list.remove(chassis_name)
|
|
return chassis_list
|
|
|
|
def _schedule_gateway(self, nb_idl, sb_idl, gateway_name, candidates,
|
|
existing_chassis, target_lrouter):
|
|
existing_chassis = existing_chassis or []
|
|
candidates = candidates or []
|
|
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:
|
|
LOG.warning('Gateway %s was not scheduled on any chassis, no '
|
|
'candidates are available', gateway_name)
|
|
return
|
|
chassis_count = min(
|
|
ovn_const.MAX_GW_CHASSIS - len(existing_chassis),
|
|
len(candidates)
|
|
)
|
|
# 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, sb_idl, candidates, 1, chassis_count, target_lrouter
|
|
)[: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)
|
|
return chassis
|
|
|
|
def _reorder_by_az(self, nb_idl, sb_idl, candidates):
|
|
chassis_selected = []
|
|
other_chassis = []
|
|
azs = set()
|
|
|
|
# Check if candidates list valid
|
|
if not candidates:
|
|
return candidates
|
|
|
|
chassis_with_azs = sb_idl.get_chassis_and_azs()
|
|
|
|
# Get list of all AZs
|
|
for chassis in candidates:
|
|
try:
|
|
azs.update(chassis_with_azs[chassis])
|
|
except KeyError:
|
|
continue
|
|
|
|
for chassis in candidates:
|
|
# Verify if chassis is in an AZ not already used
|
|
# and delete AZs of chassis from list
|
|
try:
|
|
chassis_azs = chassis_with_azs[chassis]
|
|
if azs.intersection(chassis_azs):
|
|
azs = azs.difference(chassis_azs)
|
|
chassis_selected += [chassis]
|
|
else:
|
|
other_chassis += [chassis]
|
|
except KeyError:
|
|
other_chassis += [chassis]
|
|
|
|
chassis_selected += other_chassis
|
|
|
|
return chassis_selected
|
|
|
|
@abc.abstractmethod
|
|
def _select_gateway_chassis(self, nb_idl, sb_idl, candidates,
|
|
priority_min, priority_max, target_lrouter):
|
|
"""Choose a chassis from candidates based on a specific policy.
|
|
|
|
Returns a list of chassis to use for scheduling. The value at
|
|
``ret[0]`` will be used for the chassis with ``priority_max``, the
|
|
value at ``ret[-1]`` will be used for the chassis with ``priority_min``
|
|
"""
|
|
|
|
|
|
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,
|
|
existing_chassis=None, target_lrouter=None):
|
|
return self._schedule_gateway(nb_idl, sb_idl, gateway_name,
|
|
candidates, existing_chassis, target_lrouter)
|
|
|
|
def _select_gateway_chassis(self, nb_idl, sb_idl, candidates,
|
|
priority_min, priority_max, target_lrouter):
|
|
candidates = copy.deepcopy(candidates)
|
|
secrets.SystemRandom().shuffle(candidates)
|
|
return self._reorder_by_az(nb_idl, sb_idl, candidates)
|
|
|
|
|
|
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,
|
|
existing_chassis=None, target_lrouter=None):
|
|
return self._schedule_gateway(nb_idl, sb_idl, gateway_name,
|
|
candidates, existing_chassis,
|
|
target_lrouter)
|
|
|
|
def _lrp_anti_affinity_score(self, nb_idl, target_lrouter, lrp_name):
|
|
"""Provide elevated score if LRP is part of LR with multiple LRPs.
|
|
|
|
The artificial score can be added to chassis load to make it less
|
|
likely that the chassis is selected for scheduling.
|
|
|
|
:param nb_idl: IDL for the OVN NB API
|
|
:type nb_idl: :class:`OvsdbNbOvnIdl`
|
|
:param target_lrouter: Logical Router object
|
|
:type target_lrouter: :class:`RowView`
|
|
:param lrp_name: Name of LRP.
|
|
:type lrp_name: str
|
|
:returns: Number, if lrp_name belongs to LR with multiple
|
|
LRPs.
|
|
:rtype: int
|
|
"""
|
|
if target_lrouter:
|
|
lrouter_ports = getattr(target_lrouter, 'ports', set())
|
|
if len(lrouter_ports) and nb_idl.get_lrouter_by_lrouter_port(
|
|
lrp_name) == target_lrouter:
|
|
# The `MAX_GW_CHASSIS` constant here is used mostly to get a
|
|
# multiplier that guarantees our score will outweigh natural
|
|
# LRP priority so that when other chassis are available those
|
|
# will be chosen rather than a chassis already hosting a LRP
|
|
# for this LR.
|
|
return ovn_const.MAX_GW_CHASSIS * len(target_lrouter.ports)
|
|
return 0
|
|
|
|
def _select_gateway_chassis(self, nb_idl, sb_idl, candidates,
|
|
priority_min, priority_max, target_lrouter):
|
|
"""Returns a lit of chassis from candidates ordered by priority
|
|
(highest first). Each chassis in every priority will be selected, as it
|
|
is the least loaded for that specific priority.
|
|
"""
|
|
selected_chassis = []
|
|
priorities = list(range(priority_max, priority_min - 1, -1))
|
|
all_chassis_bindings = nb_idl.get_all_chassis_gateway_bindings(
|
|
candidates, priorities=priorities)
|
|
for priority in priorities:
|
|
chassis_load = {}
|
|
for chassis, lrps in all_chassis_bindings.items():
|
|
if chassis in selected_chassis:
|
|
continue
|
|
lrps_with_prio = 0
|
|
anti_affinity_score = 0
|
|
for lrp, prio in lrps:
|
|
if prio == priority:
|
|
lrps_with_prio += 1
|
|
anti_affinity_score += self._lrp_anti_affinity_score(
|
|
nb_idl, target_lrouter, lrp)
|
|
chassis_load[chassis] = lrps_with_prio + anti_affinity_score
|
|
if len(chassis_load) == 0:
|
|
break
|
|
leastload = min(chassis_load.values())
|
|
chassis = secrets.SystemRandom().choice(
|
|
[chassis for chassis, load in chassis_load.items()
|
|
if load == leastload])
|
|
selected_chassis.append(chassis)
|
|
return self._reorder_by_az(nb_idl, sb_idl, selected_chassis)
|
|
|
|
|
|
OVN_SCHEDULER_STR_TO_CLASS = {
|
|
OVN_SCHEDULER_CHANCE: OVNGatewayChanceScheduler,
|
|
OVN_SCHEDULER_LEAST_LOADED: OVNGatewayLeastLoadedScheduler}
|
|
|
|
|
|
def get_scheduler():
|
|
return OVN_SCHEDULER_STR_TO_CLASS[ovn_conf.get_ovn_l3_scheduler()]()
|