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