From 54eff20a7226bf782c9db65112bf1e175519d60e Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Sat, 7 Jan 2023 05:32:53 +0100 Subject: [PATCH] [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 --- neutron/agent/ovn/agent/ovn_neutron_agent.py | 16 + neutron/agent/ovn/agent/ovsdb.py | 38 +++ neutron/agent/ovn/extensions/qos_hwol.py | 291 ++++++++++++++++++ .../functional/agent/ovn/agent/test_ovsdb.py | 77 +++++ .../agent/ovn/extensions/__init__.py | 0 .../agent/ovn/extensions/test_qos_hwol.py | 176 +++++++++++ ...t-qos-hwol-extension-a282afcf3f005c80.yaml | 8 + setup.cfg | 1 + 8 files changed, 607 insertions(+) create mode 100644 neutron/agent/ovn/extensions/qos_hwol.py create mode 100644 neutron/tests/functional/agent/ovn/agent/test_ovsdb.py create mode 100644 neutron/tests/functional/agent/ovn/extensions/__init__.py create mode 100644 neutron/tests/functional/agent/ovn/extensions/test_qos_hwol.py create mode 100644 releasenotes/notes/ovn-agent-qos-hwol-extension-a282afcf3f005c80.yaml diff --git a/neutron/agent/ovn/agent/ovn_neutron_agent.py b/neutron/agent/ovn/agent/ovn_neutron_agent.py index a3925eeebc9..3fb0bc491ec 100644 --- a/neutron/agent/ovn/agent/ovn_neutron_agent.py +++ b/neutron/agent/ovn/agent/ovn_neutron_agent.py @@ -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: diff --git a/neutron/agent/ovn/agent/ovsdb.py b/neutron/agent/ovn/agent/ovsdb.py index e5cf7571193..e1b45a3c7fb 100644 --- a/neutron/agent/ovn/agent/ovsdb.py +++ b/neutron/agent/ovn/agent/ovsdb.py @@ -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 diff --git a/neutron/agent/ovn/extensions/qos_hwol.py b/neutron/agent/ovn/extensions/qos_hwol.py new file mode 100644 index 00000000000..26a39b5a5f7 --- /dev/null +++ b/neutron/agent/ovn/extensions/qos_hwol.py @@ -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}) diff --git a/neutron/tests/functional/agent/ovn/agent/test_ovsdb.py b/neutron/tests/functional/agent/ovn/agent/test_ovsdb.py new file mode 100644 index 00000000000..fc8e098fba5 --- /dev/null +++ b/neutron/tests/functional/agent/ovn/agent/test_ovsdb.py @@ -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)) diff --git a/neutron/tests/functional/agent/ovn/extensions/__init__.py b/neutron/tests/functional/agent/ovn/extensions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/functional/agent/ovn/extensions/test_qos_hwol.py b/neutron/tests/functional/agent/ovn/extensions/test_qos_hwol.py new file mode 100644 index 00000000000..17678d0a210 --- /dev/null +++ b/neutron/tests/functional/agent/ovn/extensions/test_qos_hwol.py @@ -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) diff --git a/releasenotes/notes/ovn-agent-qos-hwol-extension-a282afcf3f005c80.yaml b/releasenotes/notes/ovn-agent-qos-hwol-extension-a282afcf3f005c80.yaml new file mode 100644 index 00000000000..68092938090 --- /dev/null +++ b/releasenotes/notes/ovn-agent-qos-hwol-extension-a282afcf3f005c80.yaml @@ -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. diff --git a/setup.cfg b/setup.cfg index 34b53e05284..53684cc7245 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 =