From 2d37076d610a4b6095a3f23dd66e5b192fae6960 Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Tue, 4 Mar 2014 10:47:07 -0800 Subject: [PATCH] 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 --- etc/neutron.conf | 4 + neutron/api/v2/base.py | 10 ++ neutron/common/config.py | 3 + neutron/extensions/l3.py | 3 +- neutron/notifiers/nova.py | 73 ++++++++- .../unit/notifiers/test_notifiers_nova.py | 140 ++++++++++++++++++ 6 files changed, 227 insertions(+), 6 deletions(-) diff --git a/etc/neutron.conf b/etc/neutron.conf index a8f5f2bf504..0a0cca3f09e 100644 --- a/etc/neutron.conf +++ b/etc/neutron.conf @@ -296,6 +296,10 @@ notification_driver = neutron.openstack.common.notifier.rpc_notifier # Send notification to nova when port status is active. # 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). # nova_url = http://127.0.0.1:8774 diff --git a/neutron/api/v2/base.py b/neutron/api/v2/base.py index b56901450dd..0e86e5cf464 100644 --- a/neutron/api/v2/base.py +++ b/neutron/api/v2/base.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import netaddr import webob.exc @@ -26,6 +27,7 @@ from neutron.api.v2 import attributes from neutron.api.v2 import resource as wsgi_resource from neutron.common import constants as const from neutron.common import exceptions +from neutron.notifiers import nova from neutron.openstack.common import log as logging from neutron.openstack.common.notifier import api as notifier_api from neutron import policy @@ -75,6 +77,7 @@ class Controller(object): agent_notifiers.get(const.AGENT_TYPE_DHCP) or dhcp_rpc_agent_api.DhcpAgentNotifyAPI() ) + self._nova_notifier = nova.Notifier() self._member_actions = member_actions self._primary_key = self._get_primary_key() if self._allow_pagination and self._native_pagination: @@ -414,6 +417,9 @@ class Controller(object): else: kwargs.update({self._resource: body}) obj = obj_creator(request.context, **kwargs) + + self._nova_notifier.send_network_change( + action, {}, {self._resource: obj}) return notify({self._resource: self._view(request.context, obj)}) @@ -448,6 +454,7 @@ class Controller(object): notifier_api.CONF.default_notification_level, {self._resource + '_id': id}) result = {self._resource: self._view(request.context, obj)} + self._nova_notifier.send_network_change(action, {}, result) self._send_dhcp_notification(request.context, result, notifier_method) @@ -479,6 +486,7 @@ class Controller(object): 'default' not in value)] orig_obj = self._item(request, id, field_list=field_list, parent_id=parent_id) + orig_object_copy = copy.copy(orig_obj) orig_obj.update(body[self._resource]) try: policy.enforce(request.context, @@ -505,6 +513,8 @@ class Controller(object): self._send_dhcp_notification(request.context, result, notifier_method) + self._nova_notifier.send_network_change( + action, orig_object_copy, result) return result @staticmethod diff --git a/neutron/common/config.py b/neutron/common/config.py index fd1a0bd9683..e04cc4e3381 100644 --- a/neutron/common/config.py +++ b/neutron/common/config.py @@ -83,6 +83,9 @@ core_opts = [ help=_("Ensure that configured gateway is on subnet")), cfg.BoolOpt('notify_nova_on_port_status_changes', default=True, 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', default='http://127.0.0.1:8774', help=_('URL for connection to nova')), diff --git a/neutron/extensions/l3.py b/neutron/extensions/l3.py index 7e29ce3d43f..161b5b5d62d 100644 --- a/neutron/extensions/l3.py +++ b/neutron/extensions/l3.py @@ -118,7 +118,8 @@ RESOURCE_ATTRIBUTE_MAP = { 'is_visible': True, 'default': None}, 'port_id': {'allow_post': True, 'allow_put': True, '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, 'validate': {'type:ip_address_or_none': None}, 'is_visible': True, 'default': None}, diff --git a/neutron/notifiers/nova.py b/neutron/notifiers/nova.py index 1e8ece5b415..ca68332e358 100644 --- a/neutron/notifiers/nova.py +++ b/neutron/notifiers/nova.py @@ -19,6 +19,8 @@ from oslo.config import cfg from sqlalchemy.orm import attributes as sql_attr 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 loopingcall @@ -52,6 +54,67 @@ class Notifier(object): event_sender = loopingcall.FixedIntervalLoopingCall(self.send_events) 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, previous_port_status, initiator): """Determine if nova needs to be notified due to port status change. @@ -69,14 +132,13 @@ class Notifier(object): return # We only want to notify about nova ports. - if (not port.device_owner or - not port.device_owner.startswith('compute:')): + if not self._is_compute_port(port): return # We notify nova when a vif is unplugged which only occurs when # the status goes from ACTIVE to DOWN. 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 # We only notify nova when a vif is plugged which only occurs @@ -133,10 +195,11 @@ class Notifier(object): response_error = False for event in response: try: - status = event['status'] + code = event['code'] except KeyError: response_error = True - if status == 'failed': + continue + if code != 200: LOG.warning(_("Nova event: %s returned with failed " "status"), event) else: diff --git a/neutron/tests/unit/notifiers/test_notifiers_nova.py b/neutron/tests/unit/notifiers/test_notifiers_nova.py index 3f5f9a658ee..887782952eb 100644 --- a/neutron/tests/unit/notifiers/test_notifiers_nova.py +++ b/neutron/tests/unit/notifiers/test_notifiers_nova.py @@ -14,8 +14,11 @@ # under the License. +import mock from sqlalchemy.orm import attributes as sql_attr +from oslo.config import cfg + from neutron.common import constants from neutron.db import models_v2 from neutron.notifiers import nova @@ -26,7 +29,13 @@ class TestNovaNotify(base.BaseTestCase): def setUp(self, plugin=None): 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._plugin_ref = FakePlugin() def test_notify_port_status_all_values(self): states = [constants.PORT_STATUS_ACTIVE, constants.PORT_STATUS_DOWN, @@ -102,3 +111,134 @@ class TestNovaNotify(base.BaseTestCase): event = {'server_uuid': 'device-uuid', 'status': status, 'name': event_name, 'tag': 'port-uuid'} 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()