neutron/neutron/scheduler/dhcp_agent_scheduler.py

335 lines
16 KiB
Python

# Copyright (c) 2013 OpenStack Foundation.
# All Rights Reserved.
#
# 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 collections
from operator import itemgetter
from neutron_lib.api.definitions import availability_zone as az_def
from neutron_lib import constants
from neutron_lib.db import api as db_api
from neutron_lib.objects import exceptions
from oslo_config import cfg
from oslo_log import log as logging
from neutron.agent.common import utils as agent_utils
from neutron.db.network_dhcp_agent_binding import models as ndab_model
from neutron.objects import agent as agent_obj
from neutron.objects import network
from neutron.scheduler import base_resource_filter
from neutron.scheduler import base_scheduler
LOG = logging.getLogger(__name__)
class AutoScheduler(object):
def auto_schedule_networks(self, plugin, context, host):
"""Schedule non-hosted networks to the DHCP agent on the specified
host.
"""
agents_per_network = cfg.CONF.dhcp_agents_per_network
# a list of (agent, net_ids) tuples
bindings_to_add = []
# NOTE(ralonsoh) use writer manager to call get_network. See
# https://review.opendev.org/#/c/483518/. Must be changed to READER.
with db_api.CONTEXT_WRITER.using(context):
fields = ['network_id', 'enable_dhcp', 'segment_id']
subnets = plugin.get_subnets(context, fields=fields)
net_ids = {}
net_segment_ids = collections.defaultdict(set)
for s in subnets:
if s['enable_dhcp']:
net_segment_ids[s['network_id']].add(s.get('segment_id'))
for network_id, segment_ids in net_segment_ids.items():
is_routed_network = any(segment_ids)
net_ids[network_id] = is_routed_network
if not net_ids:
LOG.debug('No non-hosted networks')
return False
dhcp_agents = agent_obj.Agent.get_objects(
context, agent_type=constants.AGENT_TYPE_DHCP,
host=host, admin_state_up=True)
segment_host_mapping = network.SegmentHostMapping.get_objects(
context, host=host)
segments_on_host = {s.segment_id for s in segment_host_mapping}
for dhcp_agent in dhcp_agents:
if agent_utils.is_agent_down(dhcp_agent.heartbeat_timestamp):
LOG.warning('DHCP agent %s is not active', dhcp_agent.id)
continue
for net_id, is_routed_network in net_ids.items():
agents = plugin.get_dhcp_agents_hosting_networks(
context, [net_id])
segments_on_network = net_segment_ids[net_id]
if is_routed_network:
if len(segments_on_network & segments_on_host) == 0:
continue
else:
if len(agents) >= agents_per_network:
continue
if any(dhcp_agent.id == agent.id for agent in agents):
continue
net = plugin.get_network(context, net_id)
az_hints = (net.get(az_def.AZ_HINTS) or
cfg.CONF.default_availability_zones)
if (az_hints and
dhcp_agent['availability_zone'] not in az_hints):
continue
bindings_to_add.append(
(dhcp_agent, net_id, is_routed_network))
# do it outside transaction so particular scheduling results don't
# make other to fail
debug_data = []
for agent, net_id, is_routed_network in bindings_to_add:
self.resource_filter.bind(
context, [agent], net_id,
force_scheduling=is_routed_network)
debug_data.append('(%s, %s, %s)' % (agent['agent_type'],
agent['host'], net_id))
LOG.debug('Resources bound (agent type, host, resource id): %s',
', '.join(debug_data))
return True
class ChanceScheduler(base_scheduler.BaseChanceScheduler, AutoScheduler):
def __init__(self):
super(ChanceScheduler, self).__init__(DhcpFilter())
class WeightScheduler(base_scheduler.BaseWeightScheduler, AutoScheduler):
def __init__(self):
super(WeightScheduler, self).__init__(DhcpFilter())
class AZAwareWeightScheduler(WeightScheduler):
def select(self, plugin, context, resource_hostable_agents,
resource_hosted_agents, num_agents_needed):
"""AZ aware scheduling
If the network has multiple AZs, agents are scheduled as
follows:
- select AZ with least agents scheduled for the network
- for AZs with same amount of scheduled agents, the AZ which
contains least weight agent will be used first
- choose agent in the AZ with WeightScheduler
"""
# The dict to record the agents in each AZ, the record will be sorted
# according to the weight of agent. So that the agent with less weight
# will be used first.
hostable_az_agents = collections.defaultdict(list)
# The dict to record the number of agents in each AZ. When the number
# of agents in each AZ is the same and num_agents_needed is less than
# the number of AZs, we want to select agents with less weight.
# Use an OrderedDict here, so that the AZ with least weight agent
# will be recorded first in the case described above. And, as a result,
# the agent with least weight will be used first.
num_az_agents = collections.OrderedDict()
# resource_hostable_agents should be a list with agents in the order of
# their weight.
resource_hostable_agents = (
super(AZAwareWeightScheduler, self).select(
plugin, context, resource_hostable_agents,
resource_hosted_agents, len(resource_hostable_agents)))
for agent in resource_hostable_agents:
az_agent = agent['availability_zone']
hostable_az_agents[az_agent].append(agent)
if az_agent not in num_az_agents:
num_az_agents[az_agent] = 0
if num_agents_needed <= 0:
return []
for agent in resource_hosted_agents:
az_agent = agent['availability_zone']
if az_agent in num_az_agents:
num_az_agents[az_agent] += 1
chosen_agents = []
while num_agents_needed > 0:
# 'min' will stably output the first min value in the list.
select_az = min(num_az_agents.items(), key=itemgetter(1))[0]
# Select the agent in AZ with least weight.
select_agent = hostable_az_agents[select_az][0]
chosen_agents.append(select_agent)
# Update the AZ-agents records.
del hostable_az_agents[select_az][0]
if not hostable_az_agents[select_az]:
del num_az_agents[select_az]
else:
num_az_agents[select_az] += 1
num_agents_needed -= 1
return chosen_agents
class DhcpFilter(base_resource_filter.BaseResourceFilter):
def get_vacant_network_dhcp_agent_binding_index(
self, context, network_id, force_scheduling):
"""Return a vacant binding_index to use and whether or not it exists.
Each NetworkDhcpAgentBinding has a binding_index which is unique per
network_id, and when creating a single binding we require to find a
'vacant' binding_index which isn't yet used - for example if we have
bindings with indices 1 and 3, then clearly binding_index == 2 is free.
:returns: binding_index.
"""
num_agents = agent_obj.Agent.count(
context, agent_type=constants.AGENT_TYPE_DHCP)
num_agents = min(num_agents, cfg.CONF.dhcp_agents_per_network)
bindings = network.NetworkDhcpAgentBinding.get_objects(
context, network_id=network_id)
return base_scheduler.get_vacant_binding_index(
num_agents, bindings, ndab_model.LOWEST_BINDING_INDEX,
force_scheduling=force_scheduling)
def bind(self, context, agents, network_id, force_scheduling=False):
"""Bind the network to the agents."""
# customize the bind logic
bound_agents = agents[:]
for agent in agents:
binding_index = self.get_vacant_network_dhcp_agent_binding_index(
context, network_id, force_scheduling)
if binding_index < ndab_model.LOWEST_BINDING_INDEX:
LOG.debug('Unable to find a vacant binding_index for '
'network %(network_id)s and agent %(agent_id)s',
{'network_id': network_id,
'agent_id': agent.id})
continue
# saving agent_id to use it after rollback to avoid
# DetachedInstanceError
agent_id = agent.id
try:
network.NetworkDhcpAgentBinding(
context, dhcp_agent_id=agent_id,
network_id=network_id,
binding_index=binding_index).create()
except exceptions.NeutronDbObjectDuplicateEntry:
# it's totally ok, someone just did our job!
bound_agents.remove(agent)
LOG.info('Agent %s already present', agent_id)
LOG.debug('Network %(network_id)s is scheduled to be '
'hosted by DHCP agent %(agent_id)s with binding_index '
'%(binding_index)d',
{'network_id': network_id,
'agent_id': agent_id,
'binding_index': binding_index})
super(DhcpFilter, self).bind(context, bound_agents, network_id)
def filter_agents(self, plugin, context, network):
"""Return the agents that can host the network.
This function returns a dictionary which has 3 keys.
n_agents: The number of agents should be scheduled. If n_agents=0,
all networks are already scheduled or no more agent can host the
network.
hostable_agents: A list of agents which can host the network.
hosted_agents: A list of agents which already hosts the network.
"""
agents_dict = self._get_network_hostable_dhcp_agents(
plugin, context, network)
if not agents_dict['hostable_agents'] or agents_dict['n_agents'] <= 0:
return {'n_agents': 0, 'hostable_agents': [],
'hosted_agents': agents_dict['hosted_agents']}
return agents_dict
def _filter_agents_with_network_access(self, plugin, context,
network, hostable_agents):
if 'candidate_hosts' in network:
hostable_dhcp_hosts = network['candidate_hosts']
else:
hostable_dhcp_hosts = plugin.filter_hosts_with_network_access(
context, network['id'],
[agent['host'] for agent in hostable_agents])
reachable_agents = [agent for agent in hostable_agents
if agent['host'] in hostable_dhcp_hosts]
return reachable_agents
def _get_dhcp_agents_hosting_network(self, plugin, context, network):
"""Return dhcp agents hosting the given network or None if a given
network is already hosted by enough number of agents.
"""
agents_per_network = cfg.CONF.dhcp_agents_per_network
# TODO(gongysh) don't schedule the networks with only
# subnets whose enable_dhcp is false
with db_api.CONTEXT_READER.using(context):
network_hosted_agents = plugin.get_dhcp_agents_hosting_networks(
context, [network['id']], hosts=network.get('candidate_hosts'))
if len(network_hosted_agents) >= agents_per_network:
LOG.debug('Network %s is already hosted by enough agents.',
network['id'])
return
return network_hosted_agents
def _get_active_agents(self, plugin, context, az_hints):
"""Return a list of active dhcp agents."""
with db_api.CONTEXT_READER.using(context):
filters = {'agent_type': [constants.AGENT_TYPE_DHCP],
'admin_state_up': [True]}
if az_hints:
filters['availability_zone'] = az_hints
active_dhcp_agents = plugin.get_agent_objects(
context, filters=filters)
if not active_dhcp_agents:
LOG.warning('No more DHCP agents')
return []
return active_dhcp_agents
def _get_network_hostable_dhcp_agents(self, plugin, context, network):
"""Provide information on hostable DHCP agents for network.
The returned value includes the number of agents that will actually
host the given network, a list of DHCP agents that can host the given
network, and a list of DHCP agents currently hosting the network.
"""
hosted_agents = self._get_dhcp_agents_hosting_network(plugin,
context, network)
if hosted_agents is None:
return {'n_agents': 0, 'hostable_agents': [], 'hosted_agents': []}
n_agents = cfg.CONF.dhcp_agents_per_network - len(hosted_agents)
az_hints = (network.get(az_def.AZ_HINTS) or
cfg.CONF.default_availability_zones)
active_dhcp_agents = self._get_active_agents(plugin, context, az_hints)
hosted_agent_ids = [agent['id'] for agent in hosted_agents]
if not active_dhcp_agents:
return {'n_agents': 0, 'hostable_agents': [],
'hosted_agents': hosted_agents}
hostable_dhcp_agents = [
agent for agent in active_dhcp_agents
if agent.id not in hosted_agent_ids and plugin.is_eligible_agent(
context, True, agent)]
hostable_dhcp_agents = self._filter_agents_with_network_access(
plugin, context, network, hostable_dhcp_agents)
if not hostable_dhcp_agents:
result = {'n_agents': 0, 'hostable_agents': [],
'hosted_agents': hosted_agents}
else:
result = {'n_agents': min(len(hostable_dhcp_agents), n_agents),
'hostable_agents': hostable_dhcp_agents,
'hosted_agents': hosted_agents}
hostable_agents_ids = [a['id'] for a in result['hostable_agents']]
hosted_agents_ids = [a['id'] for a in result['hosted_agents']]
LOG.debug('Network hostable DHCP agents. Network: %(network)s, '
'hostable agents: %(hostable_agents)s, hosted agents: '
'%(hosted_agents)s', {'network': network['id'],
'hostable_agents': hostable_agents_ids,
'hosted_agents': hosted_agents_ids})
return result