[OVN] Implement router gateway IP QoS

This patch implements router gateway IP QoS based on meter,
using the existing plugin and extension, only the driver side
is different.

Closes-Bug: #1893625

Co-Authored-By: zhanghao <hao.zhang.am.i@gmail.com>
Co-Authored-By: Rodolfo Alonso Hernandez <ralonsoh@redhat.com>

Change-Id: I46864b9234af64f190f6b6daebfd94d2e3bd0c17
This commit is contained in:
Rodolfo Alonso Hernandez 2022-03-05 18:33:46 +00:00
parent 1b68aebaba
commit 2d1b4fd80f
11 changed files with 241 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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