NFVBENCH-175 pylint w0707 correction
[nfvbench.git] / nfvbench / utils.py
1 # Copyright 2016 Cisco Systems, Inc.  All rights reserved.
2 #
3 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
4 #    not use this file except in compliance with the License. You may obtain
5 #    a copy of the License at
6 #
7 #         http://www.apache.org/licenses/LICENSE-2.0
8 #
9 #    Unless required by applicable law or agreed to in writing, software
10 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 #    License for the specific language governing permissions and limitations
13 #    under the License.
14
15 import glob
16 from math import isnan
17 import os
18 import re
19 import signal
20 import subprocess
21
22 import errno
23 import fcntl
24 from functools import wraps
25 import json
26 from .log import LOG
27 from nfvbench.traffic_gen.traffic_utils import multiplier_map
28
29 class TimeoutError(Exception):
30     pass
31
32
33 def timeout(seconds=10, error_message=os.strerror(errno.ETIME)):
34     def decorator(func):
35         def _handle_timeout(_signum, _frame):
36             raise TimeoutError(error_message)
37
38         def wrapper(*args, **kwargs):
39             signal.signal(signal.SIGALRM, _handle_timeout)
40             signal.alarm(seconds)
41             try:
42                 result = func(*args, **kwargs)
43             finally:
44                 signal.alarm(0)
45             return result
46
47         return wraps(func)(wrapper)
48
49     return decorator
50
51
52 def save_json_result(result, json_file, std_json_path, service_chain, service_chain_count,
53                      flow_count, frame_sizes):
54     """Save results in json format file."""
55     filepaths = []
56     if json_file:
57         filepaths.append(json_file)
58     if std_json_path:
59         name_parts = [service_chain, str(service_chain_count), str(flow_count)] + list(frame_sizes)
60         filename = '-'.join(name_parts) + '.json'
61         filepaths.append(os.path.join(std_json_path, filename))
62
63     if filepaths:
64         for file_path in filepaths:
65             LOG.info('Saving results in json file: %s...', file_path)
66             with open(file_path, 'w') as jfp:
67                 json.dump(result,
68                           jfp,
69                           indent=4,
70                           sort_keys=True,
71                           separators=(',', ': '),
72                           default=lambda obj: obj.to_json())
73
74
75 def dict_to_json_dict(record):
76     return json.loads(json.dumps(record, default=lambda obj: obj.to_json()))
77
78
79 def get_intel_pci(nic_slot=None, nic_ports=None):
80     """Returns two PCI address that will be used for NFVbench
81
82     @param nic_slot: The physical PCIe slot number in motherboard
83     @param nic_ports: Array of two integers indicating the ports to use on the NIC
84
85     When nic_slot and nic_ports are both supplied, the function will just return
86     the PCI addresses for them. The logic used is:
87         (1) Run "dmidecode -t slot"
88         (2) Grep for "SlotID:" with given nic_slot, and derive the bus address;
89         (3) Based on given nic_ports, generate the pci addresses based on above
90         base address;
91
92     When either nic_slot or nic_ports is not supplied, the function will
93     traverse all Intel NICs which use i40e or ixgbe driver, sorted by PCI
94     address, and return first two available ports which are not bonded
95     (802.11ad).
96     """
97
98     if nic_slot and nic_ports:
99         dmidecode = subprocess.check_output(['dmidecode', '-t', 'slot'])
100         regex = r"(?<=SlotID:{}).*?(....:..:..\..)".format(nic_slot)
101         match = re.search(regex, dmidecode.decode('utf-8'), flags=re.DOTALL)
102         if not match:
103             return None
104
105         pcis = []
106         # On some servers, the "Bus Address" returned by dmidecode is not the
107         # base pci address of the NIC. So only keeping the bus part of the
108         # address for better compability.
109         bus = match.group(1)[:match.group(1).rindex(':') + 1] + "00."
110         for port in nic_ports:
111             pcis.append(bus + str(port))
112
113         return pcis
114
115     hx = r'[0-9a-fA-F]'
116     regex = r'({hx}{{4}}:({hx}{{2}}:{hx}{{2}}\.{hx}{{1}})).*(drv={driver}|.*unused=.*{driver})'
117     pcis = []
118     try:
119         trex_base_dir = '/opt/trex'
120         contents = os.listdir(trex_base_dir)
121         trex_dir = os.path.join(trex_base_dir, contents[0])
122         process = subprocess.Popen(['python', 'dpdk_setup_ports.py', '-s'],
123                                    cwd=trex_dir,
124                                    stdout=subprocess.PIPE,
125                                    stderr=subprocess.PIPE)
126         devices, _ = process.communicate()
127     except Exception:
128         devices = ''
129
130     for driver in ['i40e', 'ixgbe']:
131         matches = re.findall(regex.format(hx=hx, driver=driver), devices.decode("utf-8"))
132         if not matches:
133             continue
134
135         matches.sort()
136         device_list = list(x[0].split('.')[0] for x in matches)
137         device_ports_list = {i: {'ports': device_list.count(i)} for i in device_list}
138         for port in matches:
139             intf_name = glob.glob("/sys/bus/pci/devices/%s/net/*" % port[0])
140             if intf_name:
141                 intf_name = intf_name[0][intf_name[0].rfind('/') + 1:]
142                 process = subprocess.Popen(['ip', '-o', '-d', 'link', 'show', intf_name],
143                                            stdout=subprocess.PIPE,
144                                            stderr=subprocess.PIPE)
145                 intf_info, _ = process.communicate()
146                 if re.search('team_slave|bond_slave', intf_info.decode("utf-8")):
147                     device_ports_list[port[0].split('.')[0]]['busy'] = True
148         for port in matches:
149             if not device_ports_list[port[0].split('.')[0]].get('busy'):
150                 pcis.append(port[1])
151             if len(pcis) == 2:
152                 break
153
154     return pcis
155
156
157
158 def parse_flow_count(flow_count):
159     flow_count = str(flow_count)
160     input_fc = flow_count
161     multiplier = 1
162     if flow_count[-1].upper() in multiplier_map:
163         multiplier = multiplier_map[flow_count[-1].upper()]
164         flow_count = flow_count[:-1]
165
166     try:
167         flow_count = int(flow_count)
168     except ValueError:
169         raise Exception("Unknown flow count format '{}'".format(input_fc)) from ValueError
170
171     return flow_count * multiplier
172
173
174 def cast_integer(value):
175     # force 0 value if NaN value from TRex to avoid error in JSON result parsing
176     return int(value) if not isnan(value) else 0
177
178
179 class RunLock(object):
180     """
181     Attempts to lock file and run current instance of NFVbench as the first,
182     otherwise raises exception.
183     """
184
185     def __init__(self, path='/tmp/nfvbench.lock'):
186         self._path = path
187         self._fd = None
188
189     def __enter__(self):
190         try:
191             self._fd = os.open(self._path, os.O_CREAT)
192             fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
193         except (OSError, IOError) as e:
194             raise Exception('Other NFVbench process is running. Please wait') from e
195
196     def __exit__(self, *args):
197         fcntl.flock(self._fd, fcntl.LOCK_UN)
198         os.close(self._fd)
199         self._fd = None
200
201         # Try to remove the lock file, but don't try too hard because it is unnecessary.
202         try:
203             os.unlink(self._path)
204         except (OSError, IOError):
205             pass