1 # Copyright (c) 2016-2018 Intel Corporation
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
18 from collections import defaultdict
19 from itertools import chain
21 from yardstick.common import exceptions
22 from yardstick.common.utils import validate_non_string_sequence
23 from yardstick.error import IncorrectConfig
24 from yardstick.error import IncorrectSetup
25 from yardstick.error import IncorrectNodeSetup
28 NETWORK_KERNEL = 'network_kernel'
29 NETWORK_DPDK = 'network_dpdk'
30 NETWORK_OTHER = 'network_other'
31 CRYPTO_KERNEL = 'crypto_kernel'
32 CRYPTO_DPDK = 'crypto_dpdk'
33 CRYPTO_OTHER = 'crypto_other'
35 LOG = logging.getLogger(__name__)
38 class DpdkBindHelperException(Exception):
42 class DpdkInterface(object):
43 TOPOLOGY_REQUIRED_KEYS = frozenset({
44 "vpci", "local_ip", "netmask", "local_mac", "driver"})
46 def __init__(self, dpdk_node, interface):
47 super(DpdkInterface, self).__init__()
48 self.dpdk_node = dpdk_node
49 self.interface = interface
53 except (AssertionError, KeyError):
58 return self.interface['local_mac']
62 return self.local_mac.lower()
65 def missing_fields(self):
66 return self.TOPOLOGY_REQUIRED_KEYS.difference(self.interface)
69 def _detect_socket(netdev):
71 socket = netdev['numa_node']
73 # Where is this documented?
74 # It seems for dual-sockets systems the second socket PCI bridge
75 # will have an address > 0x0f, e.g.
76 # Bridge PCI->PCI (P#524320 busid=0000:80:02.0 id=8086:6f04
77 if netdev['pci_bus_id'][5] == "0":
80 # this doesn't handle quad-sockets
81 # TODO: fix this for quad-socket
85 def probe_missing_values(self):
87 for netdev in self.dpdk_node.netdevs.values():
88 if netdev['address'].lower() == self.mac_lower:
89 socket = self._detect_socket(netdev)
90 self.interface.update({
91 'vpci': netdev['pci_bus_id'],
92 'driver': netdev['driver'],
98 # if we don't find all the keys then don't update
101 except (IncorrectNodeSetup, exceptions.SSHError,
102 exceptions.SSHTimeout):
103 raise IncorrectConfig(
104 "Unable to probe missing interface fields '%s', on node %s "
105 "SSH Error" % (', '.join(self.missing_fields), self.dpdk_node.node_key))
108 class DpdkNode(object):
110 def __init__(self, node_name, interfaces, ssh_helper, timeout=120):
111 super(DpdkNode, self).__init__()
112 self.interfaces = interfaces
113 self.ssh_helper = ssh_helper
114 self.node_key = node_name
115 self.timeout = timeout
116 self._dpdk_helper = None
120 self.dpdk_interfaces = {intf['name']: DpdkInterface(self, intf['virtual-interface'])
121 for intf in self.interfaces}
122 except IncorrectConfig:
123 template = "MAC address is required for all interfaces, missing on: {}"
124 errors = (intf['name'] for intf in self.interfaces if
125 'local_mac' not in intf['virtual-interface'])
126 raise IncorrectSetup(template.format(", ".join(errors)))
129 def dpdk_helper(self):
130 if not isinstance(self._dpdk_helper, DpdkBindHelper):
131 self._dpdk_helper = DpdkBindHelper(self.ssh_helper)
132 return self._dpdk_helper
135 def _interface_missing_iter(self):
136 return chain.from_iterable(self._interface_missing_map.values())
139 def _interface_missing_map(self):
140 return {name: intf.missing_fields for name, intf in self.dpdk_interfaces.items()}
142 def _probe_netdevs(self):
143 self.netdevs.update(self.dpdk_helper.find_net_devices())
145 def _force_rebind(self):
146 return self.dpdk_helper.force_dpdk_rebind()
148 def _probe_dpdk_drivers(self):
149 self.dpdk_helper.probe_real_kernel_drivers()
150 for pci, driver in self.dpdk_helper.real_kernel_interface_driver_map.items():
151 for intf in self.interfaces:
152 vintf = intf['virtual-interface']
153 # stupid substring matches
154 # don't use netdev use interface
155 if vintf['vpci'].endswith(pci):
156 vintf['driver'] = driver
157 # we can't update netdevs because we may not have netdev info
159 def _probe_missing_values(self):
160 for intf in self.dpdk_interfaces.values():
161 intf.probe_missing_values()
164 # only ssh probe if there are missing values
165 # ssh probe won't work on Ixia, so we had better define all our values
167 missing_fields_set = set(self._interface_missing_iter)
169 # if we are only missing driver then maybe we can get kernel module
171 if missing_fields_set == {'driver'}:
172 self._probe_dpdk_drivers()
173 # we can't reprobe missing values because we may not have netdev info
175 # if there are any other missing then we have to netdev probe
176 if missing_fields_set.difference({'driver'}):
177 self._probe_netdevs()
179 self._probe_missing_values()
180 except IncorrectConfig:
184 # check again and verify we have all the fields
185 if set(self._interface_missing_iter):
186 # last chance fallback, rebind everything and probe
187 # this probably won't work
189 self._probe_netdevs()
190 self._probe_missing_values()
192 errors = ("{} missing: {}".format(name, ", ".join(missing_fields)) for
193 name, missing_fields in self._interface_missing_map.items() if
195 errors = "\n".join(errors)
197 raise IncorrectSetup(errors)
200 self._dpdk_helper = None
203 class DpdkBindHelper(object):
204 DPDK_STATUS_CMD = "{dpdk_devbind} --status"
205 DPDK_BIND_CMD = "sudo {dpdk_devbind} {force} -b {driver} {vpci}"
207 NIC_ROW_RE = re.compile(r"([^ ]+) '([^']+)' (?:if=([^ ]+) )?drv=([^ ]+) "
208 r"unused=([^ ]*)(?: (\*Active\*))?")
209 SKIP_RE = re.compile('(====|<none>|^$)')
210 NIC_ROW_FIELDS = ['vpci', 'dev_type', 'iface', 'driver', 'unused', 'active']
214 HEADER_DICT_PAIRS = [
215 (re.compile('^Network.*DPDK.*$'), NETWORK_DPDK),
216 (re.compile('^Network.*kernel.*$'), NETWORK_KERNEL),
217 (re.compile('^Other network.*$'), NETWORK_OTHER),
218 (re.compile('^Crypto.*DPDK.*$'), CRYPTO_DPDK),
219 (re.compile('^Crypto.*kernel$'), CRYPTO_KERNEL),
220 (re.compile('^Other crypto.*$'), CRYPTO_OTHER),
223 FIND_NETDEVICE_STRING = r"""\
224 find /sys/devices/pci* -type d -name net -exec sh -c '{ grep -sH ^ \
225 $1/ifindex $1/address $1/operstate $1/device/vendor $1/device/device \
226 $1/device/subsystem_vendor $1/device/subsystem_device $1/device/numa_node ; \
227 printf "%s/driver:" $1 ; basename $(readlink -s $1/device/driver); } \
231 BASE_ADAPTER_RE = re.compile('^/sys/devices/(.*)/net/([^/]*)/([^:]*):(.*)$', re.M)
232 DPDK_DEVBIND = "dpdk-devbind.py"
235 def parse_netdev_info(cls, stdout):
236 network_devices = defaultdict(dict)
237 match_iter = (match.groups() for match in cls.BASE_ADAPTER_RE.finditer(stdout))
238 for bus_path, interface_name, name, value in match_iter:
239 dir_name, bus_id = os.path.split(bus_path)
240 if 'virtio' in bus_id:
241 # for some stupid reason VMs include virtio1/
243 bus_id = os.path.basename(dir_name)
245 # remove extra 'device/' from 'device/vendor,
246 # device/subsystem_vendor', etc.
248 name = name.split('/')[1]
250 network_devices[interface_name].update({
252 'interface_name': interface_name,
253 'pci_bus_id': bus_id,
256 # convert back to regular dict
257 return dict(network_devices)
259 def clean_status(self):
269 # TODO: add support for driver other than igb_uio
270 def __init__(self, ssh_helper, dpdk_driver="igb_uio"):
271 self.ssh_helper = ssh_helper
272 self.real_kernel_interface_driver_map = {}
273 self.dpdk_driver = dpdk_driver
274 self.dpdk_status = None
275 self.status_nic_row_re = None
276 self.dpdk_devbind = self.ssh_helper.join_bin_path(self.DPDK_DEVBIND)
277 self._status_cmd_attr = None
278 self.used_drivers = None
279 self.real_kernel_drivers = {}
281 self.ssh_helper = ssh_helper
284 def _dpdk_execute(self, *args, **kwargs):
285 res = self.ssh_helper.execute(*args, **kwargs)
287 template = '{} command failed with rc={}'
288 raise DpdkBindHelperException(template.format(self.dpdk_devbind, res[0]))
291 def load_dpdk_driver(self):
292 cmd_template = "sudo modprobe {} && sudo modprobe {}"
293 self.ssh_helper.execute(cmd_template.format(self.UIO_DRIVER, self.dpdk_driver))
295 def check_dpdk_driver(self):
296 return self.ssh_helper.execute("lsmod | grep -i {}".format(self.dpdk_driver))[0]
299 def _status_cmd(self):
300 if self._status_cmd_attr is None:
301 self._status_cmd_attr = self.DPDK_STATUS_CMD.format(dpdk_devbind=self.dpdk_devbind)
302 return self._status_cmd_attr
304 def _add_line(self, active_list, line):
305 if active_list is None:
308 res = self.NIC_ROW_RE.match(line)
312 new_data = {k: v for k, v in zip(self.NIC_ROW_FIELDS, res.groups())}
313 new_data['active'] = bool(new_data['active'])
314 self.dpdk_status[active_list].append(new_data)
317 def _switch_active_dict(cls, a_row, active_dict):
318 for regexp, a_dict in cls.HEADER_DICT_PAIRS:
319 if regexp.match(a_row):
323 def _parse_dpdk_status_output(self, output):
326 for a_row in output.splitlines():
327 if self.SKIP_RE.match(a_row):
329 active_dict = self._switch_active_dict(a_row, active_dict)
330 self._add_line(active_dict, a_row)
331 return self.dpdk_status
333 def _get_bound_pci_addresses(self, active_dict):
334 return [iface['vpci'] for iface in self.dpdk_status[active_dict]]
337 def dpdk_bound_pci_addresses(self):
338 return self._get_bound_pci_addresses(NETWORK_DPDK)
341 def kernel_bound_pci_addresses(self):
342 return self._get_bound_pci_addresses(NETWORK_KERNEL)
345 def interface_driver_map(self):
346 return {interface['vpci']: interface['driver']
347 for interface in chain.from_iterable(self.dpdk_status.values())}
349 def read_status(self):
350 return self._parse_dpdk_status_output(self._dpdk_execute(self._status_cmd)[1])
352 def find_net_devices(self):
353 exit_status, stdout, _ = self.ssh_helper.execute(self.FIND_NETDEVICE_STRING)
357 return self.parse_netdev_info(stdout)
359 def bind(self, pci_addresses, driver, force=True):
360 # accept single PCI or sequence of PCI
361 pci_addresses = validate_non_string_sequence(pci_addresses, [pci_addresses])
363 cmd = self.DPDK_BIND_CMD.format(dpdk_devbind=self.dpdk_devbind,
365 vpci=' '.join(list(pci_addresses)),
366 force='--force' if force else '')
368 self._dpdk_execute(cmd)
370 # update the inner status dict
373 def probe_real_kernel_drivers(self):
375 self.save_real_kernel_interface_driver_map()
377 def force_dpdk_rebind(self):
378 self.load_dpdk_driver()
380 self.save_real_kernel_interface_driver_map()
381 self.save_used_drivers()
384 # only rebind devices that are bound to DPDK
385 for pci in self.dpdk_bound_pci_addresses:
387 real_driver = self.real_kernel_interface_driver_map[pci]
388 real_driver_map.setdefault(real_driver, []).append(pci)
389 for real_driver, pcis in real_driver_map.items():
390 self.bind(pcis, real_driver, force=True)
392 def save_used_drivers(self):
393 # invert the map, so we can bind by driver type
394 self.used_drivers = {}
396 for vpci, driver in sorted(self.interface_driver_map.items()):
397 self.used_drivers.setdefault(driver, []).append(vpci)
399 KERNEL_DRIVER_RE = re.compile(r"Kernel modules: (\S+)", re.M)
400 VIRTIO_DRIVER_RE = re.compile(r"Ethernet.*Virtio network device", re.M)
401 VIRTIO_DRIVER = "virtio-pci"
403 def save_real_kernel_drivers(self):
404 # invert the map, so we can bind by driver type
405 self.real_kernel_drivers = {}
407 for vpci, driver in sorted(self.real_kernel_interface_driver_map.items()):
408 self.used_drivers.setdefault(driver, []).append(vpci)
410 def get_real_kernel_driver(self, pci):
411 out = self.ssh_helper.execute('lspci -k -s %s' % pci)[1]
412 match = self.KERNEL_DRIVER_RE.search(out)
414 return match.group(1)
416 match = self.VIRTIO_DRIVER_RE.search(out)
418 return self.VIRTIO_DRIVER
422 def save_real_kernel_interface_driver_map(self):
423 iter1 = ((pci, self.get_real_kernel_driver(pci)) for pci in self.interface_driver_map)
424 self.real_kernel_interface_driver_map = {pci: driver for pci, driver in iter1 if driver}
426 def rebind_drivers(self, force=True):
427 for driver, vpcis in self.used_drivers.items():
428 self.bind(vpcis, driver, force)