diff --git a/etc/neutron.conf b/etc/neutron.conf index f2e0e89a3..a8f5f2bf5 100644 --- a/etc/neutron.conf +++ b/etc/neutron.conf @@ -291,6 +291,34 @@ notification_driver = neutron.openstack.common.notifier.rpc_notifier # ssl_ca_file = /path/to/cafile # ======== end of WSGI parameters related to the API server ========== + +# ======== neutron nova interactions ========== +# Send notification to nova when port status is active. +# notify_nova_on_port_status_changes = True + +# URL for connection to nova (Only supports one nova region currently). +# nova_url = http://127.0.0.1:8774 + +# Name of nova region to use. Useful if keystone manages more than one region +# nova_region_name = + +# Username for connection to nova in admin context +# nova_admin_username = + +# The uuid of the admin nova tenant +# nova_admin_tenant_id = + +# Password for connection to nova in admin context. +# nova_admin_password = + +# Authorization URL for connection to nova in admin context. +# nova_admin_auth_url = + +# Number of seconds between sending events to nova if there are any events to send +# send_events_interval = 2 + +# ======== end of neutron nova interactions ========== + [quotas] # Default driver to use for quota checks # quota_driver = neutron.db.quota_db.DbQuotaDriver diff --git a/neutron/common/config.py b/neutron/common/config.py index 3abddc50c..fd1a0bd96 100644 --- a/neutron/common/config.py +++ b/neutron/common/config.py @@ -81,6 +81,28 @@ core_opts = [ help=_("The hostname Neutron is running on")), cfg.BoolOpt('force_gateway_on_subnet', default=False, 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.StrOpt('nova_url', + default='http://127.0.0.1:8774', + help=_('URL for connection to nova')), + cfg.StrOpt('nova_admin_username', + help=_('Username for connecting to nova in admin context')), + cfg.StrOpt('nova_admin_password', + help=_('Password for connection to nova in admin context'), + secret=True), + cfg.StrOpt('nova_admin_tenant_id', + help=_('The uuid of the admin nova tenant')), + cfg.StrOpt('nova_admin_auth_url', + default='http://localhost:5000/v2.0', + help=_('Authorization URL for connecting to nova in admin ' + 'context')), + cfg.StrOpt('nova_region_name', + help=_('Name of nova region to use. Useful if keystone manages' + ' more than one region.')), + cfg.IntOpt('send_events_interval', default=2, + help=_('Number of seconds between sending events to nova if ' + 'there are any events to send.')), ] core_cli_opts = [ diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index 5f8a87e2c..13b800974 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -19,6 +19,7 @@ import random import netaddr from oslo.config import cfg +from sqlalchemy import event from sqlalchemy import orm from sqlalchemy.orm import exc @@ -29,6 +30,7 @@ from neutron.db import api as db from neutron.db import models_v2 from neutron.db import sqlalchemyutils from neutron import neutron_plugin_base_v2 +from neutron.notifiers import nova from neutron.openstack.common import excutils from neutron.openstack.common import log as logging from neutron.openstack.common import uuidutils @@ -221,6 +223,16 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, def __init__(self): db.configure_db() + if cfg.CONF.notify_nova_on_port_status_changes: + # NOTE(arosen) These event listners are here to hook into when + # port status changes and notify nova about their change. + self.nova_notifier = nova.Notifier() + event.listen(models_v2.Port, 'after_insert', + self.nova_notifier.send_port_status) + event.listen(models_v2.Port, 'after_update', + self.nova_notifier.send_port_status) + event.listen(models_v2.Port.status, 'set', + self.nova_notifier.record_port_status_changed) @classmethod def register_dict_extend_funcs(cls, resource, funcs): diff --git a/neutron/db/models_v2.py b/neutron/db/models_v2.py index 1d55ff7fd..387adcfdb 100644 --- a/neutron/db/models_v2.py +++ b/neutron/db/models_v2.py @@ -130,6 +130,24 @@ class Port(model_base.BASEV2, HasId, HasTenant): device_id = sa.Column(sa.String(255), nullable=False) device_owner = sa.Column(sa.String(255), nullable=False) + def __init__(self, id=None, tenant_id=None, name=None, network_id=None, + mac_address=None, admin_state_up=None, status=None, + device_id=None, device_owner=None, fixed_ips=None): + self.id = id + self.tenant_id = tenant_id + self.name = name + self.network_id = network_id + self.mac_address = mac_address + self.admin_state_up = admin_state_up + self.device_owner = device_owner + self.device_id = device_id + # Since this is a relationship only set it if one is passed in. + if fixed_ips: + self.fixed_ips = fixed_ips + + # NOTE(arosen): status must be set last as an event is triggered on! + self.status = status + class DNSNameServer(model_base.BASEV2): """Internal representation of a DNS nameserver.""" diff --git a/neutron/notifiers/__init__.py b/neutron/notifiers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/notifiers/nova.py b/neutron/notifiers/nova.py new file mode 100644 index 000000000..1e8ece5b4 --- /dev/null +++ b/neutron/notifiers/nova.py @@ -0,0 +1,146 @@ +# Copyright (c) 2014 OpenStack Foundation. +# All Rights Reserved. +# +# 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 novaclient.v1_1.client as nclient +from novaclient.v1_1.contrib import server_external_events +from oslo.config import cfg +from sqlalchemy.orm import attributes as sql_attr + +from neutron.common import constants +from neutron.openstack.common import log as logging +from neutron.openstack.common import loopingcall + + +LOG = logging.getLogger(__name__) + +VIF_UNPLUGGED = 'network-vif-unplugged' +VIF_PLUGGED = 'network-vif-plugged' +NEUTRON_NOVA_EVENT_STATUS_MAP = {constants.PORT_STATUS_ACTIVE: 'completed', + constants.PORT_STATUS_ERROR: 'failed', + constants.PORT_STATUS_DOWN: 'completed'} + + +class Notifier(object): + + def __init__(self): + # TODO(arosen): we need to cache the endpoints and figure out + # how to deal with different regions here.... + bypass_url = "%s/%s" % (cfg.CONF.nova_url, + cfg.CONF.nova_admin_tenant_id) + self.nclient = nclient.Client( + username=cfg.CONF.nova_admin_username, + api_key=cfg.CONF.nova_admin_password, + project_id=None, + tenant_id=cfg.CONF.nova_admin_tenant_id, + auth_url=cfg.CONF.nova_admin_auth_url, + bypass_url=bypass_url, + region_name=cfg.CONF.nova_region_name, + extensions=[server_external_events]) + self.pending_events = [] + event_sender = loopingcall.FixedIntervalLoopingCall(self.send_events) + event_sender.start(interval=cfg.CONF.send_events_interval) + + 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. + """ + # clear out previous _notify_event + port._notify_event = None + # If there is no device_id set there is nothing we can do here. + if not port.device_id: + LOG.debug(_("device_id is not set on port yet.")) + return + + if not port.id: + LOG.warning(_("Port ID not set! Nova will not be notified of " + "port status change.")) + return + + # We only want to notify about nova ports. + if (not port.device_owner or + not port.device_owner.startswith('compute:')): + 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): + event_name = VIF_UNPLUGGED + + # We only notify nova when a vif is plugged which only occurs + # when the status goes from: + # NO_VALUE/DOWN/BUILD -> ACTIVE/ERROR. + elif (previous_port_status in [sql_attr.NO_VALUE, + constants.PORT_STATUS_DOWN, + constants.PORT_STATUS_BUILD] + and current_port_status in [constants.PORT_STATUS_ACTIVE, + constants.PORT_STATUS_ERROR]): + event_name = VIF_PLUGGED + # All the remaining state transitions are of no interest to nova + else: + LOG.debug(_("Ignoring state change previous_port_status: " + "%(pre_status)s current_port_status: %(cur_status)s" + " port_id %(id)s") % + {'pre_status': previous_port_status, + 'cur_status': current_port_status, + 'id': port.id}) + return + + port._notify_event = ( + {'server_uuid': port.device_id, + 'name': event_name, + 'status': NEUTRON_NOVA_EVENT_STATUS_MAP.get(current_port_status), + 'tag': port.id}) + + def send_port_status(self, mapper, connection, port): + event = getattr(port, "_notify_event", None) + if event: + self.pending_events.append(event) + port._notify_event = None + + def send_events(self): + batched_events = [] + for event in range(len(self.pending_events)): + batched_events.append(self.pending_events.pop()) + + if not batched_events: + return + + LOG.debug(_("Sending events: %s"), batched_events) + try: + response = self.nclient.server_external_events.create( + batched_events) + except Exception: + LOG.exception(_("Failed to notify nova on events: %s"), + batched_events) + else: + if not isinstance(response, list): + LOG.error(_("Error response returned from nova: %s"), + response) + return + response_error = False + for event in response: + try: + status = event['status'] + except KeyError: + response_error = True + if status == 'failed': + LOG.warning(_("Nova event: %s returned with failed " + "status"), event) + else: + LOG.info(_("Nova event response: %s"), event) + if response_error: + LOG.error(_("Error response returned from nova: %s"), + response) diff --git a/neutron/tests/unit/notifiers/__init__.py b/neutron/tests/unit/notifiers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/tests/unit/notifiers/test_notifiers_nova.py b/neutron/tests/unit/notifiers/test_notifiers_nova.py new file mode 100644 index 000000000..3f5f9a658 --- /dev/null +++ b/neutron/tests/unit/notifiers/test_notifiers_nova.py @@ -0,0 +1,104 @@ +# Copyright 2014 OpenStack Foundation +# All Rights Reserved. +# +# 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. + + +from sqlalchemy.orm import attributes as sql_attr + +from neutron.common import constants +from neutron.db import models_v2 +from neutron.notifiers import nova +from neutron.tests import base + + +class TestNovaNotify(base.BaseTestCase): + def setUp(self, plugin=None): + super(TestNovaNotify, self).setUp() + + self.nova_notifier = nova.Notifier() + + def test_notify_port_status_all_values(self): + states = [constants.PORT_STATUS_ACTIVE, constants.PORT_STATUS_DOWN, + constants.PORT_STATUS_ERROR, constants.PORT_STATUS_BUILD, + sql_attr.NO_VALUE] + # test all combinations + for previous_port_status in states: + for current_port_status in states: + port = models_v2.Port(id='port-uuid', device_id='device-uuid', + device_owner="compute:", + status=current_port_status) + self._record_port_status_changed_helper(current_port_status, + previous_port_status, + port) + + def test_port_without_device_owner_no_notify(self): + port = models_v2.Port(id='port-uuid', device_id='device-uuid', + status=constants.PORT_STATUS_ACTIVE) + self._record_port_status_changed_helper(constants.PORT_STATUS_ACTIVE, + sql_attr.NO_VALUE, + port) + + def test_port_without_device_id_no_notify(self): + port = models_v2.Port(id='port-uuid', device_owner="network:dhcp", + status=constants.PORT_STATUS_ACTIVE) + self._record_port_status_changed_helper(constants.PORT_STATUS_ACTIVE, + sql_attr.NO_VALUE, + port) + + def test_port_without_id_no_notify(self): + port = models_v2.Port(device_id='device-uuid', + device_owner="compute:", + status=constants.PORT_STATUS_ACTIVE) + self._record_port_status_changed_helper(constants.PORT_STATUS_ACTIVE, + sql_attr.NO_VALUE, + port) + + def test_non_compute_instances_no_notify(self): + port = models_v2.Port(id='port-uuid', device_id='device-uuid', + device_owner="network:dhcp", + status=constants.PORT_STATUS_ACTIVE) + self._record_port_status_changed_helper(constants.PORT_STATUS_ACTIVE, + sql_attr.NO_VALUE, + port) + + def _record_port_status_changed_helper(self, current_port_status, + previous_port_status, port): + + if not (port.device_id and port.id and port.device_owner and + port.device_owner.startswith('compute:')): + return + + if (previous_port_status == constants.PORT_STATUS_ACTIVE and + current_port_status == constants.PORT_STATUS_DOWN): + event_name = nova.VIF_UNPLUGGED + + elif (previous_port_status in [sql_attr.NO_VALUE, + constants.PORT_STATUS_DOWN, + constants.PORT_STATUS_BUILD] + and current_port_status in [constants.PORT_STATUS_ACTIVE, + constants.PORT_STATUS_ERROR]): + event_name = nova.VIF_PLUGGED + + else: + return + + status = nova.NEUTRON_NOVA_EVENT_STATUS_MAP.get(current_port_status) + self.nova_notifier.record_port_status_changed(port, + current_port_status, + previous_port_status, + None) + + event = {'server_uuid': 'device-uuid', 'status': status, + 'name': event_name, 'tag': 'port-uuid'} + self.assertEqual(event, port._notify_event) diff --git a/neutron/tests/unit/test_db_plugin.py b/neutron/tests/unit/test_db_plugin.py index a23a3a7bc..56b20d29f 100644 --- a/neutron/tests/unit/test_db_plugin.py +++ b/neutron/tests/unit/test_db_plugin.py @@ -77,8 +77,9 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase): def setUp(self, plugin=None, service_plugins=None, ext_mgr=None): - super(NeutronDbPluginV2TestCase, self).setUp() + super(NeutronDbPluginV2TestCase, self).setUp() + cfg.CONF.set_override('notify_nova_on_port_status_changes', False) # Make sure at each test according extensions for the plugin is loaded PluginAwareExtensionManager._instance = None # Save the attributes map in case the plugin will alter it