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:
Miguel Lavalle
2024-04-30 20:15:23 -05:00
parent ca5b290f09
commit 44cbbba369
3 changed files with 182 additions and 0 deletions

View File

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

View File

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

View File

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