# Copyright 2020 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ovsdbapp.backend.ovs_idl import idlutils from neutron.objects.qos import binding as qos_binding from neutron.objects.qos import policy as qos_policy from neutron.objects.qos import rule as qos_rule from neutron_lib 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 from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf LOG = logging.getLogger(__name__) OVN_QOS_DEFAULT_RULE_PRIORITY = 2002 class OVNClientQosExtension(object): """OVN client QoS extension""" def __init__(self, driver): LOG.info('Starting OVNClientQosExtension') super(OVNClientQosExtension, self).__init__() self._driver = driver self._plugin_property = None self._plugin_l3_property = None @property def _plugin(self): if self._plugin_property is None: 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 :param context: (context) Neutron request context :param policy_id: (string) Neutron QoS policy ID :return: (dict) nested dictionary of QoS rules, classified per direction and rule type {egress: {bw_limit: {max_kbps, max_burst_kbps}, dscp: {dscp_mark} ingress: {...} } """ qos_rules = {constants.EGRESS_DIRECTION: {}, constants.INGRESS_DIRECTION: {}} if policy_id is None: return qos_rules # The policy might not have any rule all_rules = qos_rule.get_rules(qos_policy.QosPolicy, context, policy_id) for rule in all_rules: if isinstance(rule, qos_rule.QosBandwidthLimitRule): r = {rule.rule_type: {'max_kbps': rule.max_kbps}} if rule.max_burst_kbps: r[rule.rule_type]['max_burst_kbps'] = rule.max_burst_kbps qos_rules[rule.direction].update(r) elif isinstance(rule, qos_rule.QosDscpMarkingRule): r = {rule.rule_type: {'dscp_mark': rule.dscp_mark}} qos_rules[constants.EGRESS_DIRECTION].update(r) else: LOG.warning('Rule type %(rule_type)s from QoS policy ' '%(policy_id)s is not supported in OVN', {'rule_type': rule.rule_type, 'policy_id': policy_id}) return qos_rules @staticmethod def _ovn_qos_rule_match(direction, port_id, ip_address, resident_port): 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 and resident_port: match += (' && ip4.%s == %s && is_chassis_resident("%s")' % (src_or_dst, ip_address, resident_port)) return match def _ovn_qos_rule(self, rules_direction, rules, port_id, network_id, fip_id=None, ip_address=None, resident_port=None, delete=False): """Generate an OVN QoS register based on several Neutron QoS rules A OVN QoS register can contain "bandwidth" and "action" parameters. "bandwidth" defines the rate speed limitation; "action" contains the DSCP value to apply. Both are not exclusive. Only one rule per port and direction can be applied; that's why two rules (bandwidth limit and DSCP) in the same direction must be combined in one OVN QoS register. http://www.openvswitch.org/support/dist-docs/ovn-nb.5.html :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; 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 resident_port: (string) for L3 floating IP bandwidth, this is a logical switch port located in the chassis where the floating IP traffic is NATed. :param 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. :return: (dict) OVN QoS rule register to be used with QoSAddCommand and QoSDelCommand. """ if not delete and not rules: return lswitch_name = utils.ovn_name(network_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, resident_port) 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. return ovn_qos_rule for rule_type, rule in rules.items(): if rule_type == qos_consts.RULE_TYPE_BANDWIDTH_LIMIT: ovn_qos_rule['rate'] = rule['max_kbps'] if rule.get('max_burst_kbps'): ovn_qos_rule['burst'] = rule['max_burst_kbps'] elif rule_type == qos_consts.RULE_TYPE_DSCP_MARKING: ovn_qos_rule.update({'dscp': rule['dscp_mark']}) return ovn_qos_rule def _port_effective_qos_policy_id(self, port): """Return port effective QoS policy If the port does not have any QoS policy reference or is a network device, then return None. """ policy_exists = bool(port.get('qos_policy_id') or port.get('qos_network_policy_id')) if not policy_exists or utils.is_network_device_port(port): return None, None if port.get('qos_policy_id'): return port['qos_policy_id'], 'port' else: return port['qos_network_policy_id'], 'network' def _update_port_qos_rules(self, txn, port_id, network_id, qos_policy_id, qos_rules): # NOTE(ralonsoh): we don't use the transaction context because the # QoS policy could belong to another user (network QoS policy). admin_context = n_context.get_admin_context() # Generate generic deletion rules for both directions. In case of # creating deletion rules, the rule content is irrelevant. for ovn_rule in [self._ovn_qos_rule(direction, {}, port_id, network_id, delete=True) for direction in constants.VALID_DIRECTIONS]: # TODO(lucasagomes): qos_del() in ovsdbapp doesn't support # if_exists=True try: txn.add(self._driver._nb_idl.qos_del(**ovn_rule)) except idlutils.RowNotFound: continue if not qos_policy_id: return # If no QoS policy is defined, there are no QoS rules. # TODO(ralonsoh): for update_network and update_policy operations, # the QoS rules can be retrieved only once. qos_rules = qos_rules or self._qos_rules(admin_context, qos_policy_id) for direction, rules in qos_rules.items(): ovn_rule = self._ovn_qos_rule(direction, rules, port_id, network_id) if ovn_rule: txn.add(self._driver._nb_idl.qos_add(**ovn_rule)) def create_port(self, txn, port, port_type=None): self.update_port(txn, port, None, reset=True, port_type=port_type) def delete_port(self, txn, port): self.update_port(txn, port, None, delete=True) def update_port(self, txn, port, original_port, reset=False, delete=False, qos_rules=None, port_type=None): if port_type == ovn_const.LSP_TYPE_EXTERNAL: # External ports (SR-IOV) QoS is handled the SR-IOV agent QoS # extension. return if (not reset and not original_port) and not delete: # If there is no information about the previous QoS policy, do not # make any change, unless the port is new or the QoS information # must be reset (delete any previous configuration and set new # one). return qos_policy_id = (None if delete else self._port_effective_qos_policy_id(port)[0]) if not reset and not delete: original_qos_policy_id = self._port_effective_qos_policy_id( original_port)[0] if qos_policy_id == original_qos_policy_id: return # No QoS policy change self._update_port_qos_rules(txn, port['id'], port['network_id'], qos_policy_id, qos_rules) def update_network(self, txn, network, original_network, reset=False, qos_rules=None): updated_port_ids = set([]) if not reset and not original_network: # If there is no information about the previous QoS policy, do not # make any change. return updated_port_ids qos_policy_id = network.get('qos_policy_id') if not reset: original_qos_policy_id = original_network.get('qos_policy_id') if qos_policy_id == original_qos_policy_id: return updated_port_ids # No QoS policy change # NOTE(ralonsoh): we don't use the transaction context because some # ports can belong to other projects. admin_context = n_context.get_admin_context() for port in qos_binding.QosPolicyPortBinding.get_ports_by_network_id( admin_context, network['id']): if utils.is_network_device_port(port): continue self._update_port_qos_rules(txn, port['id'], network['id'], qos_policy_id, qos_rules) updated_port_ids.add(port['id']) 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 if ovn_conf.is_ovn_distributed_floating_ip(): # DVR, floating IP GW is in the same compute node as private port. resident_port = floatingip['port_id'] else: # Non-DVR, floating IP GW is located where chassisredirect lrp is. resident_port = utils.ovn_cr_lrouter_port_name(gw_port_id) 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'], resident_port=resident_port) 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() bound_ports = policy.get_bound_ports() qos_rules = self._qos_rules(context, policy.id) # TODO(ralonsoh): we need to benchmark this transaction in systems with # a huge amount of ports. This can take a while and could block other # operations. with self._driver._nb_idl.transaction(check_error=True) as txn: for network_id in bound_networks: network = {'qos_policy_id': policy.id, 'id': network_id} updated_port_ids.update( self.update_network(txn, network, {}, reset=True, qos_rules=qos_rules)) # Update each port bound to this policy, not handled previously in # the network update loop port_ids = [p for p in bound_ports if p not in updated_port_ids] for port in self._plugin.get_ports(context, 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)