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