summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/netplan.md9
-rw-r--r--netplan/cli/sriov.py85
-rw-r--r--src/parse.c1
-rw-r--r--src/parse.h1
-rw-r--r--tests/generator/test_ethernets.py42
-rw-r--r--tests/test_sriov.py71
6 files changed, 150 insertions, 59 deletions
diff --git a/doc/netplan.md b/doc/netplan.md
index ca49e10..c4b0ba3 100644
--- a/doc/netplan.md
+++ b/doc/netplan.md
@@ -576,6 +576,15 @@ Example:
enp1s16f1:
link: enp1
+``virtual-function-count`` (scalar)
+
+: (SR-IOV devices only) In certain special cases VFs might need to be
+ configured outside of netplan. For such configurations ``virtual-function-count``
+ can be optionally used to set an explicit number of Virtual Functions for
+ the given Physical Function. If unset, the default is to create only as many
+ VFs as are defined in the netplan configuration. This should be used for special
+ cases only.
+
## Properties for device type ``modems:``
GSM/CDMA modem configuration is only supported for the ``NetworkManager`` backend. ``systemd-networkd`` does
not support modems.
diff --git a/netplan/cli/sriov.py b/netplan/cli/sriov.py
index 0f3bc9d..8feacf1 100644
--- a/netplan/cli/sriov.py
+++ b/netplan/cli/sriov.py
@@ -27,6 +27,34 @@ from netplan.configmanager import ConfigurationError
import netifaces
+def _get_target_interface(interfaces, config_manager, pf_link, pfs):
+ if pf_link not in pfs:
+ # handle the match: syntax, get the actual device name
+ pf_match = config_manager.ethernets[pf_link].get('match')
+ if pf_match:
+ by_name = pf_match.get('name')
+ by_mac = pf_match.get('macaddress')
+ by_driver = pf_match.get('driver')
+
+ for interface in interfaces:
+ if ((by_name and not utils.is_interface_matching_name(interface, by_name)) or
+ (by_mac and not utils.is_interface_matching_macaddress(interface, by_mac)) or
+ (by_driver and not utils.is_interface_matching_driver_name(interface, by_driver))):
+ continue
+ # we have a matching PF
+ # store the matching interface in the dictionary of
+ # active PFs, but error out if we matched more than one
+ if pf_link in pfs:
+ raise ConfigurationError('matched more than one interface for a PF device: %s' % pf_link)
+ pfs[pf_link] = interface
+ else:
+ # no match field, assume entry name is interface name
+ if pf_link in interfaces:
+ pfs[pf_link] = pf_link
+
+ return pfs.get(pf_link, None)
+
+
def get_vf_count_and_functions(interfaces, config_manager,
vf_counts, vfs, pfs):
"""
@@ -34,37 +62,25 @@ def get_vf_count_and_functions(interfaces, config_manager,
PFs and VFs, matching the former with actual networking interfaces.
Count how many VFs each PF will need.
"""
+ explicit_counts = {}
for ethernet, settings in config_manager.ethernets.items():
if not settings:
continue
if ethernet == 'renderer':
continue
+ # we now also support explicitly stating how many VFs should be
+ # allocated for a PF
+ explicit_num = settings.get('virtual-function-count')
+ if explicit_num:
+ pf = _get_target_interface(interfaces, config_manager, ethernet, pfs)
+ if pf:
+ explicit_counts[pf] = explicit_num
+ continue
+
pf_link = settings.get('link')
if pf_link and pf_link in config_manager.ethernets:
- if pf_link not in pfs:
- # handle the match: syntax, get the actual device name
- pf_match = config_manager.ethernets[pf_link].get('match')
- if pf_match:
- by_name = pf_match.get('name')
- by_mac = pf_match.get('macaddress')
- by_driver = pf_match.get('driver')
-
- for interface in interfaces:
- if ((by_name and not utils.is_interface_matching_name(interface, by_name)) or
- (by_mac and not utils.is_interface_matching_macaddress(interface, by_mac)) or
- (by_driver and not utils.is_interface_matching_driver_name(interface, by_driver))):
- continue
- # we have a matching PF
- # store the matching interface in the dictionary of
- # active PFs, but error out if we matched more than one
- if pf_link in pfs:
- raise ConfigurationError('matched more than one interface for a PF device: %s' % pf_link)
- pfs[pf_link] = interface
- else:
- # no match field, assume entry name is interface name
- if pf_link in interfaces:
- pfs[pf_link] = pf_link
+ _get_target_interface(interfaces, config_manager, pf_link, pfs)
if pf_link in pfs:
vf_counts[pfs[pf_link]] += 1
@@ -78,6 +94,15 @@ def get_vf_count_and_functions(interfaces, config_manager,
# VFs that we encounter so far
vfs[ethernet] = None
+ # sanity check: since we can explicitly state the VF count, make sure
+ # that this number isn't smaller than the actual number of VFs declared
+ # the explicit number also overrides the number of actual VFs
+ for pf, count in explicit_counts.items():
+ if pf in vf_counts and vf_counts[pf] > count:
+ raise ConfigurationError(
+ 'more VFs allocated than the explicit size declared: %s > %s' % (vf_counts[pf], count))
+ vf_counts[pf] = count
+
def set_numvfs_for_pf(pf, vf_count):
"""
@@ -91,27 +116,17 @@ def set_numvfs_for_pf(pf, vf_count):
numvfs_path = os.path.join(devdir, 'sriov_numvfs')
totalvfs_path = os.path.join(devdir, 'sriov_totalvfs')
try:
- with open(numvfs_path) as f:
- vf_current = int(f.read().strip())
with open(totalvfs_path) as f:
vf_max = int(f.read().strip())
except IOError as e:
- raise RuntimeError('failed parsing sriov_numvfs/sriov_totalvfs for %s: %s' % (pf, str(e)))
+ raise RuntimeError('failed parsing sriov_totalvfs for %s: %s' % (pf, str(e)))
except ValueError:
- raise RuntimeError('invalid sriov_numvfs/sriov_totalvfs value for %s' % pf)
+ raise RuntimeError('invalid sriov_totalvfs value for %s' % pf)
if vf_count > vf_max:
raise ConfigurationError(
'cannot allocate more VFs for PF %s than supported: %s > %s (sriov_totalvfs)' % (pf, vf_count, vf_max))
- if vf_count <= vf_current:
- # XXX: this might be a wrong assumption, but I assume that
- # the operation of adding/removing VFs is very invasive,
- # so it makes no sense to decrease the number of VFs if
- # less are needed - leaving the unused ones unconfigured?
- logging.debug('the %s PF already defines more VFs than required (%s > %s), skipping' % (pf, vf_current, vf_count))
- return False
-
try:
with open(numvfs_path, 'w') as f:
f.write(str(vf_count))
diff --git a/src/parse.c b/src/parse.c
index b9c4692..c1dc175 100644
--- a/src/parse.c
+++ b/src/parse.c
@@ -1726,6 +1726,7 @@ static const mapping_entry_handler ethernet_def_handlers[] = {
PHYSICAL_LINK_HANDLERS,
{"auth", YAML_MAPPING_NODE, handle_auth},
{"link", YAML_SCALAR_NODE, handle_netdef_id_ref, NULL, netdef_offset(sriov_link)},
+ {"virtual-function-count", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(sriov_explicit_vf_count)},
{NULL}
};
diff --git a/src/parse.h b/src/parse.h
index e11f57d..b84ff4c 100644
--- a/src/parse.h
+++ b/src/parse.h
@@ -336,6 +336,7 @@ struct net_definition {
/* these properties are only valid for SR-IOV NICs */
struct net_definition* sriov_link;
gboolean sriov_vlan_filter;
+ guint sriov_explicit_vf_count;
union {
struct NetplanNMSettings {
diff --git a/tests/generator/test_ethernets.py b/tests/generator/test_ethernets.py
index 53dd1a6..7a451f7 100644
--- a/tests/generator/test_ethernets.py
+++ b/tests/generator/test_ethernets.py
@@ -82,7 +82,7 @@ LinkLocalAddressing=ipv6
'''})
self.assert_networkd_udev(None)
- def test_eth_sriov_link(self):
+ def test_eth_sriov_vlan_filterv_link(self):
self.generate('''network:
version: 2
ethernets:
@@ -106,6 +106,21 @@ LinkLocalAddressing=ipv6
'''})
self.assert_networkd_udev(None)
+ def test_eth_sriov_virtual_functions(self):
+ self.generate('''network:
+ version: 2
+ ethernets:
+ enp1:
+ virtual-function-count: 8''')
+
+ self.assert_networkd({'enp1.network': '''[Match]
+Name=enp1
+
+[Network]
+LinkLocalAddressing=ipv6
+'''})
+ self.assert_networkd_udev(None)
+
def test_eth_match_by_driver_rename(self):
self.generate('''network:
version: 2
@@ -391,6 +406,31 @@ method=link-local
method=ignore
'''})
+ def test_eth_sriov_virtual_functions(self):
+ self.generate('''network:
+ version: 2
+ renderer: NetworkManager
+ ethernets:
+ enp1:
+ dhcp4: n
+ virtual-function-count: 8''')
+
+ self.assert_networkd({})
+ self.assert_nm({'enp1': '''[connection]
+id=netplan-enp1
+type=ethernet
+interface-name=enp1
+
+[ethernet]
+wake-on-lan=0
+
+[ipv4]
+method=link-local
+
+[ipv6]
+method=ignore
+'''})
+
def test_eth_set_mac(self):
self.generate('''network:
version: 2
diff --git a/tests/test_sriov.py b/tests/test_sriov.py
index 5a52baf..d43bdb3 100644
--- a/tests/test_sriov.py
+++ b/tests/test_sriov.py
@@ -130,6 +130,8 @@ class TestSRIOV(unittest.TestCase):
name: enp[4-5]
enp0:
mtu: 9000
+ enp8:
+ virtual-function-count: 7
enp9: {}
wlp6s0: {}
enp1s16f1:
@@ -151,7 +153,7 @@ class TestSRIOV(unittest.TestCase):
link: enp9
''', file=fd)
self.configmanager.parse()
- interfaces = ['enp1', 'enp2', 'enp3', 'enp5', 'enp0']
+ interfaces = ['enp1', 'enp2', 'enp3', 'enp5', 'enp0', 'enp8']
vf_counts = defaultdict(int)
vfs = {}
pfs = {}
@@ -162,7 +164,7 @@ class TestSRIOV(unittest.TestCase):
# check if the right vf counts have been recorded in vf_counts
self.assertDictEqual(
vf_counts,
- {'enp1': 2, 'enp2': 2, 'enp3': 1, 'enp5': 1})
+ {'enp1': 2, 'enp2': 2, 'enp3': 1, 'enp5': 1, 'enp8': 7})
# also check if the vfs and pfs dictionaries got properly set
self.assertDictEqual(
vfs,
@@ -171,7 +173,7 @@ class TestSRIOV(unittest.TestCase):
self.assertDictEqual(
pfs,
{'enp1': 'enp1', 'enp2': 'enp2', 'enp3': 'enp3',
- 'enpx': 'enp5'})
+ 'enpx': 'enp5', 'enp8': 'enp8'})
@patch('netplan.cli.utils.get_interface_driver_name')
@patch('netplan.cli.utils.get_interface_macaddress')
@@ -207,24 +209,60 @@ class TestSRIOV(unittest.TestCase):
self.assertIn('matched more than one interface for a PF device: enpx',
str(e.exception))
+ @patch('netplan.cli.utils.get_interface_driver_name')
+ @patch('netplan.cli.utils.get_interface_macaddress')
+ def test_get_vf_count_and_functions_not_enough_explicit(self, gim, gidn):
+ # we mock-out get_interface_driver_name and get_interface_macaddress
+ # to return useful values for the test
+ gim.side_effect = lambda x: '00:01:02:03:04:05' if x == 'enp3' else '00:00:00:00:00:00'
+ gidn.side_effect = lambda x: 'foo' if x == 'enp2' else 'bar'
+ with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd:
+ print('''network:
+ version: 2
+ renderer: networkd
+ ethernets:
+ renderer: networkd
+ enp1:
+ virtual-function-count: 2
+ mtu: 9000
+ enp1s16f1:
+ link: enp1
+ enp1s16f2:
+ link: enp1
+ enp1s16f3:
+ link: enp1
+''', file=fd)
+ self.configmanager.parse()
+ interfaces = ['enp1', 'wlp6s0']
+ vf_counts = defaultdict(int)
+ vfs = {}
+ pfs = {}
+
+ # call the function under test
+ with self.assertRaises(ConfigurationError) as e:
+ sriov.get_vf_count_and_functions(interfaces, self.configmanager,
+ vf_counts, vfs, pfs)
+
+ self.assertIn('more VFs allocated than the explicit size declared: 3 > 2',
+ str(e.exception))
+
def test_set_numvfs_for_pf(self):
sriov_open = MockSRIOVOpen()
- sriov_open.read_queue = ['1\n', '8\n']
+ sriov_open.read_queue = ['8\n']
with patch('builtins.open', sriov_open.open):
ret = sriov.set_numvfs_for_pf('enp1', 2)
self.assertTrue(ret)
self.assertListEqual(sriov_open.open.call_args_list,
- [call('/sys/class/net/enp1/device/sriov_numvfs'),
- call('/sys/class/net/enp1/device/sriov_totalvfs'),
+ [call('/sys/class/net/enp1/device/sriov_totalvfs'),
call('/sys/class/net/enp1/device/sriov_numvfs', 'w')])
handle = sriov_open.open()
handle.write.assert_called_once_with('2')
def test_set_numvfs_for_pf_failsafe(self):
sriov_open = MockSRIOVOpen()
- sriov_open.read_queue = ['1\n', '8\n']
+ sriov_open.read_queue = ['8\n']
sriov_open.write_queue = [IOError(16, 'Error'), None, None]
with patch('builtins.open', sriov_open.open):
@@ -236,7 +274,7 @@ class TestSRIOV(unittest.TestCase):
def test_set_numvfs_for_pf_over_max(self):
sriov_open = MockSRIOVOpen()
- sriov_open.read_queue = ['1\n', '8\n']
+ sriov_open.read_queue = ['8\n']
with patch('builtins.open', sriov_open.open):
with self.assertRaises(ConfigurationError) as e:
@@ -247,7 +285,7 @@ class TestSRIOV(unittest.TestCase):
def test_set_numvfs_for_pf_over_theoretical_max(self):
sriov_open = MockSRIOVOpen()
- sriov_open.read_queue = ['1\n', '1337\n']
+ sriov_open.read_queue = ['1337\n']
with patch('builtins.open', sriov_open.open):
with self.assertRaises(ConfigurationError) as e:
@@ -256,24 +294,11 @@ class TestSRIOV(unittest.TestCase):
self.assertIn('cannot allocate more VFs for PF enp1 than the SR-IOV maximum',
str(e.exception))
- def test_set_numvfs_for_pf_smaller(self):
- sriov_open = MockSRIOVOpen()
- sriov_open.read_queue = ['4\n', '8\n']
-
- with patch('builtins.open', sriov_open.open):
- ret = sriov.set_numvfs_for_pf('enp1', 3)
-
- self.assertFalse(ret)
- handle = sriov_open.open()
- self.assertEqual(handle.write.call_count, 0)
-
def test_set_numvfs_for_pf_read_failed(self):
sriov_open = MockSRIOVOpen()
cases = (
[IOError],
['not a number\n'],
- ['1\n', IOError],
- ['1\n', 'not a number\n'],
)
with patch('builtins.open', sriov_open.open):
@@ -284,7 +309,7 @@ class TestSRIOV(unittest.TestCase):
def test_set_numvfs_for_pf_write_failed(self):
sriov_open = MockSRIOVOpen()
- sriov_open.read_queue = ['1\n', '8\n']
+ sriov_open.read_queue = ['8\n']
sriov_open.write_queue = [IOError(16, 'Error'), IOError(16, 'Error')]
with patch('builtins.open', sriov_open.open):