Merge "[OVN] New OVN Neutron Agent extension: QoS for HWOL"

This commit is contained in:
Zuul 2023-02-02 13:39:57 +00:00 committed by Gerrit Code Review
commit 8234250e01
8 changed files with 607 additions and 0 deletions

View File

@ -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:

View File

@ -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

View 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})

View 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))

View 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)

View File

@ -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.

View File

@ -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 =