# Allow auto scheduling routers to L3 agent. It will schedule non-hosted
# routers to first L3 agent which sends sync_routers message to quantum server
# router_auto_schedule = True
+
+# Number of DHCP agents scheduled to host a network. This enables redundant
+# DHCP agents for configured networks.
+# dhcp_agents_per_network = 1
+
# =========== end of items for agent scheduler extension =====
# =========== WSGI parameters related to the API server ==============
adminContext = (context if context.is_admin else
context.elevated())
network = plugin.get_network(adminContext, network_id)
- chosen_agent = plugin.schedule_network(adminContext, network)
- if chosen_agent:
- self._notification_host(
- context, 'network_create_end',
- {'network': {'id': network_id}},
- chosen_agent['host'])
+ chosen_agents = plugin.schedule_network(adminContext, network)
+ if chosen_agents:
+ for agent in chosen_agents:
+ self._notification_host(
+ context, 'network_create_end',
+ {'network': {'id': network_id}},
+ agent['host'])
for (host, topic) in self._get_dhcp_agents(context, network_id):
self.cast(
context, self.make_msg(method,
pass
+def validate_post_plugin_load():
+ """Checks if the configuration variables are valid.
+
+ If the configuration is invalid then the method will return an error
+ message. If all is OK then it will return None.
+ """
+ if ('dhcp_agents_per_network' in cfg.CONF and
+ cfg.CONF.dhcp_agents_per_network <= 0):
+ msg = _("dhcp_agents_per_network must be >= 1. '%s' "
+ "is invalid.") % cfg.CONF.dhcp_agents_per_network
+ return msg
+
+
+def validate_pre_plugin_load():
+ """Checks if the configuration variables are valid.
+
+ If the configuration is invalid then the method will return an error
+ message. If all is OK then it will return None.
+ """
+ if cfg.CONF.core_plugin is None:
+ msg = _('Quantum core_plugin not configured!')
+ return msg
+
+
class QuantumManager(object):
"""Quantum's Manager class.
if not options:
options = {}
- if cfg.CONF.core_plugin is None:
- msg = _('Quantum core_plugin not configured!')
+ msg = validate_pre_plugin_load()
+ if msg:
LOG.critical(msg)
raise Exception(msg)
"Example: pip install quantum-sample-plugin"))
self.plugin = plugin_klass()
+ msg = validate_post_plugin_load()
+ if msg:
+ LOG.critical(msg)
+ raise Exception(msg)
+
# core plugin as a part of plugin collection simplifies
# checking extensions
# TODO(enikanorov): make core plugin the same as
help=_('Allow auto scheduling networks to DHCP agent.')),
cfg.BoolOpt('router_auto_schedule', default=True,
help=_('Allow auto scheduling routers to L3 agent.')),
+ cfg.IntOpt('dhcp_agents_per_network', default=1,
+ help=_('Number of DHCP agents scheduled to host a network.')),
]
import random
-from sqlalchemy.orm import exc
-from sqlalchemy.sql import exists
+from oslo.config import cfg
from quantum.common import constants
from quantum.db import agents_db
from quantum.db import agentschedulers_db
-from quantum.db import models_v2
from quantum.openstack.common import log as logging
can be introduced later.
"""
+ def _schedule_bind_network(self, context, agent, network_id):
+ binding = agentschedulers_db.NetworkDhcpAgentBinding()
+ binding.dhcp_agent = agent
+ binding.network_id = network_id
+ context.session.add(binding)
+ LOG.debug(_('Network %(network_id)s is scheduled to be hosted by '
+ 'DHCP agent %(agent_id)s'),
+ {'network_id': network_id,
+ 'agent_id': agent})
+
def schedule(self, plugin, context, network):
- """Schedule the network to an active DHCP agent if there
- is no active DHCP agent hosting it.
+ """Schedule the network to active DHCP agent(s).
+
+ A list of scheduled agents is returned.
"""
+ 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 context.session.begin(subtransactions=True):
dhcp_agents = plugin.get_dhcp_agents_hosting_networks(
context, [network['id']], active=True)
- if dhcp_agents:
+ if len(dhcp_agents) >= agents_per_network:
LOG.debug(_('Network %s is hosted already'),
network['id'])
return
+ n_agents = agents_per_network - len(dhcp_agents)
enabled_dhcp_agents = plugin.get_agents_db(
context, filters={
'agent_type': [constants.AGENT_TYPE_DHCP],
'admin_state_up': [True]})
if not enabled_dhcp_agents:
- LOG.warn(_('No enabled DHCP agents'))
+ LOG.warn(_('No more DHCP agents'))
return
- active_dhcp_agents = [enabled_dhcp_agent for enabled_dhcp_agent in
- enabled_dhcp_agents if not
- agents_db.AgentDbMixin.is_agent_down(
- enabled_dhcp_agent['heartbeat_timestamp'])]
+ active_dhcp_agents = [
+ agent for agent in set(enabled_dhcp_agents)
+ if not agents_db.AgentDbMixin.is_agent_down(
+ agent['heartbeat_timestamp'])
+ and agent not in dhcp_agents
+ ]
if not active_dhcp_agents:
- LOG.warn(_('No active DHCP agents'))
+ LOG.warn(_('No more DHCP agents'))
return
- chosen_agent = random.choice(active_dhcp_agents)
- binding = agentschedulers_db.NetworkDhcpAgentBinding()
- binding.dhcp_agent = chosen_agent
- binding.network_id = network['id']
- context.session.add(binding)
- LOG.debug(_('Network %(network_id)s is scheduled to be hosted by '
- 'DHCP agent %(agent_id)s'),
- {'network_id': network['id'],
- 'agent_id': chosen_agent['id']})
- return chosen_agent
+ n_agents = min(len(active_dhcp_agents), n_agents)
+ chosen_agents = random.sample(active_dhcp_agents, n_agents)
+ for agent in chosen_agents:
+ self._schedule_bind_network(context, agent, network['id'])
+ return chosen_agents
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
with context.session.begin(subtransactions=True):
query = context.session.query(agents_db.Agent)
query = query.filter(agents_db.Agent.agent_type ==
constants.AGENT_TYPE_DHCP,
agents_db.Agent.host == host,
agents_db.Agent.admin_state_up == True)
- try:
- dhcp_agent = query.one()
- except (exc.MultipleResultsFound, exc.NoResultFound):
- LOG.warn(_('No enabled DHCP agent on host %s'),
- host)
- return False
- if agents_db.AgentDbMixin.is_agent_down(
- dhcp_agent.heartbeat_timestamp):
- LOG.warn(_('DHCP agent %s is not active'), dhcp_agent.id)
- #TODO(gongysh) consider the disabled agent's network
- net_stmt = ~exists().where(
- models_v2.Network.id ==
- agentschedulers_db.NetworkDhcpAgentBinding.network_id)
- net_ids = context.session.query(
- models_v2.Network.id).filter(net_stmt).all()
- if not net_ids:
- LOG.debug(_('No non-hosted networks'))
- return False
- for net_id in net_ids:
- binding = agentschedulers_db.NetworkDhcpAgentBinding()
- binding.dhcp_agent = dhcp_agent
- binding.network_id = net_id[0]
- context.session.add(binding)
+ dhcp_agents = query.all()
+ for dhcp_agent in dhcp_agents:
+ if agents_db.AgentDbMixin.is_agent_down(
+ dhcp_agent.heartbeat_timestamp):
+ LOG.warn(_('DHCP agent %s is not active'), dhcp_agent.id)
+ continue
+ #TODO(gongysh) consider the disabled agent's network
+ fields = ['network_id', 'enable_dhcp']
+ subnets = plugin.get_subnets(context, fields=fields)
+ net_ids = set(s['network_id'] for s in subnets
+ if s['enable_dhcp'])
+ if not net_ids:
+ LOG.debug(_('No non-hosted networks'))
+ return False
+ for net_id in net_ids:
+ agents = plugin.get_dhcp_agents_hosting_networks(
+ context, [net_id], active=True)
+ if len(agents) >= agents_per_network:
+ continue
+ binding = agentschedulers_db.NetworkDhcpAgentBinding()
+ binding.dhcp_agent = dhcp_agent
+ binding.network_id = net_id
+ context.session.add(binding)
return True
import copy
import mock
+from oslo.config import cfg
from webob import exc
from quantum.api import extensions
self.assertEqual(0, len(dhcp_agents['agents']))
def test_network_auto_schedule_with_disabled(self):
- with contextlib.nested(self.network(),
- self.network()):
+ cfg.CONF.set_override('allow_overlapping_ips', True)
+ with contextlib.nested(self.subnet(),
+ self.subnet()):
dhcp_rpc = dhcp_rpc_base.DhcpRpcCallbackMixin()
self._register_agent_states()
hosta_id = self._get_agent_id(constants.AGENT_TYPE_DHCP,
self.assertEqual(0, num_hosta_nets)
self.assertEqual(2, num_hostc_nets)
+ def test_network_auto_schedule_with_no_dhcp(self):
+ cfg.CONF.set_override('allow_overlapping_ips', True)
+ with contextlib.nested(self.subnet(enable_dhcp=False),
+ self.subnet(enable_dhcp=False)):
+ dhcp_rpc = dhcp_rpc_base.DhcpRpcCallbackMixin()
+ self._register_agent_states()
+ hosta_id = self._get_agent_id(constants.AGENT_TYPE_DHCP,
+ DHCP_HOSTA)
+ hostc_id = self._get_agent_id(constants.AGENT_TYPE_DHCP,
+ DHCP_HOSTC)
+ self._disable_agent(hosta_id)
+ dhcp_rpc.get_active_networks(self.adminContext, host=DHCP_HOSTA)
+ dhcp_rpc.get_active_networks(self.adminContext, host=DHCP_HOSTC)
+ networks = self._list_networks_hosted_by_dhcp_agent(hostc_id)
+ num_hostc_nets = len(networks['networks'])
+ networks = self._list_networks_hosted_by_dhcp_agent(hosta_id)
+ num_hosta_nets = len(networks['networks'])
+ self.assertEqual(0, num_hosta_nets)
+ self.assertEqual(0, num_hostc_nets)
+
+ def test_network_auto_schedule_with_multiple_agents(self):
+ cfg.CONF.set_override('dhcp_agents_per_network', 2)
+ cfg.CONF.set_override('allow_overlapping_ips', True)
+ with contextlib.nested(self.subnet(),
+ self.subnet()):
+ dhcp_rpc = dhcp_rpc_base.DhcpRpcCallbackMixin()
+ self._register_agent_states()
+ hosta_id = self._get_agent_id(constants.AGENT_TYPE_DHCP,
+ DHCP_HOSTA)
+ hostc_id = self._get_agent_id(constants.AGENT_TYPE_DHCP,
+ DHCP_HOSTC)
+ dhcp_rpc.get_active_networks(self.adminContext, host=DHCP_HOSTA)
+ dhcp_rpc.get_active_networks(self.adminContext, host=DHCP_HOSTC)
+ networks = self._list_networks_hosted_by_dhcp_agent(hostc_id)
+ num_hostc_nets = len(networks['networks'])
+ networks = self._list_networks_hosted_by_dhcp_agent(hosta_id)
+ num_hosta_nets = len(networks['networks'])
+ self.assertEqual(2, num_hosta_nets)
+ self.assertEqual(2, num_hostc_nets)
+
def test_network_auto_schedule_with_hosted(self):
# one agent hosts all the networks, other hosts none
- with contextlib.nested(self.network(),
- self.network()) as (net1, net2):
+ cfg.CONF.set_override('allow_overlapping_ips', True)
+ with contextlib.nested(self.subnet(),
+ self.subnet()) as (sub1, sub2):
dhcp_rpc = dhcp_rpc_base.DhcpRpcCallbackMixin()
self._register_agent_states()
dhcp_rpc.get_active_networks(self.adminContext, host=DHCP_HOSTA)
# second agent will not host the network since first has got it.
dhcp_rpc.get_active_networks(self.adminContext, host=DHCP_HOSTC)
dhcp_agents = self._list_dhcp_agents_hosting_network(
- net1['network']['id'])
+ sub1['subnet']['network_id'])
hosta_id = self._get_agent_id(constants.AGENT_TYPE_DHCP,
DHCP_HOSTA)
hostc_id = self._get_agent_id(constants.AGENT_TYPE_DHCP,
'agent_type': constants.AGENT_TYPE_DHCP}
dhcp_hostc = copy.deepcopy(dhcp_hosta)
dhcp_hostc['host'] = DHCP_HOSTC
- with self.network() as net1:
+ cfg.CONF.set_override('allow_overlapping_ips', True)
+ with self.subnet() as sub1:
self._register_one_agent_state(dhcp_hosta)
dhcp_rpc.get_active_networks(self.adminContext, host=DHCP_HOSTA)
hosta_id = self._get_agent_id(constants.AGENT_TYPE_DHCP,
DHCP_HOSTA)
self._disable_agent(hosta_id, admin_state_up=False)
- with self.network() as net2:
+ with self.subnet() as sub2:
self._register_one_agent_state(dhcp_hostc)
dhcp_rpc.get_active_networks(self.adminContext,
host=DHCP_HOSTC)
dhcp_agents_1 = self._list_dhcp_agents_hosting_network(
- net1['network']['id'])
+ sub1['subnet']['network_id'])
dhcp_agents_2 = self._list_dhcp_agents_hosting_network(
- net2['network']['id'])
+ sub2['subnet']['network_id'])
hosta_nets = self._list_networks_hosted_by_dhcp_agent(hosta_id)
num_hosta_nets = len(hosta_nets['networks'])
hostc_id = self._get_agent_id(
self.assertEqual(0, result0)
self.assertEqual(1, result1)
+ def test_network_ha_scheduling_on_port_creation(self):
+ cfg.CONF.set_override('dhcp_agents_per_network', 2)
+ with self.subnet() as subnet:
+ dhcp_agents = self._list_dhcp_agents_hosting_network(
+ subnet['subnet']['network_id'])
+ result0 = len(dhcp_agents['agents'])
+ self._register_agent_states()
+ with self.port(subnet=subnet,
+ device_owner="compute:test:" + DHCP_HOSTA) as port:
+ dhcp_agents = self._list_dhcp_agents_hosting_network(
+ port['port']['network_id'])
+ result1 = len(dhcp_agents['agents'])
+ self.assertEqual(0, result0)
+ self.assertEqual(2, result1)
+
+ def test_network_ha_scheduling_on_port_creation_with_new_agent(self):
+ cfg.CONF.set_override('dhcp_agents_per_network', 3)
+ with self.subnet() as subnet:
+ dhcp_agents = self._list_dhcp_agents_hosting_network(
+ subnet['subnet']['network_id'])
+ result0 = len(dhcp_agents['agents'])
+ self._register_agent_states()
+ with self.port(subnet=subnet,
+ device_owner="compute:test:" + DHCP_HOSTA) as port:
+ dhcp_agents = self._list_dhcp_agents_hosting_network(
+ port['port']['network_id'])
+ result1 = len(dhcp_agents['agents'])
+ self._register_one_dhcp_agent()
+ with self.port(subnet=subnet,
+ device_owner="compute:test:" + DHCP_HOSTA) as port:
+ dhcp_agents = self._list_dhcp_agents_hosting_network(
+ port['port']['network_id'])
+ result2 = len(dhcp_agents['agents'])
+ self.assertEqual(0, result0)
+ self.assertEqual(2, result1)
+ self.assertEqual(3, result2)
+
def test_network_scheduler_with_disabled_agent(self):
dhcp_hosta = {
'binary': 'quantum-dhcp-agent',
topic='dhcp_agent.' + DHCP_HOSTA)]
self.assertEqual(mock_dhcp.call_args_list, expected_calls)
+ def test_network_ha_port_create_notification(self):
+ cfg.CONF.set_override('dhcp_agents_per_network', 2)
+ dhcp_hosta = {
+ 'binary': 'quantum-dhcp-agent',
+ 'host': DHCP_HOSTA,
+ 'topic': 'dhcp_agent',
+ 'configurations': {'dhcp_driver': 'dhcp_driver',
+ 'use_namespaces': True,
+ },
+ 'agent_type': constants.AGENT_TYPE_DHCP}
+ self._register_one_agent_state(dhcp_hosta)
+ dhcp_hostc = copy.deepcopy(dhcp_hosta)
+ dhcp_hostc['host'] = DHCP_HOSTC
+ self._register_one_agent_state(dhcp_hostc)
+ with mock.patch.object(self.dhcp_notifier, 'cast') as mock_dhcp:
+ with self.network(do_delete=False) as net1:
+ with self.subnet(network=net1,
+ do_delete=False) as subnet1:
+ with self.port(subnet=subnet1, no_delete=True) as port:
+ network_id = port['port']['network_id']
+ expected_calls_a = [
+ mock.call(
+ mock.ANY,
+ self.dhcp_notifier.make_msg(
+ 'network_create_end',
+ payload={'network': {'id': network_id}}),
+ topic='dhcp_agent.' + DHCP_HOSTA),
+ mock.call(
+ mock.ANY,
+ self.dhcp_notifier.make_msg(
+ 'port_create_end',
+ payload={'port': port['port']}),
+ topic='dhcp_agent.' + DHCP_HOSTA)]
+ expected_calls_c = [
+ mock.call(
+ mock.ANY,
+ self.dhcp_notifier.make_msg(
+ 'network_create_end',
+ payload={'network': {'id': network_id}}),
+ topic='dhcp_agent.' + DHCP_HOSTC),
+ mock.call(
+ mock.ANY,
+ self.dhcp_notifier.make_msg(
+ 'port_create_end',
+ payload={'port': port['port']}),
+ topic='dhcp_agent.' + DHCP_HOSTC)]
+ for expected in expected_calls_a:
+ self.assertIn(expected, mock_dhcp.call_args_list)
+ for expected in expected_calls_c:
+ self.assertIn(expected, mock_dhcp.call_args_list)
+
class OvsL3AgentNotifierTestCase(test_l3_plugin.L3NatTestCaseMixin,
test_agent_ext_plugin.AgentDBTestMixIn,
DHCP_HOSTA = 'hosta'
L3_HOSTB = 'hostb'
DHCP_HOSTC = 'hostc'
+DHCP_HOST1 = 'host1'
class AgentTestExtensionManager(object):
time=timeutils.strtime())
return [l3_hosta, l3_hostb, dhcp_hosta, dhcp_hostc]
+ def _register_one_dhcp_agent(self):
+ """Register one DHCP agent."""
+ dhcp_host = {
+ 'binary': 'quantum-dhcp-agent',
+ 'host': DHCP_HOST1,
+ 'topic': 'DHCP_AGENT',
+ 'configurations': {'dhcp_driver': 'dhcp_driver',
+ 'use_namespaces': True,
+ },
+ 'agent_type': constants.AGENT_TYPE_DHCP}
+ callback = agents_db.AgentExtRpcCallback()
+ callback.report_state(self.adminContext,
+ agent_state={'agent_state': dhcp_host},
+ time=timeutils.strtime())
+ return [dhcp_host]
+
class AgentDBTestCase(AgentDBTestMixIn,
test_db_plugin.QuantumDbPluginV2TestCase):
from quantum.common import config
from quantum.common.test_lib import test_config
from quantum.manager import QuantumManager
+from quantum.manager import validate_post_plugin_load
+from quantum.manager import validate_pre_plugin_load
from quantum.openstack.common import log as logging
from quantum.plugins.common import constants
from quantum.tests import base
self.assertIn(constants.CORE, svc_plugins.keys())
self.assertIn(constants.LOADBALANCER, svc_plugins.keys())
self.assertIn(constants.DUMMY, svc_plugins.keys())
+
+ def test_post_plugin_validation(self):
+ self.assertIsNone(validate_post_plugin_load())
+ cfg.CONF.set_override('dhcp_agents_per_network', 2)
+ self.assertIsNone(validate_post_plugin_load())
+ cfg.CONF.set_override('dhcp_agents_per_network', 0)
+ self.assertIsNotNone(validate_post_plugin_load())
+ cfg.CONF.set_override('dhcp_agents_per_network', -1)
+ self.assertIsNotNone(validate_post_plugin_load())
+
+ def test_pre_plugin_validation(self):
+ self.assertIsNotNone(validate_pre_plugin_load())
+ cfg.CONF.set_override('core_plugin', 'dummy.plugin')
+ self.assertIsNone(validate_pre_plugin_load())