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