diff --git a/neutron/common/ovn/constants.py b/neutron/common/ovn/constants.py index edafc44fe9e..736d1eb1f66 100644 --- a/neutron/common/ovn/constants.py +++ b/neutron/common/ovn/constants.py @@ -26,9 +26,11 @@ OVN_PORT_NAME_EXT_ID_KEY = 'neutron:port_name' OVN_PORT_EXT_ID_KEY = 'neutron:port_id' OVN_PORT_FIP_EXT_ID_KEY = 'neutron:port_fip' OVN_ROUTER_NAME_EXT_ID_KEY = 'neutron:router_name' +OVN_ROUTER_ID_EXT_ID_KEY = 'neutron:router_id' OVN_AZ_HINTS_EXT_ID_KEY = 'neutron:availability_zone_hints' OVN_ROUTER_IS_EXT_GW = 'neutron:is_ext_gw' OVN_GW_PORT_EXT_ID_KEY = 'neutron:gw_port_id' +OVN_GW_NETWORK_EXT_ID_KEY = 'neutron:gw_network_id' OVN_SUBNET_EXT_ID_KEY = 'neutron:subnet_id' OVN_SUBNET_EXT_IDS_KEY = 'neutron:subnet_ids' OVN_PHYSNET_EXT_ID_KEY = 'neutron:provnet-physical-network' diff --git a/neutron/common/ovn/extensions.py b/neutron/common/ovn/extensions.py index 590c461e916..46a2fe4b59b 100644 --- a/neutron/common/ovn/extensions.py +++ b/neutron/common/ovn/extensions.py @@ -52,6 +52,7 @@ from neutron_lib.api.definitions import provider_net from neutron_lib.api.definitions import qos from neutron_lib.api.definitions import qos_bw_limit_direction from neutron_lib.api.definitions import qos_default +from neutron_lib.api.definitions import qos_gateway_ip from neutron_lib.api.definitions import qos_rule_type_details from neutron_lib.api.definitions import qos_rule_type_filter from neutron_lib.api.definitions import qos_rules_alias @@ -87,6 +88,7 @@ ML2_SUPPORTED_API_EXTENSIONS_OVN_L3 = [ floatingip_pools.ALIAS, pagination.ALIAS, 'qos-fip', + qos_gateway_ip.ALIAS, sorting.ALIAS, project_id.ALIAS, dns.ALIAS, diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/qos.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/qos.py index 66d01009238..81e8203a6a8 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/qos.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/qos.py @@ -15,6 +15,7 @@ from neutron.objects.qos import binding as qos_binding from neutron.objects.qos import policy as qos_policy from neutron.objects.qos import rule as qos_rule +from neutron_lib.api.definitions import l3 as l3_api from neutron_lib import constants from neutron_lib import context as n_context from neutron_lib.plugins import constants as plugins_const @@ -118,7 +119,7 @@ class OVNClientQosExtension(object): def _ovn_qos_rule(self, rules_direction, rules, port_id, network_id, fip_id=None, ip_address=None, resident_port=None, - delete=False): + router_id=None, delete=False): """Generate an OVN QoS register based on several Neutron QoS rules A OVN QoS register can contain "bandwidth" and "action" parameters. @@ -142,6 +143,8 @@ class OVNClientQosExtension(object): :param resident_port: (string) for L3 floating IP bandwidth, this is a logical switch port located in the chassis where the floating IP traffic is NATed. + :param router_id: (string) router ID, for L3 router gateway port + bandwidth limit. :param delete: (bool) defines if this rule if going to be a partial one (without any bandwidth or DSCP information) to be used only as deletion rule. @@ -168,8 +171,13 @@ class OVNClientQosExtension(object): # All OVN QoS rules have an external ID reference to the port or the # FIP that are attached to. + # 1) L3 floating IP ports. if fip_id: key, value = ovn_const.OVN_FIP_EXT_ID_KEY, fip_id + # 2) L3 router gateway port. + elif router_id: + key, value = ovn_const.OVN_ROUTER_ID_EXT_ID_KEY, router_id + # 3) Fixed IP ports (aka VM ports) else: key, value = ovn_const.OVN_PORT_EXT_ID_KEY, port_id ovn_qos_rule['external_ids'] = {key: value} @@ -365,6 +373,49 @@ class OVNClientQosExtension(object): def disassociate_floatingip(self, txn, floatingip): self.delete_floatingip(txn, floatingip) + def _delete_gateway_ip_qos_rules(self, txn, router_id, network_id): + if network_id: + lswitch_name = utils.ovn_name(network_id) + txn.add(self.nb_idl.qos_del_ext_ids( + lswitch_name, + {ovn_const.OVN_ROUTER_ID_EXT_ID_KEY: router_id})) + + def create_router(self, txn, router): + self.update_router(txn, router) + + def update_router(self, txn, router): + gw_info = router.get(l3_api.EXTERNAL_GW_INFO) or {} + qos_policy_id = gw_info.get('qos_policy_id') + router_id = router.get('id') + gw_port_id = router.get('gw_port_id') + gw_network_id = gw_info.get('network_id') + if not (router_id and gw_port_id and gw_network_id): + # NOTE(ralonsoh): when the gateway network is detached, the gateway + # port is deleted. Any QoS policy related to this port_id is + # deleted in "self.update_port()". + LOG.debug('Router %s does not have ID or gateway assigned', router) + return + + admin_context = n_context.get_admin_context() + qos_rules = self._qos_rules(admin_context, qos_policy_id) + for direction, rules in qos_rules.items(): + # "delete=not rule": that means, when we don't have rules, we + # generate a "ovn_rule" to be used as input in a "qos_del" method. + ovn_rule = self._ovn_qos_rule( + direction, rules, gw_port_id, gw_network_id, + router_id=router_id, delete=not rules) + if rules: + # NOTE(ralonsoh): with "may_exist=True", the "qos_add" will + # create the QoS OVN rule or update the existing one. + txn.add(self.nb_idl.qos_add(**ovn_rule, may_exist=True)) + else: + # Delete, if exists, the QoS rule in this direction. + txn.add(self.nb_idl.qos_del(**ovn_rule, if_exists=True)) + + def delete_router(self, txn, router): + self._delete_gateway_ip_qos_rules(txn, router['id'], + router['gw_network_id']) + def update_policy(self, context, policy): updated_port_ids = set([]) updated_fip_ids = set([]) @@ -399,3 +450,8 @@ class OVNClientQosExtension(object): for fip in self._plugin_l3.get_floatingips( context, filters={'id': fip_ids}): self.update_floatingip(txn, fip) + + for router_binding in policy.get_bound_routers(): + router = self._plugin_l3.get_router(context, + router_binding.router_id) + self.update_router(txn, router) diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py index e4cb7fc747c..662ba68a771 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py @@ -734,6 +734,39 @@ class DBInconsistenciesPeriodics(SchemaAwarePeriodicsBase): txn.add(cmd) raise periodics.NeverAgain() + # TODO(ralonsoh): Remove this in the Z+3 cycle. This method adds the + # "external_ids:OVN_GW_NETWORK_EXT_ID_KEY" to each router that has + # a gateway (that means, that has "external_ids:OVN_GW_PORT_EXT_ID_KEY"). + # A static spacing value is used here, but this method will only run + # once per lock due to the use of periodics.NeverAgain(). + @periodics.periodic(spacing=600, run_immediately=True) + def update_logical_router_with_gateway_network_id(self): + """Update all OVN logical router registers with the GW network ID""" + if not self.has_lock: + return + + cmds = [] + context = n_context.get_admin_context() + for lr in self._nb_idl.lr_list().execute(check_error=True): + gw_port = lr.external_ids.get(ovn_const.OVN_GW_PORT_EXT_ID_KEY) + gw_net = lr.external_ids.get(ovn_const.OVN_GW_NETWORK_EXT_ID_KEY) + if not gw_port or (gw_port and gw_net): + # This router does not have a gateway network assigned yet or + # it has a gateway port and its corresponding network. + continue + + port = self._ovn_client._plugin.get_port(context, gw_port) + external_ids = { + ovn_const.OVN_GW_NETWORK_EXT_ID_KEY: port['network_id']} + cmds.append(self._nb_idl.db_set( + 'Logical_Router', lr.uuid, ('external_ids', external_ids))) + + if cmds: + with self._nb_idl.transaction(check_error=True) as txn: + for cmd in cmds: + txn.add(cmd) + raise periodics.NeverAgain() + class HashRingHealthCheckPeriodics(object): diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py index b7270039722..133701cc663 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py @@ -1278,6 +1278,8 @@ class OVNClient(object): return networks def _gen_router_ext_ids(self, router): + gw_net_id = (router.get('external_gateway_info') or + {}).get('network_id') or '' return { ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: router.get('name', 'no_router_name'), @@ -1286,7 +1288,9 @@ class OVNClient(object): ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(utils.get_revision_number( router, ovn_const.TYPE_ROUTERS)), ovn_const.OVN_AZ_HINTS_EXT_ID_KEY: - ','.join(common_utils.get_az_hints(router))} + ','.join(common_utils.get_az_hints(router)), + ovn_const.OVN_GW_NETWORK_EXT_ID_KEY: gw_net_id, + } def create_router(self, context, router, add_external_gateway=True): """Create a logical router.""" @@ -1311,6 +1315,8 @@ class OVNClient(object): added_gw_port = self._add_router_ext_gw( router, networks, txn) + self._qos_driver.create_router(txn, router) + if added_gw_port: db_rev.bump_revision(context, added_gw_port, ovn_const.TYPE_ROUTER_PORTS) @@ -1381,6 +1387,7 @@ class OVNClient(object): old_routes, routes) self.update_router_routes( context, router_id, added, removed, txn=txn) + self._qos_driver.update_router(txn, new_router) if check_rev_cmd.result == ovn_const.TXN_COMMITTED: db_rev.bump_revision(context, new_router, @@ -1403,8 +1410,13 @@ class OVNClient(object): def delete_router(self, context, router_id): """Delete a logical router.""" lrouter_name = utils.ovn_name(router_id) + ovn_router = self._nb_idl.get_lrouter(lrouter_name) + gw_network_id = ovn_router.external_ids.get( + ovn_const.OVN_GW_NETWORK_EXT_ID_KEY) if ovn_router else None + router_dict = {'id': router_id, 'gw_network_id': gw_network_id} with self._nb_idl.transaction(check_error=True) as txn: txn.add(self._nb_idl.delete_lrouter(lrouter_name)) + self._qos_driver.delete_router(txn, router_dict) db_rev.delete_revision(context, router_id, ovn_const.TYPE_ROUTERS) def get_candidates_for_scheduling(self, physnet, cms=None, diff --git a/neutron/services/ovn_l3/plugin.py b/neutron/services/ovn_l3/plugin.py index 8325995650a..1a63de6e21d 100644 --- a/neutron/services/ovn_l3/plugin.py +++ b/neutron/services/ovn_l3/plugin.py @@ -16,6 +16,7 @@ from neutron_lib.api.definitions import external_net from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import provider_net as pnet from neutron_lib.api.definitions import qos_fip as qos_fip_apidef +from neutron_lib.api.definitions import qos_gateway_ip as qos_gateway_ip_apidef from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources @@ -40,6 +41,7 @@ from neutron.db import extraroute_db from neutron.db import l3_fip_pools_db from neutron.db import l3_fip_port_details from neutron.db import l3_fip_qos +from neutron.db import l3_gateway_ip_qos from neutron.db import l3_gwmode_db from neutron.db.models import l3 as l3_models from neutron.db import ovn_revision_numbers_db as db_rev @@ -62,6 +64,7 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase, l3_fip_port_details.Fip_port_details_db_mixin, router_az_db.RouterAvailabilityZoneMixin, l3_fip_qos.FloatingQoSDbMixin, + l3_gateway_ip_qos.L3_gw_ip_qos_db_mixin, l3_fip_pools_db.FloatingIPPoolsMixin, ): """Implementation of the OVN L3 Router Service Plugin. @@ -98,17 +101,20 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase, events.PRECOMMIT_CREATE) @staticmethod - def disable_qos_fip_extension_by_extension_drivers(aliases): + def _disable_qos_extensions_by_extension_drivers(aliases): qos_service_plugin = directory.get_plugin(plugin_constants.QOS) - qos_aliases = qos_fip_apidef.ALIAS in aliases - if not qos_service_plugin and qos_aliases: + qos_fip_in_aliases = qos_fip_apidef.ALIAS in aliases + qos_gwip_in_aliases = qos_gateway_ip_apidef.ALIAS in aliases + if not qos_service_plugin and qos_fip_in_aliases: aliases.remove(qos_fip_apidef.ALIAS) + if not qos_service_plugin and qos_gwip_in_aliases: + aliases.remove(qos_gateway_ip_apidef.ALIAS) @property def supported_extension_aliases(self): if not hasattr(self, '_aliases'): self._aliases = self._supported_extension_aliases[:] - self.disable_qos_fip_extension_by_extension_drivers(self._aliases) + self._disable_qos_extensions_by_extension_drivers(self._aliases) return self._aliases @property diff --git a/neutron/tests/unit/fake_resources.py b/neutron/tests/unit/fake_resources.py index 492a3216dcf..8b2e61d1e37 100644 --- a/neutron/tests/unit/fake_resources.py +++ b/neutron/tests/unit/fake_resources.py @@ -152,6 +152,7 @@ class FakeOvsdbNbOvnIdl(object): self.ls_get = mock.Mock() self.check_liveness = mock.Mock() self.ha_chassis_group_get = mock.Mock() + self.qos_del = mock.Mock() self.qos_del_ext_ids = mock.Mock() self.meter_add = mock.Mock() self.meter_del = mock.Mock() diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_qos.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_qos.py index 8514c7dbeee..793f8488619 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_qos.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_qos.py @@ -12,11 +12,14 @@ # License for the specific language governing permissions and limitations # under the License. +import random from unittest import mock import netaddr +from neutron_lib.api.definitions import l3 as l3_apidef from neutron_lib.api.definitions import portbindings as portbindings_api from neutron_lib.api.definitions import qos as qos_api +from neutron_lib.api.definitions import qos_fip as qos_fip_apidef from neutron_lib import constants from neutron_lib import context from neutron_lib.db import api as db_api @@ -27,6 +30,8 @@ from oslo_utils import uuidutils from neutron.api import extensions from neutron.common.ovn import constants as ovn_const from neutron.core_extensions import qos as core_qos +from neutron.db import l3_fip_qos +from neutron.db import l3_gateway_ip_qos from neutron.objects import network as network_obj from neutron.objects import ports as port_obj from neutron.objects.qos import policy as policy_obj @@ -34,6 +39,7 @@ from neutron.objects.qos import rule as rule_obj from neutron.objects import router as router_obj from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \ import qos as qos_extension +from neutron.tests.unit.extensions import test_l3 from neutron.tests.unit.plugins.ml2 import test_plugin @@ -53,12 +59,21 @@ class _Context(object): return +class TestFloatingIPQoSL3NatServicePlugin( + test_l3.TestL3NatServicePlugin, + l3_fip_qos.FloatingQoSDbMixin, + l3_gateway_ip_qos.L3_gw_ip_qos_db_mixin): + supported_extension_aliases = [l3_apidef.ALIAS, 'qos', + qos_fip_apidef.ALIAS] + + class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase): CORE_PLUGIN_CLASS = 'neutron.plugins.ml2.plugin.Ml2Plugin' _extension_drivers = [qos_api.ALIAS] - l3_plugin = ('neutron.tests.unit.extensions.test_qos_fip.' - 'TestFloatingIPQoSL3NatServicePlugin') + l3_plugin = ('neutron.tests.unit.plugins.ml2.drivers.ovn.' + 'mech_driver.ovsdb.extensions.' + 'test_qos.TestFloatingIPQoSL3NatServicePlugin') def setUp(self): cfg.CONF.set_override('extension_drivers', self._extension_drivers, @@ -101,21 +116,37 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase): return port def _create_one_router(self): - self.router_gw_port = self._create_one_port(2000, self.fips_network.id) - self.router = router_obj.Router(self.ctx, id=uuidutils.generate_uuid(), - gw_port_id=self.router_gw_port.id) - self.router.create() + network = network_obj.Network( + self.ctx, id=uuidutils.generate_uuid(), project_id=self.project_id) + network.create() + router_gw_port = self._create_one_port(random.randint(1, 10000), + network.id) + router = router_obj.Router(self.ctx, id=uuidutils.generate_uuid(), + gw_port_id=router_gw_port.id) + router.create() + return router, network + + def _update_router_qos(self, router_id, qos_policy_id, attach=True): + # NOTE(ralonsoh): router QoS policy is not yet implemented in Router + # OVO. Once we have this feature, this method can be removed. + qos = policy_obj.QosPolicy.get_policy_obj(self.ctx, qos_policy_id) + if attach: + qos.attach_router(router_id) + else: + qos.detach_router(router_id) + + def _get_router(self, router_id): + return self.qos_driver._plugin_l3.get_router(self.ctx, router_id) def _initialize_objs(self): self.qos_policies = [] self.ports = [] self.networks = [] self.fips = [] - self.fips_network = network_obj.Network( - self.ctx, id=uuidutils.generate_uuid(), project_id=self.project_id) - self.fips_network.create() - self._create_one_router() + self.router_fips, self.fips_network = self._create_one_router() self.fips_ports = [] + self.routers = [] + self.router_networks = [] fip_cidr = netaddr.IPNetwork('10.10.0.0/24') for net_idx in range(2): @@ -154,6 +185,10 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase): self.ports.append( self._create_one_port(net_idx * 16 + port_idx, network.id)) + router, router_network = self._create_one_router() + self.routers.append(router) + self.router_networks.append(router_network) + @mock.patch.object(qos_extension.LOG, 'warning') @mock.patch.object(rule_obj, 'get_rules') def test__qos_rules(self, mock_get_rules, mock_warning): @@ -479,6 +514,8 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase): - port22: qos_policy1 --> handled during "update_network", not updated fip1: qos_policy0 fip2: qos_policy1 + router1: qos_policy0 + router2: qos_policy1 """ self.ports[1].qos_policy_id = self.qos_policies[0].id self.ports[1].update() @@ -494,11 +531,15 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase): self.fips[0].update() self.fips[1].qos_policy_id = self.qos_policies[1].id self.fips[1].update() + self._update_router_qos(self.routers[0].id, self.qos_policies[0].id) + self._update_router_qos(self.routers[1].id, self.qos_policies[1].id) mock_qos_rules = mock.Mock() with mock.patch.object(self.qos_driver, '_qos_rules', return_value=mock_qos_rules), \ mock.patch.object(self.qos_driver, 'update_floatingip') as \ - mock_update_fip: + mock_update_fip, \ + mock.patch.object(self.qos_driver, 'update_router') as \ + mock_update_router: self.qos_driver.update_policy(self.ctx, self.qos_policies[0]) updated_ports = [self.ports[1], self.ports[3], self.ports[4]] calls = [mock.call(self.txn, port.id, port.network_id, @@ -512,6 +553,11 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase): self.fips[0].id) mock_update_fip.assert_called_once_with(self.txn, fip) + with db_api.CONTEXT_READER.using(self.ctx): + router = self.qos_driver._plugin_l3.get_router(self.ctx, + self.routers[0].id) + mock_update_router.assert_called_once_with(self.txn, router) + def test_update_floatingip(self): # NOTE(ralonsoh): this rule will always apply: # - If the FIP is being deleted, "qos_del_ext_ids" is called; @@ -531,7 +577,7 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase): nb_idl.reset_mock() # Attach a port and a router, not QoS policy - fip.router_id = self.router.id + fip.router_id = self.router_fips.id fip.fixed_port_id = self.fips_ports[0].id fip.update() self.qos_driver.update_floatingip(txn, fip) @@ -587,7 +633,7 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase): fip.router_id = None fip.fixed_port_id = None fip.update() - original_fip.router_id = self.router.id + original_fip.router_id = self.router_fips.id original_fip.fixed_port_id = self.fips_ports[0].id original_fip.qos_policy_id = self.qos_policies[1].id original_fip.update() @@ -604,3 +650,30 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase): nb_idl.qos_del_ext_ids.assert_called_once() nb_idl.qos_add.assert_not_called() nb_idl.qos_del.assert_not_called() + + def test_update_router(self): + nb_idl = self.qos_driver._driver._nb_idl + txn = mock.Mock() + + # Update router, no QoS policy set. + router = self._get_router(self.routers[0].id) + self.qos_driver.update_router(txn, router) + nb_idl.qos_add.assert_not_called() + self.assertEqual(2, nb_idl.qos_del.call_count) + nb_idl.reset_mock() + + # Add QoS policy. + self._update_router_qos(router['id'], self.qos_policies[0].id) + router = self._get_router(self.routers[0].id) + self.qos_driver.update_router(txn, router) + nb_idl.qos_add.assert_called_once() + nb_idl.qos_del.assert_called_once() + nb_idl.reset_mock() + + # Remove QoS + self._update_router_qos(router['id'], self.qos_policies[0].id, + attach=False) + router = self._get_router(self.routers[0].id) + self.qos_driver.update_router(txn, router) + nb_idl.qos_add.assert_not_called() + self.assertEqual(2, nb_idl.qos_del.call_count) diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py index 9015e7ba30b..ea189b6a403 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py @@ -589,3 +589,29 @@ class TestDBInconsistenciesPeriodics(testlib_api.SqlTestCaseLight, mock.call('Logical_Router_Port', 'lrp-port1', ('options', opt))] self.fake_ovn_client._nb_idl.db_set.assert_has_calls( expected_calls) + + def test_update_logical_router_with_gateway_network_id(self): + nb_idl = self.fake_ovn_client._nb_idl + # lr0: GW port ID, not GW network ID --> we need to add network ID. + lr0 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={ + 'name': 'lr0', + 'external_ids': {constants.OVN_GW_PORT_EXT_ID_KEY: 'port0'}}) + # lr1: GW port ID and not GW network ID --> register already updated. + lr1 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={ + 'name': 'lr1', + 'external_ids': {constants.OVN_GW_PORT_EXT_ID_KEY: 'port1', + constants.OVN_GW_NETWORK_EXT_ID_KEY: 'net1'}}) + # lr2: no GW port ID (nor GW network ID) --> no QoS. + lr2 = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={ + 'name': 'lr2', 'external_ids': {}}) + nb_idl.lr_list.return_value.execute.return_value = (lr0, lr1, lr2) + self.fake_ovn_client._plugin.get_port.return_value = { + 'network_id': 'net0'} + + self.assertRaises( + periodics.NeverAgain, + self.periodic.update_logical_router_with_gateway_network_id) + ext_ids = {constants.OVN_GW_NETWORK_EXT_ID_KEY: 'net0'} + expected_calls = [mock.call('Logical_Router', lr0.uuid, + ('external_ids', ext_ids))] + nb_idl.db_set.assert_has_calls(expected_calls) diff --git a/neutron/tests/unit/services/ovn_l3/test_plugin.py b/neutron/tests/unit/services/ovn_l3/test_plugin.py index c59f8a77c75..59eeda8e505 100644 --- a/neutron/tests/unit/services/ovn_l3/test_plugin.py +++ b/neutron/tests/unit/services/ovn_l3/test_plugin.py @@ -432,6 +432,7 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): self.l3_inst._nb_ovn.update_lrouter.assert_called_once_with( 'neutron-router-id', enabled=True, external_ids={ ovn_const.OVN_GW_PORT_EXT_ID_KEY: '', + ovn_const.OVN_GW_NETWORK_EXT_ID_KEY: '', ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'router', ovn_const.OVN_AZ_HINTS_EXT_ID_KEY: ''}) @@ -453,6 +454,7 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): external_ids={ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'test', ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_GW_PORT_EXT_ID_KEY: '', + ovn_const.OVN_GW_NETWORK_EXT_ID_KEY: '', ovn_const.OVN_AZ_HINTS_EXT_ID_KEY: ''}) @mock.patch.object(utils, 'get_lrouter_non_gw_routes') @@ -547,6 +549,7 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): external_ids = {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'router', ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_GW_PORT_EXT_ID_KEY: 'gw-port-id', + ovn_const.OVN_GW_NETWORK_EXT_ID_KEY: 'ext-network-id', ovn_const.OVN_AZ_HINTS_EXT_ID_KEY: ''} self.l3_inst._nb_ovn.create_lrouter.assert_called_once_with( 'neutron-router-id', external_ids=external_ids, @@ -581,6 +584,9 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase): def test_delete_router_with_ext_gw(self, gprs): self.get_router.return_value = self.fake_router_with_ext_gw self.get_subnet.return_value = self.fake_ext_subnet + self.l3_inst._nb_ovn.get_lrouter.return_value = ( + fake_resources.FakeOVNRouter.from_neutron_router( + self.fake_router_with_ext_gw)) self.l3_inst.delete_router(self.context, 'router-id') diff --git a/releasenotes/notes/ovn-router-gateway-qos-0897e5572c27fe78.yaml b/releasenotes/notes/ovn-router-gateway-qos-0897e5572c27fe78.yaml new file mode 100644 index 00000000000..8a5e1e72f73 --- /dev/null +++ b/releasenotes/notes/ovn-router-gateway-qos-0897e5572c27fe78.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support for router gateway IP QoS in OVN backend. The L3 OVN router + plugin now can apply router QoS policy rules on the router gateway port.