Merge "Bugfix: to list all used loop device"
[yardstick.git] / yardstick / common / utils.py
1 # Copyright 2013: Mirantis Inc.
2 # All Rights Reserved.
3 #
4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
5 #    not use this file except in compliance with the License. You may obtain
6 #    a copy of the License at
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
10 #    Unless required by applicable law or agreed to in writing, software
11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 #    License for the specific language governing permissions and limitations
14 #    under the License.
15
16 import collections
17 from contextlib import closing
18 import datetime
19 import errno
20 import importlib
21 import ipaddress
22 import logging
23 import os
24 import random
25 import re
26 import socket
27 import subprocess
28 import sys
29
30 import six
31 from flask import jsonify
32 from six.moves import configparser
33 from oslo_serialization import jsonutils
34 from oslo_utils import encodeutils
35
36 import yardstick
37
38 logger = logging.getLogger(__name__)
39 logger.setLevel(logging.DEBUG)
40
41
42 # Decorator for cli-args
43 def cliargs(*args, **kwargs):
44     def _decorator(func):
45         func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
46         return func
47     return _decorator
48
49
50 def itersubclasses(cls, _seen=None):
51     """Generator over all subclasses of a given class in depth first order."""
52
53     if not isinstance(cls, type):
54         raise TypeError("itersubclasses must be called with "
55                         "new-style classes, not %.100r" % cls)
56     _seen = _seen or set()
57     try:
58         subs = cls.__subclasses__()
59     except TypeError:   # fails only when cls is type
60         subs = cls.__subclasses__(cls)
61     for sub in subs:
62         if sub not in _seen:
63             _seen.add(sub)
64             yield sub
65             for sub in itersubclasses(sub, _seen):
66                 yield sub
67
68
69 def import_modules_from_package(package, raise_exception=False):
70     """Import modules given a package name
71
72     :param: package - Full package name. For example: rally.deploy.engines
73     """
74     yardstick_root = os.path.dirname(os.path.dirname(yardstick.__file__))
75     path = os.path.join(yardstick_root, *package.split('.'))
76     for root, _, files in os.walk(path):
77         matches = (filename for filename in files if filename.endswith('.py')
78                    and not filename.startswith('__'))
79         new_package = os.path.relpath(root, yardstick_root).replace(os.sep,
80                                                                     '.')
81         module_names = set(
82             '{}.{}'.format(new_package, filename.rsplit('.py', 1)[0])
83             for filename in matches)
84         # Find modules which haven't already been imported
85         missing_modules = module_names.difference(sys.modules)
86         logger.debug('Importing modules: %s', missing_modules)
87         for module_name in missing_modules:
88             try:
89                 importlib.import_module(module_name)
90             except (ImportError, SyntaxError) as exc:
91                 if raise_exception:
92                     raise exc
93                 logger.exception('Unable to import module %s', module_name)
94
95
96 NON_NONE_DEFAULT = object()
97
98
99 def get_key_with_default(data, key, default=NON_NONE_DEFAULT):
100     value = data.get(key, default)
101     if value is NON_NONE_DEFAULT:
102         raise KeyError(key)
103     return value
104
105
106 def make_dict_from_map(data, key_map):
107     return {dest_key: get_key_with_default(data, src_key, default)
108             for dest_key, (src_key, default) in key_map.items()}
109
110
111 def makedirs(d):
112     try:
113         os.makedirs(d)
114     except OSError as e:
115         if e.errno != errno.EEXIST:
116             raise
117
118
119 def remove_file(path):
120     try:
121         os.remove(path)
122     except OSError as e:
123         if e.errno != errno.ENOENT:
124             raise
125
126
127 def execute_command(cmd, **kwargs):
128     exec_msg = "Executing command: '%s'" % cmd
129     logger.debug(exec_msg)
130
131     output = subprocess.check_output(cmd.split(), **kwargs)
132     return encodeutils.safe_decode(output, incoming='utf-8').split(os.linesep)
133
134
135 def source_env(env_file):
136     p = subprocess.Popen(". %s; env" % env_file, stdout=subprocess.PIPE,
137                          shell=True)
138     output = p.communicate()[0]
139
140     # sometimes output type would be binary_type, and it don't have splitlines
141     # method, so we need to decode
142     if isinstance(output, six.binary_type):
143         output = encodeutils.safe_decode(output)
144     env = dict(line.split('=', 1) for line in output.splitlines() if '=' in line)
145     os.environ.update(env)
146     return env
147
148
149 def read_json_from_file(path):
150     with open(path, 'r') as f:
151         j = f.read()
152     # don't use jsonutils.load() it conflicts with already decoded input
153     return jsonutils.loads(j)
154
155
156 def write_json_to_file(path, data, mode='w'):
157     with open(path, mode) as f:
158         jsonutils.dump(data, f)
159
160
161 def write_file(path, data, mode='w'):
162     with open(path, mode) as f:
163         f.write(data)
164
165
166 def parse_ini_file(path):
167     parser = configparser.ConfigParser()
168
169     try:
170         files = parser.read(path)
171     except configparser.MissingSectionHeaderError:
172         logger.exception('invalid file type')
173         raise
174     else:
175         if not files:
176             raise RuntimeError('file not exist')
177
178     try:
179         default = {k: v for k, v in parser.items('DEFAULT')}
180     except configparser.NoSectionError:
181         default = {}
182
183     config = dict(DEFAULT=default,
184                   **{s: {k: v for k, v in parser.items(
185                       s)} for s in parser.sections()})
186
187     return config
188
189
190 def get_port_mac(sshclient, port):
191     cmd = "ifconfig |grep HWaddr |grep %s |awk '{print $5}' " % port
192     status, stdout, stderr = sshclient.execute(cmd)
193
194     if status:
195         raise RuntimeError(stderr)
196     return stdout.rstrip()
197
198
199 def get_port_ip(sshclient, port):
200     cmd = "ifconfig %s |grep 'inet addr' |awk '{print $2}' " \
201         "|cut -d ':' -f2 " % port
202     status, stdout, stderr = sshclient.execute(cmd)
203
204     if status:
205         raise RuntimeError(stderr)
206     return stdout.rstrip()
207
208
209 def flatten_dict_key(data):
210     next_data = {}
211
212     # use list, because iterable is too generic
213     if not any(isinstance(v, (collections.Mapping, list))
214                for v in data.values()):
215         return data
216
217     for k, v in data.items():
218         if isinstance(v, collections.Mapping):
219             for n_k, n_v in v.items():
220                 next_data["%s.%s" % (k, n_k)] = n_v
221         # use list because iterable is too generic
222         elif isinstance(v, collections.Iterable) and not isinstance(v, six.string_types):
223             for index, item in enumerate(v):
224                 next_data["%s%d" % (k, index)] = item
225         else:
226             next_data[k] = v
227
228     return flatten_dict_key(next_data)
229
230
231 def translate_to_str(obj):
232     if isinstance(obj, collections.Mapping):
233         return {str(k): translate_to_str(v) for k, v in obj.items()}
234     elif isinstance(obj, list):
235         return [translate_to_str(ele) for ele in obj]
236     elif isinstance(obj, six.text_type):
237         return str(obj)
238     return obj
239
240
241 def result_handler(status, data):
242     result = {
243         'status': status,
244         'result': data
245     }
246     return jsonify(result)
247
248
249 def change_obj_to_dict(obj):
250     dic = {}
251     for k, v in vars(obj).items():
252         try:
253             vars(v)
254         except TypeError:
255             dic.update({k: v})
256     return dic
257
258
259 def set_dict_value(dic, keys, value):
260     return_dic = dic
261
262     for key in keys.split('.'):
263         return_dic.setdefault(key, {})
264         if key == keys.split('.')[-1]:
265             return_dic[key] = value
266         else:
267             return_dic = return_dic[key]
268     return dic
269
270
271 def get_free_port(ip):
272     with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
273         port = random.randint(5000, 10000)
274         while s.connect_ex((ip, port)) == 0:
275             port = random.randint(5000, 10000)
276         return port
277
278
279 def mac_address_to_hex_list(mac):
280     octets = ["0x{:02x}".format(int(elem, 16)) for elem in mac.split(':')]
281     assert len(octets) == 6 and all(len(octet) == 4 for octet in octets)
282     return octets
283
284
285 def safe_ip_address(ip_addr):
286     """ get ip address version v6 or v4 """
287     try:
288         return ipaddress.ip_address(six.text_type(ip_addr))
289     except ValueError:
290         logging.error("%s is not valid", ip_addr)
291         return None
292
293
294 def get_ip_version(ip_addr):
295     """ get ip address version v6 or v4 """
296     try:
297         address = ipaddress.ip_address(six.text_type(ip_addr))
298     except ValueError:
299         logging.error("%s is not valid", ip_addr)
300         return None
301     else:
302         return address.version
303
304
305 def ip_to_hex(ip_addr, separator=''):
306     try:
307         address = ipaddress.ip_address(six.text_type(ip_addr))
308     except ValueError:
309         logging.error("%s is not valid", ip_addr)
310         return ip_addr
311
312     if address.version != 4:
313         return ip_addr
314
315     if not separator:
316         return '{:08x}'.format(int(address))
317
318     return separator.join('{:02x}'.format(octet) for octet in address.packed)
319
320
321 def try_int(s, *args):
322     """Convert to integer if possible."""
323     try:
324         return int(s)
325     except (TypeError, ValueError):
326         return args[0] if args else s
327
328
329 class SocketTopology(dict):
330
331     @classmethod
332     def parse_cpuinfo(cls, cpuinfo):
333         socket_map = {}
334
335         lines = cpuinfo.splitlines()
336
337         core_details = []
338         core_lines = {}
339         for line in lines:
340             if line.strip():
341                 name, value = line.split(":", 1)
342                 core_lines[name.strip()] = try_int(value.strip())
343             else:
344                 core_details.append(core_lines)
345                 core_lines = {}
346
347         for core in core_details:
348             socket_map.setdefault(core["physical id"], {}).setdefault(
349                 core["core id"], {})[core["processor"]] = (
350                 core["processor"], core["core id"], core["physical id"])
351
352         return cls(socket_map)
353
354     def sockets(self):
355         return sorted(self.keys())
356
357     def cores(self):
358         return sorted(core for cores in self.values() for core in cores)
359
360     def processors(self):
361         return sorted(
362             proc for cores in self.values() for procs in cores.values() for
363             proc in procs)
364
365
366 def config_to_dict(config):
367     return {section: dict(config.items(section)) for section in
368             config.sections()}
369
370
371 def validate_non_string_sequence(value, default=None, raise_exc=None):
372     # NOTE(ralonsoh): refactor this function to check if raise_exc is an
373     # Exception. Remove duplicate code, this function is duplicated in this
374     # repository.
375     if isinstance(value, collections.Sequence) and not isinstance(value, six.string_types):
376         return value
377     if raise_exc:
378         raise raise_exc  # pylint: disable=raising-bad-type
379     return default
380
381
382 def join_non_strings(separator, *non_strings):
383     try:
384         non_strings = validate_non_string_sequence(non_strings[0], raise_exc=RuntimeError)
385     except (IndexError, RuntimeError):
386         pass
387     return str(separator).join(str(non_string) for non_string in non_strings)
388
389
390 def safe_decode_utf8(s):
391     """Safe decode a str from UTF"""
392     if six.PY3 and isinstance(s, bytes):
393         return s.decode('utf-8', 'surrogateescape')
394     return s
395
396
397 class ErrorClass(object):
398
399     def __init__(self, *args, **kwargs):
400         if 'test' not in kwargs:
401             raise RuntimeError
402
403     def __getattr__(self, item):
404         raise AttributeError
405
406
407 class Timer(object):
408     def __init__(self):
409         super(Timer, self).__init__()
410         self.start = self.delta = None
411
412     def __enter__(self):
413         self.start = datetime.datetime.now()
414         return self
415
416     def __exit__(self, *_):
417         self.delta = datetime.datetime.now() - self.start
418
419     def __getattr__(self, item):
420         return getattr(self.delta, item)
421
422
423 def read_meminfo(ssh_client):
424     """Read "/proc/meminfo" file and parse all keys and values"""
425
426     cpuinfo = six.BytesIO()
427     ssh_client.get_file_obj('/proc/meminfo', cpuinfo)
428     lines = cpuinfo.getvalue().decode('utf-8')
429     matches = re.findall(r"([\w\(\)]+):\s+(\d+)( kB)*", lines)
430     output = {}
431     for match in matches:
432         output[match[0]] = match[1]
433
434     return output
435
436
437 def find_relative_file(path, task_path):
438     """
439     Find file in one of places: in abs of path or relative to a directory path,
440     in this order.
441
442     :param path:
443     :param task_path:
444     :return str: full path to file
445     """
446     # fixme: create schema to validate all fields have been provided
447     for lookup in [os.path.abspath(path), os.path.join(task_path, path)]:
448         try:
449             with open(lookup):
450                 return lookup
451         except IOError:
452             pass
453     raise IOError(errno.ENOENT, 'Unable to find {} file'.format(path))
454
455
456 def open_relative_file(path, task_path):
457     try:
458         return open(path)
459     except IOError as e:
460         if e.errno == errno.ENOENT:
461             return open(os.path.join(task_path, path))
462         raise