neutron/neutron/agent/ovn/extensions/qos_hwol.py

292 lines
10 KiB
Python

# Copyright (c) 2023 Red Hat, Inc.
# All Rights Reserved.
#
# 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 oslo_log import log as logging
from ovsdbapp.backend.ovs_idl import event as row_event
from neutron.agent.linux import devlink
from neutron.agent.ovn.agent import ovsdb as agent_ovsdb
from neutron.agent.ovn.extensions import extension_manager
from neutron.common.ovn import constants as ovn_const
from neutron.common.ovn import utils as ovn_utils
from neutron.plugins.ml2.drivers.mech_sriov.agent import eswitch_manager
# NOTE(ralonsoh): move ``pci_lib`` to ``neutron.agent.linux``.
from neutron.plugins.ml2.drivers.mech_sriov.agent import pci_lib
LOG = logging.getLogger(__name__)
# NOTE(ralonsoh): move these constants from ``eswitch_manager`` to ``pci_lib``.
MAX_TX_RATE = eswitch_manager.IP_LINK_CAPABILITY_RATE
MIN_TX_RATE = eswitch_manager.IP_LINK_CAPABILITY_MIN_TX_RATE
TX_RATES = eswitch_manager.IP_LINK_CAPABILITY_RATES
NB_IDL_TABLES = ['QoS',
'Logical_Switch_Port',
'Logical_Switch',
]
SB_IDL_TABLES = ['Chassis',
'Chassis_Private',
'Encap',
'Port_Binding',
'Datapath_Binding',
]
class OVSInterfaceEvent(row_event.RowEvent):
LOG_MSG = 'Port ID %s, port name %s (event: %s)'
def __init__(self, ovn_agent):
self.ovn_agent = ovn_agent
events = (self.ROW_CREATE, self.ROW_DELETE)
table = 'Interface'
super().__init__(events, table, None)
def match_fn(self, event, row, old):
if not row.external_ids.get('iface-id'):
return False
return True
def run(self, event, row, old):
if event == self.ROW_CREATE:
self.ovn_agent.qos_hwol_ext.add_port(
row.external_ids['iface-id'], row.name)
elif event == self.ROW_DELETE:
self.ovn_agent.qos_hwol_ext.remove_egress(
row.external_ids['iface-id'])
LOG.debug(self.LOG_MSG, row.external_ids['iface-id'], row.name, event)
class QoSBandwidthLimitEvent(row_event.RowEvent):
LOG_MSG = 'QoS register %s, port ID %s, max_kbps: %s (event: %s)'
def __init__(self, ovn_agent):
self.ovn_agent = ovn_agent
table = 'QoS'
events = (self.ROW_CREATE, self.ROW_UPDATE, self.ROW_DELETE)
super().__init__(events, table, None)
def match_fn(self, event, row, old):
if not self.ovn_agent.sb_post_fork_event.is_set():
return False
# Check if the port has a Port ID and if this ID is bound to this host.
port_id = row.external_ids.get(ovn_const.OVN_PORT_EXT_ID_KEY)
if not port_id or not self.ovn_agent.qos_hwol_ext.get_port(port_id):
return False
if event in (self.ROW_CREATE, self.ROW_DELETE):
# Check direction, only egress rules ('from-lport') accepted.
if row.direction != 'from-lport':
return False
elif event == self.ROW_UPDATE:
try:
if row.bandwidth['rate'] == old.bandwidth['rate']:
return False
except (KeyError, AttributeError):
# No "rate" update.
return False
return True
def run(self, event, row, old):
port_id = row.external_ids[ovn_const.OVN_PORT_EXT_ID_KEY]
max_bw_kbps = int(row.bandwidth['rate'])
LOG.debug(self.LOG_MSG, str(row.uuid), port_id, max_bw_kbps, event)
max_kbps, min_kbps = agent_ovsdb.get_port_qos(self.ovn_agent.nb_idl,
port_id)
self.ovn_agent.qos_hwol_ext.update_egress(port_id, max_kbps, min_kbps)
class QoSMinimumBandwidthEvent(row_event.RowEvent):
LOG_MSG = 'Port ID %s, min_kbps: %s (event: %s)'
def __init__(self, ovn_agent):
self.ovn_agent = ovn_agent
table = 'Logical_Switch_Port'
events = (self.ROW_UPDATE, )
super().__init__(events, table, None)
def match_fn(self, event, row, old):
if not self.ovn_agent.sb_post_fork_event.is_set():
return False
# The "qos_min_rate" set on the LSP has always egress direction.
# Check if "options:qos_min_rate" has changed.
try:
ovn_min_rate = ovn_const.LSP_OPTIONS_QOS_MIN_RATE
if row.options.get(ovn_min_rate) == old.options.get(ovn_min_rate):
return False
except (KeyError, AttributeError):
return False
if not self.ovn_agent.qos_hwol_ext.get_port(row.name):
return False
return True
def run(self, event, row, old):
min_bw_kbps = int(row.options[ovn_const.LSP_OPTIONS_QOS_MIN_RATE])
LOG.debug(self.LOG_MSG, row.name, min_bw_kbps, event)
max_kbps, min_kbps = agent_ovsdb.get_port_qos(self.ovn_agent.nb_idl,
row.name)
self.ovn_agent.qos_hwol_ext.update_egress(row.name, max_kbps, min_kbps)
class _PortBindingChassisEvent(row_event.RowEvent):
def __init__(self, ovn_agent, events):
self.ovn_agent = ovn_agent
self.ovs_idl = self.ovn_agent.ovs_idl
table = 'Port_Binding'
super().__init__(events, table, None)
self.event_name = self.__class__.__name__
def run(self, event, row, old):
pass
class PortBindingChassisCreatedEvent(_PortBindingChassisEvent):
LOG_MSG = 'Port ID %s, datapath %s, OVS port name: %s (event: %s)'
def __init__(self, ovn_agent):
events = (self.ROW_UPDATE,)
super().__init__(ovn_agent, events)
def match_fn(self, event, row, old):
try:
return (row.chassis[0].name == self.ovn_agent.chassis and
not old.chassis)
except (IndexError, AttributeError):
return False
def run(self, event, row, old):
ovs_port_name = agent_ovsdb.get_ovs_port_name(self.ovs_idl,
row.logical_port)
net_name = ovn_utils.get_network_name_from_datapath(row.datapath)
LOG.debug(self.LOG_MSG, row.logical_port, net_name, ovs_port_name,
event)
max_kbps, min_kbps = agent_ovsdb.get_port_qos(self.ovn_agent.nb_idl,
row.logical_port)
self.ovn_agent.qos_hwol_ext.update_egress(row.logical_port, max_kbps,
min_kbps)
class QoSHardwareOffloadExtension(extension_manager.OVNAgentExtension):
def __init__(self):
super().__init__()
# _ovs_ports = {Neutron port ID: OVS port name}
self._ovs_ports = {}
@property
def ovs_idl_events(self):
return [OVSInterfaceEvent,
]
@property
def nb_idl_tables(self):
return NB_IDL_TABLES
@property
def nb_idl_events(self):
return [QoSBandwidthLimitEvent,
QoSMinimumBandwidthEvent,
]
@property
def sb_idl_tables(self):
return SB_IDL_TABLES
@property
def sb_idl_events(self):
return [PortBindingChassisCreatedEvent,
]
def add_port(self, port_id, port_name):
self._ovs_ports[port_id] = port_name
def del_port(self, port_id):
return self._ovs_ports.pop(port_id, None)
def get_port(self, port_id):
return self._ovs_ports.get(port_id)
@staticmethod
def _set_device_rate(pf_name, vf_index, rates):
"""Set device rate: max_tx_rate, min_tx_rate
@param pf_name: Physical Function name
@param vf_index: Virtual Function index
@param rates: dictionary with rate type (str) and the value (int)
in Kbps. Example:
{'max_tx_rate': 20000, 'min_tx_rate': 10000}
{'max_tx_rate': 30000}
{'min_tx_rate': 5000}
"""
LOG.debug('Setting rates on device %(pf_name)s, VF number '
'%(vf_index)s: %(rates)s',
{'pf_name': pf_name, 'vf_index': vf_index, 'rates': rates})
if not pf_name:
LOG.warning('Empty PF name, rates cannot be set')
return
pci_dev_wrapper = pci_lib.PciDeviceIPWrapper(pf_name)
return pci_dev_wrapper.set_vf_rate(vf_index, rates)
@staticmethod
def _kbps_2_mbps(rate_kbps):
if rate_kbps == 0: # Delete the BW setting.
return 0
elif 0 < rate_kbps < 1000: # Any value under 1000kbps --> 1Mbps
return 1
else:
return int(rate_kbps / 1000.0)
def _get_port_representor(self, port_id):
port_name = self.get_port(port_id)
if not port_name:
return
pr = devlink.get_port(port_name)
if not pr:
return
return pr
def update_egress(self, port_id, max_kbps, min_kbps):
pr = self._get_port_representor(port_id)
if not pr:
return
_qos = {MAX_TX_RATE: self._kbps_2_mbps(int(max_kbps)),
MIN_TX_RATE: self._kbps_2_mbps(int(min_kbps))}
self._set_device_rate(pr['pf_name'], pr['vf_num'], _qos)
def reset_egress(self, port_id):
pr = self._get_port_representor(port_id)
if not pr:
return
self._set_device_rate(pr['pf_name'], pr['vf_num'],
{MAX_TX_RATE: 0, MIN_TX_RATE: 0})
def remove_egress(self, port_id):
pr = self._get_port_representor(port_id)
self.del_port(port_id)
if not pr:
return
self._set_device_rate(pr['pf_name'], pr['vf_num'],
{MAX_TX_RATE: 0, MIN_TX_RATE: 0})