Merge "Add support for VPP interface"

This commit is contained in:
Jenkins 2017-04-05 17:31:14 +00:00 committed by Gerrit Code Review
commit f26b56c8f2
8 changed files with 503 additions and 10 deletions

View File

@ -0,0 +1,14 @@
{ "network_config": [
{
"type": "vpp_interface",
"name": "nic2",
"addresses": [
{
"ip_netmask": "192.0.2.1/24"
}
],
"uio_driver": "uio_pci_generic",
"options": "vlan-strip-offload off"
}
]
}

View File

@ -0,0 +1,18 @@
network_config:
-
type: vpp_interface
name: nic2
addresses:
-
ip_netmask: 192.0.2.1/24
# DPDK poll-mode driver name. Defaults to 'vfio-pci', other possible value
# is 'uio_pci_generic'. It is also possible to specify other driver names
# such as 'igb_uio', however, it is assumed that any required kernel
# modules for those drivers are already loaded when os-net-config is
# invoked.
uio_driver: uio_pci_generic
# Interface options such as vlan stripping and tx/rx transmit queues
# specification. Reference for those configurations can
# be found at https://wiki.fd.io/view/VPP/Command-line_Arguments
# Example: 'vlan-strip-offload on num-rx-queues 3'
#options: "vlan-strip-offload off"

View File

@ -95,6 +95,8 @@ class NetConfig(object):
self.add_ovs_dpdk_port(obj)
elif isinstance(obj, objects.OvsDpdkBond):
self.add_ovs_dpdk_bond(obj)
elif isinstance(obj, objects.VppInterface):
self.add_vpp_interface(obj)
def add_interface(self, interface):
"""Add an Interface object to the net config object.
@ -201,6 +203,13 @@ class NetConfig(object):
"""
raise NotImplementedError("add_ovs_dpdk_bond is not implemented.")
def add_vpp_interface(self, vpp_interface):
"""Add a VppInterface object to the net config object.
:param vpp_interface: The VppInterface object to add.
"""
raise NotImplementedError("add_vpp_interface is not implemented.")
def apply(self, cleanup=False):
"""Apply the network configuration.

View File

@ -54,6 +54,10 @@ def nfvswitch_config_path():
return "/etc/sysconfig/nfvswitch"
def vpp_config_path():
return "/etc/vpp/startup.conf"
def route_config_path(name):
return "/etc/sysconfig/network-scripts/route-%s" % name
@ -116,6 +120,7 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.linuxbond_data = {}
self.ib_interface_data = {}
self.linuxteam_data = {}
self.vpp_interface_data = {}
self.member_names = {}
self.renamed_interfaces = {}
self.bond_primary_ifaces = {}
@ -626,6 +631,21 @@ class IfcfgNetConfig(os_net_config.NetConfig):
if ovs_dpdk_bond.routes:
self._add_routes(ovs_dpdk_bond.name, ovs_dpdk_bond.routes)
def add_vpp_interface(self, vpp_interface):
"""Add a VppInterface object to the net config object
:param vpp_interface: The VppInterface object to add
"""
vpp_interface.pci_dev = utils.get_pci_address(vpp_interface.name,
False)
vpp_interface.hwaddr = utils.interface_mac(vpp_interface.name)
if not self.noop:
self.ifdown(vpp_interface.name)
remove_ifcfg_config(vpp_interface.name)
logger.info('adding vpp interface: %s %s'
% (vpp_interface.name, vpp_interface.pci_dev))
self.vpp_interface_data[vpp_interface.name] = vpp_interface
def generate_ivs_config(self, ivs_uplinks, ivs_interfaces):
"""Generate configuration content for ivs."""
@ -690,6 +710,7 @@ class IfcfgNetConfig(os_net_config.NetConfig):
restart_bridges = []
restart_linux_bonds = []
restart_linux_teams = []
restart_vpp = False
update_files = {}
all_file_names = []
ivs_uplinks = [] # ivs physical uplinks
@ -698,6 +719,7 @@ class IfcfgNetConfig(os_net_config.NetConfig):
nfvswitch_internal_ifaces = [] # nfvswitch internal/management ports
stop_dhclient_interfaces = []
ovs_needs_restart = False
vpp_interfaces = self.vpp_interface_data.values()
for interface_name, iface_data in self.interface_data.items():
route_data = self.route_data.get(interface_name, '')
@ -906,6 +928,15 @@ class IfcfgNetConfig(os_net_config.NetConfig):
logger.info('No changes required for InfiniBand iface: %s' %
interface_name)
if self.vpp_interface_data:
vpp_path = self.root_dir + vpp_config_path()
vpp_config = utils.generate_vpp_config(vpp_path, vpp_interfaces)
if utils.diff(vpp_path, vpp_config):
restart_vpp = True
update_files[vpp_path] = vpp_config
else:
logger.info('No changes required for VPP')
if cleanup:
for ifcfg_file in glob.iglob(cleanup_pattern()):
if ifcfg_file not in all_file_names:
@ -932,6 +963,9 @@ class IfcfgNetConfig(os_net_config.NetConfig):
for bridge in restart_bridges:
self.ifdown(bridge, iftype='bridge')
for vpp_interface in vpp_interfaces:
self.ifdown(vpp_interface.name)
for oldname, newname in self.renamed_interfaces.items():
self.ifrename(oldname, newname)
@ -1009,4 +1043,13 @@ class IfcfgNetConfig(os_net_config.NetConfig):
for vlan in restart_vlans:
self.ifup(vlan)
if not self.noop:
if restart_vpp:
logger.info('Restarting VPP')
utils.restart_vpp(vpp_interfaces)
if self.vpp_interface_data:
logger.info('Updating VPP mapping')
utils.update_vpp_mapping(vpp_interfaces)
return update_files

View File

@ -68,6 +68,8 @@ def object_from_json(json):
return OvsDpdkPort.from_json(json)
elif obj_type == "ovs_dpdk_bond":
return OvsDpdkBond.from_json(json)
elif obj_type == "vpp_interface":
return VppInterface.from_json(json)
def _get_required_field(json, name, object_name):
@ -1131,3 +1133,57 @@ class OvsDpdkBond(_BaseOpts):
defroute=defroute, dhclient_args=dhclient_args,
dns_servers=dns_servers,
nm_controlled=nm_controlled)
class VppInterface(_BaseOpts):
"""Base class for VPP Interface.
Vector Packet Processing (VPP) is a high performance packet processing
stack that runs in user space in Linux. VPP is used as an alternative to
kernel networking stack for accelerated network data path. VPP uses DPDK
poll-mode drivers to bind system interfaces rather than kernel drivers.
VPP bound interfaces are not visible to kernel networking stack, so we
must handle them separately.
The following parameters can be specified in addition to base Interface:
- uio_driver: DPDK poll-mode driver name. Defaults to 'vfio-pci', valid
values are 'uio_pci_generic' and 'vfio-pci'.
- options: Interface options such as vlan stripping and tx/rx transmit
queues specification. Defaults to None. Reference for those
configurations can be found at
https://wiki.fd.io/view/VPP/Command-line_Arguments
Example: 'vlan-strip-offload on num-rx-queues 3'
Note that 'name' attribute is used to indicate the kernel nic that should
be bound to VPP. Once VPP binds the interface, a mapping file will be
updated with the interface's information, and this file will be used in
subsequent runs of os-net-config.
"""
def __init__(self, name, 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, uio_driver='vfio-pci',
options=None):
addresses = addresses or []
super(VppInterface, self).__init__(name, use_dhcp, use_dhcpv6,
addresses, routes, mtu, primary,
nic_mapping, persist_mapping,
defroute, dhclient_args,
dns_servers, nm_controlled)
self.uio_driver = uio_driver
self.options = options
# pci_dev contains pci address for the interface, it will be populated
# when interface is added to config object. It will be determined
# either through ethtool or by looking up the dpdk mapping file.
self.pci_dev = None
@staticmethod
def from_json(json):
name = _get_required_field(json, 'name', 'VppInterface')
uio_driver = json.get('uio_driver', 'vfio-pci')
options = json.get('options', '')
opts = _BaseOpts.base_opts_from_json(json)
return VppInterface(name, *opts, uio_driver=uio_driver,
options=options)

View File

@ -988,3 +988,19 @@ class TestOvsDpdkBond(base.TestCase):
self.assertEqual("vfio-pci", dpdk_port1.driver)
iface2 = dpdk_port1.members[0]
self.assertEqual("eth2", iface2.name)
class TestVppInterface(base.TestCase):
def test_vpp_interface_from_json(self):
data = """{
"type": "vpp_interface",
"name": "em1",
"uio_driver": "uio_pci_generic",
"options": "vlan-strip-offload off"
}
"""
vpp_interface = objects.object_from_json(json.loads(data))
self.assertEqual("em1", vpp_interface.name)
self.assertEqual("uio_pci_generic", vpp_interface.uio_driver)
self.assertEqual("vlan-strip-offload off", vpp_interface.options)

View File

@ -21,6 +21,7 @@ import shutil
import tempfile
import yaml
from os_net_config import objects
from os_net_config.tests import base
from os_net_config import utils
@ -38,6 +39,33 @@ supports-register-dump: yes
supports-priv-flags: no
'''
_VPPCTL_OUTPUT = '''
Name Idx State Counter Count
GigabitEthernet0/9/0 1 down
local0 0 down
'''
_INITIAL_VPP_CONFIG = '''
unix {
nodaemon
log /tmp/vpp.log
full-coredump
}
api-trace {
on
}
api-segment {
gid vpp
}
dpdk {
}
'''
class TestUtils(base.TestCase):
@ -83,7 +111,7 @@ class TestUtils(base.TestCase):
out = _PCI_OUTPUT
return out, None
self.stubs.Set(processutils, 'execute', test_execute)
pci = utils._get_pci_address('nic2', False)
pci = utils.get_pci_address('nic2', False)
self.assertEqual('0000:00:19.0', pci)
def test_get_pci_address_exception(self):
@ -91,7 +119,7 @@ class TestUtils(base.TestCase):
if 'ethtool' in name:
raise processutils.ProcessExecutionError
self.stubs.Set(processutils, 'execute', test_execute)
pci = utils._get_pci_address('nic2', False)
pci = utils.get_pci_address('nic2', False)
self.assertEqual(None, pci)
def test_get_pci_address_error(self):
@ -99,7 +127,7 @@ class TestUtils(base.TestCase):
if 'ethtool' in name:
return None, 'Error'
self.stubs.Set(processutils, 'execute', test_execute)
pci = utils._get_pci_address('nic2', False)
pci = utils.get_pci_address('nic2', False)
self.assertEqual(None, pci)
def test_bind_dpdk_interfaces(self):
@ -241,3 +269,99 @@ class TestUtils(base.TestCase):
self.assertEqual(utils._is_active_nic('enp129s2'), False)
shutil.rmtree(tmpdir)
def test_get_vpp_interface_name(self):
def test_execute(name, dummy1, dummy2=None, dummy3=None):
if 'systemctl' in name:
return None, None
if 'vppctl' in name:
return _VPPCTL_OUTPUT, None
self.stubs.Set(processutils, 'execute', test_execute)
self.assertEqual('GigabitEthernet0/9/0',
utils._get_vpp_interface_name('0000:00:09.0'))
self.assertIsNone(utils._get_vpp_interface_name(None))
self.assertIsNone(utils._get_vpp_interface_name('0000:01:09.0'))
self.assertRaises(utils.VppException,
utils._get_vpp_interface_name, '0000:09.0')
def test_generate_vpp_config(self):
tmpdir = tempfile.mkdtemp()
config_path = os.path.join(tmpdir, 'startup.conf')
with open(config_path, 'w') as f:
f.write(_INITIAL_VPP_CONFIG)
vpp_exec_path = os.path.join(tmpdir, 'vpp-exec')
utils._VPP_EXEC_FILE = vpp_exec_path
int1 = objects.VppInterface('em1', options="vlan-strip-offload off")
int1.pci_dev = '0000:00:09.0'
int2 = objects.VppInterface('em2')
int2.pci_dev = '0000:00:09.1'
interfaces = [int1, int2]
expected_config = '''
unix {
exec %s
nodaemon
log /tmp/vpp.log
full-coredump
}
api-trace {
on
}
api-segment {
gid vpp
}
dpdk {
dev 0000:00:09.1
uio-driver vfio-pci
dev 0000:00:09.0 {vlan-strip-offload off}
}
''' % vpp_exec_path
self.assertEqual(expected_config,
utils.generate_vpp_config(config_path, interfaces))
def test_update_vpp_mapping(self):
def test_get_dpdk_map():
return [{'name': 'eth1', 'pci_address': '0000:00:09.0',
'mac_address': '01:02:03:04:05:06',
'driver': 'vfio-pci'}]
self.stubs.Set(utils, '_get_dpdk_map', test_get_dpdk_map)
def test_execute(name, dummy1, dummy2=None, dummy3=None):
return None, None
self.stubs.Set(processutils, 'execute', test_execute)
def test_get_vpp_interface_name(pci_dev):
return 'GigabitEthernet0/9/0'
self.stubs.Set(utils, '_get_vpp_interface_name',
test_get_vpp_interface_name)
int1 = objects.VppInterface('eth1', options="vlan-strip-offload off")
int1.pci_dev = '0000:00:09.0'
int1.hwaddr = '01:02:03:04:05:06'
int2 = objects.VppInterface('eth2')
int2.pci_dev = '0000:00:09.1'
int2.hwaddr = '01:02:03:04:05:07'
interfaces = [int1, int2]
utils.update_vpp_mapping(interfaces)
contents = utils.get_file_data(utils._DPDK_MAPPING_FILE)
dpdk_test = [{'name': 'eth1', 'pci_address': '0000:00:09.0',
'mac_address': '01:02:03:04:05:06',
'driver': 'vfio-pci'},
{'name': 'eth2', 'pci_address': '0000:00:09.1',
'mac_address': '01:02:03:04:05:07',
'driver': 'vfio-pci'}]
dpdk_map = yaml.load(contents) if contents else []
self.assertEqual(2, len(dpdk_map))
self.assertListEqual(dpdk_test, dpdk_map)

View File

@ -35,11 +35,19 @@ _SYS_CLASS_NET = '/sys/class/net'
# driver: vfio-pci
_DPDK_MAPPING_FILE = '/var/lib/os-net-config/dpdk_mapping.yaml'
# 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'
class OvsDpdkBindException(ValueError):
pass
class VppException(ValueError):
pass
def write_config(filename, data):
with open(filename, 'w') as f:
f.write(str(data))
@ -82,7 +90,7 @@ def interface_mac(name):
return f.read().rstrip()
except IOError:
# If the interface is bound to a DPDK driver, get the mac address from
# the dpdk mapping file as /sys files will be removed after binding.
# the DPDK mapping file as /sys files will be removed after binding.
dpdk_mac_address = _get_dpdk_mac_address(name)
if dpdk_mac_address:
return dpdk_mac_address
@ -187,7 +195,7 @@ def diff(filename, data):
def bind_dpdk_interfaces(ifname, driver, noop):
pci_address = _get_pci_address(ifname, noop)
pci_address = get_pci_address(ifname, noop)
if not noop:
if pci_address:
# modbprobe of the driver has to be done before binding.
@ -218,7 +226,7 @@ def bind_dpdk_interfaces(ifname, driver, noop):
{'name': ifname, 'driver': driver})
def _get_pci_address(ifname, noop):
def get_pci_address(ifname, noop):
# TODO(skramaja): Validate if the given interface supports dpdk
if not noop:
try:
@ -241,11 +249,10 @@ def _get_pci_address(ifname, noop):
# Once the interface is bound to a DPDK driver, all the references to the
# interface including '/sys' and '/proc', will be removed. And there is no
# way to identify the nic name after it is bound. So, the DPDK bound nic info
# is stored persistently in a file and is used to for nic numbering on
# subsequent runs of os-net-config.
# is stored persistently in _DPDK_MAPPING_FILE and is used to for nic numbering
# on subsequent runs of os-net-config.
def _update_dpdk_map(ifname, pci_address, mac_address, driver):
contents = get_file_data(_DPDK_MAPPING_FILE)
dpdk_map = yaml.load(contents) if contents else []
dpdk_map = _get_dpdk_map()
for item in dpdk_map:
if item['pci_address'] == pci_address:
item['name'] = ifname
@ -263,9 +270,215 @@ def _update_dpdk_map(ifname, pci_address, mac_address, driver):
write_yaml_config(_DPDK_MAPPING_FILE, dpdk_map)
def _get_dpdk_map():
contents = get_file_data(_DPDK_MAPPING_FILE)
dpdk_map = yaml.load(contents) if contents else []
return dpdk_map
def _get_dpdk_mac_address(name):
contents = get_file_data(_DPDK_MAPPING_FILE)
dpdk_map = yaml.load(contents) if contents else []
for item in dpdk_map:
if item['name'] == name:
return item['mac_address']
def restart_vpp(vpp_interfaces):
for vpp_int in vpp_interfaces:
if 'vfio-pci' in vpp_int.uio_driver:
processutils.execute('modprobe', 'vfio-pci')
logger.info('Restarting VPP')
processutils.execute('systemctl', 'restart', 'vpp')
def _get_vpp_interface_name(pci_addr):
"""Get VPP interface name from a given PCI address
From a running VPP instance, attempt to find the interface name from
a given PCI address of a NIC.
VppException will be raised if pci_addr is not formatted correctly.
ProcessExecutionError will be raised if VPP interface mapped to pci_addr
is not found.
:param pci_addr: PCI address to lookup, in the form of DDDD:BB:SS.F, where
- DDDD = Domain
- BB = Bus Number
- SS = Slot number
- F = Function
:return: VPP interface name. None if an interface is not found.
"""
if not pci_addr:
return None
try:
processutils.execute('systemctl', 'is-active', 'vpp')
out, err = processutils.execute('vppctl', 'show', 'interfaces')
m = re.search(r':([0-9a-fA-F]{2}):([0-9a-fA-F]{2}).([0-9a-fA-F])',
pci_addr)
if m:
formatted_pci = "%x/%x/%x" % (int(m.group(1), 16),
int(m.group(2), 16),
int(m.group(3), 16))
else:
raise VppException('Invalid PCI address format: %s' % pci_addr)
m = re.search(r'^(\w+%s)\s+' % formatted_pci, out, re.MULTILINE)
if m:
logger.debug('VPP interface found: %s' % m.group(1))
return m.group(1)
else:
logger.debug('Interface with pci address %s not bound to VPP'
% pci_addr)
return None
except processutils.ProcessExecutionError:
logger.debug('Interface with pci address %s not bound to vpp' %
pci_addr)
def generate_vpp_config(vpp_config_path, vpp_interfaces):
"""Generate configuration content for VPP
Generate interface related configuration content for VPP. Current
configuration will be preserved, with interface related configurations
updated or inserted. The config only affects 'dpdk' section of VPP config
file, and only those lines affecting interfaces, specifically, lines
containing the following:
dpdk {
...
dev <pci_dev> {<options>}
uio-driver <uio_driver_name>
...
}
:param vpp_config_path: VPP Configuration file path
:param vpp_interfaces: List of VPP interface objects
:return: updated VPP config content.
"""
data = get_file_data(vpp_config_path)
# Add interface config to 'dpdk' section
for vpp_interface in vpp_interfaces:
if vpp_interface.pci_dev:
logger.info('vpp interface %s pci dev: %s'
% (vpp_interface.name, vpp_interface.pci_dev))
if vpp_interface.options:
int_cfg = '%s {%s}' % (vpp_interface.pci_dev,
vpp_interface.options)
else:
int_cfg = vpp_interface.pci_dev
# Make sure 'dpdk' section exists in the config
if not re.search(r'^\s*dpdk\s*\{', data, re.MULTILINE):
data += "\ndpdk {\n}\n"
# Find existing config line for the device we are trying to
# configure, the line should look like 'dev <pci_dev> ...'
# If such config line is found, we will replace the line with
# appropriate configuration, otherwise, add a new config line
# in 'dpdk' section of the config.
m = re.search(r'^\s*dev\s+%s\s*(\{[^}]*\})?\s*'
% vpp_interface.pci_dev, data,
re.IGNORECASE | re.MULTILINE)
if m:
data = re.sub(m.group(0), ' dev %s\n' % int_cfg, data)
else:
data = re.sub(r'(^\s*dpdk\s*\{)',
r'\1\n dev %s\n' % int_cfg,
data,
flags=re.MULTILINE)
if vpp_interface.uio_driver:
# Check if there is existing uio-driver configuration, if
# found, the line will be replaced with the appropriate
# configuration, otherwise, add a new line in 'dpdk' section.
m = re.search(r'^\s*uio-driver.*$', data, re.MULTILINE)
if m:
data = re.sub(m.group(0), r' uio-driver %s'
% vpp_interface.uio_driver, data)
else:
data = re.sub(r'(dpdk\s*\{)',
r'\1\n uio-driver %s'
% vpp_interface.uio_driver,
data)
else:
logger.debug('pci address not found for interface %s, may have'
'already been bound to vpp' % vpp_interface.name)
# Add start up script for VPP to config. This script will be executed by
# VPP on service start.
if not re.search(r'^\s*unix\s*\{', data, re.MULTILINE):
data += "\nunix {\n}\n"
m = re.search(r'^\s*(exec|startup-config).*$',
data,
re.IGNORECASE | re.MULTILINE)
if m:
data = re.sub(m.group(0), ' exec %s' % _VPP_EXEC_FILE, data)
else:
data = re.sub(r'(^\s*unix\s*\{)',
r'\1\n exec %s' % _VPP_EXEC_FILE,
data,
flags=re.MULTILINE)
# Make sure startup script exists to avoid VPP startup failure.
open(_VPP_EXEC_FILE, 'a').close()
return data
def update_vpp_mapping(vpp_interfaces):
"""Verify VPP interface binding and update mapping file
VppException will be raised if interfaces are not properly bound.
:param vpp_interfaces: List of VPP interface objects
"""
vpp_start_cli = ""
for vpp_int in vpp_interfaces:
if not vpp_int.pci_dev:
dpdk_map = _get_dpdk_map()
for dpdk_int in dpdk_map:
if dpdk_int['name'] == vpp_int.name:
vpp_int.pci_dev = dpdk_int['pci_address']
break
else:
raise VppException('Interface %s has no PCI address and is not'
' found in mapping file' % vpp_int.name)
# Try to get VPP interface name. In case VPP service is down
# for some reason, we will restart VPP and try again. Currently
# only trying one more time, can turn into a retry_counter if needed
# in the future.
for i in range(2):
vpp_name = _get_vpp_interface_name(vpp_int.pci_dev)
if not vpp_name:
restart_vpp(vpp_interfaces)
else:
break
else:
raise VppException('Interface %s with pci address %s not '
'bound to vpp'
% (vpp_int.name, vpp_int.pci_dev))
# Generate content of startup script for VPP
for address in vpp_int.addresses:
vpp_start_cli += 'set interface state %s up\n' % vpp_name
vpp_start_cli += 'set interface ip address %s %s/%s\n' \
% (vpp_name, address.ip, address.prefixlen)
logger.info('Updating mapping for vpp interface %s:'
'pci_dev: %s mac address: %s uio driver: %s'
% (vpp_int.name, vpp_int.pci_dev, vpp_int.hwaddr,
vpp_int.uio_driver))
_update_dpdk_map(vpp_int.name, vpp_int.pci_dev, vpp_int.hwaddr,
vpp_int.uio_driver)
# Enable VPP service to make the VPP interface configuration
# persistent.
processutils.execute('systemctl', 'enable', 'vpp')
if diff(_VPP_EXEC_FILE, vpp_start_cli):
write_config(_VPP_EXEC_FILE, vpp_start_cli)
restart_vpp(vpp_interfaces)