[SR-IOV] Fix QoS extension to set min/max values

"ip link" commands allow to define VF rates independently.
That means, first "rate" (max BW) can be set and in a second
command "min" (min BW) (check LP bug description).

However Pyroute2 command to set the VF rates requires to set both.
If one value is missing ("min_tx_rate", "max_tx_rate"), the library
sets this value to 0; in other words, the value is deleted.

The Pyroute2 structures are built depending on the parameter names.
In this case, {'vf': {'rate': ...}} will create a "vf_rate" [1]
nla structure, that requires "min_tx_rate" and "max_tx_rate".
This is part of the full structure passed to the "iproute" library
[2].

This is an example of code that only sets the "max_tx_rate" for
the 15th VF of "enp196s0f0":
  $ from neutron.plugins.ml2.drivers.mech_sriov.agent import pci_lib
    pci = pci_lib.PciDeviceIPWrapper("enp196s0f0")
    pci.set_vf_rate(15, {'max_tx_rate': 10})

The "msg" [3] (structure passed to "iproute" library) is this:
https://paste.opendev.org/show/b2FZBOebGOCHMrYhPr6X/. The
"min_tx_rate" is set to the default value 0.

This patch reads first the existing rates ("min_tx_rate",
"max_tx_rate") and populates the command parameters accordingly.

[1]a9564dff8e/pyroute2.core/pr2modules/netlink/rtnl/ifinfmsg/__init__.py (L712-L717)
[2]c8d9d92544/ip/ipaddress.c (L454-L470)
[3]a9564dff8e/pyroute2.core/pr2modules/iproute/linux.py (L1499)

Closes-Bug: #1962844
Change-Id: Ibbb6d938355440c42850812e368224b76b1fce19
This commit is contained in:
Rodolfo Alonso Hernandez 2022-03-03 02:57:07 +00:00
parent b072cbf05f
commit cdff281f64
6 changed files with 121 additions and 91 deletions

View File

@ -33,6 +33,8 @@ IP_LINK_CAPABILITY_STATE = 'state'
IP_LINK_CAPABILITY_VLAN = 'vlan'
IP_LINK_CAPABILITY_RATE = 'max_tx_rate'
IP_LINK_CAPABILITY_MIN_TX_RATE = 'min_tx_rate'
IP_LINK_CAPABILITY_RATES = (IP_LINK_CAPABILITY_RATE,
IP_LINK_CAPABILITY_MIN_TX_RATE)
IP_LINK_CAPABILITY_SPOOFCHK = 'spoofchk'
IP_LINK_SUB_CAPABILITY_QOS = 'qos'
@ -172,6 +174,9 @@ class EmbSwitch(object):
if pci_slot not in exclude_devices:
self.pci_slot_map[pci_slot] = vf_index
def _get_vfs(self):
return self.pci_dev_wrapper.device(self.dev_name).link.get_vfs()
def get_pci_slot_list(self):
"""Get list of VF addresses."""
return self.pci_slot_map.keys()
@ -208,40 +213,45 @@ class EmbSwitch(object):
return self.pci_dev_wrapper.set_vf_state(vf_index, state,
auto=propagate_uplink_state)
def set_device_rate(self, pci_slot, rate_type, rate_kbps):
def set_device_rate(self, pci_slot, rates):
"""Set device rate: max_tx_rate, min_tx_rate
@param pci_slot: Virtual Function address
@param rate_type: device rate name type. Could be 'max_tx_rate' and
'min_tx_rate' ('rate' is not supported anymore).
@param rate_kbps: device rate in kbps
@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}
"""
vf_index = self._get_vf_index(pci_slot)
# NOTE(ralonsoh): ip link sets rate in Mbps therefore we need to
# convert the rate_kbps value from kbps to Mbps.
# Zero means to disable the rate so the lowest rate available is 1Mbps.
# Floating numbers are not allowed
if 0 < rate_kbps < 1000:
rate_mbps = 1
else:
rate_mbps = helpers.round_val(rate_kbps / 1000.0)
rates_mbps = {}
for rate_type, rate_kbps in rates.items():
if 0 < rate_kbps < 1000:
rate_mbps = 1
else:
rate_mbps = helpers.round_val(rate_kbps / 1000.0)
log_dict = {
'rate_mbps': rate_mbps,
'rate_kbps': rate_kbps,
'vf_index': vf_index,
'rate_type': rate_type
}
if rate_kbps % 1000 != 0:
LOG.debug("'%(rate_type)s' for SR-IOV ports is counted in Mbps; "
"setting %(rate_mbps)s Mbps limit for port %(vf_index)s "
"instead of %(rate_kbps)s kbps",
log_dict)
else:
LOG.debug("Setting %(rate_mbps)s Mbps limit for port %(vf_index)s",
log_dict)
rates_mbps[rate_type] = rate_mbps
return self.pci_dev_wrapper.set_vf_rate(vf_index, rate_type, rate_mbps)
missing_rate_types = set(IP_LINK_CAPABILITY_RATES) - rates.keys()
if missing_rate_types:
# A key is missing. As explained in LP#1962844, in order not to
# delete an existing rate ('max_tx_rate', 'min_tx_rate'), it is
# needed to read the current VF rates and set the same value again.
vf = self._get_vfs()[int(vf_index)]
# Devices without 'min_tx_rate' support will return None in this
# value. If current value is 0, there is no need to set it again.
for _type in (_type for _type in missing_rate_types if vf[_type]):
rates_mbps[_type] = vf[_type]
LOG.debug('Setting %s limits (in Mbps) for port VF %s',
rates_mbps, vf_index)
return self.pci_dev_wrapper.set_vf_rate(vf_index, rates_mbps)
def _get_vf_index(self, pci_slot):
vf_index = self.pci_slot_map.get(pci_slot)

View File

@ -91,20 +91,22 @@ class PciDeviceIPWrapper(ip_lib.IPWrapper):
vf_config = {'vf': vf_index, 'spoofchk': int(enabled)}
ip.link.set_vf_feature(vf_config)
def set_vf_rate(self, vf_index, rate_type, rate_value):
"""sets vf rate.
def set_vf_rate(self, vf_index, rates):
"""sets vf rates.
@param vf_index: vf index
@param rate_type: vf rate type ('max_tx_rate', 'min_tx_rate')
@param rate_value: vf rate in Mbps
@param rates: dictionary with rate type (str) and the value (int)
in Mbps. Example:
{'max_tx_rate': 20, 'min_tx_rate': 10}
{'max_tx_rate': 30}
{'min_tx_rate': 5}
"""
ip = self.device(self.dev_name)
vf_config = {'vf': vf_index, 'rate': {rate_type: int(rate_value)}}
vf_config = {'vf': vf_index, 'rate': rates}
try:
ip.link.set_vf_feature(vf_config)
except ip_lib.InvalidArgument:
# NOTE(ralonsoh): some NICs do not support "min_tx_rate" parameter.
# https://bugs.launchpad.net/neutron/+bug/1918464
LOG.error('Device %(device)s does not support ip-link vf '
'"%(rate_type)s" parameter.',
{'device': self.dev_name, 'rate_type': rate_type})
'"min_tx_rate" parameter. Rates: %(rates)s',
{'device': self.dev_name, 'rates': rates})

View File

@ -404,8 +404,12 @@ def get_link_vfs(device, namespace):
for vinfo in vfinfo_list.get_attrs('IFLA_VF_INFO'):
mac = vinfo.get_attr('IFLA_VF_MAC')
link_state = vinfo.get_attr('IFLA_VF_LINK_STATE')
rate = vinfo.get_attr('IFLA_VF_RATE', default={})
vfs[mac['vf']] = {'mac': mac['mac'],
'link_state': link_state['link_state']}
'link_state': link_state['link_state'],
'max_tx_rate': rate.get('max_tx_rate'),
'min_tx_rate': rate.get('min_tx_rate'),
}
return vfs

View File

@ -430,6 +430,11 @@ class TestEmbSwitch(base.BaseTestCase):
"eswitch_manager.PciOsWrapper.scan_vf_devices",
return_value=self.SCANNED_DEVICES):
self.emb_switch = esm.EmbSwitch(self.DEV_NAME, exclude_devices)
self.mock_get_vfs = mock.patch.object(esm.EmbSwitch,
'_get_vfs').start()
self.vf_rates = [{esm.IP_LINK_CAPABILITY_RATE: 500,
esm.IP_LINK_CAPABILITY_MIN_TX_RATE: 250}]
self.mock_get_vfs.return_value = self.vf_rates
def test_get_assigned_devices_info(self):
with mock.patch.object(pci_lib.PciDeviceIPWrapper, 'get_assigned_macs',
@ -508,61 +513,64 @@ class TestEmbSwitch(base.BaseTestCase):
self.emb_switch.set_device_spoofcheck,
self.WRONG_PCI_SLOT, True)
def test_set_device_rate_ok(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent.pci_lib."
"PciDeviceIPWrapper.set_vf_rate") as pci_lib_mock:
self.emb_switch.set_device_rate(
self.PCI_SLOT, esm.IP_LINK_CAPABILITY_RATE, 2000)
pci_lib_mock.assert_called_with(
0, esm.IP_LINK_CAPABILITY_RATE, 2)
@mock.patch.object(pci_lib.PciDeviceIPWrapper, 'set_vf_rate')
def test_set_device_rate_ok(self, mock_set_vf_rate):
self.emb_switch.set_device_rate(
self.PCI_SLOT, {esm.IP_LINK_CAPABILITY_RATE: 2000})
self.vf_rates[0][esm.IP_LINK_CAPABILITY_RATE] = 2
mock_set_vf_rate.assert_called_with(0, self.vf_rates[0])
def test_set_device_max_rate_ok2(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent.pci_lib."
"PciDeviceIPWrapper.set_vf_rate") as pci_lib_mock:
self.emb_switch.set_device_rate(
self.PCI_SLOT, esm.IP_LINK_CAPABILITY_RATE, 99)
pci_lib_mock.assert_called_with(
0, esm.IP_LINK_CAPABILITY_RATE, 1)
# No 'min_tx_rate' support
vf_rates = [{esm.IP_LINK_CAPABILITY_RATE: 500,
esm.IP_LINK_CAPABILITY_MIN_TX_RATE: None}]
self.mock_get_vfs.return_value = vf_rates
self.emb_switch.set_device_rate(
self.PCI_SLOT, {esm.IP_LINK_CAPABILITY_RATE: 2000})
vf_rates[0] = {esm.IP_LINK_CAPABILITY_RATE: 2}
mock_set_vf_rate.assert_called_with(0, vf_rates[0])
def test_set_device_max_rate_rounded_ok(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent.pci_lib."
"PciDeviceIPWrapper.set_vf_rate") as pci_lib_mock:
self.emb_switch.set_device_rate(
self.PCI_SLOT, esm.IP_LINK_CAPABILITY_RATE, 2001)
pci_lib_mock.assert_called_with(0, esm.IP_LINK_CAPABILITY_RATE, 2)
@mock.patch.object(pci_lib.PciDeviceIPWrapper, 'set_vf_rate')
def test_set_device_max_rate_ok2(self, mock_set_vf_rate):
self.emb_switch.set_device_rate(
self.PCI_SLOT, {esm.IP_LINK_CAPABILITY_RATE: 99})
self.vf_rates[0][esm.IP_LINK_CAPABILITY_RATE] = 1
mock_set_vf_rate.assert_called_with(0, self.vf_rates[0])
def test_set_device_max_rate_rounded_ok2(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent.pci_lib."
"PciDeviceIPWrapper.set_vf_rate") as pci_lib_mock:
self.emb_switch.set_device_rate(
self.PCI_SLOT, esm.IP_LINK_CAPABILITY_RATE, 2499)
pci_lib_mock.assert_called_with(
0, esm.IP_LINK_CAPABILITY_RATE, 2)
@mock.patch.object(pci_lib.PciDeviceIPWrapper, 'set_vf_rate')
def test_set_device_max_rate_rounded_ok(self, mock_set_vf_rate):
self.emb_switch.set_device_rate(
self.PCI_SLOT, {esm.IP_LINK_CAPABILITY_RATE: 2001})
self.vf_rates[0][esm.IP_LINK_CAPABILITY_RATE] = 2
mock_set_vf_rate.assert_called_with(0, self.vf_rates[0])
def test_set_device_max_rate_rounded_ok3(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent.pci_lib."
"PciDeviceIPWrapper.set_vf_rate") as pci_lib_mock:
self.emb_switch.set_device_rate(
self.PCI_SLOT, esm.IP_LINK_CAPABILITY_RATE, 2500)
pci_lib_mock.assert_called_with(
0, esm.IP_LINK_CAPABILITY_RATE, 3)
@mock.patch.object(pci_lib.PciDeviceIPWrapper, 'set_vf_rate')
def test_set_device_max_rate_rounded_ok2(self, mock_set_vf_rate):
self.emb_switch.set_device_rate(
self.PCI_SLOT, {esm.IP_LINK_CAPABILITY_RATE: 2499})
self.vf_rates[0][esm.IP_LINK_CAPABILITY_RATE] = 2
mock_set_vf_rate.assert_called_with(0, self.vf_rates[0])
def test_set_device_max_rate_disable(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent.pci_lib."
"PciDeviceIPWrapper.set_vf_rate") as pci_lib_mock:
self.emb_switch.set_device_rate(
self.PCI_SLOT, esm.IP_LINK_CAPABILITY_RATE, 0)
pci_lib_mock.assert_called_with(
0, esm.IP_LINK_CAPABILITY_RATE, 0)
@mock.patch.object(pci_lib.PciDeviceIPWrapper, 'set_vf_rate')
def test_set_device_max_rate_rounded_ok3(self, mock_set_vf_rate):
self.emb_switch.set_device_rate(
self.PCI_SLOT, {esm.IP_LINK_CAPABILITY_RATE: 2500})
self.vf_rates[0][esm.IP_LINK_CAPABILITY_RATE] = 3
mock_set_vf_rate.assert_called_with(0, self.vf_rates[0])
def test_set_device_max_rate_fail(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent.pci_lib."
"PciDeviceIPWrapper.set_vf_rate"):
self.assertRaises(
exc.InvalidPciSlotError,
self.emb_switch.set_device_rate,
self.WRONG_PCI_SLOT,
esm.IP_LINK_CAPABILITY_RATE, 1000)
@mock.patch.object(pci_lib.PciDeviceIPWrapper, 'set_vf_rate')
def test_set_device_max_rate_disable(self, mock_set_vf_rate):
self.emb_switch.set_device_rate(
self.PCI_SLOT, {esm.IP_LINK_CAPABILITY_RATE: 0})
self.vf_rates[0][esm.IP_LINK_CAPABILITY_RATE] = 0
mock_set_vf_rate.assert_called_with(0, self.vf_rates[0])
@mock.patch.object(pci_lib.PciDeviceIPWrapper, 'set_vf_rate')
def test_set_device_max_rate_fail(self, *args):
self.assertRaises(
exc.InvalidPciSlotError,
self.emb_switch.set_device_rate,
self.WRONG_PCI_SLOT,
{esm.IP_LINK_CAPABILITY_RATE: 1000})
def test_get_pci_device(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent.pci_lib."

View File

@ -92,12 +92,12 @@ class TestPciLib(base.BaseTestCase):
self.mock_ip_device.link.set_vf_feature.assert_called_once_with(vf)
def test_set_vf_rate(self):
self.pci_wrapper.set_vf_rate(self.VF_INDEX, 'max_tx_rate', 20)
self.pci_wrapper.set_vf_rate(self.VF_INDEX, {'max_tx_rate': 20})
vf = {'vf': self.VF_INDEX, 'rate': {'max_tx_rate': 20}}
self.mock_ip_device.link.set_vf_feature.assert_called_once_with(vf)
self.mock_ip_device.link.set_vf_feature.reset_mock()
self.pci_wrapper.set_vf_rate(self.VF_INDEX, 'min_tx_rate', 10)
self.pci_wrapper.set_vf_rate(self.VF_INDEX, {'min_tx_rate': 10})
vf = {'vf': self.VF_INDEX, 'rate': {'min_tx_rate': 10}}
self.mock_ip_device.link.set_vf_feature.assert_called_once_with(vf)
@ -105,8 +105,9 @@ class TestPciLib(base.BaseTestCase):
def test_set_vf_rate_exception(self, mock_log):
self.mock_ip_device.link.set_vf_feature.side_effect = (
ip_lib.InvalidArgument)
self.pci_wrapper.set_vf_rate(self.VF_INDEX, 'min_tx_rate', 10)
self.pci_wrapper.set_vf_rate(self.VF_INDEX, {'min_tx_rate': 10})
mock_log.error.assert_called_once_with(
'Device %(device)s does not support ip-link vf "%(rate_type)s" '
'parameter.', {'device': self.DEV_NAME, 'rate_type': 'min_tx_rate'}
'Device %(device)s does not support ip-link vf "min_tx_rate" '
'parameter. Rates: %(rates)s',
{'device': self.DEV_NAME, 'rates': {'min_tx_rate': 10}}
)

View File

@ -238,9 +238,11 @@ class IpLibTestCase(base.BaseTestCase):
vf_info.append(pyroute2.netlink.nlmsg_base())
mac_info = {'mac': 'mac_%s' % idx, 'vf': idx}
link_state = {'link_state': idx} # see SR-IOV pci_lib.LinkState
rates = {'max_tx_rate': idx * 1000, 'min_tx_rate': idx * 500}
vf_info[idx].setvalue(
{'attrs': [('IFLA_VF_MAC', mac_info),
('IFLA_VF_LINK_STATE', link_state)]})
('IFLA_VF_LINK_STATE', link_state),
('IFLA_VF_RATE', rates)]})
vfinfo_list = pyroute2.netlink.nlmsg_base()
vfinfo_list.setvalue({'attrs': [('IFLA_VF_INFO', vf_info[0]),
('IFLA_VF_INFO', vf_info[1]),
@ -254,10 +256,13 @@ class IpLibTestCase(base.BaseTestCase):
with mock.patch.object(priv_lib, '_run_iproute_link') as mock_iplink:
mock_iplink.return_value = [value]
result = priv_lib.get_link_vfs('device', 'namespace')
self.assertEqual({0: {'mac': 'mac_0', 'link_state': 0},
1: {'mac': 'mac_1', 'link_state': 1},
2: {'mac': 'mac_2', 'link_state': 2}},
result)
exp = {0: {'mac': 'mac_0', 'link_state': 0,
'max_tx_rate': 0, 'min_tx_rate': 0},
1: {'mac': 'mac_1', 'link_state': 1,
'max_tx_rate': 1000, 'min_tx_rate': 500},
2: {'mac': 'mac_2', 'link_state': 2,
'max_tx_rate': 2000, 'min_tx_rate': 1000}}
self.assertEqual(exp, result)
class MakeSerializableTestCase(base.BaseTestCase):