NFVBENCH-171 Not accurate flow count with some IP and UDP ranges combinations
[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 gcd
17 from math import isnan
18 import os
19 import re
20 import signal
21 import subprocess
22
23 import errno
24 import fcntl
25 from functools import wraps
26 import json
27 from .log import LOG
28 from nfvbench.traffic_gen.traffic_utils import multiplier_map
29
30 class TimeoutError(Exception):
31     pass
32
33
34 def timeout(seconds=10, error_message=os.strerror(errno.ETIME)):
35     def decorator(func):
36         def _handle_timeout(_signum, _frame):
37             raise TimeoutError(error_message)
38
39         def wrapper(*args, **kwargs):
40             signal.signal(signal.SIGALRM, _handle_timeout)
41             signal.alarm(seconds)
42             try:
43                 result = func(*args, **kwargs)
44             finally:
45                 signal.alarm(0)
46             return result
47
48         return wraps(func)(wrapper)
49
50     return decorator
51
52
53 def save_json_result(result, json_file, std_json_path, service_chain, service_chain_count,
54                      flow_count, frame_sizes):
55     """Save results in json format file."""
56     filepaths = []
57     if json_file:
58         filepaths.append(json_file)
59     if std_json_path:
60         name_parts = [service_chain, str(service_chain_count), str(flow_count)] + list(frame_sizes)
61         filename = '-'.join(name_parts) + '.json'
62         filepaths.append(os.path.join(std_json_path, filename))
63
64     if filepaths:
65         for file_path in filepaths:
66             LOG.info('Saving results in json file: %s...', file_path)
67             with open(file_path, 'w') as jfp:
68                 json.dump(result,
69                           jfp,
70                           indent=4,
71                           sort_keys=True,
72                           separators=(',', ': '),
73                           default=lambda obj: obj.to_json())
74
75
76 def dict_to_json_dict(record):
77     return json.loads(json.dumps(record, default=lambda obj: obj.to_json()))
78
79
80 def get_intel_pci(nic_slot=None, nic_ports=None):
81     """Returns two PCI address that will be used for NFVbench
82
83     @param nic_slot: The physical PCIe slot number in motherboard
84     @param nic_ports: Array of two integers indicating the ports to use on the NIC
85
86     When nic_slot and nic_ports are both supplied, the function will just return
87     the PCI addresses for them. The logic used is:
88         (1) Run "dmidecode -t slot"
89         (2) Grep for "SlotID:" with given nic_slot, and derive the bus address;
90         (3) Based on given nic_ports, generate the pci addresses based on above
91         base address;
92
93     When either nic_slot or nic_ports is not supplied, the function will
94     traverse all Intel NICs which use i40e or ixgbe driver, sorted by PCI
95     address, and return first two available ports which are not bonded
96     (802.11ad).
97     """
98
99     if nic_slot and nic_ports:
100         dmidecode = subprocess.check_output(['dmidecode', '-t', 'slot'])
101         regex = r"(?<=SlotID:{}).*?(....:..:..\..)".format(nic_slot)
102         match = re.search(regex, dmidecode.decode('utf-8'), flags=re.DOTALL)
103         if not match:
104             return None
105
106         pcis = []
107         # On some servers, the "Bus Address" returned by dmidecode is not the
108         # base pci address of the NIC. So only keeping the bus part of the
109         # address for better compability.
110         bus = match.group(1)[:match.group(1).rindex(':') + 1] + "00."
111         for port in nic_ports:
112             pcis.append(bus + str(port))
113
114         return pcis
115
116     hx = r'[0-9a-fA-F]'
117     regex = r'({hx}{{4}}:({hx}{{2}}:{hx}{{2}}\.{hx}{{1}})).*(drv={driver}|.*unused=.*{driver})'
118     pcis = []
119     try:
120         trex_base_dir = '/opt/trex'
121         contents = os.listdir(trex_base_dir)
122         trex_dir = os.path.join(trex_base_dir, contents[0])
123         process = subprocess.Popen(['python', 'dpdk_setup_ports.py', '-s'],
124                                    cwd=trex_dir,
125                                    stdout=subprocess.PIPE,
126                                    stderr=subprocess.PIPE)
127         devices, _ = process.communicate()
128     except Exception:
129         devices = ''
130
131     for driver in ['i40e', 'ixgbe']:
132         matches = re.findall(regex.format(hx=hx, driver=driver), devices.decode("utf-8"))
133         if not matches:
134             continue
135
136         matches.sort()
137         device_list = list(x[0].split('.')[0] for x in matches)
138         device_ports_list = {i: {'ports': device_list.count(i)} for i in device_list}
139         for port in matches:
140             intf_name = glob.glob("/sys/bus/pci/devices/%s/net/*" % port[0])
141             if intf_name:
142                 intf_name = intf_name[0][intf_name[0].rfind('/') + 1:]
143                 process = subprocess.Popen(['ip', '-o', '-d', 'link', 'show', intf_name],
144                                            stdout=subprocess.PIPE,
145                                            stderr=subprocess.PIPE)
146                 intf_info, _ = process.communicate()
147                 if re.search('team_slave|bond_slave', intf_info.decode("utf-8")):
148                     device_ports_list[port[0].split('.')[0]]['busy'] = True
149         for port in matches:
150             if not device_ports_list[port[0].split('.')[0]].get('busy'):
151                 pcis.append(port[1])
152             if len(pcis) == 2:
153                 break
154
155     return pcis
156
157
158
159 def parse_flow_count(flow_count):
160     flow_count = str(flow_count)
161     input_fc = flow_count
162     multiplier = 1
163     if flow_count[-1].upper() in multiplier_map:
164         multiplier = multiplier_map[flow_count[-1].upper()]
165         flow_count = flow_count[:-1]
166
167     try:
168         flow_count = int(flow_count)
169     except ValueError:
170         raise Exception("Unknown flow count format '{}'".format(input_fc)) from ValueError
171
172     return flow_count * multiplier
173
174
175 def cast_integer(value):
176     # force 0 value if NaN value from TRex to avoid error in JSON result parsing
177     return int(value) if not isnan(value) else 0
178
179
180 class RunLock(object):
181     """
182     Attempts to lock file and run current instance of NFVbench as the first,
183     otherwise raises exception.
184     """
185
186     def __init__(self, path='/tmp/nfvbench.lock'):
187         self._path = path
188         self._fd = None
189
190     def __enter__(self):
191         try:
192             self._fd = os.open(self._path, os.O_CREAT)
193             fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
194         except (OSError, IOError) as e:
195             raise Exception('Other NFVbench process is running. Please wait') from e
196
197     def __exit__(self, *args):
198         fcntl.flock(self._fd, fcntl.LOCK_UN)
199         os.close(self._fd)
200         self._fd = None
201
202         # Try to remove the lock file, but don't try too hard because it is unnecessary.
203         try:
204             os.unlink(self._path)
205         except (OSError, IOError):
206             pass
207
208
209 def get_divisors(n):
210     for i in range(1, int(n / 2) + 1):
211         if n % i == 0:
212             yield i
213     yield n
214
215
216 def lcm(a, b):
217     """
218     Calculate the maximum possible value for both IP and ports,
219     eventually for maximum possible flow.
220     """
221     if a != 0 and b != 0:
222         lcm_value = a * b // gcd(a, b)
223         return lcm_value
224     raise TypeError(" IP size or port range can't be zero !")
225
226
227 def find_tuples_equal_to_lcm_value(a, b, lcm_value):
228     """
229     Find numbers from two list matching a LCM value.
230     """
231     for x in a:
232         for y in b:
233             if lcm(x, y) == lcm_value:
234                 yield (x, y)
235
236
237 def find_max_size(max_size, tuples, flow):
238     if tuples:
239         if max_size > tuples[-1][0]:
240             max_size = tuples[-1][0]
241             return int(max_size)
242         if max_size > tuples[-1][1]:
243             max_size = tuples[-1][1]
244             return int(max_size)
245
246     for i in range(max_size, 1, -1):
247         if flow % i == 0:
248             return int(i)
249     return 1