diff --git a/neutron/db/l3_db.py b/neutron/db/l3_db.py index 9f022e03ee2..4dd5261846d 100644 --- a/neutron/db/l3_db.py +++ b/neutron/db/l3_db.py @@ -664,7 +664,24 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase, raise n_exc.BadRequest(resource='router', msg=msg) return port_id_specified, subnet_id_specified - def _check_router_port(self, context, port_id, device_id): + def _check_router_port(self, context, port_id, device_id, + router_id=None): + """Check that a port is available for an attachment to a router + + :param context: The context of the request. + :param port_id: The port to be attached. + :param device_id: This method will check that device_id corresponds to + the device_id of the port. It raises PortInUse exception if it + doesn't. + :param router_id: This method will use this router_id to notify + third party code that an attachment is occurring on this router. + Third party code can prevent this attachment by raising an exception + that will be caught and reraised with a + RouterInterfaceAttachmentConflict exception. + :returns: The port description returned by the core plugin. + :raises: PortInUse if the device_id is not the same as the port's one. + :raises: BadRequest if the port has no fixed IP. + """ port = self._core_plugin.get_port(context, port_id) if port['device_id'] != device_id: raise n_exc.PortInUse(net_id=port['network_id'], @@ -673,6 +690,9 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase, if not port['fixed_ips']: msg = _('Router port must have at least one fixed IP') raise n_exc.BadRequest(resource='router', msg=msg) + if router_id is not None: + self._notify_attaching_interface(context, router_id, + port['network_id']) return port def _validate_router_port_info(self, context, router, port_id): @@ -712,6 +732,31 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase, raise n_exc.BadRequest(resource='router', msg=msg) return port, subnets + def _notify_attaching_interface(self, context, router_id, network_id): + """Notify third party code that an interface is being attached to a + router + + :param context: The context of the request. + :param router_id: The id of the router the interface is being + attached to. + :param network_id: The id of the network the port belongs to. + :raises: RouterInterfaceAttachmentConflict if a third party code + prevent the port to be attach to the router. + """ + try: + registry.notify(resources.ROUTER_INTERFACE, + events.BEFORE_CREATE, + self, + context=context, + router_id=router_id, + network_id=network_id + ) + except exceptions.CallbackFailure as e: + # raise the underlying exception + reason = (_('cannot perform router interface attachment ' + 'due to %(reason)s') % {'reason': e}) + raise l3.RouterInterfaceAttachmentConflict(reason=reason) + def _add_interface_by_port(self, context, router, port_id, owner): # Update owner before actual process in order to avoid the # case where a port might get attached to a router without the @@ -735,6 +780,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase, def _add_interface_by_subnet(self, context, router, subnet_id, owner): subnet = self._core_plugin.get_subnet(context, subnet_id) + self._notify_attaching_interface(context, router.id, + subnet['network_id']) if not subnet['gateway_ip']: msg = _('Subnet for router interface must have a gateway IP') raise n_exc.BadRequest(resource='router', msg=msg) @@ -796,7 +843,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase, if add_by_port: port_id = interface_info['port_id'] - port = self._check_router_port(context, port_id, '') + port = self._check_router_port(context, port_id, '', + router_id=router_id) revert_value = {'device_id': '', 'device_owner': port['device_owner']} with p_utils.update_port_on_error( diff --git a/neutron/db/l3_dvr_db.py b/neutron/db/l3_dvr_db.py index 39d8f3925ab..ea7c51c7c9b 100644 --- a/neutron/db/l3_dvr_db.py +++ b/neutron/db/l3_dvr_db.py @@ -319,7 +319,8 @@ class L3_NAT_with_dvr_db_mixin(l3_db.L3_NAT_db_mixin, if add_by_port: port_id = interface_info['port_id'] - port = self._check_router_port(context, port_id, '') + port = self._check_router_port(context, port_id, '', + router_id=router_id) revert_value = {'device_id': '', 'device_owner': port['device_owner']} with p_utils.update_port_on_error( diff --git a/neutron/extensions/l3.py b/neutron/extensions/l3.py index a8d5029bed4..325db2a1bfb 100644 --- a/neutron/extensions/l3.py +++ b/neutron/extensions/l3.py @@ -79,6 +79,11 @@ class RouterExternalGatewayInUseByFloatingIp(nexception.InUse): "gateway to external network %(net_id)s is required by one or " "more floating IPs.") + +class RouterInterfaceAttachmentConflict(nexception.Conflict): + message = _("Error %(reason)s while attempting the operation.") + + ROUTERS = 'routers' FLOATINGIP = 'floatingip' FLOATINGIPS = '%ss' % FLOATINGIP diff --git a/neutron/tests/functional/services/l3_router/test_l3_dvr_router_plugin.py b/neutron/tests/functional/services/l3_router/test_l3_dvr_router_plugin.py index 912abb88207..77579972012 100644 --- a/neutron/tests/functional/services/l3_router/test_l3_dvr_router_plugin.py +++ b/neutron/tests/functional/services/l3_router/test_l3_dvr_router_plugin.py @@ -16,6 +16,9 @@ import mock from neutron_lib import constants from neutron.api.rpc.handlers import l3_rpc +from neutron.callbacks import events +from neutron.callbacks import registry +from neutron.callbacks import resources from neutron.common import topics from neutron import context from neutron.extensions import external_net @@ -1533,6 +1536,41 @@ class L3DvrTestCase(ml2_test_base.ML2TestFramework): self.assertEqual(1, len(agents['agents'])) self.assertEqual(self.l3_agent['id'], agents['agents'][0]['id']) + def test_add_router_interface_by_subnet_notification(self): + notif_handler = mock.Mock() + registry.subscribe(notif_handler.callback, + resources.ROUTER_INTERFACE, + events.BEFORE_CREATE) + router = self._create_router() + with self.network() as net, \ + self.subnet(network=net) as subnet: + self.l3_plugin.add_router_interface( + self.context, router['id'], + {'subnet_id': subnet['subnet']['id']}) + kwargs = {'context': self.context, 'router_id': router['id'], + 'network_id': net['network']['id']} + notif_handler.callback.assert_called_once_with( + resources.ROUTER_INTERFACE, events.BEFORE_CREATE, + mock.ANY, **kwargs) + + def test_add_router_interface_by_port_notification(self): + notif_handler = mock.Mock() + registry.subscribe(notif_handler.callback, + resources.ROUTER_INTERFACE, + events.BEFORE_CREATE) + router = self._create_router() + with self.network() as net, \ + self.subnet(network=net) as subnet, \ + self.port(subnet=subnet) as port: + self.l3_plugin.add_router_interface( + self.context, router['id'], + {'port_id': port['port']['id']}) + kwargs = {'context': self.context, 'router_id': router['id'], + 'network_id': net['network']['id']} + notif_handler.callback.assert_called_once_with( + resources.ROUTER_INTERFACE, events.BEFORE_CREATE, + mock.ANY, **kwargs) + class L3DvrTestCaseMigration(L3DvrTestCase): def test_update_router_db_centralized_to_distributed_with_ports(self): diff --git a/neutron/tests/unit/db/test_l3_db.py b/neutron/tests/unit/db/test_l3_db.py index faa9658fdd7..44d5a34724c 100644 --- a/neutron/tests/unit/db/test_l3_db.py +++ b/neutron/tests/unit/db/test_l3_db.py @@ -227,6 +227,18 @@ class TestL3_NAT_dbonly_mixin(base.BaseTestCase): mock_get_assoc_data.assert_called_once_with( mock.ANY, fip, floatingip_db) + def test__notify_attaching_interface(self): + with mock.patch.object(l3_db.registry, 'notify') as mock_notify: + context = mock.MagicMock() + router_id = 'router_id' + net_id = 'net_id' + self.db._notify_attaching_interface(context, router_id, net_id) + kwargs = {'context': context, 'router_id': router_id, + 'network_id': net_id} + mock_notify.assert_called_once_with( + resources.ROUTER_INTERFACE, events.BEFORE_CREATE, self.db, + **kwargs) + class L3_NAT_db_mixin(base.BaseTestCase): def setUp(self): diff --git a/neutron/tests/unit/db/test_l3_dvr_db.py b/neutron/tests/unit/db/test_l3_dvr_db.py index e43fc0f0b39..4b0a5876b70 100644 --- a/neutron/tests/unit/db/test_l3_dvr_db.py +++ b/neutron/tests/unit/db/test_l3_dvr_db.py @@ -653,6 +653,37 @@ class L3DvrTestCase(test_db_base_plugin_v2.NeutronDbPluginV2TestCase): mock_notify.assert_called_once_with( 'router', 'before_update', self.mixin, **kwargs) + def test_validate_add_router_interface_by_subnet_notify_advanced_services( + self): + router = {'name': 'foo_router', 'admin_state_up': False} + router_db = self._create_router(router) + with self.network() as net, \ + self.subnet(network={'network': net['network']}) as sub, \ + mock.patch.object( + self.mixin, + '_notify_attaching_interface') as mock_notify: + interface_info = {'subnet_id': sub['subnet']['id']} + self.mixin.add_router_interface(self.ctx, router_db.id, + interface_info) + mock_notify.assert_called_once_with(self.ctx, router_db.id, + sub['subnet']['network_id']) + + def test_validate_add_router_interface_by_port_notify_advanced_services( + self): + router = {'name': 'foo_router', 'admin_state_up': False} + router_db = self._create_router(router) + with self.network() as net, \ + self.subnet(network={'network': net['network']}) as sub, \ + self.port(subnet=sub) as port, \ + mock.patch.object( + self.mixin, + '_notify_attaching_interface') as mock_notify: + interface_info = {'port_id': port['port']['id']} + self.mixin.add_router_interface(self.ctx, router_db.id, + interface_info) + mock_notify.assert_called_once_with(self.ctx, router_db.id, + net['network']['id']) + def _test_update_arp_entry_for_dvr_service_port( self, device_owner, action): router_dict = {'name': 'test_router', 'admin_state_up': True,