User defined router flavor driver with no LSP
There is a use case where a user defined router flavor requires router interfaces that don't have a corresponding OVN LSP. In this use case, Neutron acts only as an IP address manager for the router interfaces. This change adds a user defined router flavor driver that implements the described use case. The new functionality is completely contained in the new driver, with no logic added to the rest of ML2/OVN. This is accomplished as follows: 1) When an interface is added to a router, the driver deletes the LSP and the OVN revision number. 2) When an interface is about to be removed from a router, the driver re-creates the LSP and the OVN revision number. In this way, ML2/OVN can later delete the port normally. Closes-Bug: #2078382 Change-Id: I14d675af2da281cc5cd435cae947ccdb13ece12b
This commit is contained in:
@@ -19,12 +19,15 @@ from neutron_lib.callbacks import priority_group
|
||||
from neutron_lib.callbacks import registry
|
||||
from neutron_lib.callbacks import resources
|
||||
from neutron_lib import constants as const
|
||||
from neutron_lib.db import api as db_api
|
||||
from neutron_lib.exceptions import l3 as l3_exc
|
||||
from neutron_lib.plugins import constants as plugin_constants
|
||||
from neutron_lib.plugins import directory
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from neutron.db import l3_attrs_db
|
||||
from neutron.objects import router
|
||||
from neutron.services.l3_router.service_providers import base
|
||||
|
||||
|
||||
@@ -194,3 +197,63 @@ class UserDefined(base.L3ServiceProvider):
|
||||
return
|
||||
LOG.debug('Got request to update the status of a floating ip '
|
||||
'associated to a router of user defined flavor %s', fip)
|
||||
|
||||
|
||||
@registry.has_registry_receivers
|
||||
class UserDefinedNoLsp(UserDefined):
|
||||
|
||||
@registry.receives(resources.ROUTER_INTERFACE, [events.AFTER_CREATE])
|
||||
def _process_add_router_interface(self, resource, event, trigger, payload):
|
||||
router = payload.states[0]
|
||||
context = payload.context
|
||||
if not self._is_user_defined_provider(context, router):
|
||||
return
|
||||
port = payload.metadata['port']
|
||||
subnets = payload.metadata['subnets']
|
||||
router_interface_info = self.l3plugin._make_router_interface_info(
|
||||
router.id, port['tenant_id'], port['id'], port['network_id'],
|
||||
subnets[-1]['id'], [subnet['id'] for subnet in subnets])
|
||||
self.l3plugin._ovn_client.delete_port(context, port['id'],
|
||||
port_object=port)
|
||||
LOG.debug('Got request to add interface %s to a user defined flavor '
|
||||
'router with id %s. The OVN LSP was deleted.',
|
||||
router_interface_info, router.id)
|
||||
|
||||
def _get_port_to_recreate(self, context, router_id, subnet_id):
|
||||
with db_api.CONTEXT_READER.using(context):
|
||||
objs = router.RouterPort.get_objects(
|
||||
context, router_id=router_id,
|
||||
port_type=const.DEVICE_OWNER_ROUTER_INTF)
|
||||
router_ports = [
|
||||
self.l3plugin._plugin._make_port_dict(rp.db_obj.port)
|
||||
for rp in objs]
|
||||
for rp in router_ports:
|
||||
for ip in rp['fixed_ips']:
|
||||
if ip['subnet_id'] == subnet_id:
|
||||
return rp
|
||||
raise l3_exc.RouterInterfaceNotFoundForSubnet(router_ports=router_id,
|
||||
subnet_id=subnet_id)
|
||||
|
||||
@registry.receives(resources.ROUTER_INTERFACE, [events.BEFORE_DELETE])
|
||||
def _process_before_remove_router_interface(self, resource, event, trigger,
|
||||
payload):
|
||||
context = payload.context
|
||||
router_id = payload.resource_id
|
||||
router_obj = router.Router.get_object(context, id=router_id)
|
||||
if not self._is_user_defined_provider(context, router_obj):
|
||||
return
|
||||
subnet_id = payload.metadata['subnet_id']
|
||||
port = self._get_port_to_recreate(context, router_id, subnet_id)
|
||||
|
||||
# If the LSP exists, the interface is being removed by port and the
|
||||
# port has fixed ips in more than one subnet, then the port has been
|
||||
# recreated in a previous notification of this event. Do nothing
|
||||
nbdb_idl = self.l3plugin._ovn_client._nb_idl
|
||||
if nbdb_idl.lookup('Logical_Switch_Port', port['id'], default=None):
|
||||
return
|
||||
|
||||
# The ovn client creates the revision row when creating the LSP
|
||||
self.l3plugin._ovn_client.create_port(context, port)
|
||||
LOG.debug('Recreated OVN LSP for port with id %s before removing '
|
||||
'interface for user defined flavor router with id %s',
|
||||
port['id'], router_id)
|
||||
|
@@ -15,9 +15,11 @@ from unittest import mock
|
||||
|
||||
from neutron_lib.callbacks import events
|
||||
from neutron_lib import constants as const
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from neutron.db.models import l3
|
||||
from neutron.db.models import l3_attrs
|
||||
from neutron.objects import router as l3_obj
|
||||
from neutron.services.ovn_l3.service_providers import user_defined
|
||||
from neutron.tests.unit import testlib_api
|
||||
|
||||
@@ -159,3 +161,112 @@ class TestUserDefined(testlib_api.SqlTestCase):
|
||||
self.provider._process_precommit_router_create('resource', 'event',
|
||||
self, payload)
|
||||
self.assertTrue(self.router['extra_attributes'].ha)
|
||||
|
||||
|
||||
class TestUserDefinedNoLsp(testlib_api.SqlTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestUserDefinedNoLsp, self).setUp()
|
||||
self.setup_coreplugin(DB_PLUGIN_KLASS)
|
||||
self.fake_l3 = mock.MagicMock()
|
||||
self.fake_l3._make_router_interface_info = mock.MagicMock(
|
||||
return_value='router_interface_info')
|
||||
self.provider = user_defined.UserDefinedNoLsp(self.fake_l3)
|
||||
self.context = mock.MagicMock()
|
||||
self.router = l3.Router(id='fake-uuid',
|
||||
flavor_id='fake-uuid')
|
||||
mock_flavor_plugin = mock.MagicMock()
|
||||
mock_flavor_plugin.get_flavor = mock.MagicMock(
|
||||
return_value={'id': 'fake-uuid'})
|
||||
mock_flavor_plugin.get_flavor_next_provider = mock.MagicMock(
|
||||
return_value=[{'driver': self.provider._user_defined_provider}])
|
||||
self.provider._flavor_plugin_ref = mock_flavor_plugin
|
||||
|
||||
@mock.patch.object(user_defined.LOG, 'debug')
|
||||
def test__add_router_interface(self, log_mock):
|
||||
payload = events.DBEventPayload(
|
||||
self.context,
|
||||
states=(self.router, self.router),
|
||||
resource_id=self.router['id'],
|
||||
metadata={'subnet_ids': ['subnet-id'],
|
||||
'port': {'tenant_id': 'tenant-id',
|
||||
'id': 'id',
|
||||
'network_id': 'network-id'},
|
||||
'subnets': [{'id': 'id'}]})
|
||||
fl_plg = self.provider._flavor_plugin_ref
|
||||
l3_plg = self.fake_l3
|
||||
self.provider._process_add_router_interface('resource',
|
||||
'event',
|
||||
self,
|
||||
payload)
|
||||
l3_plg._make_router_interface_info.assert_called_once()
|
||||
fl_plg.get_flavor.assert_called_once()
|
||||
fl_plg.get_flavor_next_provider.assert_called_once()
|
||||
l3_plg._ovn_client.delete_port.assert_called_once()
|
||||
log_mock.assert_called_once()
|
||||
|
||||
@mock.patch.object(l3_obj.RouterPort, 'get_objects')
|
||||
@mock.patch.object(l3_obj.Router, 'get_object')
|
||||
@mock.patch.object(user_defined.LOG, 'debug')
|
||||
def _test__process_before_remove_router_interface(self, port_exists,
|
||||
log_mock, grouter_mock,
|
||||
get_objects_mock):
|
||||
payload = events.DBEventPayload(
|
||||
self.context,
|
||||
resource_id=self.router['id'],
|
||||
metadata={'subnet_id': 'subnet-id'})
|
||||
grouter_mock.return_value = {'id': 'fake-uuid',
|
||||
'flavor_id': 'fake-uuid'}
|
||||
nbdb_idl_mock = mock.MagicMock()
|
||||
nbdb_idl_mock.lookup = mock.MagicMock()
|
||||
if port_exists:
|
||||
nbdb_idl_mock.lookup.return_value = 'a-port'
|
||||
else:
|
||||
nbdb_idl_mock.lookup.return_value = None
|
||||
self.fake_l3._ovn_client = mock.MagicMock()
|
||||
self.fake_l3._ovn_client._nb_idl = nbdb_idl_mock
|
||||
|
||||
port_id = uuidutils.generate_uuid()
|
||||
other_port_id = uuidutils.generate_uuid()
|
||||
rp = l3_obj.RouterPort(
|
||||
self.context,
|
||||
port_id=port_id,
|
||||
router_id=uuidutils.generate_uuid(),
|
||||
port_type=const.DEVICE_OWNER_ROUTER_INTF)
|
||||
rp.create()
|
||||
other_rp = l3_obj.RouterPort(
|
||||
self.context,
|
||||
port_id=other_port_id,
|
||||
router_id=uuidutils.generate_uuid(),
|
||||
port_type=const.DEVICE_OWNER_ROUTER_INTF)
|
||||
other_rp.create()
|
||||
get_objects_mock.return_value = [rp, other_rp]
|
||||
|
||||
gen_ports = (port for port in
|
||||
[{'id': other_port_id,
|
||||
'fixed_ips': [{'subnet_id': 'other-subnet-id'}],
|
||||
'standard_attr_id': 'standard_attr_id'},
|
||||
{'id': port_id,
|
||||
'fixed_ips': [{'subnet_id': 'subnet-id'}],
|
||||
'standard_attr_id': 'standard_attr_id'}])
|
||||
self.fake_l3._plugin = mock.MagicMock()
|
||||
self.fake_l3._plugin._make_port_dict = mock.MagicMock(
|
||||
side_effect=lambda p: next(gen_ports))
|
||||
|
||||
self.provider._process_before_remove_router_interface('resource',
|
||||
'event',
|
||||
self,
|
||||
payload)
|
||||
|
||||
if port_exists:
|
||||
self.fake_l3._ovn_client.create_port.assert_not_called()
|
||||
log_mock.assert_not_called()
|
||||
else:
|
||||
self.fake_l3._ovn_client.create_port.assert_called_once()
|
||||
log_mock.assert_called_once()
|
||||
|
||||
def test__process_before_remove_router_interface_port_exists(self):
|
||||
self._test__process_before_remove_router_interface(True)
|
||||
|
||||
def test__process_before_remove_router_interface_port_doesnt_exist(self):
|
||||
self._test__process_before_remove_router_interface(False)
|
||||
|
@@ -0,0 +1,8 @@
|
||||
---
|
||||
features:
|
||||
- A new sample OVN user defined router flavor driver has been added that
|
||||
enables the creation of router interfaces with no associated underlying
|
||||
Logical Switch Ports. In this scenario, Neutron only acts as the
|
||||
IP address manager for the router interfaces. This enables user defined
|
||||
router flavors to have total control of the traffic traversing the
|
||||
router interfaces while bypassing the OVN processing.
|
Reference in New Issue
Block a user