Skip to content

Commit 1ccf7e0

Browse files
sil2100Łukasz 'sil2100' Zemczak
andauthored
SR-IOV support for explicitly defining the VF count (#130)
This is a possible feature request to the just-landed SR-IOV support in netplan. This PR introduces an optional 'virtual-function-count:' parameter that can be defined for physical functions to force the allocation of a given number of VFs, regardless of how many are actually used in the netplan config. There are of course safety checks to ensure that we can't request less VFs than actually needed in netplan. This feature request came from the OpenStack team. We did consider such a thing originally, but then decided that it's better if we let netplan handle it. This PR adds this as an option (not recommended for everyday usage tho). Co-authored-by: Łukasz 'sil2100' Zemczak <[email protected]>
1 parent b7f1d9b commit 1ccf7e0

File tree

6 files changed

+150
-59
lines changed

6 files changed

+150
-59
lines changed

doc/netplan.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,15 @@ Example:
576576
enp1s16f1:
577577
link: enp1
578578

579+
``virtual-function-count`` (scalar)
580+
581+
: (SR-IOV devices only) In certain special cases VFs might need to be
582+
configured outside of netplan. For such configurations ``virtual-function-count``
583+
can be optionally used to set an explicit number of Virtual Functions for
584+
the given Physical Function. If unset, the default is to create only as many
585+
VFs as are defined in the netplan configuration. This should be used for special
586+
cases only.
587+
579588
## Properties for device type ``modems:``
580589
GSM/CDMA modem configuration is only supported for the ``NetworkManager`` backend. ``systemd-networkd`` does
581590
not support modems.

netplan/cli/sriov.py

Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,44 +27,60 @@
2727
import netifaces
2828

2929

30+
def _get_target_interface(interfaces, config_manager, pf_link, pfs):
31+
if pf_link not in pfs:
32+
# handle the match: syntax, get the actual device name
33+
pf_match = config_manager.ethernets[pf_link].get('match')
34+
if pf_match:
35+
by_name = pf_match.get('name')
36+
by_mac = pf_match.get('macaddress')
37+
by_driver = pf_match.get('driver')
38+
39+
for interface in interfaces:
40+
if ((by_name and not utils.is_interface_matching_name(interface, by_name)) or
41+
(by_mac and not utils.is_interface_matching_macaddress(interface, by_mac)) or
42+
(by_driver and not utils.is_interface_matching_driver_name(interface, by_driver))):
43+
continue
44+
# we have a matching PF
45+
# store the matching interface in the dictionary of
46+
# active PFs, but error out if we matched more than one
47+
if pf_link in pfs:
48+
raise ConfigurationError('matched more than one interface for a PF device: %s' % pf_link)
49+
pfs[pf_link] = interface
50+
else:
51+
# no match field, assume entry name is interface name
52+
if pf_link in interfaces:
53+
pfs[pf_link] = pf_link
54+
55+
return pfs.get(pf_link, None)
56+
57+
3058
def get_vf_count_and_functions(interfaces, config_manager,
3159
vf_counts, vfs, pfs):
3260
"""
3361
Go through the list of netplan ethernet devices and identify which are
3462
PFs and VFs, matching the former with actual networking interfaces.
3563
Count how many VFs each PF will need.
3664
"""
65+
explicit_counts = {}
3766
for ethernet, settings in config_manager.ethernets.items():
3867
if not settings:
3968
continue
4069
if ethernet == 'renderer':
4170
continue
4271

72+
# we now also support explicitly stating how many VFs should be
73+
# allocated for a PF
74+
explicit_num = settings.get('virtual-function-count')
75+
if explicit_num:
76+
pf = _get_target_interface(interfaces, config_manager, ethernet, pfs)
77+
if pf:
78+
explicit_counts[pf] = explicit_num
79+
continue
80+
4381
pf_link = settings.get('link')
4482
if pf_link and pf_link in config_manager.ethernets:
45-
if pf_link not in pfs:
46-
# handle the match: syntax, get the actual device name
47-
pf_match = config_manager.ethernets[pf_link].get('match')
48-
if pf_match:
49-
by_name = pf_match.get('name')
50-
by_mac = pf_match.get('macaddress')
51-
by_driver = pf_match.get('driver')
52-
53-
for interface in interfaces:
54-
if ((by_name and not utils.is_interface_matching_name(interface, by_name)) or
55-
(by_mac and not utils.is_interface_matching_macaddress(interface, by_mac)) or
56-
(by_driver and not utils.is_interface_matching_driver_name(interface, by_driver))):
57-
continue
58-
# we have a matching PF
59-
# store the matching interface in the dictionary of
60-
# active PFs, but error out if we matched more than one
61-
if pf_link in pfs:
62-
raise ConfigurationError('matched more than one interface for a PF device: %s' % pf_link)
63-
pfs[pf_link] = interface
64-
else:
65-
# no match field, assume entry name is interface name
66-
if pf_link in interfaces:
67-
pfs[pf_link] = pf_link
83+
_get_target_interface(interfaces, config_manager, pf_link, pfs)
6884

6985
if pf_link in pfs:
7086
vf_counts[pfs[pf_link]] += 1
@@ -78,6 +94,15 @@ def get_vf_count_and_functions(interfaces, config_manager,
7894
# VFs that we encounter so far
7995
vfs[ethernet] = None
8096

97+
# sanity check: since we can explicitly state the VF count, make sure
98+
# that this number isn't smaller than the actual number of VFs declared
99+
# the explicit number also overrides the number of actual VFs
100+
for pf, count in explicit_counts.items():
101+
if pf in vf_counts and vf_counts[pf] > count:
102+
raise ConfigurationError(
103+
'more VFs allocated than the explicit size declared: %s > %s' % (vf_counts[pf], count))
104+
vf_counts[pf] = count
105+
81106

82107
def set_numvfs_for_pf(pf, vf_count):
83108
"""
@@ -91,27 +116,17 @@ def set_numvfs_for_pf(pf, vf_count):
91116
numvfs_path = os.path.join(devdir, 'sriov_numvfs')
92117
totalvfs_path = os.path.join(devdir, 'sriov_totalvfs')
93118
try:
94-
with open(numvfs_path) as f:
95-
vf_current = int(f.read().strip())
96119
with open(totalvfs_path) as f:
97120
vf_max = int(f.read().strip())
98121
except IOError as e:
99-
raise RuntimeError('failed parsing sriov_numvfs/sriov_totalvfs for %s: %s' % (pf, str(e)))
122+
raise RuntimeError('failed parsing sriov_totalvfs for %s: %s' % (pf, str(e)))
100123
except ValueError:
101-
raise RuntimeError('invalid sriov_numvfs/sriov_totalvfs value for %s' % pf)
124+
raise RuntimeError('invalid sriov_totalvfs value for %s' % pf)
102125

103126
if vf_count > vf_max:
104127
raise ConfigurationError(
105128
'cannot allocate more VFs for PF %s than supported: %s > %s (sriov_totalvfs)' % (pf, vf_count, vf_max))
106129

107-
if vf_count <= vf_current:
108-
# XXX: this might be a wrong assumption, but I assume that
109-
# the operation of adding/removing VFs is very invasive,
110-
# so it makes no sense to decrease the number of VFs if
111-
# less are needed - leaving the unused ones unconfigured?
112-
logging.debug('the %s PF already defines more VFs than required (%s > %s), skipping' % (pf, vf_current, vf_count))
113-
return False
114-
115130
try:
116131
with open(numvfs_path, 'w') as f:
117132
f.write(str(vf_count))

src/parse.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1726,6 +1726,7 @@ static const mapping_entry_handler ethernet_def_handlers[] = {
17261726
PHYSICAL_LINK_HANDLERS,
17271727
{"auth", YAML_MAPPING_NODE, handle_auth},
17281728
{"link", YAML_SCALAR_NODE, handle_netdef_id_ref, NULL, netdef_offset(sriov_link)},
1729+
{"virtual-function-count", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(sriov_explicit_vf_count)},
17291730
{NULL}
17301731
};
17311732

src/parse.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ struct net_definition {
336336
/* these properties are only valid for SR-IOV NICs */
337337
struct net_definition* sriov_link;
338338
gboolean sriov_vlan_filter;
339+
guint sriov_explicit_vf_count;
339340

340341
union {
341342
struct NetplanNMSettings {

tests/generator/test_ethernets.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def test_eth_mtu(self):
8282
'''})
8383
self.assert_networkd_udev(None)
8484

85-
def test_eth_sriov_link(self):
85+
def test_eth_sriov_vlan_filterv_link(self):
8686
self.generate('''network:
8787
version: 2
8888
ethernets:
@@ -101,6 +101,21 @@ def test_eth_sriov_link(self):
101101
'enp1s16f1.network': '''[Match]
102102
Name=enp1s16f1
103103
104+
[Network]
105+
LinkLocalAddressing=ipv6
106+
'''})
107+
self.assert_networkd_udev(None)
108+
109+
def test_eth_sriov_virtual_functions(self):
110+
self.generate('''network:
111+
version: 2
112+
ethernets:
113+
enp1:
114+
virtual-function-count: 8''')
115+
116+
self.assert_networkd({'enp1.network': '''[Match]
117+
Name=enp1
118+
104119
[Network]
105120
LinkLocalAddressing=ipv6
106121
'''})
@@ -387,6 +402,31 @@ def test_eth_sriov_link(self):
387402
[ipv4]
388403
method=link-local
389404
405+
[ipv6]
406+
method=ignore
407+
'''})
408+
409+
def test_eth_sriov_virtual_functions(self):
410+
self.generate('''network:
411+
version: 2
412+
renderer: NetworkManager
413+
ethernets:
414+
enp1:
415+
dhcp4: n
416+
virtual-function-count: 8''')
417+
418+
self.assert_networkd({})
419+
self.assert_nm({'enp1': '''[connection]
420+
id=netplan-enp1
421+
type=ethernet
422+
interface-name=enp1
423+
424+
[ethernet]
425+
wake-on-lan=0
426+
427+
[ipv4]
428+
method=link-local
429+
390430
[ipv6]
391431
method=ignore
392432
'''})

tests/test_sriov.py

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ def test_get_vf_count_and_functions(self, gim, gidn):
130130
name: enp[4-5]
131131
enp0:
132132
mtu: 9000
133+
enp8:
134+
virtual-function-count: 7
133135
enp9: {}
134136
wlp6s0: {}
135137
enp1s16f1:
@@ -151,7 +153,7 @@ def test_get_vf_count_and_functions(self, gim, gidn):
151153
link: enp9
152154
''', file=fd)
153155
self.configmanager.parse()
154-
interfaces = ['enp1', 'enp2', 'enp3', 'enp5', 'enp0']
156+
interfaces = ['enp1', 'enp2', 'enp3', 'enp5', 'enp0', 'enp8']
155157
vf_counts = defaultdict(int)
156158
vfs = {}
157159
pfs = {}
@@ -162,7 +164,7 @@ def test_get_vf_count_and_functions(self, gim, gidn):
162164
# check if the right vf counts have been recorded in vf_counts
163165
self.assertDictEqual(
164166
vf_counts,
165-
{'enp1': 2, 'enp2': 2, 'enp3': 1, 'enp5': 1})
167+
{'enp1': 2, 'enp2': 2, 'enp3': 1, 'enp5': 1, 'enp8': 7})
166168
# also check if the vfs and pfs dictionaries got properly set
167169
self.assertDictEqual(
168170
vfs,
@@ -171,7 +173,7 @@ def test_get_vf_count_and_functions(self, gim, gidn):
171173
self.assertDictEqual(
172174
pfs,
173175
{'enp1': 'enp1', 'enp2': 'enp2', 'enp3': 'enp3',
174-
'enpx': 'enp5'})
176+
'enpx': 'enp5', 'enp8': 'enp8'})
175177

176178
@patch('netplan.cli.utils.get_interface_driver_name')
177179
@patch('netplan.cli.utils.get_interface_macaddress')
@@ -207,24 +209,60 @@ def test_get_vf_count_and_functions_many_match(self, gim, gidn):
207209
self.assertIn('matched more than one interface for a PF device: enpx',
208210
str(e.exception))
209211

212+
@patch('netplan.cli.utils.get_interface_driver_name')
213+
@patch('netplan.cli.utils.get_interface_macaddress')
214+
def test_get_vf_count_and_functions_not_enough_explicit(self, gim, gidn):
215+
# we mock-out get_interface_driver_name and get_interface_macaddress
216+
# to return useful values for the test
217+
gim.side_effect = lambda x: '00:01:02:03:04:05' if x == 'enp3' else '00:00:00:00:00:00'
218+
gidn.side_effect = lambda x: 'foo' if x == 'enp2' else 'bar'
219+
with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd:
220+
print('''network:
221+
version: 2
222+
renderer: networkd
223+
ethernets:
224+
renderer: networkd
225+
enp1:
226+
virtual-function-count: 2
227+
mtu: 9000
228+
enp1s16f1:
229+
link: enp1
230+
enp1s16f2:
231+
link: enp1
232+
enp1s16f3:
233+
link: enp1
234+
''', file=fd)
235+
self.configmanager.parse()
236+
interfaces = ['enp1', 'wlp6s0']
237+
vf_counts = defaultdict(int)
238+
vfs = {}
239+
pfs = {}
240+
241+
# call the function under test
242+
with self.assertRaises(ConfigurationError) as e:
243+
sriov.get_vf_count_and_functions(interfaces, self.configmanager,
244+
vf_counts, vfs, pfs)
245+
246+
self.assertIn('more VFs allocated than the explicit size declared: 3 > 2',
247+
str(e.exception))
248+
210249
def test_set_numvfs_for_pf(self):
211250
sriov_open = MockSRIOVOpen()
212-
sriov_open.read_queue = ['1\n', '8\n']
251+
sriov_open.read_queue = ['8\n']
213252

214253
with patch('builtins.open', sriov_open.open):
215254
ret = sriov.set_numvfs_for_pf('enp1', 2)
216255

217256
self.assertTrue(ret)
218257
self.assertListEqual(sriov_open.open.call_args_list,
219-
[call('/sys/class/net/enp1/device/sriov_numvfs'),
220-
call('/sys/class/net/enp1/device/sriov_totalvfs'),
258+
[call('/sys/class/net/enp1/device/sriov_totalvfs'),
221259
call('/sys/class/net/enp1/device/sriov_numvfs', 'w')])
222260
handle = sriov_open.open()
223261
handle.write.assert_called_once_with('2')
224262

225263
def test_set_numvfs_for_pf_failsafe(self):
226264
sriov_open = MockSRIOVOpen()
227-
sriov_open.read_queue = ['1\n', '8\n']
265+
sriov_open.read_queue = ['8\n']
228266
sriov_open.write_queue = [IOError(16, 'Error'), None, None]
229267

230268
with patch('builtins.open', sriov_open.open):
@@ -236,7 +274,7 @@ def test_set_numvfs_for_pf_failsafe(self):
236274

237275
def test_set_numvfs_for_pf_over_max(self):
238276
sriov_open = MockSRIOVOpen()
239-
sriov_open.read_queue = ['1\n', '8\n']
277+
sriov_open.read_queue = ['8\n']
240278

241279
with patch('builtins.open', sriov_open.open):
242280
with self.assertRaises(ConfigurationError) as e:
@@ -247,7 +285,7 @@ def test_set_numvfs_for_pf_over_max(self):
247285

248286
def test_set_numvfs_for_pf_over_theoretical_max(self):
249287
sriov_open = MockSRIOVOpen()
250-
sriov_open.read_queue = ['1\n', '1337\n']
288+
sriov_open.read_queue = ['1337\n']
251289

252290
with patch('builtins.open', sriov_open.open):
253291
with self.assertRaises(ConfigurationError) as e:
@@ -256,24 +294,11 @@ def test_set_numvfs_for_pf_over_theoretical_max(self):
256294
self.assertIn('cannot allocate more VFs for PF enp1 than the SR-IOV maximum',
257295
str(e.exception))
258296

259-
def test_set_numvfs_for_pf_smaller(self):
260-
sriov_open = MockSRIOVOpen()
261-
sriov_open.read_queue = ['4\n', '8\n']
262-
263-
with patch('builtins.open', sriov_open.open):
264-
ret = sriov.set_numvfs_for_pf('enp1', 3)
265-
266-
self.assertFalse(ret)
267-
handle = sriov_open.open()
268-
self.assertEqual(handle.write.call_count, 0)
269-
270297
def test_set_numvfs_for_pf_read_failed(self):
271298
sriov_open = MockSRIOVOpen()
272299
cases = (
273300
[IOError],
274301
['not a number\n'],
275-
['1\n', IOError],
276-
['1\n', 'not a number\n'],
277302
)
278303

279304
with patch('builtins.open', sriov_open.open):
@@ -284,7 +309,7 @@ def test_set_numvfs_for_pf_read_failed(self):
284309

285310
def test_set_numvfs_for_pf_write_failed(self):
286311
sriov_open = MockSRIOVOpen()
287-
sriov_open.read_queue = ['1\n', '8\n']
312+
sriov_open.read_queue = ['8\n']
288313
sriov_open.write_queue = [IOError(16, 'Error'), IOError(16, 'Error')]
289314

290315
with patch('builtins.open', sriov_open.open):

0 commit comments

Comments
 (0)