[OVN] New OVN Neutron Agent extension: QoS for HWOL
Added a new OVN Neutron Agent extension: QoS for hardware offloaded ports. This extension will enforce the minimum bandwidth and maximum bandwidth egress QoS rules for ports with hardware offload (DevLink ports). This extension uses the "ip-link" commands to set the "ceil" and "rate" parameters on the corresponding virtual functions. Related-Bug: #1998608 Change-Id: Id436e43868fa0d3fbc843adb55f333582ed0134f
This commit is contained in:
parent
2202cc1d89
commit
54eff20a72
@ -61,10 +61,26 @@ class OVNNeutronAgent(service.Service):
|
|||||||
self.ext_manager = ext_mgr.OVNAgentExtensionManager(self._conf)
|
self.ext_manager = ext_mgr.OVNAgentExtensionManager(self._conf)
|
||||||
self.ext_manager.initialize(None, 'ovn', self.ext_manager_api)
|
self.ext_manager.initialize(None, 'ovn', self.ext_manager_api)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ovs_idl(self):
|
||||||
|
return self.ext_manager_api.ovs_idl
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nb_idl(self):
|
||||||
|
return self.ext_manager_api.nb_idl
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nb_post_fork_event(self):
|
||||||
|
return self.ext_manager_api.nb_post_fork_event
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sb_idl(self):
|
def sb_idl(self):
|
||||||
return self.ext_manager_api.sb_idl
|
return self.ext_manager_api.sb_idl
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sb_post_fork_event(self):
|
||||||
|
return self.ext_manager_api.sb_post_fork_event
|
||||||
|
|
||||||
def _load_config(self, ovs_idl):
|
def _load_config(self, ovs_idl):
|
||||||
self.chassis = ovsdb.get_own_chassis_name(ovs_idl)
|
self.chassis = ovsdb.get_own_chassis_name(ovs_idl)
|
||||||
try:
|
try:
|
||||||
|
@ -18,6 +18,7 @@ from ovsdbapp.backend.ovs_idl import idlutils
|
|||||||
from ovsdbapp.schema.open_vswitch import impl_idl as impl_idl_ovs
|
from ovsdbapp.schema.open_vswitch import impl_idl as impl_idl_ovs
|
||||||
|
|
||||||
from neutron.agent.ovsdb.native import connection as ovsdb_conn
|
from neutron.agent.ovsdb.native import connection as ovsdb_conn
|
||||||
|
from neutron.common.ovn import constants as ovn_const
|
||||||
from neutron.common.ovn import utils as ovn_utils
|
from neutron.common.ovn import utils as ovn_utils
|
||||||
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf as config
|
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf as config
|
||||||
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import impl_idl_ovn
|
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import impl_idl_ovn
|
||||||
@ -140,3 +141,40 @@ def get_own_chassis_name(ovs_idl):
|
|||||||
"""
|
"""
|
||||||
ext_ids = ovs_idl.db_get('Open_vSwitch', '.', 'external_ids').execute()
|
ext_ids = ovs_idl.db_get('Open_vSwitch', '.', 'external_ids').execute()
|
||||||
return ext_ids['system-id']
|
return ext_ids['system-id']
|
||||||
|
|
||||||
|
|
||||||
|
def get_ovs_port_name(ovs_idl, port_id):
|
||||||
|
"""Return the OVS port name given the Neutron port ID"""
|
||||||
|
int_list = ovs_idl.db_list('Interface', columns=['name', 'external_ids'],
|
||||||
|
if_exists=True).execute(check_error=True,
|
||||||
|
log_errors=False)
|
||||||
|
for interface in int_list:
|
||||||
|
if interface['external_ids'].get('iface-id') == port_id:
|
||||||
|
return interface['name']
|
||||||
|
|
||||||
|
|
||||||
|
def get_port_qos(nb_idl, port_id):
|
||||||
|
"""Retrieve the QoS egress max-bw and min-bw values (in kbps) of a LSP
|
||||||
|
|
||||||
|
There could be max-bw rules ingress (to-lport) and egress (from-lport);
|
||||||
|
this method is only returning the egress one. The min-bw rule is only
|
||||||
|
implemented for egress traffic.
|
||||||
|
"""
|
||||||
|
lsp = nb_idl.lsp_get(port_id).execute(check_error=True)
|
||||||
|
if not lsp:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
net_name = lsp.external_ids[ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY]
|
||||||
|
ls = nb_idl.lookup('Logical_Switch', net_name)
|
||||||
|
for qos_rule in iter(r for r in ls.qos_rules if
|
||||||
|
r.external_ids[ovn_const.OVN_PORT_EXT_ID_KEY]):
|
||||||
|
if qos_rule.direction != 'from-lport':
|
||||||
|
continue
|
||||||
|
|
||||||
|
max_kbps = int(qos_rule.bandwidth.get('rate', 0))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
max_kbps = 0
|
||||||
|
|
||||||
|
min_kbps = int(lsp.options.get(ovn_const.LSP_OPTIONS_QOS_MIN_RATE, 0))
|
||||||
|
return max_kbps, min_kbps
|
||||||
|
291
neutron/agent/ovn/extensions/qos_hwol.py
Normal file
291
neutron/agent/ovn/extensions/qos_hwol.py
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
# 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})
|
77
neutron/tests/functional/agent/ovn/agent/test_ovsdb.py
Normal file
77
neutron/tests/functional/agent/ovn/agent/test_ovsdb.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# 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 neutron_lib import constants
|
||||||
|
from neutron_lib.services.qos import constants as qos_consts
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
|
from neutron.agent.ovn.agent import ovsdb as agent_ovsdb
|
||||||
|
from neutron.common.ovn import constants as ovn_const
|
||||||
|
from neutron.common.ovn import utils as ovn_utils
|
||||||
|
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions import qos \
|
||||||
|
as ovn_qos
|
||||||
|
from neutron.tests.functional import base
|
||||||
|
|
||||||
|
|
||||||
|
class GetPortQosTestCase(base.TestOVNFunctionalBase):
|
||||||
|
|
||||||
|
def test_get_port_qos(self):
|
||||||
|
network_id = uuidutils.generate_uuid()
|
||||||
|
network_name = ovn_utils.ovn_name(network_id)
|
||||||
|
ext_ids = {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: network_name}
|
||||||
|
ls = self.nb_api.ls_add(network_name).execute(check_error=True)
|
||||||
|
lsp_name = ('port-' + uuidutils.generate_uuid())[:15]
|
||||||
|
self.nb_api.create_lswitch_port(
|
||||||
|
lsp_name, ls.name, external_ids=ext_ids).execute(check_error=True)
|
||||||
|
lsp = self.nb_api.lsp_get(lsp_name).execute(check_error=True)
|
||||||
|
self.assertIsNone(lsp.options.get(ovn_const.LSP_OPTIONS_QOS_MIN_RATE))
|
||||||
|
|
||||||
|
# Set min-bw rule in the LSP.
|
||||||
|
min_qos_value = 30000
|
||||||
|
options = {ovn_const.LSP_OPTIONS_QOS_MIN_RATE: str(min_qos_value)}
|
||||||
|
self.nb_api.update_lswitch_qos_options(lsp, **options).execute(
|
||||||
|
check_error=True)
|
||||||
|
lsp = self.nb_api.lsp_get(lsp_name).execute(check_error=True)
|
||||||
|
self.assertEqual(min_qos_value,
|
||||||
|
int(lsp.options[ovn_const.LSP_OPTIONS_QOS_MIN_RATE]))
|
||||||
|
|
||||||
|
# Create the QoS register with the max-bw rule.
|
||||||
|
qos_extension = ovn_qos.OVNClientQosExtension()
|
||||||
|
max_qos_value = 50000
|
||||||
|
rules = {
|
||||||
|
qos_consts.RULE_TYPE_BANDWIDTH_LIMIT: {'max_kbps': max_qos_value}}
|
||||||
|
ovn_rules = qos_extension._ovn_qos_rule(constants.EGRESS_DIRECTION,
|
||||||
|
rules, lsp.name, network_id)
|
||||||
|
self.nb_api.qos_add(**ovn_rules, may_exist=True).execute(
|
||||||
|
check_error=True)
|
||||||
|
|
||||||
|
# Retrieve the min-bw and max-bw egress rules associated to a port.
|
||||||
|
max_kbps, min_kbps = agent_ovsdb.get_port_qos(self.nb_api, lsp.name)
|
||||||
|
self.assertEqual((max_qos_value, min_qos_value), (max_kbps, min_kbps))
|
||||||
|
|
||||||
|
# Remove the min-bw rule.
|
||||||
|
options = {ovn_const.LSP_OPTIONS_QOS_MIN_RATE: str(0)}
|
||||||
|
lsp = self.nb_api.lsp_get(lsp_name).execute(check_error=True)
|
||||||
|
self.nb_api.update_lswitch_qos_options(lsp, **options).execute(
|
||||||
|
check_error=True)
|
||||||
|
max_kbps, min_kbps = agent_ovsdb.get_port_qos(self.nb_api, lsp.name)
|
||||||
|
self.assertEqual((max_qos_value, 0), (max_kbps, min_kbps))
|
||||||
|
|
||||||
|
# Remove the max-bw rukle
|
||||||
|
ext_ids = {ovn_const.OVN_PORT_EXT_ID_KEY: lsp_name}
|
||||||
|
self.nb_api.qos_del_ext_ids(
|
||||||
|
network_name, ext_ids).execute(check_error=True)
|
||||||
|
max_kbps, min_kbps = agent_ovsdb.get_port_qos(self.nb_api, lsp.name)
|
||||||
|
self.assertEqual((0, 0), (max_kbps, min_kbps))
|
176
neutron/tests/functional/agent/ovn/extensions/test_qos_hwol.py
Normal file
176
neutron/tests/functional/agent/ovn/extensions/test_qos_hwol.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
# 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 unittest import mock
|
||||||
|
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
|
from neutron.agent.common import ovs_lib
|
||||||
|
from neutron.agent.ovn.agent import ovsdb as agent_ovsdb
|
||||||
|
from neutron.agent.ovn.extensions import qos_hwol
|
||||||
|
from neutron.common.ovn import constants as ovn_const
|
||||||
|
from neutron.common.ovn import utils
|
||||||
|
from neutron.common import utils as n_utils
|
||||||
|
from neutron.tests.common import net_helpers
|
||||||
|
from neutron.tests.functional import base
|
||||||
|
|
||||||
|
|
||||||
|
class OVSInterfaceEventTestCase(base.TestOVNFunctionalBase):
|
||||||
|
|
||||||
|
def test_port_creation_and_deletion(self):
|
||||||
|
def check_add_port_called():
|
||||||
|
try:
|
||||||
|
mock_agent.qos_hwol_ext.add_port.assert_has_calls(
|
||||||
|
[mock.call('port_iface-id', port_name)])
|
||||||
|
return True
|
||||||
|
except AssertionError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_remove_egress_called():
|
||||||
|
try:
|
||||||
|
mock_agent.qos_hwol_ext.remove_egress.assert_has_calls(
|
||||||
|
[mock.call('port_iface-id')])
|
||||||
|
return True
|
||||||
|
except AssertionError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
mock_agent = mock.Mock()
|
||||||
|
events = [qos_hwol.OVSInterfaceEvent(mock_agent)]
|
||||||
|
agent_ovsdb.MonitorAgentOvsIdl(events=events).start()
|
||||||
|
br = self.useFixture(net_helpers.OVSBridgeFixture()).bridge
|
||||||
|
self.ovs_bridge = ovs_lib.OVSBridge(br.br_name)
|
||||||
|
port_name = ('port-' + uuidutils.generate_uuid())[:8]
|
||||||
|
|
||||||
|
self.ovs_bridge.add_port(
|
||||||
|
port_name, ('external_ids', {'iface-id': 'port_iface-id'}))
|
||||||
|
n_utils.wait_until_true(check_add_port_called, timeout=5)
|
||||||
|
|
||||||
|
self.ovs_bridge.delete_port(port_name)
|
||||||
|
n_utils.wait_until_true(check_remove_egress_called, timeout=5)
|
||||||
|
|
||||||
|
|
||||||
|
class QoSBandwidthLimitEventTestCase(base.TestOVNFunctionalBase):
|
||||||
|
|
||||||
|
def setUp(self, **kwargs):
|
||||||
|
super().setUp(**kwargs)
|
||||||
|
self.net = self._make_network(self.fmt, 'n1', True)['network']
|
||||||
|
res = self._create_subnet(self.fmt, self.net['id'], '10.0.0.0/24')
|
||||||
|
self.subnet = self.deserialize(self.fmt, res)['subnet']
|
||||||
|
res = self._create_port(self.fmt, self.net['id'])
|
||||||
|
self.port = self.deserialize(self.fmt, res)['port']
|
||||||
|
|
||||||
|
def test_qos_bw_limit_created_and_updated(self):
|
||||||
|
def check_update_egress_called(rate):
|
||||||
|
try:
|
||||||
|
mock_agent.qos_hwol_ext.update_egress.assert_has_calls(
|
||||||
|
[mock.call(port_id, rate, 0)])
|
||||||
|
return True
|
||||||
|
except AssertionError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
mock_agent = mock.Mock(nb_idl=self.nb_api)
|
||||||
|
events = [qos_hwol.QoSBandwidthLimitEvent(mock_agent)]
|
||||||
|
agent_ovsdb.MonitorAgentOvnNbIdl(qos_hwol.NB_IDL_TABLES,
|
||||||
|
events).start()
|
||||||
|
lswitch_name = utils.ovn_name(self.net['id'])
|
||||||
|
port_id = self.port['id']
|
||||||
|
ovn_rule = {'switch': lswitch_name,
|
||||||
|
'priority': 1000,
|
||||||
|
'direction': 'from-lport',
|
||||||
|
'match': 'inport == ' + port_id,
|
||||||
|
'rate': 10000,
|
||||||
|
'external_ids': {ovn_const.OVN_PORT_EXT_ID_KEY: port_id}}
|
||||||
|
self.nb_api.qos_add(**ovn_rule).execute(check_error=True)
|
||||||
|
n_utils.wait_until_true(
|
||||||
|
lambda: check_update_egress_called(ovn_rule['rate']), timeout=5)
|
||||||
|
|
||||||
|
ovn_rule['rate'] = 15000
|
||||||
|
self.nb_api.qos_add(**ovn_rule, may_exist=True).execute(
|
||||||
|
check_error=True)
|
||||||
|
n_utils.wait_until_true(
|
||||||
|
lambda: check_update_egress_called(ovn_rule['rate']), timeout=5)
|
||||||
|
|
||||||
|
|
||||||
|
class QoSMinimumBandwidthEventTestCase(base.TestOVNFunctionalBase):
|
||||||
|
|
||||||
|
def setUp(self, **kwargs):
|
||||||
|
super().setUp(**kwargs)
|
||||||
|
self.net = self._make_network(self.fmt, 'n1', True)['network']
|
||||||
|
res = self._create_subnet(self.fmt, self.net['id'], '10.0.0.0/24')
|
||||||
|
self.subnet = self.deserialize(self.fmt, res)['subnet']
|
||||||
|
res = self._create_port(self.fmt, self.net['id'])
|
||||||
|
self.port = self.deserialize(self.fmt, res)['port']
|
||||||
|
|
||||||
|
def test_qos_min_bw_created_and_updated(self):
|
||||||
|
def check_update_egress_called(max_kbps, min_kbps):
|
||||||
|
try:
|
||||||
|
mock_agent.qos_hwol_ext.update_egress.assert_has_calls(
|
||||||
|
[mock.call(port_id, max_kbps, min_kbps)])
|
||||||
|
return True
|
||||||
|
except AssertionError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
mock_agent = mock.Mock(nb_idl=self.nb_api)
|
||||||
|
events = [qos_hwol.QoSMinimumBandwidthEvent(mock_agent)]
|
||||||
|
agent_ovsdb.MonitorAgentOvnNbIdl(qos_hwol.NB_IDL_TABLES,
|
||||||
|
events).start()
|
||||||
|
port_id = self.port['id']
|
||||||
|
min_kbps = 5000
|
||||||
|
lsp = self.nb_api.lsp_get(port_id).execute(check_error=True)
|
||||||
|
options = {ovn_const.LSP_OPTIONS_QOS_MIN_RATE: str(min_kbps)}
|
||||||
|
self.nb_api.update_lswitch_qos_options(lsp, **options).execute(
|
||||||
|
check_error=True)
|
||||||
|
n_utils.wait_until_true(
|
||||||
|
lambda: check_update_egress_called(0, min_kbps), timeout=5)
|
||||||
|
|
||||||
|
|
||||||
|
class PortBindingChassisCreatedEventTestCase(base.TestOVNFunctionalBase):
|
||||||
|
|
||||||
|
def setUp(self, **kwargs):
|
||||||
|
super().setUp(**kwargs)
|
||||||
|
self.net = self._make_network(self.fmt, 'n1', True)['network']
|
||||||
|
res = self._create_subnet(self.fmt, self.net['id'], '10.0.0.0/24')
|
||||||
|
self.subnet = self.deserialize(self.fmt, res)['subnet']
|
||||||
|
res = self._create_port(self.fmt, self.net['id'])
|
||||||
|
self.port = self.deserialize(self.fmt, res)['port']
|
||||||
|
|
||||||
|
@mock.patch.object(agent_ovsdb, 'get_ovs_port_name')
|
||||||
|
@mock.patch.object(agent_ovsdb, 'get_port_qos')
|
||||||
|
def test_port_binding_chassis_create_event(self, mock_get_port_qos,
|
||||||
|
*args):
|
||||||
|
def check_update_egress_called(max_kbps, min_kbps):
|
||||||
|
try:
|
||||||
|
mock_agent.qos_hwol_ext.update_egress.assert_has_calls(
|
||||||
|
[mock.call(self.port['id'], max_kbps, min_kbps)])
|
||||||
|
return True
|
||||||
|
except AssertionError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
max_kbps, min_kbps = 1000, 800
|
||||||
|
mock_get_port_qos.return_value = max_kbps, min_kbps
|
||||||
|
mock_agent = mock.Mock(nb_idl=self.nb_api)
|
||||||
|
events = [qos_hwol.PortBindingChassisCreatedEvent(mock_agent)]
|
||||||
|
chassis_name = self.add_fake_chassis('ovn-host-fake')
|
||||||
|
mock_agent.chassis = chassis_name
|
||||||
|
agent_ovsdb.MonitorAgentOvnSbIdl(qos_hwol.SB_IDL_TABLES, events,
|
||||||
|
chassis=chassis_name).start()
|
||||||
|
lsp_columns = {}
|
||||||
|
lsp_name = self.port['id']
|
||||||
|
ls_name = utils.ovn_name(self.net['id'])
|
||||||
|
self.nb_api.create_lswitch_port(
|
||||||
|
lsp_name, ls_name, **lsp_columns).execute(check_error=True)
|
||||||
|
self.sb_api.lsp_bind(lsp_name, chassis_name).execute(check_error=True)
|
||||||
|
n_utils.wait_until_true(
|
||||||
|
lambda: check_update_egress_called(max_kbps, min_kbps), timeout=5)
|
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Added a new OVN Neutron Agent extension: QoS for hardware offloaded ports.
|
||||||
|
This extension will enforce the minimum and maximum bandwidth egress QoS
|
||||||
|
rules for ports with hardware offload (DevLink ports). This extension
|
||||||
|
uses the "ip-link" commands to set the "ceil" and "rate" parameters on
|
||||||
|
the corresponding virtual functions.
|
@ -142,6 +142,7 @@ neutron.agent.l3.extensions =
|
|||||||
conntrack_helper = neutron.agent.l3.extensions.conntrack_helper:ConntrackHelperAgentExtension
|
conntrack_helper = neutron.agent.l3.extensions.conntrack_helper:ConntrackHelperAgentExtension
|
||||||
ndp_proxy = neutron.agent.l3.extensions.ndp_proxy:NDPProxyAgentExtension
|
ndp_proxy = neutron.agent.l3.extensions.ndp_proxy:NDPProxyAgentExtension
|
||||||
neutron.agent.ovn.extensions =
|
neutron.agent.ovn.extensions =
|
||||||
|
qos_hwol = neutron.agent.ovn.extensions.qos_hwol:QoSHardwareOffloadExtension
|
||||||
noop = neutron.agent.ovn.extensions.noop:NoopOVNAgentExtension
|
noop = neutron.agent.ovn.extensions.noop:NoopOVNAgentExtension
|
||||||
testing = neutron.tests.functional.agent.ovn.agent.fake_ovn_agent_extension:FakeOVNAgentExtension
|
testing = neutron.tests.functional.agent.ovn.agent.fake_ovn_agent_extension:FakeOVNAgentExtension
|
||||||
neutron.services.logapi.drivers =
|
neutron.services.logapi.drivers =
|
||||||
|
Loading…
Reference in New Issue
Block a user