Merge "[OVN] Implement floating IP QoS in OVN backend"

This commit is contained in:
Zuul
2020-10-05 11:02:29 +00:00
committed by Gerrit Code Review
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

@@ -643,7 +643,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'],
@@ -879,7 +880,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 '
@@ -896,21 +899,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:
@@ -926,6 +919,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)
@@ -934,26 +929,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:
@@ -242,25 +260,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',
@@ -935,7 +937,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',
@@ -961,7 +965,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',
@@ -982,7 +988,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',
@@ -1006,7 +1014,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',
@@ -1161,7 +1171,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',
@@ -1185,7 +1197,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',
@@ -1219,7 +1233,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',
@@ -1248,7 +1264,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',
@@ -1281,7 +1299,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