Send network-changed notifications to nova

This patch notifies nova whenever a floatingip or fixed_ip is updated.

Implements blueprint: nova-event-callback

DocImpact - This notifications are off by default.

Change-Id: Ifbe9d856e80e512d5595fd72ea2d7c047ce0de9d
This commit is contained in:
Aaron Rosen 2014-03-04 10:47:07 -08:00
parent f1681df4ca
commit 314951e1b4
6 changed files with 227 additions and 6 deletions

View File

@ -296,6 +296,10 @@ notification_driver = neutron.openstack.common.notifier.rpc_notifier
# Send notification to nova when port status is active. # Send notification to nova when port status is active.
# notify_nova_on_port_status_changes = True # notify_nova_on_port_status_changes = True
# Send notifications to nova when port data (fixed_ips/floatingips) change
# so nova can update it's cache.
# notify_nova_on_port_data_changes = True
# URL for connection to nova (Only supports one nova region currently). # URL for connection to nova (Only supports one nova region currently).
# nova_url = http://127.0.0.1:8774 # nova_url = http://127.0.0.1:8774

View File

@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import copy
import netaddr import netaddr
import webob.exc import webob.exc
@ -26,6 +27,7 @@ from neutron.api.v2 import attributes
from neutron.api.v2 import resource as wsgi_resource from neutron.api.v2 import resource as wsgi_resource
from neutron.common import constants as const from neutron.common import constants as const
from neutron.common import exceptions from neutron.common import exceptions
from neutron.notifiers import nova
from neutron.openstack.common import log as logging from neutron.openstack.common import log as logging
from neutron.openstack.common.notifier import api as notifier_api from neutron.openstack.common.notifier import api as notifier_api
from neutron import policy from neutron import policy
@ -75,6 +77,7 @@ class Controller(object):
agent_notifiers.get(const.AGENT_TYPE_DHCP) or agent_notifiers.get(const.AGENT_TYPE_DHCP) or
dhcp_rpc_agent_api.DhcpAgentNotifyAPI() dhcp_rpc_agent_api.DhcpAgentNotifyAPI()
) )
self._nova_notifier = nova.Notifier()
self._member_actions = member_actions self._member_actions = member_actions
self._primary_key = self._get_primary_key() self._primary_key = self._get_primary_key()
if self._allow_pagination and self._native_pagination: if self._allow_pagination and self._native_pagination:
@ -414,6 +417,9 @@ class Controller(object):
else: else:
kwargs.update({self._resource: body}) kwargs.update({self._resource: body})
obj = obj_creator(request.context, **kwargs) obj = obj_creator(request.context, **kwargs)
self._nova_notifier.send_network_change(
action, {}, {self._resource: obj})
return notify({self._resource: self._view(request.context, return notify({self._resource: self._view(request.context,
obj)}) obj)})
@ -448,6 +454,7 @@ class Controller(object):
notifier_api.CONF.default_notification_level, notifier_api.CONF.default_notification_level,
{self._resource + '_id': id}) {self._resource + '_id': id})
result = {self._resource: self._view(request.context, obj)} result = {self._resource: self._view(request.context, obj)}
self._nova_notifier.send_network_change(action, {}, result)
self._send_dhcp_notification(request.context, self._send_dhcp_notification(request.context,
result, result,
notifier_method) notifier_method)
@ -479,6 +486,7 @@ class Controller(object):
'default' not in value)] 'default' not in value)]
orig_obj = self._item(request, id, field_list=field_list, orig_obj = self._item(request, id, field_list=field_list,
parent_id=parent_id) parent_id=parent_id)
orig_object_copy = copy.copy(orig_obj)
orig_obj.update(body[self._resource]) orig_obj.update(body[self._resource])
try: try:
policy.enforce(request.context, policy.enforce(request.context,
@ -505,6 +513,8 @@ class Controller(object):
self._send_dhcp_notification(request.context, self._send_dhcp_notification(request.context,
result, result,
notifier_method) notifier_method)
self._nova_notifier.send_network_change(
action, orig_object_copy, result)
return result return result
@staticmethod @staticmethod

View File

@ -83,6 +83,9 @@ core_opts = [
help=_("Ensure that configured gateway is on subnet")), help=_("Ensure that configured gateway is on subnet")),
cfg.BoolOpt('notify_nova_on_port_status_changes', default=True, cfg.BoolOpt('notify_nova_on_port_status_changes', default=True,
help=_("Send notification to nova when port status changes")), help=_("Send notification to nova when port status changes")),
cfg.BoolOpt('notify_nova_on_port_data_changes', default=True,
help=_("Send notification to nova when port data (fixed_ips/"
"floatingip) changes so nova can update its cache.")),
cfg.StrOpt('nova_url', cfg.StrOpt('nova_url',
default='http://127.0.0.1:8774', default='http://127.0.0.1:8774',
help=_('URL for connection to nova')), help=_('URL for connection to nova')),

View File

@ -118,7 +118,8 @@ RESOURCE_ATTRIBUTE_MAP = {
'is_visible': True, 'default': None}, 'is_visible': True, 'default': None},
'port_id': {'allow_post': True, 'allow_put': True, 'port_id': {'allow_post': True, 'allow_put': True,
'validate': {'type:uuid_or_none': None}, 'validate': {'type:uuid_or_none': None},
'is_visible': True, 'default': None}, 'is_visible': True, 'default': None,
'required_by_policy': True},
'fixed_ip_address': {'allow_post': True, 'allow_put': True, 'fixed_ip_address': {'allow_post': True, 'allow_put': True,
'validate': {'type:ip_address_or_none': None}, 'validate': {'type:ip_address_or_none': None},
'is_visible': True, 'default': None}, 'is_visible': True, 'default': None},

View File

@ -19,6 +19,8 @@ from oslo.config import cfg
from sqlalchemy.orm import attributes as sql_attr from sqlalchemy.orm import attributes as sql_attr
from neutron.common import constants from neutron.common import constants
from neutron import context
from neutron import manager
from neutron.openstack.common import log as logging from neutron.openstack.common import log as logging
from neutron.openstack.common import loopingcall from neutron.openstack.common import loopingcall
@ -52,6 +54,67 @@ class Notifier(object):
event_sender = loopingcall.FixedIntervalLoopingCall(self.send_events) event_sender = loopingcall.FixedIntervalLoopingCall(self.send_events)
event_sender.start(interval=cfg.CONF.send_events_interval) event_sender.start(interval=cfg.CONF.send_events_interval)
def _is_compute_port(self, port):
try:
if (port['device_id'] and
port['device_owner'].startswith('compute:')):
return True
except (KeyError, AttributeError):
pass
return False
def _get_network_changed_event(self, device_id):
return {'name': 'network-changed',
'server_uuid': device_id}
@property
def _plugin(self):
# NOTE(arosen): this cannot be set in __init__ currently since
# this class is initalized at the same time as NeutronManager()
# which is decorated with synchronized()
if not hasattr(self, '_plugin_ref'):
self._plugin_ref = manager.NeutronManager.get_plugin()
return self._plugin_ref
def send_network_change(self, action, original_obj,
returned_obj):
"""Called when a network change is made that nova cares about.
:param action: the event that occured.
:param original_obj: the previous value of resource before action.
:param returned_obj: the body returned to client as result of action.
"""
if not cfg.CONF.notify_nova_on_port_data_changes:
return
event = self.create_port_changed_event(action, original_obj,
returned_obj)
if event:
self.pending_events.append(event)
def create_port_changed_event(self, action, original_obj, returned_obj):
port = None
if action == 'update_port':
port = returned_obj['port']
elif action in ['update_floatingip', 'create_floatingip',
'delete_floatingip']:
# NOTE(arosen) if we are associating a floatingip the
# port_id is in the returned_obj. Otherwise on disassociate
# it's in the original_object
port_id = (returned_obj['floatingip'].get('port_id') or
original_obj.get('port_id'))
if port_id is None:
return
ctx = context.get_admin_context()
port = self._plugin.get_port(ctx, port_id)
if port and self._is_compute_port(port):
return self._get_network_changed_event(port['device_id'])
def record_port_status_changed(self, port, current_port_status, def record_port_status_changed(self, port, current_port_status,
previous_port_status, initiator): previous_port_status, initiator):
"""Determine if nova needs to be notified due to port status change. """Determine if nova needs to be notified due to port status change.
@ -69,14 +132,13 @@ class Notifier(object):
return return
# We only want to notify about nova ports. # We only want to notify about nova ports.
if (not port.device_owner or if not self._is_compute_port(port):
not port.device_owner.startswith('compute:')):
return return
# We notify nova when a vif is unplugged which only occurs when # We notify nova when a vif is unplugged which only occurs when
# the status goes from ACTIVE to DOWN. # the status goes from ACTIVE to DOWN.
if (previous_port_status == constants.PORT_STATUS_ACTIVE and if (previous_port_status == constants.PORT_STATUS_ACTIVE and
current_port_status == constants.PORT_STATUS_DOWN): current_port_status == constants.PORT_STATUS_DOWN):
event_name = VIF_UNPLUGGED event_name = VIF_UNPLUGGED
# We only notify nova when a vif is plugged which only occurs # We only notify nova when a vif is plugged which only occurs
@ -133,10 +195,11 @@ class Notifier(object):
response_error = False response_error = False
for event in response: for event in response:
try: try:
status = event['status'] code = event['code']
except KeyError: except KeyError:
response_error = True response_error = True
if status == 'failed': continue
if code != 200:
LOG.warning(_("Nova event: %s returned with failed " LOG.warning(_("Nova event: %s returned with failed "
"status"), event) "status"), event)
else: else:

View File

@ -14,8 +14,11 @@
# under the License. # under the License.
import mock
from sqlalchemy.orm import attributes as sql_attr from sqlalchemy.orm import attributes as sql_attr
from oslo.config import cfg
from neutron.common import constants from neutron.common import constants
from neutron.db import models_v2 from neutron.db import models_v2
from neutron.notifiers import nova from neutron.notifiers import nova
@ -26,7 +29,13 @@ class TestNovaNotify(base.BaseTestCase):
def setUp(self, plugin=None): def setUp(self, plugin=None):
super(TestNovaNotify, self).setUp() super(TestNovaNotify, self).setUp()
class FakePlugin(object):
def get_port(self, context, port_id):
return {'device_id': 'instance_uuid',
'device_owner': 'compute:None'}
self.nova_notifier = nova.Notifier() self.nova_notifier = nova.Notifier()
self.nova_notifier._plugin_ref = FakePlugin()
def test_notify_port_status_all_values(self): def test_notify_port_status_all_values(self):
states = [constants.PORT_STATUS_ACTIVE, constants.PORT_STATUS_DOWN, states = [constants.PORT_STATUS_ACTIVE, constants.PORT_STATUS_DOWN,
@ -102,3 +111,134 @@ class TestNovaNotify(base.BaseTestCase):
event = {'server_uuid': 'device-uuid', 'status': status, event = {'server_uuid': 'device-uuid', 'status': status,
'name': event_name, 'tag': 'port-uuid'} 'name': event_name, 'tag': 'port-uuid'}
self.assertEqual(event, port._notify_event) self.assertEqual(event, port._notify_event)
def test_update_fixed_ip_changed(self):
returned_obj = {'port':
{'device_owner': u'compute:dfd',
'id': u'bee50827-bcee-4cc8-91c1-a27b0ce54222',
'device_id': u'instance_uuid'}}
expected_event = {'server_uuid': 'instance_uuid',
'name': 'network-changed'}
event = self.nova_notifier.create_port_changed_event('update_port',
{}, returned_obj)
self.assertEqual(event, expected_event)
def test_create_floatingip_notify(self):
returned_obj = {'floatingip':
{'port_id': u'bee50827-bcee-4cc8-91c1-a27b0ce54222'}}
expected_event = {'server_uuid': 'instance_uuid',
'name': 'network-changed'}
event = self.nova_notifier.create_port_changed_event(
'create_floatingip', {}, returned_obj)
self.assertEqual(event, expected_event)
def test_create_floatingip_no_port_id_no_notify(self):
returned_obj = {'floatingip':
{'port_id': None}}
event = self.nova_notifier.create_port_changed_event(
'create_floatingip', {}, returned_obj)
self.assertFalse(event, None)
def test_delete_floatingip_notify(self):
returned_obj = {'floatingip':
{'port_id': u'bee50827-bcee-4cc8-91c1-a27b0ce54222'}}
expected_event = {'server_uuid': 'instance_uuid',
'name': 'network-changed'}
event = self.nova_notifier.create_port_changed_event(
'delete_floatingip', {}, returned_obj)
self.assertEqual(expected_event, event)
def test_delete_floatingip_no_port_id_no_notify(self):
returned_obj = {'floatingip':
{'port_id': None}}
event = self.nova_notifier.create_port_changed_event(
'delete_floatingip', {}, returned_obj)
self.assertEqual(event, None)
def test_associate_floatingip_notify(self):
returned_obj = {'floatingip':
{'port_id': u'5a39def4-3d3f-473d-9ff4-8e90064b9cc1'}}
original_obj = {'port_id': None}
expected_event = {'server_uuid': 'instance_uuid',
'name': 'network-changed'}
event = self.nova_notifier.create_port_changed_event(
'update_floatingip', original_obj, returned_obj)
self.assertEqual(expected_event, event)
def test_disassociate_floatingip_notify(self):
returned_obj = {'floatingip': {'port_id': None}}
original_obj = {'port_id': '5a39def4-3d3f-473d-9ff4-8e90064b9cc1'}
expected_event = {'server_uuid': 'instance_uuid',
'name': 'network-changed'}
event = self.nova_notifier.create_port_changed_event(
'update_floatingip', original_obj, returned_obj)
self.assertEqual(expected_event, event)
def test_no_notification_notify_nova_on_port_data_changes_false(self):
cfg.CONF.set_override('notify_nova_on_port_data_changes', False)
with mock.patch.object(self.nova_notifier,
'send_events') as send_events:
self.nova_notifier.send_network_change('update_floatingip',
{}, {})
self.assertFalse(send_events.called, False)
def test_nova_send_events_returns_bad_list(self):
with mock.patch.object(
self.nova_notifier.nclient.server_external_events,
'create') as nclient_create:
nclient_create.return_value = 'i am a string!'
self.nova_notifier.send_events()
def test_nova_send_events_raises(self):
with mock.patch.object(
self.nova_notifier.nclient.server_external_events,
'create') as nclient_create:
nclient_create.side_effect = Exception
self.nova_notifier.send_events()
def test_nova_send_events_returns_non_200(self):
with mock.patch.object(
self.nova_notifier.nclient.server_external_events,
'create') as nclient_create:
nclient_create.return_value = [{'code': 404,
'name': 'network-changed',
'server_uuid': 'uuid'}]
self.nova_notifier.pending_events.append(
{'name': 'network-changed', 'server_uuid': 'uuid'})
self.nova_notifier.send_events()
def test_nova_send_events_return_200(self):
with mock.patch.object(
self.nova_notifier.nclient.server_external_events,
'create') as nclient_create:
nclient_create.return_value = [{'code': 200,
'name': 'network-changed',
'server_uuid': 'uuid'}]
self.nova_notifier.pending_events.append(
{'name': 'network-changed', 'server_uuid': 'uuid'})
self.nova_notifier.send_events()
def test_nova_send_events_multiple(self):
with mock.patch.object(
self.nova_notifier.nclient.server_external_events,
'create') as nclient_create:
nclient_create.return_value = [{'code': 200,
'name': 'network-changed',
'server_uuid': 'uuid'},
{'code': 200,
'name': 'network-changed',
'server_uuid': 'uuid'}]
self.nova_notifier.pending_events.append(
{'name': 'network-changed', 'server_uuid': 'uuid'})
self.nova_notifier.pending_events.append(
{'name': 'network-changed', 'server_uuid': 'uuid'})
self.nova_notifier.send_events()