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:
parent
827cc51705
commit
2d37076d61
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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')),
|
||||||
|
@ -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},
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user