From 9e785a4ce06f9781181cd7981f0e50a3f3dece1f Mon Sep 17 00:00:00 2001 From: Karthik S Date: Tue, 12 Dec 2017 23:44:26 -0500 Subject: [PATCH] Add network objects sriov_vf and sriov_pf sriov_pf: configure numvfs of the device, create and enable sriov_numvfs service for the persistence of the numvfs configuration across reboots. sriov_vf: configure the ifcfgs for the Vfs Change-Id: I0e55d1556328fcb90b66c898c534b76e41ca9618 Implements: blueprint sriov-vfs-as-network-interface --- etc/os-net-config/samples/sriov_pf.json | 20 ++++ etc/os-net-config/samples/sriov_pf.yaml | 29 +++++ os_net_config/__init__.py | 18 +++ os_net_config/cli.py | 22 +++- os_net_config/impl_ifcfg.py | 36 +++++- os_net_config/objects.py | 72 +++++++++++- os_net_config/schema.yaml | 91 +++++++++++++++ os_net_config/sriov_config.py | 109 ++++++++++++++++++ os_net_config/tests/test_cli.py | 17 +++ os_net_config/tests/test_impl_ifcfg.py | 47 ++++++++ os_net_config/tests/test_objects.py | 57 +++++++++ os_net_config/tests/test_utils.py | 53 +++++++++ os_net_config/utils.py | 87 +++++++++++++- .../sriov-vf-in-hosts-529c2bf0cb3294b3.yaml | 7 ++ setup.cfg | 3 +- 15 files changed, 659 insertions(+), 9 deletions(-) create mode 100644 etc/os-net-config/samples/sriov_pf.json create mode 100644 etc/os-net-config/samples/sriov_pf.yaml create mode 100644 os_net_config/sriov_config.py create mode 100644 releasenotes/notes/sriov-vf-in-hosts-529c2bf0cb3294b3.yaml diff --git a/etc/os-net-config/samples/sriov_pf.json b/etc/os-net-config/samples/sriov_pf.json new file mode 100644 index 00000000..f2235fc6 --- /dev/null +++ b/etc/os-net-config/samples/sriov_pf.json @@ -0,0 +1,20 @@ +{ + "network_config": [ + { + "type": "sriov_pf", + "name": "p2p1", + "numvfs": 10, + "use_dhcp": false + }, + { + "type": "sriov_vf", + "device": "p2p1", + "vfid": 5, + "addresses": [ + { + "ip_netmask": "192.0.2.1/24" + } + ] + } + ] +} diff --git a/etc/os-net-config/samples/sriov_pf.yaml b/etc/os-net-config/samples/sriov_pf.yaml new file mode 100644 index 00000000..7458e4eb --- /dev/null +++ b/etc/os-net-config/samples/sriov_pf.yaml @@ -0,0 +1,29 @@ + + +network_config: + # sriov_pf type shall be used to configure the PF's of NICs. + # The numvfs configured for the PF's shall be set on the sriov_numvfs of the + # sysfs for the corresponding NIC and the persistence of the same across reboots + # shall be handled + - + type: sriov_pf + # nic name or nic number of the NIC that needs to be configured for SRIOV + name: p2p1 + # number of VFs required on the particular NIC + numvfs: 10 + # Dont set the IP address on the PF + use_dhcp: false + + # sriov_vf type shall be used to configure the VF's of NICs. + # It requires the PF devices to be configured via sriov_pf types + # "name" parameter is not required for VF's. It'll be derived from the VF id + # and the PF device name. + - type: sriov_vf + # The PF device needs to be configured via the sriov_pf type + # the PF device could be a nic number (ex: nic5) or nic name (ex: ens20f0) + device: p2p1 + # The VF id shall be the VF number between 0 to (numvfs-1), where numvfs + # is the member of the sriov_pf type + vfid: 5 + addresses: + - ip_netmask: 192.0.2.1/24 diff --git a/os_net_config/__init__.py b/os_net_config/__init__.py index 656feb77..c909701c 100644 --- a/os_net_config/__init__.py +++ b/os_net_config/__init__.py @@ -100,6 +100,10 @@ class NetConfig(object): self.add_ovs_dpdk_port(obj) elif isinstance(obj, objects.OvsDpdkBond): self.add_ovs_dpdk_bond(obj) + elif isinstance(obj, objects.SriovPF): + self.add_sriov_pf(obj) + elif isinstance(obj, objects.SriovVF): + self.add_sriov_vf(obj) elif isinstance(obj, objects.VppInterface): self.add_vpp_interface(obj) elif isinstance(obj, objects.VppBond): @@ -216,6 +220,20 @@ class NetConfig(object): """ raise NotImplementedError("add_ovs_dpdk_bond is not implemented.") + def add_sriov_pf(self, sriov_pf): + """Add a SriovPF object to the net config object. + + :param sriov_pf: The SriovPF object to add. + """ + raise NotImplementedError("add_sriov_pf is not implemented.") + + def add_sriov_vf(self, sriov_vf): + """Add a SriovVF object to the net config object. + + :param sriov_vf: The SriovVF object to add. + """ + raise NotImplementedError("add_sriov_vf is not implemented.") + def add_vpp_interface(self, vpp_interface): """Add a VppInterface object to the net config object. diff --git a/os_net_config/cli.py b/os_net_config/cli.py index 311f4d2f..f0678efb 100644 --- a/os_net_config/cli.py +++ b/os_net_config/cli.py @@ -25,10 +25,10 @@ from os_net_config import impl_eni from os_net_config import impl_ifcfg from os_net_config import impl_iproute from os_net_config import objects +from os_net_config import utils from os_net_config import validator from os_net_config import version - logger = logging.getLogger(__name__) @@ -138,6 +138,7 @@ def main(argv=sys.argv): configure_logger(opts.verbose, opts.debug) logger.info('Using config file at: %s' % opts.config_file) iface_array = [] + configure_sriov = False provider = None if opts.provider: @@ -245,9 +246,26 @@ def main(argv=sys.argv): else: logger.warning('\n'.join(validation_errors)) + # Look for the presence of SriovPF types in the first parse of the json + # if SriovPFs exists then PF devices needs to be configured so that the VF + # devices are created. + # In the second parse, all other objects shall be added for iface_json in iface_array: obj = objects.object_from_json(iface_json) - provider.add_object(obj) + if isinstance(obj, objects.SriovPF): + configure_sriov = True + provider.add_object(obj) + + if configure_sriov and not opts.noop: + utils.configure_sriov_pfs() + + for iface_json in iface_array: + # All objects other than the sriov_pf will be added here. + # The VFs are expected to be available now and an exception + # SriovVfNotFoundException shall be raised if not available. + obj = objects.object_from_json(iface_json) + if not isinstance(obj, objects.SriovPF): + provider.add_object(obj) files_changed = provider.apply(cleanup=opts.cleanup, activate=not opts.no_activate) if opts.noop: diff --git a/os_net_config/impl_ifcfg.py b/os_net_config/impl_ifcfg.py index cd332f21..39a55876 100644 --- a/os_net_config/impl_ifcfg.py +++ b/os_net_config/impl_ifcfg.py @@ -163,9 +163,8 @@ class IfcfgNetConfig(os_net_config.NetConfig): data += "VLAN=yes\n" if base_opt.device: data += "PHYSDEV=%s\n" % base_opt.device - else: - if base_opt.linux_bond_name: - data += "PHYSDEV=%s\n" % base_opt.linux_bond_name + elif base_opt.linux_bond_name: + data += "PHYSDEV=%s\n" % base_opt.linux_bond_name elif isinstance(base_opt, objects.IvsInterface): data += "TYPE=IVSIntPort\n" elif isinstance(base_opt, objects.NfvswitchInternal): @@ -670,6 +669,37 @@ class IfcfgNetConfig(os_net_config.NetConfig): if ovs_dpdk_bond.routes: self._add_routes(ovs_dpdk_bond.name, ovs_dpdk_bond.routes) + def add_sriov_pf(self, sriov_pf): + """Add a SriovPF object to the net config object + + :param sriov_pf: The SriovPF object to add + """ + logger.info('adding sriov pf: %s' % sriov_pf.name) + data = self._add_common(sriov_pf) + logger.debug('sriov pf data: %s' % data) + utils.update_sriov_pf_map(sriov_pf.name, sriov_pf.numvfs, self.noop) + self.interface_data[sriov_pf.name] = data + + def add_sriov_vf(self, sriov_vf): + """Add a SriovVF object to the net config object + + :param sriov_vf: The SriovVF object to add + """ + # Retrieve the VF's name, using its PF device name and VF id. + # Note: The VF's name could be read only after setting the numvfs of + # the corresponding parent PF device. Untill this point the name field + # for VFs will be a empty string. An exception SriovVfNotFoundException + # shall be raised when the VF could not be found + sriov_vf.name = utils.get_vf_devname(sriov_vf.device, sriov_vf.vfid, + self.noop) + logger.info('adding sriov vf: %s for pf: %s, vfid: %d' + % (sriov_vf.name, sriov_vf.device, sriov_vf.vfid)) + data = self._add_common(sriov_vf) + logger.debug('sriov vf data: %s' % data) + self.interface_data[sriov_vf.name] = data + if sriov_vf.routes: + self._add_routes(sriov_vf.name, sriov_vf.routes) + def add_vpp_interface(self, vpp_interface): """Add a VppInterface object to the net config object diff --git a/os_net_config/objects.py b/os_net_config/objects.py index af3a50cc..e8f3c62c 100644 --- a/os_net_config/objects.py +++ b/os_net_config/objects.py @@ -29,7 +29,6 @@ from os_net_config import utils logger = logging.getLogger(__name__) _MAPPED_NICS = None - STANDALONE_FAIL_MODE = 'standalone' DEFAULT_OVS_BRIDGE_FAIL_MODE = STANDALONE_FAIL_MODE @@ -82,6 +81,10 @@ def object_from_json(json): return ContrailVrouter.from_json(json) elif obj_type == "contrail_vrouter_dpdk": return ContrailVrouterDpdk.from_json(json) + elif obj_type == "sriov_pf": + return SriovPF.from_json(json) + elif obj_type == "sriov_vf": + return SriovVF.from_json(json) def _get_required_field(json, name, object_name): @@ -407,7 +410,6 @@ class Vlan(_BaseOpts): persist_mapping, defroute, dhclient_args, dns_servers, nm_controlled, onboot) self.vlan_id = int(vlan_id) - mapped_nic_names = mapped_nics(nic_mapping) if device in mapped_nic_names: self.device = mapped_nic_names[device] @@ -1097,6 +1099,72 @@ class OvsDpdkPort(_BaseOpts): ovs_extra=ovs_extra, rx_queue=rx_queue) +class SriovVF(_BaseOpts): + """Base class for SR-IOV VF.""" + + def __init__(self, device, vfid, use_dhcp=False, use_dhcpv6=False, + addresses=None, routes=None, mtu=None, primary=False, + nic_mapping=None, persist_mapping=False, defroute=True, + dhclient_args=None, dns_servers=None, nm_controlled=False, + onboot=True): + addresses = addresses or [] + routes = routes or [] + dns_servers = dns_servers or [] + mapped_nic_names = mapped_nics(nic_mapping) + if device in mapped_nic_names: + device = mapped_nic_names[device] + # Empty strings are set for the name field. + # The provider shall identify the VF name from the PF device name + # (device) and the VF id. + super(SriovVF, self).__init__("", use_dhcp, use_dhcpv6, addresses, + routes, mtu, primary, nic_mapping, + persist_mapping, defroute, + dhclient_args, dns_servers, + nm_controlled, onboot) + self.vfid = vfid + self.device = device + + @staticmethod + def from_json(json): + # Get the VF id + vfid = _get_required_field(json, 'vfid', 'SriovVF') + # Get the PF device name + device = _get_required_field(json, 'device', 'SriovVF') + opts = _BaseOpts.base_opts_from_json(json) + return SriovVF(device, vfid, *opts) + + +class SriovPF(_BaseOpts): + """Base class for SR-IOV PF.""" + + def __init__(self, name, numvfs, use_dhcp=False, use_dhcpv6=False, + addresses=None, routes=None, mtu=None, primary=False, + nic_mapping=None, persist_mapping=False, defroute=True, + dhclient_args=None, dns_servers=None, nm_controlled=False, + onboot=True, members=None): + addresses = addresses or [] + routes = routes or [] + dns_servers = dns_servers or [] + super(SriovPF, self).__init__(name, use_dhcp, use_dhcpv6, addresses, + routes, mtu, primary, nic_mapping, + persist_mapping, defroute, + dhclient_args, dns_servers, + nm_controlled, onboot) + self.numvfs = numvfs + mapped_nic_names = mapped_nics(nic_mapping) + if name in mapped_nic_names: + self.name = mapped_nic_names[name] + else: + self.name = name + + @staticmethod + def from_json(json): + name = _get_required_field(json, 'name', 'SriovPF') + numvfs = _get_required_field(json, 'numvfs', 'SriovPF') + opts = _BaseOpts.base_opts_from_json(json) + return SriovPF(name, numvfs, *opts) + + class OvsDpdkBond(_BaseOpts): """Base class for OVS DPDK bonds.""" diff --git a/os_net_config/schema.yaml b/os_net_config/schema.yaml index 66d4edb0..2f7efb6c 100644 --- a/os_net_config/schema.yaml +++ b/os_net_config/schema.yaml @@ -232,6 +232,94 @@ definitions: - name additionalProperties: False + sriov_pf: + type: object + properties: + type: + enum: ["sriov_pf"] + name: + $ref: "#/definitions/string_or_param" + primary: + $ref: "#/definitions/bool_or_param" + numvfs: + $ref: "#/definitions/int_or_param" + hotplug: + $ref: "#/definitions/bool_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + onboot: + $ref: "#/definitions/bool_or_param" + required: + - type + - name + - numvfs + additionalProperties: False + + sriov_vf: + type: object + properties: + type: + enum: ["sriov_vf"] + primary: + $ref: "#/definitions/bool_or_param" + device: + $ref: "#/definitions/string_or_param" + vfid: + $ref: "#/definitions/int_or_param" + hotplug: + $ref: "#/definitions/bool_or_param" + # common options: + use_dhcp: + $ref: "#/definitions/bool_or_param" + use_dhcp6: + $ref: "#/definitions/bool_or_param" + addresses: + $ref: "#/definitions/list_of_address" + routes: + $ref: "#/definitions/list_of_route" + mtu: + $ref: "#/definitions/int_or_param" + nic_mapping: + $ref: "#/definitions/nic_mapping" + persist_mapping: + $ref: "#/definitions/bool_or_param" + defroute: + $ref: "#/definitions/bool_or_param" + dhclient_args: + $ref: "#/definitions/string_or_param" + dns_servers: + $ref: "#/definitions/list_of_ip_address_string_or_param" + nm_controlled: + $ref: "#/definitions/bool_or_param" + onboot: + $ref: "#/definitions/bool_or_param" + required: + - type + - device + - vfid + additionalProperties: False + vlan: type: object properties: @@ -394,6 +482,7 @@ definitions: items: oneOf: - $ref: "#/definitions/interface" + - $ref: "#/definitions/sriov_vf" - $ref: "#/definitions/vlan" minItems: 1 ovs_options: @@ -1166,6 +1255,8 @@ type: array items: oneOf: - $ref: "#/definitions/interface" + - $ref: "#/definitions/sriov_pf" + - $ref: "#/definitions/sriov_vf" - $ref: "#/definitions/vlan" - $ref: "#/definitions/ovs_bridge" - $ref: "#/definitions/ovs_user_bridge" diff --git a/os_net_config/sriov_config.py b/os_net_config/sriov_config.py new file mode 100644 index 00000000..dece520a --- /dev/null +++ b/os_net_config/sriov_config.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- + +# Copyright 2014 Red Hat, Inc. +# +# 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. + +# +# The sriov_config.py module does the SR-IOV PF configuration. +# It'll be invoked by the sriov_config systemd service for the persistence of +# the SR-IOV configuration across reboots. And os-net-config:utils also invokes +# it for the first time configuration. +# An entry point os-net-config-sriov is added for invocation of this module. + +import logging +import os +import sys +import time +import yaml + +logger = logging.getLogger(__name__) + +# File to contain the list of SR-IOV nics and the numvfs +# Format of the file shall be +# +# - name: eth1 +# numvfs: 5 +_SRIOV_PF_CONFIG_FILE = '/var/lib/os-net-config/sriov_pf.yaml' +_SYS_CLASS_NET = '/sys/class/net' +# maximum retries for checking the creation of VFs +_MAX_SRIOV_VFS_CONFIG_RETRIES = 60 + + +class SRIOVNumvfsException(ValueError): + pass + + +def get_file_data(filename): + if not os.path.exists(filename): + return '' + try: + with open(filename, 'r') as f: + return f.read() + except IOError: + logger.error("Error reading file: %s" % filename) + return '' + + +def _get_sriov_pf_map(): + contents = get_file_data(_SRIOV_PF_CONFIG_FILE) + sriov_pf_map = yaml.load(contents) if contents else [] + return sriov_pf_map + + +def _configure_sriov_pf(): + sriov_pf_map = _get_sriov_pf_map() + for item in sriov_pf_map: + try: + sriov_numvfs_path = ("/sys/class/net/%s/device/sriov_numvfs" + % item['name']) + with open(sriov_numvfs_path, 'w') as f: + f.write("%d" % item['numvfs']) + except IOError as exc: + msg = ("Unable to configure pf: %s with numvfs: %d\n%s" + % (item['name'], item['numvfs'], exc)) + raise SRIOVNumvfsException(msg) + + +def _wait_for_vf_creation(): + sriov_map = _get_sriov_pf_map() + for item in sriov_map: + count = 0 + while count < _MAX_SRIOV_VFS_CONFIG_RETRIES: + pf = item['name'] + numvfs = item['numvfs'] + vf_path = os.path.join(_SYS_CLASS_NET, pf, + "device/virtfn%d/net" % (numvfs - 1)) + if os.path.isdir(vf_path): + vf_nic = os.listdir(vf_path) + if len(vf_nic) == 1 and pf in vf_nic[0]: + logger.info("VFs created for PF: %s" % pf) + break + else: + logger.debug("VF device name not present for PF %s" % pf) + else: + logger.info("Attempt#%d, VFs for PF %s is not yet created" + % (count + 1, pf)) + time.sleep(1) + count += 1 + + +def main(argv=None): + # Configure the PF's + _configure_sriov_pf() + # Wait for the VF's to get created + _wait_for_vf_creation() + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/os_net_config/tests/test_cli.py b/os_net_config/tests/test_cli.py index b71d98f1..b48fd32e 100644 --- a/os_net_config/tests/test_cli.py +++ b/os_net_config/tests/test_cli.py @@ -193,6 +193,23 @@ class TestCli(base.TestCase): '-c %s --detailed-exit-codes' % interface_yaml, exitcodes=(0,)) + def test_sriov_noop_output(self): + ivs_yaml = os.path.join(SAMPLE_BASE, 'sriov_pf.yaml') + ivs_json = os.path.join(SAMPLE_BASE, 'sriov_pf.json') + stdout_yaml, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' + '-c %s' % ivs_yaml) + self.assertEqual('', stderr) + stdout_json, stderr = self.run_cli('ARG0 --provider=ifcfg --noop ' + '--exit-on-validation-errors ' + '-c %s' % ivs_json) + self.assertEqual('', stderr) + sanity_devices = ['DEVICE=p2p1', + 'DEVICE=p2p1_5'] + for dev in sanity_devices: + self.assertIn(dev, stdout_yaml) + self.assertEqual(stdout_yaml, stdout_json) + def test_ovs_dpdk_bond_noop_output(self): ivs_yaml = os.path.join(SAMPLE_BASE, 'ovs_dpdk_bond.yaml') ivs_json = os.path.join(SAMPLE_BASE, 'ovs_dpdk_bond.json') diff --git a/os_net_config/tests/test_impl_ifcfg.py b/os_net_config/tests/test_impl_ifcfg.py index b2fbee31..1d9bdbd2 100644 --- a/os_net_config/tests/test_impl_ifcfg.py +++ b/os_net_config/tests/test_impl_ifcfg.py @@ -1096,6 +1096,53 @@ DNS2=5.6.7.8 bond_data = self.get_linux_bond_config('bond1') self.assertEqual(_NM_CONTROLLED_BOND, bond_data) + def test_network_sriov_vf(self): + nic_mapping = {'nic1': 'eth0', 'nic2': 'eth1', 'nic3': 'eth2'} + self.stubbed_mapped_nics = nic_mapping + addresses = [objects.Address('10.0.0.30/24')] + vf = objects.SriovVF(device='nic3', vfid=7, addresses=addresses) + + def test_get_vf_devname(device, vfid, noop): + self.assertEqual(device, 'eth2') + self.assertEqual(vfid, 7) + return 'eth2_7' + self.stubs.Set(utils, 'get_vf_devname', + test_get_vf_devname) + self.provider.add_sriov_vf(vf) + vf_config = """# This file is autogenerated by os-net-config +DEVICE=eth2_7 +HOTPLUG=no +ONBOOT=yes +NM_CONTROLLED=no +PEERDNS=no +BOOTPROTO=static +IPADDR=10.0.0.30 +NETMASK=255.255.255.0 +""" + self.assertEqual(vf_config, self.get_interface_config('eth2_7')) + + def test_network_sriov_pf(self): + nic_mapping = {'nic1': 'eth0', 'nic2': 'eth1', 'nic3': 'eth2'} + self.stubbed_mapped_nics = nic_mapping + + pf = objects.SriovPF(name='nic3', numvfs=10) + + def test_update_sriov_pf_map(name, numvfs, noop): + self.assertEqual(name, 'eth2') + self.assertEqual(numvfs, 10) + self.stubs.Set(utils, 'update_sriov_pf_map', + test_update_sriov_pf_map) + self.provider.add_sriov_pf(pf) + pf_config = """# This file is autogenerated by os-net-config +DEVICE=eth2 +HOTPLUG=no +ONBOOT=yes +NM_CONTROLLED=no +PEERDNS=no +BOOTPROTO=none +""" + self.assertEqual(pf_config, self.get_interface_config('eth2')) + def test_network_ovs_dpdk_bridge_and_port(self): nic_mapping = {'nic1': 'eth0', 'nic2': 'eth1', 'nic3': 'eth2'} self.stubbed_mapped_nics = nic_mapping diff --git a/os_net_config/tests/test_objects.py b/os_net_config/tests/test_objects.py index 00a63a73..edcabf25 100644 --- a/os_net_config/tests/test_objects.py +++ b/os_net_config/tests/test_objects.py @@ -1187,6 +1187,63 @@ class TestNicMapping(base.TestCase): self.assertEqual("bar", interface2.name) +class TestSriovPF(base.TestCase): + + def test_from_json_numvfs(self): + data = '{"type": "sriov_pf", "name": "em1", "numvfs": 16,' \ + '"use_dhcp": false}' + pf = objects.object_from_json(json.loads(data)) + self.assertEqual("em1", pf.name) + self.assertEqual(16, pf.numvfs) + self.assertFalse(pf.use_dhcp) + + def test_from_json_numvfs_nic1(self): + def dummy_mapped_nics(nic_mapping=None): + return {"nic1": "em4"} + self.stubs.Set(objects, 'mapped_nics', dummy_mapped_nics) + + data = '{"type": "sriov_pf", "name": "nic1", "numvfs": 16,' \ + '"use_dhcp": false}' + pf = objects.object_from_json(json.loads(data)) + self.assertEqual("em4", pf.name) + self.assertEqual(16, pf.numvfs) + self.assertFalse(pf.use_dhcp) + + +class TestSriovVF(base.TestCase): + + def test_from_json_vfid(self): + data = '{"type": "sriov_vf", "device": "em1", "vfid": 16,' \ + '"use_dhcp": false}' + vf = objects.object_from_json(json.loads(data)) + self.assertEqual("em1", vf.device) + self.assertEqual(16, vf.vfid) + self.assertFalse(vf.use_dhcp) + self.assertEqual("", vf.name) + + def test_from_json_name_ignored(self): + data = '{"type": "sriov_vf", "device": "em1", "vfid": 16,' \ + '"use_dhcp": false, "name": "em1_16"}' + vf = objects.object_from_json(json.loads(data)) + self.assertEqual("em1", vf.device) + self.assertEqual(16, vf.vfid) + self.assertFalse(vf.use_dhcp) + self.assertEqual("", vf.name) + + def test_from_json_vfid_nic1(self): + def dummy_mapped_nics(nic_mapping=None): + return {"nic1": "em4"} + self.stubs.Set(objects, 'mapped_nics', dummy_mapped_nics) + + data = '{"type": "sriov_vf", "device": "nic1", "vfid": 16,' \ + '"use_dhcp": false}' + vf = objects.object_from_json(json.loads(data)) + self.assertEqual("em4", vf.device) + self.assertEqual(16, vf.vfid) + self.assertFalse(vf.use_dhcp) + self.assertEqual("", vf.name) + + class TestOvsDpdkBond(base.TestCase): # We want to test the function, not the dummy.. diff --git a/os_net_config/tests/test_utils.py b/os_net_config/tests/test_utils.py index 83ac01ff..23c9c6ce 100644 --- a/os_net_config/tests/test_utils.py +++ b/os_net_config/tests/test_utils.py @@ -81,11 +81,14 @@ class TestUtils(base.TestCase): super(TestUtils, self).setUp() rand = str(int(random.random() * 100000)) utils._DPDK_MAPPING_FILE = '/tmp/dpdk_mapping_' + rand + '.yaml' + utils._SRIOV_PF_CONFIG_FILE = '/tmp/sriov_pf_' + rand + '.yaml' def tearDown(self): super(TestUtils, self).tearDown() if os.path.isfile(utils._DPDK_MAPPING_FILE): os.remove(utils._DPDK_MAPPING_FILE) + if os.path.isfile(utils._SRIOV_PF_CONFIG_FILE): + os.remove(utils._SRIOV_PF_CONFIG_FILE) def test_ordered_active_nics(self): @@ -113,6 +116,56 @@ class TestUtils(base.TestCase): shutil.rmtree(tmpdir) + def test_update_sriov_pf_map_new(self): + utils.update_sriov_pf_map('eth1', 10, False) + contents = utils.get_file_data(utils._SRIOV_PF_CONFIG_FILE) + sriov_pf_map = yaml.load(contents) if contents else [] + self.assertEqual(1, len(sriov_pf_map)) + test_sriov_pf_map = [{'name': 'eth1', 'numvfs': 10}] + self.assertListEqual(test_sriov_pf_map, sriov_pf_map) + + def test_update_sriov_pf_map_exist(self): + pf_initial = [{'name': 'eth1', 'numvfs': 10}] + utils.write_yaml_config(utils._SRIOV_PF_CONFIG_FILE, pf_initial) + + utils.update_sriov_pf_map('eth1', 20, False) + pf_final = [{'name': 'eth1', 'numvfs': 20}] + contents = utils.get_file_data(utils._SRIOV_PF_CONFIG_FILE) + + pf_map = yaml.load(contents) if contents else [] + self.assertEqual(1, len(pf_map)) + self.assertListEqual(pf_final, pf_map) + + def test_get_vf_devname_net_dir_not_found(self): + tmpdir = tempfile.mkdtemp() + self.stubs.Set(utils, '_SYS_CLASS_NET', tmpdir) + + self.assertRaises(utils.SriovVfNotFoundException, + utils.get_vf_devname, "eth1", 1, False) + shutil.rmtree(tmpdir) + + def test_get_vf_devname_vf_dir_not_found(self): + tmpdir = tempfile.mkdtemp() + self.stubs.Set(utils, '_SYS_CLASS_NET', tmpdir) + + vf_path = os.path.join(utils._SYS_CLASS_NET, 'eth1/device/virtfn1/net') + os.makedirs(vf_path) + + self.assertRaises(utils.SriovVfNotFoundException, + utils.get_vf_devname, "eth1", 1, False) + shutil.rmtree(tmpdir) + + def test_get_vf_devname_vf_dir_found(self): + tmpdir = tempfile.mkdtemp() + self.stubs.Set(utils, '_SYS_CLASS_NET', tmpdir) + + vf_path = os.path.join(utils._SYS_CLASS_NET, + 'eth1/device/virtfn1/net/eth1_1') + os.makedirs(vf_path) + + self.assertEqual(utils.get_vf_devname("eth1", 1, False), "eth1_1") + shutil.rmtree(tmpdir) + def test_get_pci_address_success(self): def test_execute(name, dummy1, dummy2=None, dummy3=None): if 'ethtool' in name: diff --git a/os_net_config/utils.py b/os_net_config/utils.py index dd9bb739..c8dff2dd 100644 --- a/os_net_config/utils.py +++ b/os_net_config/utils.py @@ -21,9 +21,9 @@ import re import time import yaml +from os_net_config import sriov_config from oslo_concurrency import processutils - logger = logging.getLogger(__name__) _SYS_CLASS_NET = '/sys/class/net' # File to contain the DPDK mapped nics, as nic name will not be available after @@ -36,6 +36,29 @@ _SYS_CLASS_NET = '/sys/class/net' # driver: vfio-pci _DPDK_MAPPING_FILE = '/var/lib/os-net-config/dpdk_mapping.yaml' +# File to contain the list of SR-IOV nics and the numvfs +# Format of the file shall be +# +# - name: eth1 +# numvfs: 5 +_SRIOV_PF_CONFIG_FILE = '/var/lib/os-net-config/sriov_pf.yaml' + +# sriov_numvfs service shall be configured so that the numvfs for each of the +# SR-IOV PF device shall be configured during reboot as well +_SRIOV_CONFIG_SERVICE_FILE = "/etc/systemd/system/sriov_config.service" +_SRIOV_CONFIG_DEVICE_CONTENT = """[Unit] +Description=SR-IOV numvfs configuration +After=systemd-udev-settle.service +Before=openvswitch.service + +[Service] +Type=oneshot +ExecStart=os-net-config-sriov + +[Install] +WantedBy=multi-user.target +""" + # VPP startup operational configuration file. The content of this file will # be executed when VPP starts as if typed from CLI. _VPP_EXEC_FILE = '/etc/vpp/vpp-exec' @@ -53,6 +76,10 @@ class ContrailVrouterException(ValueError): pass +class SriovVfNotFoundException(ValueError): + pass + + def write_config(filename, data): with open(filename, 'w') as f: f.write(str(data)) @@ -347,6 +374,64 @@ def _get_dpdk_mac_address(name): return item['mac_address'] +def update_sriov_pf_map(ifname, numvfs, noop): + if not noop: + sriov_map = _get_sriov_pf_map() + for item in sriov_map: + if item['name'] == ifname: + item['numvfs'] = numvfs + break + else: + new_item = {} + new_item['name'] = ifname + new_item['numvfs'] = numvfs + sriov_map.append(new_item) + + write_yaml_config(_SRIOV_PF_CONFIG_FILE, sriov_map) + + +def _get_sriov_pf_map(): + contents = get_file_data(_SRIOV_PF_CONFIG_FILE) + sriov_map = yaml.load(contents) if contents else [] + return sriov_map + + +def _configure_sriov_config_service(): + """Generate the sriov_config.service + + sriov_config service shall configure the numvfs for the SriovPF nics + during reboot of the nodes. + """ + with open(_SRIOV_CONFIG_SERVICE_FILE, 'w') as f: + f.write(_SRIOV_CONFIG_DEVICE_CONTENT) + processutils.execute('systemctl', 'enable', 'sriov_config') + + +def configure_sriov_pfs(): + logger.info("Configuring PFs now") + sriov_config.main() + _configure_sriov_config_service() + + +def get_vf_devname(pf_name, vfid, noop): + if noop: + logger.info("NOOP: returning VF name as %s_%d" % (pf_name, vfid)) + return "%s_%d" % (pf_name, vfid) + vf_path = os.path.join(_SYS_CLASS_NET, pf_name, "device/virtfn%d/net" + % vfid) + if os.path.isdir(vf_path): + vf_nic = os.listdir(vf_path) + else: + msg = "NIC %s with VF id: %d could not be found" % (pf_name, vfid) + raise SriovVfNotFoundException(msg) + if len(vf_nic) != 1: + msg = "VF name could not be identified in %s" % vf_path + raise SriovVfNotFoundException(msg) + # The VF's actual device name shall be the only directory seen in the path + # /sys/class/net//device/virtfn/net + return vf_nic[0] + + def restart_vpp(vpp_interfaces): for vpp_int in vpp_interfaces: if 'vfio-pci' in vpp_int.uio_driver: diff --git a/releasenotes/notes/sriov-vf-in-hosts-529c2bf0cb3294b3.yaml b/releasenotes/notes/sriov-vf-in-hosts-529c2bf0cb3294b3.yaml new file mode 100644 index 00000000..5db4f1f6 --- /dev/null +++ b/releasenotes/notes/sriov-vf-in-hosts-529c2bf0cb3294b3.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + sriov_pf and sriov_vf object types are added to allow configuration and + usage of the SR-IOV PF and VF devices in the host machine. The sriov_config + service file shall be used to reconfigure the SR-IOV devices after reboots. + VLAN suport for SR-IOV VF device is not available in this patch. diff --git a/setup.cfg b/setup.cfg index 393fda57..4fa35de1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ setup-hooks = [entry_points] console_scripts = os-net-config = os_net_config.cli:main + os-net-config-sriov = os_net_config.sriov_config:main [build_sphinx] source-dir = doc/source @@ -36,6 +37,6 @@ all_files = 1 upload-dir = doc/build/html [egg_info] -tag_build = +tag_build = tag_date = 0 tag_svn_revision = 0