Merge "Check for network already created k8"
[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 signal
27 import socket
28 import subprocess
29 import sys
30 import time
31 import threading
32
33 import six
34 from flask import jsonify
35 from six.moves import configparser
36 from oslo_serialization import jsonutils
37 from oslo_utils import encodeutils
38
39 import yardstick
40 from yardstick.common import exceptions
41
42
43 logger = logging.getLogger(__name__)
44 logger.setLevel(logging.DEBUG)
45
46
47 # Decorator for cli-args
48 def cliargs(*args, **kwargs):
49     def _decorator(func):
50         func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
51         return func
52     return _decorator
53
54
55 def itersubclasses(cls, _seen=None):
56     """Generator over all subclasses of a given class in depth first order."""
57
58     if not isinstance(cls, type):
59         raise TypeError("itersubclasses must be called with "
60                         "new-style classes, not %.100r" % cls)
61     _seen = _seen or set()
62     try:
63         subs = cls.__subclasses__()
64     except TypeError:   # fails only when cls is type
65         subs = cls.__subclasses__(cls)
66     for sub in subs:
67         if sub not in _seen:
68             _seen.add(sub)
69             yield sub
70             for sub in itersubclasses(sub, _seen):
71                 yield sub
72
73
74 def import_modules_from_package(package, raise_exception=False):
75     """Import modules given a package name
76
77     :param: package - Full package name. For example: rally.deploy.engines
78     """
79     yardstick_root = os.path.dirname(os.path.dirname(yardstick.__file__))
80     path = os.path.join(yardstick_root, *package.split('.'))
81     for root, _, files in os.walk(path):
82         matches = (filename for filename in files if filename.endswith('.py')
83                    and not filename.startswith('__'))
84         new_package = os.path.relpath(root, yardstick_root).replace(os.sep,
85                                                                     '.')
86         module_names = set(
87             '{}.{}'.format(new_package, filename.rsplit('.py', 1)[0])
88             for filename in matches)
89         # Find modules which haven't already been imported
90         missing_modules = module_names.difference(sys.modules)
91         logger.debug('Importing modules: %s', missing_modules)
92         for module_name in missing_modules:
93             try:
94                 importlib.import_module(module_name)
95             except (ImportError, SyntaxError) as exc:
96                 if raise_exception:
97                     raise exc
98                 logger.exception('Unable to import module %s', module_name)
99
100
101 NON_NONE_DEFAULT = object()
102
103
104 def get_key_with_default(data, key, default=NON_NONE_DEFAULT):
105     value = data.get(key, default)
106     if value is NON_NONE_DEFAULT:
107         raise KeyError(key)
108     return value
109
110
111 def make_dict_from_map(data, key_map):
112     return {dest_key: get_key_with_default(data, src_key, default)
113             for dest_key, (src_key, default) in key_map.items()}
114
115
116 def makedirs(d):
117     try:
118         os.makedirs(d)
119     except OSError as e:
120         if e.errno != errno.EEXIST:
121             raise
122
123
124 def remove_file(path):
125     try:
126         os.remove(path)
127     except OSError as e:
128         if e.errno != errno.ENOENT:
129             raise
130
131
132 def execute_command(cmd, **kwargs):
133     exec_msg = "Executing command: '%s'" % cmd
134     logger.debug(exec_msg)
135
136     output = subprocess.check_output(cmd.split(), **kwargs)
137     return encodeutils.safe_decode(output, incoming='utf-8').split(os.linesep)
138
139
140 def source_env(env_file):
141     p = subprocess.Popen(". %s; env" % env_file, stdout=subprocess.PIPE,
142                          shell=True)
143     output = p.communicate()[0]
144
145     # sometimes output type would be binary_type, and it don't have splitlines
146     # method, so we need to decode
147     if isinstance(output, six.binary_type):
148         output = encodeutils.safe_decode(output)
149     env = dict(line.split('=', 1) for line in output.splitlines() if '=' in line)
150     os.environ.update(env)
151     return env
152
153
154 def read_json_from_file(path):
155     with open(path, 'r') as f:
156         j = f.read()
157     # don't use jsonutils.load() it conflicts with already decoded input
158     return jsonutils.loads(j)
159
160
161 def write_json_to_file(path, data, mode='w'):
162     with open(path, mode) as f:
163         jsonutils.dump(data, f)
164
165
166 def write_file(path, data, mode='w'):
167     with open(path, mode) as f:
168         f.write(data)
169
170
171 def parse_ini_file(path):
172     parser = configparser.ConfigParser()
173
174     try:
175         files = parser.read(path)
176     except configparser.MissingSectionHeaderError:
177         logger.exception('invalid file type')
178         raise
179     else:
180         if not files:
181             raise RuntimeError('file not exist')
182
183     try:
184         default = {k: v for k, v in parser.items('DEFAULT')}
185     except configparser.NoSectionError:
186         default = {}
187
188     config = dict(DEFAULT=default,
189                   **{s: {k: v for k, v in parser.items(
190                       s)} for s in parser.sections()})
191
192     return config
193
194
195 def get_port_mac(sshclient, port):
196     cmd = "ifconfig |grep HWaddr |grep %s |awk '{print $5}' " % port
197     status, stdout, stderr = sshclient.execute(cmd)
198
199     if status:
200         raise RuntimeError(stderr)
201     return stdout.rstrip()
202
203
204 def get_port_ip(sshclient, port):
205     cmd = "ifconfig %s |grep 'inet addr' |awk '{print $2}' " \
206         "|cut -d ':' -f2 " % port
207     status, stdout, stderr = sshclient.execute(cmd)
208
209     if status:
210         raise RuntimeError(stderr)
211     return stdout.rstrip()
212
213
214 def flatten_dict_key(data):
215     next_data = {}
216
217     # use list, because iterable is too generic
218     if not any(isinstance(v, (collections.Mapping, list))
219                for v in data.values()):
220         return data
221
222     for k, v in data.items():
223         if isinstance(v, collections.Mapping):
224             for n_k, n_v in v.items():
225                 next_data["%s.%s" % (k, n_k)] = n_v
226         # use list because iterable is too generic
227         elif isinstance(v, collections.Iterable) and not isinstance(v, six.string_types):
228             for index, item in enumerate(v):
229                 next_data["%s%d" % (k, index)] = item
230         else:
231             next_data[k] = v
232
233     return flatten_dict_key(next_data)
234
235
236 def translate_to_str(obj):
237     if isinstance(obj, collections.Mapping):
238         return {str(k): translate_to_str(v) for k, v in obj.items()}
239     elif isinstance(obj, list):
240         return [translate_to_str(ele) for ele in obj]
241     elif isinstance(obj, six.text_type):
242         return str(obj)
243     return obj
244
245
246 def result_handler(status, data):
247     result = {
248         'status': status,
249         'result': data
250     }
251     return jsonify(result)
252
253
254 def change_obj_to_dict(obj):
255     dic = {}
256     for k, v in vars(obj).items():
257         try:
258             vars(v)
259         except TypeError:
260             dic.update({k: v})
261     return dic
262
263
264 def set_dict_value(dic, keys, value):
265     return_dic = dic
266
267     for key in keys.split('.'):
268         return_dic.setdefault(key, {})
269         if key == keys.split('.')[-1]:
270             return_dic[key] = value
271         else:
272             return_dic = return_dic[key]
273     return dic
274
275
276 def get_free_port(ip):
277     with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
278         port = random.randint(5000, 10000)
279         while s.connect_ex((ip, port)) == 0:
280             port = random.randint(5000, 10000)
281         return port
282
283
284 def mac_address_to_hex_list(mac):
285     try:
286         octets = ["0x{:02x}".format(int(elem, 16)) for elem in mac.split(':')]
287     except ValueError:
288         raise exceptions.InvalidMacAddress(mac_address=mac)
289     if len(octets) != 6 or all(len(octet) != 4 for octet in octets):
290         raise exceptions.InvalidMacAddress(mac_address=mac)
291     return octets
292
293
294 def safe_ip_address(ip_addr):
295     """ get ip address version v6 or v4 """
296     try:
297         return ipaddress.ip_address(six.text_type(ip_addr))
298     except ValueError:
299         logging.error("%s is not valid", ip_addr)
300         return None
301
302
303 def get_ip_version(ip_addr):
304     """ get ip address version v6 or v4 """
305     try:
306         address = ipaddress.ip_address(six.text_type(ip_addr))
307     except ValueError:
308         logging.error("%s is not valid", ip_addr)
309         return None
310     else:
311         return address.version
312
313
314 def make_ip_addr(ip, mask):
315     """
316     :param ip[str]: ip adddress
317     :param mask[str]: /24 prefix of 255.255.255.0 netmask
318     :return: IPv4Interface object
319     """
320     try:
321         return ipaddress.ip_interface(six.text_type('/'.join([ip, mask])))
322     except (TypeError, ValueError):
323         # None so we can skip later
324         return None
325
326
327 def ip_to_hex(ip_addr, separator=''):
328     try:
329         address = ipaddress.ip_address(six.text_type(ip_addr))
330     except ValueError:
331         logging.error("%s is not valid", ip_addr)
332         return ip_addr
333
334     if address.version != 4:
335         return ip_addr
336
337     if not separator:
338         return '{:08x}'.format(int(address))
339
340     return separator.join('{:02x}'.format(octet) for octet in address.packed)
341
342
343 def get_mask_from_ip_range(ip_low, ip_high):
344     _ip_low = ipaddress.ip_address(ip_low)
345     _ip_high = ipaddress.ip_address(ip_high)
346     _ip_low_int = int(_ip_low)
347     _ip_high_int = int(_ip_high)
348     return _ip_high.max_prefixlen - (_ip_high_int ^ _ip_low_int).bit_length()
349
350
351 def try_int(s, *args):
352     """Convert to integer if possible."""
353     try:
354         return int(s)
355     except (TypeError, ValueError):
356         return args[0] if args else s
357
358
359 class SocketTopology(dict):
360
361     @classmethod
362     def parse_cpuinfo(cls, cpuinfo):
363         socket_map = {}
364
365         lines = cpuinfo.splitlines()
366
367         core_details = []
368         core_lines = {}
369         for line in lines:
370             if line.strip():
371                 name, value = line.split(":", 1)
372                 core_lines[name.strip()] = try_int(value.strip())
373             else:
374                 core_details.append(core_lines)
375                 core_lines = {}
376
377         for core in core_details:
378             socket_map.setdefault(core["physical id"], {}).setdefault(
379                 core["core id"], {})[core["processor"]] = (
380                 core["processor"], core["core id"], core["physical id"])
381
382         return cls(socket_map)
383
384     def sockets(self):
385         return sorted(self.keys())
386
387     def cores(self):
388         return sorted(core for cores in self.values() for core in cores)
389
390     def processors(self):
391         return sorted(
392             proc for cores in self.values() for procs in cores.values() for
393             proc in procs)
394
395
396 def config_to_dict(config):
397     return {section: dict(config.items(section)) for section in
398             config.sections()}
399
400
401 def validate_non_string_sequence(value, default=None, raise_exc=None):
402     # NOTE(ralonsoh): refactor this function to check if raise_exc is an
403     # Exception. Remove duplicate code, this function is duplicated in this
404     # repository.
405     if isinstance(value, collections.Sequence) and not isinstance(value, six.string_types):
406         return value
407     if raise_exc:
408         raise raise_exc  # pylint: disable=raising-bad-type
409     return default
410
411
412 def join_non_strings(separator, *non_strings):
413     try:
414         non_strings = validate_non_string_sequence(non_strings[0], raise_exc=RuntimeError)
415     except (IndexError, RuntimeError):
416         pass
417     return str(separator).join(str(non_string) for non_string in non_strings)
418
419
420 def safe_decode_utf8(s):
421     """Safe decode a str from UTF"""
422     if six.PY3 and isinstance(s, bytes):
423         return s.decode('utf-8', 'surrogateescape')
424     return s
425
426
427 class ErrorClass(object):
428
429     def __init__(self, *args, **kwargs):
430         if 'test' not in kwargs:
431             raise RuntimeError
432
433     def __getattr__(self, item):
434         raise AttributeError
435
436
437 class Timer(object):
438     def __init__(self, timeout=None, raise_exception=True):
439         super(Timer, self).__init__()
440         self.start = self.delta = None
441         self._timeout = int(timeout) if timeout else None
442         self._timeout_flag = False
443         self._raise_exception = raise_exception
444
445     def _timeout_handler(self, *args):
446         self._timeout_flag = True
447         if self._raise_exception:
448             raise exceptions.TimerTimeout(timeout=self._timeout)
449         self.__exit__()
450
451     def __enter__(self):
452         self.start = datetime.datetime.now()
453         if self._timeout:
454             signal.signal(signal.SIGALRM, self._timeout_handler)
455             signal.alarm(self._timeout)
456         return self
457
458     def __exit__(self, *_):
459         if self._timeout:
460             signal.alarm(0)
461         self.delta = datetime.datetime.now() - self.start
462
463     def __getattr__(self, item):
464         return getattr(self.delta, item)
465
466     def __iter__(self):
467         self._raise_exception = False
468         return self.__enter__()
469
470     def next(self):  # pragma: no cover
471         # NOTE(ralonsoh): Python 2 support.
472         if not self._timeout_flag:
473             return datetime.datetime.now()
474         raise StopIteration()
475
476     def __next__(self):  # pragma: no cover
477         # NOTE(ralonsoh): Python 3 support.
478         return self.next()
479
480     def __del__(self):  # pragma: no cover
481         signal.alarm(0)
482
483     def delta_time_sec(self):
484         return (datetime.datetime.now() - self.start).total_seconds()
485
486
487 def read_meminfo(ssh_client):
488     """Read "/proc/meminfo" file and parse all keys and values"""
489
490     cpuinfo = six.BytesIO()
491     ssh_client.get_file_obj('/proc/meminfo', cpuinfo)
492     lines = cpuinfo.getvalue().decode('utf-8')
493     matches = re.findall(r"([\w\(\)]+):\s+(\d+)( kB)*", lines)
494     output = {}
495     for match in matches:
496         output[match[0]] = match[1]
497
498     return output
499
500
501 def find_relative_file(path, task_path):
502     """
503     Find file in one of places: in abs of path or relative to a directory path,
504     in this order.
505
506     :param path:
507     :param task_path:
508     :return str: full path to file
509     """
510     # fixme: create schema to validate all fields have been provided
511     for lookup in [os.path.abspath(path), os.path.join(task_path, path)]:
512         try:
513             with open(lookup):
514                 return lookup
515         except IOError:
516             pass
517     raise IOError(errno.ENOENT, 'Unable to find {} file'.format(path))
518
519
520 def open_relative_file(path, task_path):
521     try:
522         return open(path)
523     except IOError as e:
524         if e.errno == errno.ENOENT:
525             return open(os.path.join(task_path, path))
526         raise
527
528
529 def wait_until_true(predicate, timeout=60, sleep=1, exception=None):
530     """Wait until callable predicate is evaluated as True
531
532     When in a thread different from the main one, Timer(timeout) will fail
533     because signal is not handled. In this case
534
535     :param predicate: (func) callable deciding whether waiting should continue
536     :param timeout: (int) timeout in seconds how long should function wait
537     :param sleep: (int) polling interval for results in seconds
538     :param exception: exception instance to raise on timeout. If None is passed
539                       (default) then WaitTimeout exception is raised.
540     """
541     if isinstance(threading.current_thread(), threading._MainThread):
542         try:
543             with Timer(timeout=timeout):
544                 while not predicate():
545                     time.sleep(sleep)
546         except exceptions.TimerTimeout:
547             if exception and issubclass(exception, Exception):
548                 raise exception  # pylint: disable=raising-bad-type
549             raise exceptions.WaitTimeout
550     else:
551         with Timer() as timer:
552             while timer.delta_time_sec() < timeout:
553                 if predicate():
554                     return
555                 time.sleep(sleep)
556         if exception and issubclass(exception, Exception):
557             raise exception  # pylint: disable=raising-bad-type
558         raise exceptions.WaitTimeout
559
560
561 def send_socket_command(host, port, command):
562     """Send a string command to a specific port in a host
563
564     :param host: (str) ip or hostname of the host
565     :param port: (int) port number
566     :param command: (str) command to send
567     :return: 0 if success, error number if error
568     """
569     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
570     ret = 0
571     try:
572         err_number = sock.connect_ex((host, int(port)))
573         if err_number != 0:
574             return err_number
575         sock.sendall(six.b(command))
576     except Exception:  # pylint: disable=broad-except
577         ret = 1
578     finally:
579         sock.close()
580     return ret