Send a callback before attaching a subnet to a router

Attaching a subnet to a router may be forbidden in some cases.
To prevent this attachment, we need to send a BEFORE_CREATE event
with "router_interface" as resource, and reraise any exception
that tells neutron a third party code forbid this attachment.

Change-Id: Ia71e0c5e9e3e073053ead7e1e7c3040087f5ee13
Closes-bug: 1537091
This commit is contained in:
Mathieu Rohon 2016-09-12 07:58:00 +00:00
parent b05549f671
commit 71dd840d30
6 changed files with 138 additions and 3 deletions

View File

@ -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(

View File

@ -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(

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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,