Send DHCP notifications regardless of agent status

The Neutron service, when under load, may not be able to process
agent heartbeats in a timely fashion.  This can result in
agents being erroneously considered inactive.  Previously, DHCP
notifications for which active agents could not be found were
silently dropped.  This change ensures that notifications for
a given network are sent to agents even if those agents do not
appear to be active.

Additionally, if no enabled dhcp agents can be found for a given
network, an error will be logged.  Raising an exception might be
preferable, but has such a large testing impact that it will be
submitted as a separate patch if deemed necessary.

Partial-bug: #1192381
Change-Id: Id3e639d9cf3d16708fd66a4baebd3fbeeed3dde8
This commit is contained in:
Maru Newby 2013-12-10 16:10:42 +00:00
parent a7bdec1a31
commit 5a8e67be49
6 changed files with 108 additions and 7 deletions

View File

@ -43,12 +43,11 @@ class DhcpAgentNotifyAPI(proxy.RpcProxy):
super(DhcpAgentNotifyAPI, self).__init__( super(DhcpAgentNotifyAPI, self).__init__(
topic=topic, default_version=self.BASE_RPC_API_VERSION) topic=topic, default_version=self.BASE_RPC_API_VERSION)
def _get_dhcp_agents(self, context, network_id): def _get_enabled_dhcp_agents(self, context, network_id):
"""Return enabled dhcp agents associated with the given network."""
plugin = manager.NeutronManager.get_plugin() plugin = manager.NeutronManager.get_plugin()
dhcp_agents = plugin.get_dhcp_agents_hosting_networks( agents = plugin.get_dhcp_agents_hosting_networks(context, [network_id])
context, [network_id], active=True) return [x for x in agents if x.admin_state_up]
return [(dhcp_agent.host, dhcp_agent.topic) for
dhcp_agent in dhcp_agents]
def _notification_host(self, context, method, payload, host): def _notification_host(self, context, method, payload, host):
"""Notify the agent on host.""" """Notify the agent on host."""
@ -76,11 +75,33 @@ class DhcpAgentNotifyAPI(proxy.RpcProxy):
context, 'network_create_end', context, 'network_create_end',
{'network': {'id': network_id}}, {'network': {'id': network_id}},
agent['host']) agent['host'])
for (host, topic) in self._get_dhcp_agents(context, network_id): agents = self._get_enabled_dhcp_agents(context, network_id)
if not agents:
LOG.error(_("No DHCP agents are associated with network "
"'%(net_id)s'. Unable to send notification "
"for '%(method)s' with payload: %(payload)s"),
{
'net_id': network_id,
'method': method,
'payload': payload,
})
return
active_agents = [x for x in agents if x.is_active]
if active_agents != agents:
LOG.warning(_("Only %(active)d of %(total)d DHCP agents "
"associated with network '%(net_id)s' are "
"marked as active, so notifications may "
"be sent to inactive agents."),
{
'active': len(active_agents),
'total': len(agents),
'net_id': network_id,
})
for agent in agents:
self.cast( self.cast(
context, self.make_msg(method, context, self.make_msg(method,
payload=payload), payload=payload),
topic='%s.%s' % (topic, host)) topic='%s.%s' % (agent.topic, agent.host))
else: else:
# besides the non-agentscheduler plugin, # besides the non-agentscheduler plugin,
# There is no way to query who is hosting the network # There is no way to query who is hosting the network

View File

@ -66,6 +66,10 @@ class Agent(model_base.BASEV2, models_v2.HasId):
# configurations: a json dict string, I think 4095 is enough # configurations: a json dict string, I think 4095 is enough
configurations = sa.Column(sa.String(4095), nullable=False) configurations = sa.Column(sa.String(4095), nullable=False)
@property
def is_active(self):
return not AgentDbMixin.is_agent_down(self.heartbeat_timestamp)
class AgentDbMixin(ext_agent.AgentPluginBase): class AgentDbMixin(ext_agent.AgentPluginBase):
"""Mixin class to add agent extension to db_plugin_base_v2.""" """Mixin class to add agent extension to db_plugin_base_v2."""

View File

View File

View File

@ -0,0 +1,76 @@
# Copyright (c) 2013 Red Hat, Inc.
#
# 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 contextlib
import mock
from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api
from neutron.common import utils
from neutron import manager
from neutron.tests import base
class TestDhcpAgentNotifyAPI(base.BaseTestCase):
def setUp(self):
super(TestDhcpAgentNotifyAPI, self).setUp()
self.notify = dhcp_rpc_agent_api.DhcpAgentNotifyAPI()
def test_get_enabled_dhcp_agents_filters_disabled_agents(self):
disabled_agent = mock.Mock()
disabled_agent.admin_state_up = False
enabled_agent = mock.Mock()
with mock.patch.object(manager.NeutronManager,
'get_plugin') as mock_get_plugin:
mock_get_plugin.return_value = mock_plugin = mock.Mock()
with mock.patch.object(
mock_plugin, 'get_dhcp_agents_hosting_networks'
) as mock_get_agents:
mock_get_agents.return_value = [disabled_agent, enabled_agent]
result = self.notify._get_enabled_dhcp_agents('ctx', 'net_id')
self.assertEqual(result, [enabled_agent])
def _test_notification(self, agents):
with contextlib.nested(
mock.patch.object(manager.NeutronManager, 'get_plugin'),
mock.patch.object(utils, 'is_extension_supported'),
mock.patch.object(self.notify, '_get_enabled_dhcp_agents')
) as (m1, m2, mock_get_agents):
mock_get_agents.return_value = agents
self.notify._notification(mock.Mock(), 'foo', {}, 'net_id')
def test_notification_sends_cast_for_enabled_agent(self):
with mock.patch.object(self.notify, 'cast') as mock_cast:
self._test_notification([mock.Mock()])
self.assertEqual(mock_cast.call_count, 1)
def test_notification_logs_error_for_no_enabled_agents(self):
with mock.patch.object(self.notify, 'cast') as mock_cast:
with mock.patch.object(dhcp_rpc_agent_api.LOG,
'error') as mock_log:
self._test_notification([])
self.assertEqual(mock_cast.call_count, 0)
self.assertEqual(mock_log.call_count, 1)
def test_notification_logs_warning_for_inactive_agents(self):
agent = mock.Mock()
agent.is_active = False
with mock.patch.object(self.notify, 'cast') as mock_cast:
with mock.patch.object(dhcp_rpc_agent_api.LOG,
'warning') as mock_log:
self._test_notification([agent])
self.assertEqual(mock_cast.call_count, 1)
self.assertEqual(mock_log.call_count, 1)