Merge "Add "render-only" option to "task" command"
[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     env = dict(line.split('=', 1) for line in output.splitlines() if '=' in line)
140     os.environ.update(env)
141     return env
142
143
144 def read_json_from_file(path):
145     with open(path, 'r') as f:
146         j = f.read()
147     # don't use jsonutils.load() it conflicts with already decoded input
148     return jsonutils.loads(j)
149
150
151 def write_json_to_file(path, data, mode='w'):
152     with open(path, mode) as f:
153         jsonutils.dump(data, f)
154
155
156 def write_file(path, data, mode='w'):
157     with open(path, mode) as f:
158         f.write(data)
159
160
161 def parse_ini_file(path):
162     parser = configparser.ConfigParser()
163
164     try:
165         files = parser.read(path)
166     except configparser.MissingSectionHeaderError:
167         logger.exception('invalid file type')
168         raise
169     else:
170         if not files:
171             raise RuntimeError('file not exist')
172
173     try:
174         default = {k: v for k, v in parser.items('DEFAULT')}
175     except configparser.NoSectionError:
176         default = {}
177
178     config = dict(DEFAULT=default,
179                   **{s: {k: v for k, v in parser.items(
180                       s)} for s in parser.sections()})
181
182     return config
183
184
185 def get_port_mac(sshclient, port):
186     cmd = "ifconfig |grep HWaddr |grep %s |awk '{print $5}' " % port
187     status, stdout, stderr = sshclient.execute(cmd)
188
189     if status:
190         raise RuntimeError(stderr)
191     return stdout.rstrip()
192
193
194 def get_port_ip(sshclient, port):
195     cmd = "ifconfig %s |grep 'inet addr' |awk '{print $2}' " \
196         "|cut -d ':' -f2 " % port
197     status, stdout, stderr = sshclient.execute(cmd)
198
199     if status:
200         raise RuntimeError(stderr)
201     return stdout.rstrip()
202
203
204 def flatten_dict_key(data):
205     next_data = {}
206
207     # use list, because iterable is too generic
208     if not any(isinstance(v, (collections.Mapping, list))
209                for v in data.values()):
210         return data
211
212     for k, v in data.items():
213         if isinstance(v, collections.Mapping):
214             for n_k, n_v in v.items():
215                 next_data["%s.%s" % (k, n_k)] = n_v
216         # use list because iterable is too generic
217         elif isinstance(v, collections.Iterable) and not isinstance(v, six.string_types):
218             for index, item in enumerate(v):
219                 next_data["%s%d" % (k, index)] = item
220         else:
221             next_data[k] = v
222
223     return flatten_dict_key(next_data)
224
225
226 def translate_to_str(obj):
227     if isinstance(obj, collections.Mapping):
228         return {str(k): translate_to_str(v) for k, v in obj.items()}
229     elif isinstance(obj, list):
230         return [translate_to_str(ele) for ele in obj]
231     elif isinstance(obj, six.text_type):
232         return str(obj)
233     return obj
234
235
236 def result_handler(status, data):
237     result = {
238         'status': status,
239         'result': data
240     }
241     return jsonify(result)
242
243
244 def change_obj_to_dict(obj):
245     dic = {}
246     for k, v in vars(obj).items():
247         try:
248             vars(v)
249         except TypeError:
250             dic.update({k: v})
251     return dic
252
253
254 def set_dict_value(dic, keys, value):
255     return_dic = dic
256
257     for key in keys.split('.'):
258         return_dic.setdefault(key, {})
259         if key == keys.split('.')[-1]:
260             return_dic[key] = value
261         else:
262             return_dic = return_dic[key]
263     return dic
264
265
266 def get_free_port(ip):
267     with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
268         port = random.randint(5000, 10000)
269         while s.connect_ex((ip, port)) == 0:
270             port = random.randint(5000, 10000)
271         return port
272
273
274 def mac_address_to_hex_list(mac):
275     octets = ["0x{:02x}".format(int(elem, 16)) for elem in mac.split(':')]
276     assert len(octets) == 6 and all(len(octet) == 4 for octet in octets)
277     return octets
278
279
280 def safe_ip_address(ip_addr):
281     """ get ip address version v6 or v4 """
282     try:
283         return ipaddress.ip_address(six.text_type(ip_addr))
284     except ValueError:
285         logging.error("%s is not valid", ip_addr)
286         return None
287
288
289 def get_ip_version(ip_addr):
290     """ get ip address version v6 or v4 """
291     try:
292         address = ipaddress.ip_address(six.text_type(ip_addr))
293     except ValueError:
294         logging.error("%s is not valid", ip_addr)
295         return None
296     else:
297         return address.version
298
299
300 def ip_to_hex(ip_addr, separator=''):
301     try:
302         address = ipaddress.ip_address(six.text_type(ip_addr))
303     except ValueError:
304         logging.error("%s is not valid", ip_addr)
305         return ip_addr
306
307     if address.version != 4:
308         return ip_addr
309
310     if not separator:
311         return '{:08x}'.format(int(address))
312
313     return separator.join('{:02x}'.format(octet) for octet in address.packed)
314
315
316 def try_int(s, *args):
317     """Convert to integer if possible."""
318     try:
319         return int(s)
320     except (TypeError, ValueError):
321         return args[0] if args else s
322
323
324 class SocketTopology(dict):
325
326     @classmethod
327     def parse_cpuinfo(cls, cpuinfo):
328         socket_map = {}
329
330         lines = cpuinfo.splitlines()
331
332         core_details = []
333         core_lines = {}
334         for line in lines:
335             if line.strip():
336                 name, value = line.split(":", 1)
337                 core_lines[name.strip()] = try_int(value.strip())
338             else:
339                 core_details.append(core_lines)
340                 core_lines = {}
341
342         for core in core_details:
343             socket_map.setdefault(core["physical id"], {}).setdefault(
344                 core["core id"], {})[core["processor"]] = (
345                 core["processor"], core["core id"], core["physical id"])
346
347         return cls(socket_map)
348
349     def sockets(self):
350         return sorted(self.keys())
351
352     def cores(self):
353         return sorted(core for cores in self.values() for core in cores)
354
355     def processors(self):
356         return sorted(
357             proc for cores in self.values() for procs in cores.values() for
358             proc in procs)
359
360
361 def config_to_dict(config):
362     return {section: dict(config.items(section)) for section in
363             config.sections()}
364
365
366 def validate_non_string_sequence(value, default=None, raise_exc=None):
367     # NOTE(ralonsoh): refactor this function to check if raise_exc is an
368     # Exception. Remove duplicate code, this function is duplicated in this
369     # repository.
370     if isinstance(value, collections.Sequence) and not isinstance(value, six.string_types):
371         return value
372     if raise_exc:
373         raise raise_exc  # pylint: disable=raising-bad-type
374     return default
375
376
377 def join_non_strings(separator, *non_strings):
378     try:
379         non_strings = validate_non_string_sequence(non_strings[0], raise_exc=RuntimeError)
380     except (IndexError, RuntimeError):
381         pass
382     return str(separator).join(str(non_string) for non_string in non_strings)
383
384
385 def safe_decode_utf8(s):
386     """Safe decode a str from UTF"""
387     if six.PY3 and isinstance(s, bytes):
388         return s.decode('utf-8', 'surrogateescape')
389     return s
390
391
392 class ErrorClass(object):
393
394     def __init__(self, *args, **kwargs):
395         if 'test' not in kwargs:
396             raise RuntimeError
397
398     def __getattr__(self, item):
399         raise AttributeError
400
401
402 class Timer(object):
403     def __init__(self):
404         super(Timer, self).__init__()
405         self.start = self.delta = None
406
407     def __enter__(self):
408         self.start = datetime.datetime.now()
409         return self
410
411     def __exit__(self, *_):
412         self.delta = datetime.datetime.now() - self.start
413
414     def __getattr__(self, item):
415         return getattr(self.delta, item)
416
417
418 def read_meminfo(ssh_client):
419     """Read "/proc/meminfo" file and parse all keys and values"""
420
421     cpuinfo = six.BytesIO()
422     ssh_client.get_file_obj('/proc/meminfo', cpuinfo)
423     lines = cpuinfo.getvalue().decode('utf-8')
424     matches = re.findall(r"([\w\(\)]+):\s+(\d+)( kB)*", lines)
425     output = {}
426     for match in matches:
427         output[match[0]] = match[1]
428
429     return output
430
431
432 def find_relative_file(path, task_path):
433     """
434     Find file in one of places: in abs of path or relative to a directory path,
435     in this order.
436
437     :param path:
438     :param task_path:
439     :return str: full path to file
440     """
441     # fixme: create schema to validate all fields have been provided
442     for lookup in [os.path.abspath(path), os.path.join(task_path, path)]:
443         try:
444             with open(lookup):
445                 return lookup
446         except IOError:
447             pass
448     raise IOError(errno.ENOENT, 'Unable to find {} file'.format(path))
449
450
451 def open_relative_file(path, task_path):
452     try:
453         return open(path)
454     except IOError as e:
455         if e.errno == errno.ENOENT:
456             return open(os.path.join(task_path, path))
457         raise