diff --git a/lower-constraints.txt b/lower-constraints.txt index 9633e5a2a34..0ba48448dd3 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -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 diff --git a/neutron/common/ovn/constants.py b/neutron/common/ovn/constants.py index 5046878cbf8..2977162553e 100644 --- a/neutron/common/ovn/constants.py +++ b/neutron/common/ovn/constants.py @@ -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' diff --git a/neutron/common/ovn/extensions.py b/neutron/common/ovn/extensions.py index ab88c7332f3..c1dbc6a08ca 100644 --- a/neutron/common/ovn/extensions.py +++ b/neutron/common/ovn/extensions.py @@ -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', diff --git a/neutron/common/ovn/utils.py b/neutron/common/ovn/utils.py index 6567573b1f7..981324c6566 100644 --- a/neutron/common/ovn/utils.py +++ b/neutron/common/ovn/utils.py @@ -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- + return 'cr-lrp-%s' % _id + + def ovn_provnet_port_name(network_id): # The name of OVN lswitch provider network port entry will be # provnet-. The port is created for network having diff --git a/neutron/extensions/qos_fip.py b/neutron/extensions/qos_fip.py index 27b17dd33ce..cbd42148343 100644 --- a/neutron/extensions/qos_fip.py +++ b/neutron/extensions/qos_fip.py @@ -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": diff --git a/neutron/objects/qos/policy.py b/neutron/objects/qos/policy.py index 14c22feb464..a0651c4501a 100644 --- a/neutron/objects/qos/policy.py +++ b/neutron/objects/qos/policy.py @@ -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, diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/qos.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/qos.py index 38ee1e9a20a..fda6a3d6756 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/qos.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/qos.py @@ -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) diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py index 372ad975e53..4e4176196fd 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py @@ -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 ' diff --git a/neutron/services/ovn_l3/plugin.py b/neutron/services/ovn_l3/plugin.py index e9bf11a6801..cc38f1d2267 100644 --- a/neutron/services/ovn_l3/plugin.py +++ b/neutron/services/ovn_l3/plugin.py @@ -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): diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_qos.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_qos.py index 32a159ca92e..7ee7801af20 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_qos.py +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_qos.py @@ -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, {}) diff --git a/neutron/tests/unit/extensions/test_qos_fip.py b/neutron/tests/unit/extensions/test_qos_fip.py index da545d0bf83..eaab2b158f9 100644 --- a/neutron/tests/unit/extensions/test_qos_fip.py +++ b/neutron/tests/unit/extensions/test_qos_fip.py @@ -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): diff --git a/neutron/tests/unit/fake_resources.py b/neutron/tests/unit/fake_resources.py index 86157d31c2a..e53ff170b58 100644 --- a/neutron/tests/unit/fake_resources.py +++ b/neutron/tests/unit/fake_resources.py @@ -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): diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_qos.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_qos.py index 28886d46e28..5ebc7a3c1c5 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_qos.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/test_qos.py @@ -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() diff --git a/neutron/tests/unit/services/ovn_l3/test_plugin.py b/neutron/tests/unit/services/ovn_l3/test_plugin.py index 9d061e82de1..4100b999d3b 100644 --- a/neutron/tests/unit/services/ovn_l3/test_plugin.py +++ b/neutron/tests/unit/services/ovn_l3/test_plugin.py @@ -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', diff --git a/requirements.txt b/requirements.txt index 2d59616b674..a06e3cb2535 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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