]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Send network-changed notifications to nova
authorAaron Rosen <aaronorosen@gmail.com>
Tue, 4 Mar 2014 18:47:07 +0000 (10:47 -0800)
committerAaron Rosen <aaronorosen@gmail.com>
Fri, 14 Mar 2014 00:19:43 +0000 (17:19 -0700)
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
neutron/api/v2/base.py
neutron/common/config.py
neutron/extensions/l3.py
neutron/notifiers/nova.py
neutron/tests/unit/notifiers/test_notifiers_nova.py

index a8f5f2bf50477afecf4541161b0888f0d4425886..0a0cca3f09e8dcd8314911cf630dc3337d17a27b 100644 (file)
@@ -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
 
index b56901450ddd31565d23c029335465555e93e547..0e86e5cf4644a4d008edcd0fe7319e149d348f29 100644 (file)
@@ -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
index fd1a0bd9683f02b9c3041292008697d1aec78e77..e04cc4e338142d36bbf7d9843501a452d49464a2 100644 (file)
@@ -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')),
index 7e29ce3d43f156c1bee2de478740379137bddae1..161b5b5d62d40f8368b695c73998f02d824f3466 100644 (file)
@@ -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},
index 1e8ece5b415f1ba3695fbb2b092cb4c78407a106..ca68332e35874174afb79fba898c8c0dd4e80c38 100644 (file)
@@ -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:
index 3f5f9a658ee9ea85a9664311417b641ee297eb62..887782952ebd5bff1d5884150c800af04bcda237 100644 (file)
 #    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()