[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.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
|
||||
def sb_idl(self):
|
||||
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):
|
||||
self.chassis = ovsdb.get_own_chassis_name(ovs_idl)
|
||||
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 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.conf.plugins.ml2.drivers.ovn import ovn_conf as config
|
||||
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()
|
||||
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
|
||||
ndp_proxy = neutron.agent.l3.extensions.ndp_proxy:NDPProxyAgentExtension
|
||||
neutron.agent.ovn.extensions =
|
||||
qos_hwol = neutron.agent.ovn.extensions.qos_hwol:QoSHardwareOffloadExtension
|
||||
noop = neutron.agent.ovn.extensions.noop:NoopOVNAgentExtension
|
||||
testing = neutron.tests.functional.agent.ovn.agent.fake_ovn_agent_extension:FakeOVNAgentExtension
|
||||
neutron.services.logapi.drivers =
|
||||
|
Loading…
Reference in New Issue
Block a user