[OVN] Implement floating IP QoS in OVN backend

This patch implements in the OVN backend the existing floating
IP QoS extension.

The OVN client, using the existing QoS extension, will retrieve
the QoS rules attached to each floating IP, the router where the
floating IP lives and the router gateway port. The QoS rules
will be applied on the router gateway port.

The OVN NB QoS rules for floating IP addresses have a "match"
field containing a tuple of parameters:
- The direction of the flow:
    'inport == "src"' or
    'outport == "dst"'
- The IP address to match:
    'ip4.src == 1.2.3.4' or
    'ip4.dst == 1.2.3.4'
- The chassis where the port is located:
    'is_chassis_resident("chassis")'

Closes-Bug: #1877408
Related-Bug: #1596611
Depends-On: https://review.opendev.org/#/c/727847/

Change-Id: Ib65d8edcb0a415f6d698c952334d3b4bb0d9fff6
This commit is contained in:
Rodolfo Alonso Hernandez 2020-04-21 14:53:34 +00:00
parent d189d83bd7
commit e7e71b2ca6
15 changed files with 380 additions and 97 deletions

View File

@ -80,7 +80,7 @@ oslo.versionedobjects==1.35.1
oslotest==3.2.0
osprofiler==2.3.0
ovs==2.8.0
ovsdbapp==1.3.0
ovsdbapp==1.4.0
Paste==2.0.2
PasteDeploy==1.5.0
pbr==4.0.0

View File

@ -39,6 +39,7 @@ OVN_CIDRS_EXT_ID_KEY = 'neutron:cidrs'
OVN_FIP_EXT_ID_KEY = 'neutron:fip_id'
OVN_FIP_PORT_EXT_ID_KEY = 'neutron:fip_port_id'
OVN_FIP_EXT_MAC_KEY = 'neutron:fip_external_mac'
OVN_FIP_NET_ID = 'neutron:fip_network_id'
OVN_REV_NUM_EXT_ID_KEY = 'neutron:revision_number'
OVN_QOS_POLICY_EXT_ID_KEY = 'neutron:qos_policy_id'
OVN_SG_IDS_EXT_ID_KEY = 'neutron:security_group_ids'

View File

@ -17,6 +17,12 @@ from neutron_lib.api.definitions import availability_zone as az_def
from neutron_lib.api.definitions import expose_port_forwarding_in_fip
from neutron_lib.api.definitions import fip_pf_description
from neutron_lib.api.definitions import floating_ip_port_forwarding
from neutron_lib.api.definitions import port_resource_request
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_rule_type_details
from neutron_lib.api.definitions import qos_rules_alias
from neutron_lib.api.definitions import router_availability_zone as raz_def
from neutron_lib.api.definitions import segment as seg_def
@ -32,6 +38,7 @@ ML2_SUPPORTED_API_EXTENSIONS_OVN_L3 = [
'ext-gw-mode',
'fip-port-details',
'pagination',
'qos-fip',
'sorting',
'project-id',
'dns-integration',
@ -54,6 +61,12 @@ ML2_SUPPORTED_API_EXTENSIONS = [
'network-ip-availability',
'port-security',
'provider',
port_resource_request.ALIAS,
qos.ALIAS,
qos_bw_limit_direction.ALIAS,
qos_default.ALIAS,
qos_rule_type_details.ALIAS,
qos_rules_alias.ALIAS,
'quotas',
'rbac-address-scope',
'rbac-policies',

View File

@ -74,6 +74,12 @@ def ovn_lrouter_port_name(id):
return constants.LRP_PREFIX + '%s' % id
def ovn_cr_lrouter_port_name(_id):
# The name of the OVN chassisredirect lrouter port entry will be
# cr-lrp-<UUID>
return 'cr-lrp-%s' % _id
def ovn_provnet_port_name(network_id):
# The name of OVN lswitch provider network port entry will be
# provnet-<Network-UUID>. The port is created for network having

View File

@ -12,6 +12,7 @@
# under the License.
from neutron_lib.api.definitions import l3
from neutron_lib.api.definitions import qos
from neutron_lib.api import extensions
from neutron_lib.services.qos import constants as qos_consts
@ -26,6 +27,7 @@ EXTENDED_ATTRIBUTES_2_0 = {
'validate': {'type:uuid_or_none': None}}
}
}
REQUIRED_EXTENSIONS = [l3.ALIAS, qos.ALIAS]
class Qos_fip(extensions.ExtensionDescriptor):
@ -48,7 +50,7 @@ class Qos_fip(extensions.ExtensionDescriptor):
return "2017-07-20T00:00:00-00:00"
def get_required_extensions(self):
return ["router", "qos"]
return REQUIRED_EXTENSIONS
def get_extended_resources(self, version):
if version == "2.0":

View File

@ -335,12 +335,12 @@ class QosPolicy(rbac_db.NeutronRbacObject):
self.id)
def get_bound_floatingips(self):
return binding.QosPolicyFloatingIPBinding.get_objects(self.obj_context,
self.id)
return binding.QosPolicyFloatingIPBinding.get_objects(
self.obj_context, policy_id=self.id)
def get_bound_routers(self):
return binding.QosPolicyRouterGatewayIPBinding.get_objects(
self.obj_context, self.id)
self.obj_context, policy_id=self.id)
@classmethod
def _get_bound_tenant_ids(cls, session, binding_db, bound_db,

View File

@ -19,10 +19,12 @@ from neutron.objects.qos import policy as qos_policy
from neutron.objects.qos import rule as qos_rule
from neutron_lib import constants
from neutron_lib import context as n_context
from neutron_lib.plugins import constants as plugins_const
from neutron_lib.plugins import directory
from neutron_lib.services.qos import constants as qos_consts
from oslo_log import log as logging
from neutron.common.ovn import constants as ovn_const
from neutron.common.ovn import utils
@ -38,6 +40,7 @@ class OVNClientQosExtension(object):
super(OVNClientQosExtension, self).__init__()
self._driver = driver
self._plugin_property = None
self._plugin_l3_property = None
@property
def _plugin(self):
@ -45,6 +48,12 @@ class OVNClientQosExtension(object):
self._plugin_property = directory.get_plugin()
return self._plugin_property
@property
def _plugin_l3(self):
if self._plugin_l3_property is None:
self._plugin_l3_property = directory.get_plugin(plugins_const.L3)
return self._plugin_l3_property
@staticmethod
def _qos_rules(context, policy_id):
"""QoS Neutron rules classified per direction and type
@ -81,8 +90,25 @@ class OVNClientQosExtension(object):
'policy_id': policy_id})
return qos_rules
@staticmethod
def _ovn_qos_rule_match(direction, port_id, ip_address):
if direction == constants.EGRESS_DIRECTION:
in_or_out = 'inport'
src_or_dst = 'src'
else:
in_or_out = 'outport'
src_or_dst = 'dst'
match = '%s == "%s"' % (in_or_out, port_id)
if ip_address:
match += (' && ip4.%s == %s && is_chassis_resident("%s")' %
(src_or_dst, ip_address,
utils.ovn_cr_lrouter_port_name(port_id)))
return match
def _ovn_qos_rule(self, rules_direction, rules, port_id, network_id,
delete=False):
fip_id=None, ip_address=None, delete=False):
"""Generate an OVN QoS register based on several Neutron QoS rules
A OVN QoS register can contain "bandwidth" and "action" parameters.
@ -96,8 +122,13 @@ class OVNClientQosExtension(object):
:param rules_direction: (string) rules direction (egress, ingress).
:param rules: (dict) {bw_limit: {max_kbps, max_burst_kbps},
dscp: {dscp_mark}}
:param port_id: (string) port ID.
:param port_id: (string) port ID; for L3 floating IP bandwidth
limit this is the router gateway port ID.
:param network_id: (string) network ID.
:param fip_id: (string) floating IP ID, for L3 floating IP bandwidth
limit.
:param ip_address: (string) IP address, for L3 floating IP 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.
@ -108,17 +139,17 @@ class OVNClientQosExtension(object):
return
lswitch_name = utils.ovn_name(network_id)
if rules_direction == constants.EGRESS_DIRECTION:
direction = 'from-lport'
match = 'inport == "{}"'.format(port_id)
else:
direction = 'to-lport'
match = 'outport == "{}"'.format(port_id)
direction = (
'from-lport' if rules_direction == constants.EGRESS_DIRECTION else
'to-lport')
match = self._ovn_qos_rule_match(rules_direction, port_id, ip_address)
ovn_qos_rule = {'switch': lswitch_name, 'direction': direction,
'priority': OVN_QOS_DEFAULT_RULE_PRIORITY,
'match': match}
if fip_id:
ovn_qos_rule['external_ids'] = {
ovn_const.OVN_FIP_EXT_ID_KEY: fip_id}
if delete:
# Any specific rule parameter is left undefined.
@ -234,6 +265,42 @@ class OVNClientQosExtension(object):
return updated_port_ids
def create_floatingip(self, txn, floatingip):
self.update_floatingip(txn, floatingip)
def update_floatingip(self, txn, floatingip):
router_id = floatingip.get('router_id')
qos_policy_id = floatingip.get('qos_policy_id')
if floatingip['floating_network_id']:
lswitch_name = utils.ovn_name(floatingip['floating_network_id'])
txn.add(self._driver._nb_idl.qos_del_ext_ids(
lswitch_name,
{ovn_const.OVN_FIP_EXT_ID_KEY: floatingip['id']}))
if not (router_id and qos_policy_id):
return
admin_context = n_context.get_admin_context()
router_db = self._plugin_l3._get_router(admin_context, router_id)
gw_port_id = router_db.get('gw_port_id')
if not gw_port_id:
return
qos_rules = self._qos_rules(admin_context, qos_policy_id)
for direction, rules in qos_rules.items():
ovn_rule = self._ovn_qos_rule(
direction, rules, gw_port_id,
floatingip['floating_network_id'], fip_id=floatingip['id'],
ip_address=floatingip['floating_ip_address'])
if ovn_rule:
txn.add(self._driver._nb_idl.qos_add(**ovn_rule))
def delete_floatingip(self, txn, floatingip):
self.update_floatingip(txn, floatingip)
def disassociate_floatingip(self, txn, floatingip):
self.delete_floatingip(txn, floatingip)
def update_policy(self, context, policy):
updated_port_ids = set([])
bound_networks = policy.get_bound_networks()
@ -256,3 +323,8 @@ class OVNClientQosExtension(object):
filters={'id': port_ids}):
self.update_port(txn, port, {}, reset=True,
qos_rules=qos_rules)
for fip_binding in policy.get_bound_floatingips():
fip = self._plugin_l3.get_floatingip(context,
fip_binding.fip_id)
self.update_floatingip(txn, fip)

View File

@ -648,7 +648,8 @@ class OVNClient(object):
floatingip, ovn_const.TYPE_FLOATINGIPS)),
ovn_const.OVN_FIP_PORT_EXT_ID_KEY: floatingip['port_id'],
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: gw_lrouter_name,
ovn_const.OVN_FIP_EXT_MAC_KEY: port_db['mac_address']}
ovn_const.OVN_FIP_EXT_MAC_KEY: port_db['mac_address'],
ovn_const.OVN_FIP_NET_ID: floatingip['floating_network_id']}
columns = {'type': 'dnat_and_snat',
'logical_ip': floatingip['fixed_ip_address'],
'external_ip': floatingip['floating_ip_address'],
@ -884,7 +885,9 @@ class OVNClient(object):
def create_floatingip(self, context, floatingip):
try:
self._create_or_update_floatingip(floatingip)
with self._nb_idl.transaction(check_error=True) as txn:
self._create_or_update_floatingip(floatingip, txn=txn)
self._qos_driver.create_floatingip(txn, floatingip)
except Exception as e:
with excutils.save_and_reraise_exception():
LOG.error('Unable to create floating ip in gateway '
@ -901,21 +904,11 @@ class OVNClient(object):
n_context.get_admin_context(), floatingip['id'],
const.FLOATINGIP_STATUS_ACTIVE)
# TODO(lucasagomes): The ``fip_object`` parameter was added to
# keep things backward compatible since old FIPs might not have
# the OVN_FIP_EXT_ID_KEY in their external_ids field. Remove it
# in the Rocky release.
def update_floatingip(self, context, floatingip, fip_object=None):
def update_floatingip(self, context, floatingip):
fip_status = None
router_id = None
ovn_fip = self._nb_idl.get_floatingip(floatingip['id'])
if not ovn_fip and fip_object:
router_id = fip_object.get('router_id')
ovn_fip = self._nb_idl.get_floatingip_by_ips(
router_id, fip_object['fixed_ip_address'],
fip_object['floating_ip_address'])
check_rev_cmd = self._nb_idl.check_revision_number(
floatingip['id'], floatingip, ovn_const.TYPE_FLOATINGIPS)
with self._nb_idl.transaction(check_error=True) as txn:
@ -931,6 +924,8 @@ class OVNClient(object):
self._create_or_update_floatingip(floatingip, txn=txn)
fip_status = const.FLOATINGIP_STATUS_ACTIVE
self._qos_driver.update_floatingip(txn, floatingip)
if check_rev_cmd.result == ovn_const.TXN_COMMITTED:
db_rev.bump_revision(
context, floatingip, ovn_const.TYPE_FLOATINGIPS)
@ -939,26 +934,20 @@ class OVNClient(object):
self._l3_plugin.update_floatingip_status(
context, floatingip['id'], fip_status)
# TODO(lucasagomes): The ``fip_object`` parameter was added to
# keep things backward compatible since old FIPs might not have
# the OVN_FIP_EXT_ID_KEY in their external_ids field. Remove it
# in the Rocky release.
def delete_floatingip(self, context, fip_id, fip_object=None):
def delete_floatingip(self, context, fip_id):
router_id = None
ovn_fip = self._nb_idl.get_floatingip(fip_id)
if not ovn_fip and fip_object:
router_id = fip_object.get('router_id')
ovn_fip = self._nb_idl.get_floatingip_by_ips(
router_id, fip_object['fixed_ip_address'],
fip_object['floating_ip_address'])
if ovn_fip:
lrouter = ovn_fip['external_ids'].get(
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY,
utils.ovn_name(router_id))
fip_net_id = ovn_fip['external_ids'].get(ovn_const.OVN_FIP_NET_ID)
fip_dict = {'floating_network_id': fip_net_id, 'id': fip_id}
try:
self._delete_floatingip(ovn_fip, lrouter)
with self._nb_idl.transaction(check_error=True) as txn:
self._delete_floatingip(ovn_fip, lrouter, txn=txn)
self._qos_driver.delete_floatingip(txn, fip_dict)
except Exception as e:
with excutils.save_and_reraise_exception():
LOG.error('Unable to delete floating ip in gateway '

View File

@ -20,6 +20,7 @@ from neutron.quota import resource_registry
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 as qos_api
from neutron_lib.callbacks import events
from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources
@ -30,6 +31,7 @@ from neutron_lib.exceptions import availability_zone as az_exc
from neutron_lib.plugins import constants as plugin_constants
from neutron_lib.plugins import directory
from neutron_lib.services import base as service_base
from oslo_config import cfg
from oslo_log import log
from oslo_utils import excutils
@ -38,7 +40,9 @@ from neutron.common.ovn import extensions
from neutron.common.ovn import utils
from neutron.db.availability_zone import router as router_az_db
from neutron.db import l3_fip_port_details
from neutron.db import l3_fip_qos
from neutron.db import ovn_revision_numbers_db as db_rev
from neutron.extensions import qos_fip as qos_fip_api
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_client
from neutron.scheduler import l3_ovn_scheduler
from neutron.services.portforwarding.drivers.ovn import driver \
@ -56,7 +60,8 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
l3_gwmode_db.L3_NAT_db_mixin,
dns_db.DNSDbMixin,
l3_fip_port_details.Fip_port_details_db_mixin,
router_az_db.RouterAvailabilityZoneMixin):
router_az_db.RouterAvailabilityZoneMixin,
l3_fip_qos.FloatingQoSDbMixin):
"""Implementation of the OVN L3 Router Service Plugin.
This class implements a L3 service plugin that provides
@ -66,7 +71,7 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
# TODO(mjozefcz): Start consuming it from neutron-lib
# once available.
supported_extension_aliases = (
_supported_extension_aliases = (
extensions.ML2_SUPPORTED_API_EXTENSIONS_OVN_L3)
@resource_registry.tracked_resources(router=l3_models.Router,
@ -89,6 +94,19 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
self.create_floatingip_precommit, resources.FLOATING_IP,
events.PRECOMMIT_CREATE)
@staticmethod
def disable_qos_fip_extension_by_extension_drivers(aliases):
if (qos_api.ALIAS not in cfg.CONF.ml2.extension_drivers and
qos_fip_api.FIP_QOS_ALIAS in aliases):
aliases.remove(qos_fip_api.FIP_QOS_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)
return self._aliases
@property
def _ovn_client(self):
if self._ovn_client_inst is None:
@ -240,25 +258,13 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
return fip
def delete_floatingip(self, context, id):
# TODO(lucasagomes): Passing ``original_fip`` object as a
# parameter to the OVNClient's delete_floatingip() method is done
# for backward-compatible reasons. Remove it in the Rocky release
# of OpenStack.
original_fip = self.get_floatingip(context, id)
super(OVNL3RouterPlugin, self).delete_floatingip(context, id)
self._ovn_client.delete_floatingip(context, id,
fip_object=original_fip)
self._ovn_client.delete_floatingip(context, id)
def update_floatingip(self, context, id, floatingip):
# TODO(lucasagomes): Passing ``original_fip`` object as a
# parameter to the OVNClient's update_floatingip() method is done
# for backward-compatible reasons. Remove it in the Rocky release
# of OpenStack.
original_fip = self.get_floatingip(context, id)
fip = super(OVNL3RouterPlugin, self).update_floatingip(context, id,
floatingip)
self._ovn_client.update_floatingip(context, fip,
fip_object=original_fip)
self._ovn_client.update_floatingip(context, fip)
return fip
def update_floatingip_status(self, context, floatingip_id, status):

View File

@ -12,12 +12,14 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
from unittest import mock
from neutron_lib import constants
from neutron_lib.services.qos import constants as qos_constants
from neutron.common.ovn import utils as ovn_utils
from neutron.db import l3_db
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \
import qos as qos_extension
from neutron.tests.functional import base
@ -63,19 +65,32 @@ class TestOVNClientQosExtension(base.TestOVNFunctionalBase):
self._add_logical_switch()
_ovn_client = _OVNClient(self.nb_api)
self.qos_driver = qos_extension.OVNClientQosExtension(_ovn_client)
self.gw_port_id = 'gw_port_id'
self._mock_get_router = mock.patch.object(l3_db.L3_NAT_dbonly_mixin,
'_get_router')
self.mock_get_router = self._mock_get_router.start()
self.mock_get_router.return_value = {'gw_port_id': self.gw_port_id}
self._mock_qos_rules = mock.patch.object(self.qos_driver,
'_qos_rules')
self.mock_qos_rules = self._mock_qos_rules.start()
self.fip = {'router_id': 'router_id', 'qos_policy_id': 'qos_policy_id',
'floating_network_id': self.network_1,
'id': 'fip_id', 'floating_ip_address': '1.2.3.4'}
def _add_logical_switch(self):
self.network_1 = 'network_1'
with self.nb_api.transaction(check_error=True) as txn:
txn.add(self.nb_api.ls_add(ovn_utils.ovn_name(self.network_1)))
def _check_rules(self, rules, port_id, network_id):
def _check_rules(self, rules, port_id, network_id, fip_id=None,
ip_address=None):
egress_ovn_rule = self.qos_driver._ovn_qos_rule(
constants.EGRESS_DIRECTION, rules.get(constants.EGRESS_DIRECTION),
port_id, network_id)
port_id, network_id, fip_id=fip_id, ip_address=ip_address)
ingress_ovn_rule = self.qos_driver._ovn_qos_rule(
constants.INGRESS_DIRECTION,
rules.get(constants.INGRESS_DIRECTION), port_id, network_id)
rules.get(constants.INGRESS_DIRECTION), port_id, network_id,
fip_id=fip_id, ip_address=ip_address)
with self.nb_api.transaction(check_error=True):
ls = self.qos_driver._driver._nb_idl.lookup(
@ -100,10 +115,8 @@ class TestOVNClientQosExtension(base.TestOVNFunctionalBase):
port = 'port1'
def update_and_check(qos_rules):
with self.nb_api.transaction(check_error=True) as txn, \
mock.patch.object(self.qos_driver,
'_qos_rules') as mock_rules:
mock_rules.return_value = qos_rules
with self.nb_api.transaction(check_error=True) as txn:
self.mock_qos_rules.return_value = qos_rules
self.qos_driver._update_port_qos_rules(
txn, port, self.network_1, 'qos1', None)
self._check_rules(qos_rules, port, self.network_1)
@ -112,3 +125,27 @@ class TestOVNClientQosExtension(base.TestOVNFunctionalBase):
update_and_check(QOS_RULES_2)
update_and_check(QOS_RULES_3)
update_and_check({})
def _update_fip_and_check(self, fip, qos_rules):
with self.nb_api.transaction(check_error=True) as txn:
self.mock_qos_rules.return_value = qos_rules
self.qos_driver.update_floatingip(txn, fip)
self._check_rules(qos_rules, self.gw_port_id, self.network_1,
fip_id='fip_id', ip_address='1.2.3.4')
def test_create_floatingip(self):
self._update_fip_and_check(self.fip, QOS_RULES_1)
def test_update_floatingip(self):
fip_updated = copy.deepcopy(self.fip)
fip_updated['qos_policy_id'] = 'another_qos_policy'
self._update_fip_and_check(self.fip, QOS_RULES_1)
self._update_fip_and_check(fip_updated, QOS_RULES_2)
self._update_fip_and_check(fip_updated, QOS_RULES_3)
self._update_fip_and_check(fip_updated, {})
def test_delete_floatingip(self):
self._update_fip_and_check(self.fip, QOS_RULES_1)
fip_dict = {'floating_network_id': self.fip['floating_network_id'],
'id': self.fip['id']}
self._update_fip_and_check(fip_dict, {})

View File

@ -51,7 +51,8 @@ class TestFloatingIPQoSIntPlugin(
class TestFloatingIPQoSL3NatServicePlugin(
test_l3.TestL3NatServicePlugin,
l3_fip_qos.FloatingQoSDbMixin):
supported_extension_aliases = [l3_apidef.ALIAS, qos_fip.FIP_QOS_ALIAS]
supported_extension_aliases = [l3_apidef.ALIAS, 'qos',
qos_fip.FIP_QOS_ALIAS]
class FloatingIPQoSDBTestCaseBase(object):

View File

@ -146,6 +146,7 @@ class FakeOvsdbNbOvnIdl(object):
self.ls_get = mock.Mock()
self.check_liveness = mock.Mock()
self.ha_chassis_group_get = mock.Mock()
self.qos_del_ext_ids = mock.Mock()
class FakeOvsdbSbOvnIdl(object):

View File

@ -15,18 +15,21 @@
from unittest import mock
import netaddr
from neutron_lib.api.definitions import qos as qos_api
from neutron_lib import constants
from neutron_lib import context
from neutron_lib.services.qos import constants as qos_constants
from oslo_config import cfg
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 import manager
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
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.plugins.ml2 import test_plugin
@ -51,19 +54,20 @@ class _Context(object):
class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase):
CORE_PLUGIN_CLASS = 'neutron.plugins.ml2.plugin.Ml2Plugin'
_extension_drivers = ['qos']
_extension_drivers = [qos_api.ALIAS]
l3_plugin = ('neutron.tests.unit.extensions.test_qos_fip.'
'TestFloatingIPQoSL3NatServicePlugin')
def setUp(self):
cfg.CONF.set_override('extension_drivers', self._extension_drivers,
group='ml2')
cfg.CONF.set_override('service_plugins', self._extension_drivers)
extensions.register_custom_supported_check(qos_api.ALIAS, lambda: True,
plugin_agnostic=True)
super(TestOVNClientQosExtension, self).setUp()
self.setup_coreplugin(self.CORE_PLUGIN_CLASS, load_plugins=True)
manager.init()
self._mock_qos_loaded = mock.patch.object(
core_qos.QosCoreResourceExtension, 'plugin_loaded')
self.mock_qos_loaded = self._mock_qos_loaded.start()
self.txn = _Context()
mock_driver = mock.Mock()
mock_driver._nb_idl.transaction.return_value = self.txn
@ -81,10 +85,34 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase):
return obj_cls.modify_fields_to_db(
self.get_random_object_fields(obj_cls))
def _create_one_port(self, mac_address_int, network_id):
mac_address = netaddr.EUI(mac_address_int)
port = port_obj.Port(
self.ctx, project_id=self.project_id,
network_id=network_id, device_owner='',
admin_state_up=True, status='DOWN', device_id='2',
mac_address=mac_address)
port.create()
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()
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.fips_ports = []
fip_cidr = netaddr.IPNetwork('10.10.0.0/24')
for net_idx in range(2):
qos_policy = policy_obj.QosPolicy(
self.ctx, id=uuidutils.generate_uuid(),
@ -96,10 +124,21 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase):
# the port dictionary extended with the QoS policy information; see
# QoSPlugin._extend_port_resource_request
qos_rule = rule_obj.QosDscpMarkingRule(
self.ctx, dscp=20, id=uuidutils.generate_uuid(),
self.ctx, dscp_mark=20, id=uuidutils.generate_uuid(),
qos_policy_id=qos_policy.id)
qos_rule.create()
self.fips_ports.append(self._create_one_port(1000 + net_idx,
self.fips_network.id))
fip_ip = str(netaddr.IPAddress(fip_cidr.ip + net_idx + 1))
fip = router_obj.FloatingIP(
self.ctx, id=uuidutils.generate_uuid(),
project_id=self.project_id, floating_ip_address=fip_ip,
floating_network_id=self.fips_network.id,
floating_port_id=self.fips_ports[-1].id)
fip.create()
self.fips.append(fip)
network = network_obj.Network(
self.ctx, id=uuidutils.generate_uuid(),
project_id=self.project_id)
@ -107,14 +146,8 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase):
self.networks.append(network)
for port_idx in range(3):
mac_address = netaddr.EUI(net_idx * 16 + port_idx)
port = port_obj.Port(
self.ctx, project_id=self.project_id,
network_id=network.id, device_owner='',
admin_state_up=True, status='DOWN', device_id='2',
mac_address=mac_address)
port.create()
self.ports.append(port)
self.ports.append(
self._create_one_port(net_idx * 16 + port_idx, network.id))
@mock.patch.object(qos_extension.LOG, 'warning')
@mock.patch.object(rule_obj, 'get_rules')
@ -150,36 +183,59 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase):
self.assertEqual(expected,
self.qos_driver._qos_rules(mock.ANY, mock.ANY))
def test__ovn_qos_rule_ingress(self):
def _test__ovn_qos_rule_ingress(self, fip_id=None, ip_address=None):
direction = constants.INGRESS_DIRECTION
rule = {qos_constants.RULE_TYPE_BANDWIDTH_LIMIT: QOS_RULE_BW_1}
match = self.qos_driver._ovn_qos_rule_match(
direction, 'port_id', ip_address)
expected = {'burst': 100, 'rate': 200, 'direction': 'to-lport',
'match': 'outport == "port_id"',
'match': match,
'priority': qos_extension.OVN_QOS_DEFAULT_RULE_PRIORITY,
'switch': 'neutron-network_id'}
if fip_id:
expected['external_ids'] = {ovn_const.OVN_FIP_EXT_ID_KEY: fip_id}
result = self.qos_driver._ovn_qos_rule(
direction, rule, 'port_id', 'network_id')
direction, rule, 'port_id', 'network_id', fip_id=fip_id,
ip_address=ip_address)
self.assertEqual(expected, result)
def test__ovn_qos_rule_egress(self):
def test__ovn_qos_rule_ingress(self):
self._test__ovn_qos_rule_ingress()
def test__ovn_qos_rule_ingress_fip(self):
self._test__ovn_qos_rule_ingress(fip_id='fipid', ip_address='1.2.3.4')
def _test__ovn_qos_rule_egress(self, fip_id=None, ip_address=None):
direction = constants.EGRESS_DIRECTION
rule = {qos_constants.RULE_TYPE_DSCP_MARKING: QOS_RULE_DSCP_1}
expected = {'direction': 'from-lport', 'match': 'inport == "port_id"',
match = self.qos_driver._ovn_qos_rule_match(
direction, 'port_id', ip_address)
expected = {'direction': 'from-lport', 'match': match,
'dscp': 16, 'switch': 'neutron-network_id',
'priority': qos_extension.OVN_QOS_DEFAULT_RULE_PRIORITY}
if fip_id:
expected['external_ids'] = {ovn_const.OVN_FIP_EXT_ID_KEY: fip_id}
result = self.qos_driver._ovn_qos_rule(
direction, rule, 'port_id', 'network_id')
direction, rule, 'port_id', 'network_id', fip_id, ip_address)
self.assertEqual(expected, result)
rule = {qos_constants.RULE_TYPE_BANDWIDTH_LIMIT: QOS_RULE_BW_2,
qos_constants.RULE_TYPE_DSCP_MARKING: QOS_RULE_DSCP_2}
expected = {'direction': 'from-lport', 'match': 'inport == "port_id"',
expected = {'direction': 'from-lport', 'match': match,
'rate': 300, 'dscp': 20, 'switch': 'neutron-network_id',
'priority': qos_extension.OVN_QOS_DEFAULT_RULE_PRIORITY}
if fip_id:
expected['external_ids'] = {ovn_const.OVN_FIP_EXT_ID_KEY: fip_id}
result = self.qos_driver._ovn_qos_rule(
direction, rule, 'port_id', 'network_id')
direction, rule, 'port_id', 'network_id', fip_id, ip_address)
self.assertEqual(expected, result)
def test__ovn_qos_rule_egress(self):
self._test__ovn_qos_rule_egress()
def test__ovn_qos_rule_egress_fip(self):
self._test__ovn_qos_rule_egress(fip_id='fipid', ip_address='1.2.3.4')
def test__port_effective_qos_policy_id(self):
port = {'qos_policy_id': 'qos1'}
self.assertEqual(('qos1', 'port'),
@ -349,6 +405,8 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase):
- port21: qos_policy0 --> handled during "update_network", not updated
handled during "update_port" and updated
- port22: qos_policy1 --> handled during "update_network", not updated
fip1: qos_policy0
fip2: qos_policy1
"""
self.ports[1].qos_policy_id = self.qos_policies[0].id
self.ports[1].update()
@ -360,9 +418,15 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase):
self.ports[5].update()
self.networks[1].qos_policy_id = self.qos_policies[0].id
self.networks[1].update()
self.fips[0].qos_policy_id = self.qos_policies[0].id
self.fips[0].update()
self.fips[1].qos_policy_id = self.qos_policies[1].id
self.fips[1].update()
mock_qos_rules = mock.Mock()
with mock.patch.object(self.qos_driver, '_qos_rules',
return_value=mock_qos_rules):
return_value=mock_qos_rules), \
mock.patch.object(self.qos_driver, 'update_floatingip') as \
mock_update_fip:
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,
@ -371,3 +435,74 @@ class TestOVNClientQosExtension(test_plugin.Ml2PluginV2TestCase):
# We can't ensure the call order because we are not enforcing any order
# when retrieving the port and the network list.
self.mock_rules.assert_has_calls(calls, any_order=True)
fip = self.qos_driver._plugin_l3._make_floatingip_dict(self.fips[0])
mock_update_fip.asssert_called_once_with(self.txn, fip, reset=True)
def test_update_floatingip(self):
nb_idl = self.qos_driver._driver._nb_idl
fip = self.fips[0]
original_fip = self.fips[1]
txn = mock.Mock()
# Update FIP, no QoS policy nor port/router
self.qos_driver.update_floatingip(txn, fip)
nb_idl.qos_del_ext_ids.assert_called_once()
nb_idl.qos_add.assert_not_called()
nb_idl.reset_mock()
# Attach a port and a router, not QoS policy
fip.router_id = self.router.id
fip.fixed_port_id = self.fips_ports[0].id
fip.update()
self.qos_driver.update_floatingip(txn, fip)
nb_idl.qos_del_ext_ids.assert_called_once()
nb_idl.qos_add.assert_not_called()
nb_idl.reset_mock()
# Add a QoS policy
fip.qos_policy_id = self.qos_policies[0].id
fip.update()
self.qos_driver.update_floatingip(txn, fip)
nb_idl.qos_del_ext_ids.assert_called_once()
nb_idl.qos_add.assert_called_once()
nb_idl.reset_mock()
# Remove QoS
fip.qos_policy_id = None
fip.update()
original_fip.qos_policy_id = self.qos_policies[0].id
original_fip.update()
self.qos_driver.update_floatingip(txn, fip)
nb_idl.qos_del_ext_ids.assert_called_once()
nb_idl.qos_add.assert_not_called()
nb_idl.reset_mock()
# Add again another QoS policy
fip.qos_policy_id = self.qos_policies[1].id
fip.update()
original_fip.qos_policy_id = None
original_fip.update()
self.qos_driver.update_floatingip(txn, fip)
nb_idl.qos_del_ext_ids.assert_called_once()
nb_idl.qos_add.assert_called_once()
nb_idl.reset_mock()
# Detach the port and the router
fip.router_id = None
fip.fixed_port_id = None
fip.update()
original_fip.router_id = self.router.id
original_fip.fixed_port_id = self.fips_ports[0].id
original_fip.qos_policy_id = self.qos_policies[1].id
original_fip.update()
self.qos_driver.update_floatingip(txn, fip)
nb_idl.qos_del_ext_ids.assert_called_once()
nb_idl.qos_add.assert_not_called()
nb_idl.reset_mock()
# Force reset (delete any QoS)
fip_dict = {'floating_network_id': fip.floating_network_id,
'id': fip.id}
self.qos_driver.update_floatingip(txn, fip_dict)
nb_idl.qos_del_ext_ids.assert_called_once()
nb_idl.qos_add.assert_not_called()

View File

@ -911,7 +911,9 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self.fake_floating_ip['port_id'],
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name(
self.fake_floating_ip['router_id']),
ovn_const.OVN_FIP_EXT_MAC_KEY: 'aa:aa:aa:aa:aa:aa'}
ovn_const.OVN_FIP_EXT_MAC_KEY: 'aa:aa:aa:aa:aa:aa',
ovn_const.OVN_FIP_NET_ID:
self.fake_floating_ip['floating_network_id']}
self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with(
'neutron-router-id',
type='dnat_and_snat',
@ -937,7 +939,9 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self.fake_floating_ip['port_id'],
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name(
self.fake_floating_ip['router_id']),
ovn_const.OVN_FIP_EXT_MAC_KEY: '00:01:02:03:04:05'}
ovn_const.OVN_FIP_EXT_MAC_KEY: '00:01:02:03:04:05',
ovn_const.OVN_FIP_NET_ID:
self.fake_floating_ip['floating_network_id']}
self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with(
'neutron-router-id', type='dnat_and_snat', logical_ip='10.0.0.10',
external_ip='192.168.0.10', external_mac='00:01:02:03:04:05',
@ -963,7 +967,9 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self.fake_floating_ip['port_id'],
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name(
self.fake_floating_ip['router_id']),
ovn_const.OVN_FIP_EXT_MAC_KEY: '00:01:02:03:04:05'}
ovn_const.OVN_FIP_EXT_MAC_KEY: '00:01:02:03:04:05',
ovn_const.OVN_FIP_NET_ID:
self.fake_floating_ip['floating_network_id']}
self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with(
'neutron-router-id', type='dnat_and_snat', logical_ip='10.0.0.10',
external_ip='192.168.0.10',
@ -984,7 +990,9 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self.fake_floating_ip['port_id'],
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name(
self.fake_floating_ip['router_id']),
ovn_const.OVN_FIP_EXT_MAC_KEY: 'aa:aa:aa:aa:aa:aa'}
ovn_const.OVN_FIP_EXT_MAC_KEY: 'aa:aa:aa:aa:aa:aa',
ovn_const.OVN_FIP_NET_ID:
self.fake_floating_ip['floating_network_id']}
self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with(
'neutron-router-id',
type='dnat_and_snat',
@ -1010,7 +1018,9 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self.fake_floating_ip['port_id'],
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name(
self.fake_floating_ip['router_id']),
ovn_const.OVN_FIP_EXT_MAC_KEY: 'aa:aa:aa:aa:aa:aa'}
ovn_const.OVN_FIP_EXT_MAC_KEY: 'aa:aa:aa:aa:aa:aa',
ovn_const.OVN_FIP_NET_ID:
self.fake_floating_ip['floating_network_id']}
self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with(
'neutron-router-id',
type='dnat_and_snat',
@ -1167,7 +1177,9 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self.fake_floating_ip_new['port_id'],
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name(
self.fake_floating_ip_new['router_id']),
ovn_const.OVN_FIP_EXT_MAC_KEY: 'aa:aa:aa:aa:aa:aa'}
ovn_const.OVN_FIP_EXT_MAC_KEY: 'aa:aa:aa:aa:aa:aa',
ovn_const.OVN_FIP_NET_ID:
self.fake_floating_ip['floating_network_id']}
self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with(
'neutron-new-router-id',
type='dnat_and_snat',
@ -1191,7 +1203,9 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self.fake_floating_ip_new['port_id'],
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name(
self.fake_floating_ip_new['router_id']),
ovn_const.OVN_FIP_EXT_MAC_KEY: 'aa:aa:aa:aa:aa:aa'}
ovn_const.OVN_FIP_EXT_MAC_KEY: 'aa:aa:aa:aa:aa:aa',
ovn_const.OVN_FIP_NET_ID:
self.fake_floating_ip['floating_network_id']}
self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with(
'neutron-new-router-id',
type='dnat_and_snat',
@ -1225,7 +1239,9 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self.fake_floating_ip_new['port_id'],
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name(
self.fake_floating_ip_new['router_id']),
ovn_const.OVN_FIP_EXT_MAC_KEY: '00:01:02:03:04:05'}
ovn_const.OVN_FIP_EXT_MAC_KEY: '00:01:02:03:04:05',
ovn_const.OVN_FIP_NET_ID:
self.fake_floating_ip['floating_network_id']}
self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with(
'neutron-new-router-id', type='dnat_and_snat',
logical_ip='10.10.10.10', external_ip='192.168.0.10',
@ -1254,7 +1270,9 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self.fake_floating_ip_new['port_id'],
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name(
self.fake_floating_ip_new['router_id']),
ovn_const.OVN_FIP_EXT_MAC_KEY: 'aa:aa:aa:aa:aa:aa'}
ovn_const.OVN_FIP_EXT_MAC_KEY: 'aa:aa:aa:aa:aa:aa',
ovn_const.OVN_FIP_NET_ID:
self.fake_floating_ip['floating_network_id']}
self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with(
'neutron-new-router-id',
type='dnat_and_snat',
@ -1287,7 +1305,9 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self.fake_floating_ip_new['port_id'],
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name(
self.fake_floating_ip_new['router_id']),
ovn_const.OVN_FIP_EXT_MAC_KEY: 'aa:aa:aa:aa:aa:aa'}
ovn_const.OVN_FIP_EXT_MAC_KEY: 'aa:aa:aa:aa:aa:aa',
ovn_const.OVN_FIP_NET_ID:
self.fake_floating_ip['floating_network_id']}
self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with(
'neutron-new-router-id',
type='dnat_and_snat',

View File

@ -45,7 +45,7 @@ oslo.versionedobjects>=1.35.1 # Apache-2.0
osprofiler>=2.3.0 # Apache-2.0
os-ken >= 0.3.0 # Apache-2.0
ovs>=2.8.0 # Apache-2.0
ovsdbapp>=1.3.0 # Apache-2.0
ovsdbapp>=1.4.0 # Apache-2.0
psutil>=3.2.2 # BSD
pyroute2>=0.5.13;sys_platform!='win32' # Apache-2.0 (+ dual licensed GPL2)
pyOpenSSL>=17.1.0 # Apache-2.0