Merge "Module to manage pip packages"
[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):
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):
91                 logger.exception('Unable to import module %s', module_name)
92
93
94 def makedirs(d):
95     try:
96         os.makedirs(d)
97     except OSError as e:
98         if e.errno != errno.EEXIST:
99             raise
100
101
102 def remove_file(path):
103     try:
104         os.remove(path)
105     except OSError as e:
106         if e.errno != errno.ENOENT:
107             raise
108
109
110 def execute_command(cmd, **kwargs):
111     exec_msg = "Executing command: '%s'" % cmd
112     logger.debug(exec_msg)
113
114     output = subprocess.check_output(cmd.split(), **kwargs)
115     return encodeutils.safe_decode(output, incoming='utf-8').split(os.linesep)
116
117
118 def source_env(env_file):
119     p = subprocess.Popen(". %s; env" % env_file, stdout=subprocess.PIPE,
120                          shell=True)
121     output = p.communicate()[0]
122     env = dict(line.split('=', 1) for line in output.splitlines() if '=' in line)
123     os.environ.update(env)
124     return env
125
126
127 def read_json_from_file(path):
128     with open(path, 'r') as f:
129         j = f.read()
130     # don't use jsonutils.load() it conflicts with already decoded input
131     return jsonutils.loads(j)
132
133
134 def write_json_to_file(path, data, mode='w'):
135     with open(path, mode) as f:
136         jsonutils.dump(data, f)
137
138
139 def write_file(path, data, mode='w'):
140     with open(path, mode) as f:
141         f.write(data)
142
143
144 def parse_ini_file(path):
145     parser = configparser.ConfigParser()
146
147     try:
148         files = parser.read(path)
149     except configparser.MissingSectionHeaderError:
150         logger.exception('invalid file type')
151         raise
152     else:
153         if not files:
154             raise RuntimeError('file not exist')
155
156     try:
157         default = {k: v for k, v in parser.items('DEFAULT')}
158     except configparser.NoSectionError:
159         default = {}
160
161     config = dict(DEFAULT=default,
162                   **{s: {k: v for k, v in parser.items(
163                       s)} for s in parser.sections()})
164
165     return config
166
167
168 def get_port_mac(sshclient, port):
169     cmd = "ifconfig |grep HWaddr |grep %s |awk '{print $5}' " % port
170     status, stdout, stderr = sshclient.execute(cmd)
171
172     if status:
173         raise RuntimeError(stderr)
174     return stdout.rstrip()
175
176
177 def get_port_ip(sshclient, port):
178     cmd = "ifconfig %s |grep 'inet addr' |awk '{print $2}' " \
179         "|cut -d ':' -f2 " % port
180     status, stdout, stderr = sshclient.execute(cmd)
181
182     if status:
183         raise RuntimeError(stderr)
184     return stdout.rstrip()
185
186
187 def flatten_dict_key(data):
188     next_data = {}
189
190     # use list, because iterable is too generic
191     if not any(isinstance(v, (collections.Mapping, list))
192                for v in data.values()):
193         return data
194
195     for k, v in data.items():
196         if isinstance(v, collections.Mapping):
197             for n_k, n_v in v.items():
198                 next_data["%s.%s" % (k, n_k)] = n_v
199         # use list because iterable is too generic
200         elif isinstance(v, collections.Iterable) and not isinstance(v, six.string_types):
201             for index, item in enumerate(v):
202                 next_data["%s%d" % (k, index)] = item
203         else:
204             next_data[k] = v
205
206     return flatten_dict_key(next_data)
207
208
209 def translate_to_str(obj):
210     if isinstance(obj, collections.Mapping):
211         return {str(k): translate_to_str(v) for k, v in obj.items()}
212     elif isinstance(obj, list):
213         return [translate_to_str(ele) for ele in obj]
214     elif isinstance(obj, six.text_type):
215         return str(obj)
216     return obj
217
218
219 def result_handler(status, data):
220     result = {
221         'status': status,
222         'result': data
223     }
224     return jsonify(result)
225
226
227 def change_obj_to_dict(obj):
228     dic = {}
229     for k, v in vars(obj).items():
230         try:
231             vars(v)
232         except TypeError:
233             dic.update({k: v})
234     return dic
235
236
237 def set_dict_value(dic, keys, value):
238     return_dic = dic
239
240     for key in keys.split('.'):
241         return_dic.setdefault(key, {})
242         if key == keys.split('.')[-1]:
243             return_dic[key] = value
244         else:
245             return_dic = return_dic[key]
246     return dic
247
248
249 def get_free_port(ip):
250     with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
251         port = random.randint(5000, 10000)
252         while s.connect_ex((ip, port)) == 0:
253             port = random.randint(5000, 10000)
254         return port
255
256
257 def mac_address_to_hex_list(mac):
258     octets = ["0x{:02x}".format(int(elem, 16)) for elem in mac.split(':')]
259     assert len(octets) == 6 and all(len(octet) == 4 for octet in octets)
260     return octets
261
262
263 def safe_ip_address(ip_addr):
264     """ get ip address version v6 or v4 """
265     try:
266         return ipaddress.ip_address(six.text_type(ip_addr))
267     except ValueError:
268         logging.error("%s is not valid", ip_addr)
269         return None
270
271
272 def get_ip_version(ip_addr):
273     """ get ip address version v6 or v4 """
274     try:
275         address = ipaddress.ip_address(six.text_type(ip_addr))
276     except ValueError:
277         logging.error("%s is not valid", ip_addr)
278         return None
279     else:
280         return address.version
281
282
283 def ip_to_hex(ip_addr, separator=''):
284     try:
285         address = ipaddress.ip_address(six.text_type(ip_addr))
286     except ValueError:
287         logging.error("%s is not valid", ip_addr)
288         return ip_addr
289
290     if address.version != 4:
291         return ip_addr
292
293     if not separator:
294         return '{:08x}'.format(int(address))
295
296     return separator.join('{:02x}'.format(octet) for octet in address.packed)
297
298
299 def try_int(s, *args):
300     """Convert to integer if possible."""
301     try:
302         return int(s)
303     except (TypeError, ValueError):
304         return args[0] if args else s
305
306
307 class SocketTopology(dict):
308
309     @classmethod
310     def parse_cpuinfo(cls, cpuinfo):
311         socket_map = {}
312
313         lines = cpuinfo.splitlines()
314
315         core_details = []
316         core_lines = {}
317         for line in lines:
318             if line.strip():
319                 name, value = line.split(":", 1)
320                 core_lines[name.strip()] = try_int(value.strip())
321             else:
322                 core_details.append(core_lines)
323                 core_lines = {}
324
325         for core in core_details:
326             socket_map.setdefault(core["physical id"], {}).setdefault(
327                 core["core id"], {})[core["processor"]] = (
328                 core["processor"], core["core id"], core["physical id"])
329
330         return cls(socket_map)
331
332     def sockets(self):
333         return sorted(self.keys())
334
335     def cores(self):
336         return sorted(core for cores in self.values() for core in cores)
337
338     def processors(self):
339         return sorted(
340             proc for cores in self.values() for procs in cores.values() for
341             proc in procs)
342
343
344 def config_to_dict(config):
345     return {section: dict(config.items(section)) for section in
346             config.sections()}
347
348
349 def validate_non_string_sequence(value, default=None, raise_exc=None):
350     # NOTE(ralonsoh): refactor this function to check if raise_exc is an
351     # Exception. Remove duplicate code, this function is duplicated in this
352     # repository.
353     if isinstance(value, collections.Sequence) and not isinstance(value, six.string_types):
354         return value
355     if raise_exc:
356         raise raise_exc  # pylint: disable=raising-bad-type
357     return default
358
359
360 def join_non_strings(separator, *non_strings):
361     try:
362         non_strings = validate_non_string_sequence(non_strings[0], raise_exc=RuntimeError)
363     except (IndexError, RuntimeError):
364         pass
365     return str(separator).join(str(non_string) for non_string in non_strings)
366
367
368 def safe_decode_utf8(s):
369     """Safe decode a str from UTF"""
370     if six.PY3 and isinstance(s, bytes):
371         return s.decode('utf-8', 'surrogateescape')
372     return s
373
374
375 class ErrorClass(object):
376
377     def __init__(self, *args, **kwargs):
378         if 'test' not in kwargs:
379             raise RuntimeError
380
381     def __getattr__(self, item):
382         raise AttributeError
383
384
385 class Timer(object):
386     def __init__(self):
387         super(Timer, self).__init__()
388         self.start = self.delta = None
389
390     def __enter__(self):
391         self.start = datetime.datetime.now()
392         return self
393
394     def __exit__(self, *_):
395         self.delta = datetime.datetime.now() - self.start
396
397     def __getattr__(self, item):
398         return getattr(self.delta, item)
399
400
401 def read_meminfo(ssh_client):
402     """Read "/proc/meminfo" file and parse all keys and values"""
403
404     cpuinfo = six.BytesIO()
405     ssh_client.get_file_obj('/proc/meminfo', cpuinfo)
406     lines = cpuinfo.getvalue().decode('utf-8')
407     matches = re.findall(r"([\w\(\)]+):\s+(\d+)( kB)*", lines)
408     output = {}
409     for match in matches:
410         output[match[0]] = match[1]
411
412     return output