blob: e0ef723a7b0680899383f72fa0d84fc9ef5d8133 [file] [log] [blame]
Dennis Dmitriev6f59add2016-10-18 13:45:27 +03001# Copyright 2016 Mirantis, Inc.
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
15import copy
16import os
17import shutil
Artem Panchenkodb0a97f2017-06-27 19:09:13 +030018import StringIO
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030019import time
20import traceback
Dmitry Tyzhnenkob610afd2018-02-19 15:43:45 +020021import signal
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030022
dis2b2d8632016-12-08 17:56:57 +020023import jinja2
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030024import paramiko
25import yaml
Anna Arhipovab8869ae2023-04-06 12:40:42 +020026import logging
27from multiprocessing import Process, BoundedSemaphore
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030028from devops.helpers import ssh_client
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030029
30from tcp_tests import logger
31from tcp_tests import settings
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030032
33LOG = logger.logger
34
35
36def get_test_method_name():
37 raise NotImplementedError
38
39
40def update_yaml(yaml_tree=None, yaml_value='', is_uniq=True,
41 yaml_file=settings.TIMESTAT_PATH_YAML, remote=None):
42 """Store/update a variable in YAML file.
43
44 yaml_tree - path to the variable in YAML file, will be created if absent,
45 yaml_value - value of the variable, will be overwritten if exists,
46 is_uniq - If false, add the unique two-digit suffix to the variable name.
47 """
48 def get_file(path, remote=None, mode="r"):
49 if remote:
50 return remote.open(path, mode)
51 else:
52 return open(path, mode)
53
54 if yaml_tree is None:
55 yaml_tree = []
56 with get_file(yaml_file, remote) as file_obj:
57 yaml_data = yaml.safe_load(file_obj)
58
59 # Walk through the 'yaml_data' dict, find or create a tree using
60 # sub-keys in order provided in 'yaml_tree' list
61 item = yaml_data
62 for n in yaml_tree[:-1]:
63 if n not in item:
64 item[n] = {}
65 item = item[n]
66
67 if is_uniq:
68 last = yaml_tree[-1]
69 else:
70 # Create an uniq suffix in range '_00' to '_99'
71 for n in range(100):
72 last = str(yaml_tree[-1]) + '_' + str(n).zfill(2)
73 if last not in item:
74 break
75
76 item[last] = yaml_value
77 with get_file(yaml_file, remote, mode='w') as file_obj:
78 yaml.dump(yaml_data, file_obj, default_flow_style=False)
79
80
81class TimeStat(object):
82 """Context manager for measuring the execution time of the code.
83
84 Usage:
85 with TimeStat([name],[is_uniq=True]):
86 """
87
88 def __init__(self, name=None, is_uniq=False):
89 if name:
90 self.name = name
91 else:
92 self.name = 'timestat'
93 self.is_uniq = is_uniq
94 self.begin_time = 0
95 self.end_time = 0
96 self.total_time = 0
97
98 def __enter__(self):
99 self.begin_time = time.time()
100 return self
101
102 def __exit__(self, exc_type, exc_value, exc_tb):
103 self.end_time = time.time()
104 self.total_time = self.end_time - self.begin_time
105
106 # Create a path where the 'self.total_time' will be stored.
107 yaml_path = []
108
109 # There will be a list of one or two yaml subkeys:
110 # - first key name is the method name of the test
111 method_name = get_test_method_name()
112 if method_name:
113 yaml_path.append(method_name)
114
115 # - second (subkey) name is provided from the decorator (the name of
116 # the just executed function), or manually.
117 yaml_path.append(self.name)
118
119 try:
120 update_yaml(yaml_path, '{:.2f}'.format(self.total_time),
121 self.is_uniq)
122 except Exception:
123 LOG.error("Error storing time statistic for {0}"
124 " {1}".format(yaml_path, traceback.format_exc()))
125 raise
126
127 @property
128 def spent_time(self):
129 return time.time() - self.begin_time
130
131
132def reduce_occurrences(items, text):
133 """ Return string without items(substrings)
134 Args:
135 items: iterable of strings
136 test: string
137 Returns:
138 string
139 Raise:
140 AssertionError if any substing not present in source text
141 """
142 for item in items:
143 LOG.debug(
144 "Verifying string {} is shown in "
145 "\"\"\"\n{}\n\"\"\"".format(item, text))
146 assert text.count(item) != 0
147 text = text.replace(item, "", 1)
148 return text
149
150
151def generate_keys():
Artem Panchenkodb0a97f2017-06-27 19:09:13 +0300152 file_obj = StringIO.StringIO()
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300153 key = paramiko.RSAKey.generate(1024)
Artem Panchenkodb0a97f2017-06-27 19:09:13 +0300154 key.write_private_key(file_obj)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300155 public = key.get_base64()
Artem Panchenkodb0a97f2017-06-27 19:09:13 +0300156 private = file_obj.getvalue()
157 file_obj.close()
158 return {'private': private,
159 'public': public}
160
161
162def load_keyfile(file_path):
163 with open(file_path, 'r') as private_key_file:
164 private = private_key_file.read()
165 key = paramiko.RSAKey(file_obj=StringIO.StringIO(private))
166 public = key.get_base64()
167 return {'private': private,
168 'public': public}
169
170
171def dump_keyfile(file_path, key):
172 key = paramiko.RSAKey(file_obj=StringIO.StringIO(key['private']))
173 key.write_private_key_file(file_path)
Dennis Dmitriev9b02c8b2017-11-13 15:31:35 +0200174 os.chmod(file_path, 0o644)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300175
176
177def clean_dir(dirpath):
178 shutil.rmtree(dirpath)
179
180
Vladimir Jigulin174aab12019-01-28 22:17:46 +0400181def retry(tries_number=2, exception=Exception, interval=0):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300182 def _retry(func):
Hanna Arhipova168fc022020-09-04 14:36:17 +0300183 if tries_number < 1:
184 LOG.warning('ERROR! @retry is called with no tries!')
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300185
186 def wrapper(*args, **kwargs):
187 iter_number = 1
188 while True:
189 try:
190 LOG.debug('Calling function "{0}" with args "{1}" and '
191 'kwargs "{2}". Try # {3}.'.format(func.__name__,
192 args,
193 kwargs,
194 iter_number))
195 return func(*args, **kwargs)
196 except exception as e:
197 if iter_number > tries_number:
198 LOG.debug('Failed to execute function "{0}" with {1} '
199 'tries!'.format(func.__name__, tries_number))
200 raise e
Vladimir Jigulin174aab12019-01-28 22:17:46 +0400201 else:
202 if interval > 0:
203 time.sleep(interval)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300204 iter_number += 1
205 return wrapper
206 return _retry
207
208
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300209class YamlEditor(object):
210 """Manipulations with local or remote .yaml files.
211
212 Usage:
213
214 with YamlEditor("tasks.yaml") as editor:
215 editor.content[key] = "value"
216
217 with YamlEditor("astute.yaml", ip=self.admin_ip) as editor:
218 editor.content[key] = "value"
219 """
220
221 def __init__(self, file_path, host=None, port=None,
222 username=None, password=None, private_keys=None,
223 document_id=0,
224 default_flow_style=False, default_style=None):
225 self.__file_path = file_path
226 self.host = host
227 self.port = port or 22
228 self.username = username
229 self.__password = password
230 self.__private_keys = private_keys or []
231 self.__content = None
232 self.__documents = [{}, ]
233 self.__document_id = document_id
234 self.__original_content = None
235 self.default_flow_style = default_flow_style
236 self.default_style = default_style
237
238 @property
239 def file_path(self):
240 """Open file path
241
242 :rtype: str
243 """
244 return self.__file_path
245
246 @property
247 def content(self):
248 if self.__content is None:
249 self.__content = self.get_content()
250 return self.__content
251
252 @content.setter
253 def content(self, new_content):
254 self.__content = new_content
255
256 def __get_file(self, mode="r"):
257 if self.host:
Dmitry Tyzhnenko80ce0202019-02-07 13:27:19 +0200258 keys = map(paramiko.RSAKey.from_private_key,
259 map(StringIO.StringIO, self.__private_keys))
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300260 remote = ssh_client.SSHClient(
261 host=self.host,
262 port=self.port,
263 username=self.username,
264 password=self.__password,
Dmitry Tyzhnenko80ce0202019-02-07 13:27:19 +0200265 private_keys=keys)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300266
267 return remote.open(self.__file_path, mode=mode)
268 else:
269 return open(self.__file_path, mode=mode)
270
271 def get_content(self):
272 """Return a single document from YAML"""
273 def multi_constructor(loader, tag_suffix, node):
274 """Stores all unknown tags content into a dict
275
276 Original yaml:
277 !unknown_tag
278 - some content
279
280 Python object:
281 {"!unknown_tag": ["some content", ]}
282 """
283 if type(node.value) is list:
284 if type(node.value[0]) is tuple:
285 return {node.tag: loader.construct_mapping(node)}
286 else:
287 return {node.tag: loader.construct_sequence(node)}
288 else:
289 return {node.tag: loader.construct_scalar(node)}
290
291 yaml.add_multi_constructor("!", multi_constructor)
Dmitry Tyzhnenkoc56b77e2018-05-21 11:01:43 +0300292 with self.__get_file(mode="a+") as file_obj:
293 file_obj.seek(0)
294 self.__documents = [x for x in yaml.load_all(file_obj)] or [{}, ]
295 # try:
296 # self.__documents = [x for x in yaml.load_all(file_obj)]
297 # except IOError:
298 # self.__documents[self.__document_id] = {}
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300299 return self.__documents[self.__document_id]
300
301 def write_content(self, content=None):
302 if content:
303 self.content = content
304 self.__documents[self.__document_id] = self.content
305
306 def representer(dumper, data):
307 """Represents a dict key started with '!' as a YAML tag
308
309 Assumes that there is only one !tag in the dict at the
310 current indent.
311
312 Python object:
313 {"!unknown_tag": ["some content", ]}
314
315 Resulting yaml:
316 !unknown_tag
317 - some content
318 """
319 key = data.keys()[0]
320 if key.startswith("!"):
321 value = data[key]
322 if type(value) is dict:
323 node = dumper.represent_mapping(key, value)
324 elif type(value) is list:
325 node = dumper.represent_sequence(key, value)
326 else:
327 node = dumper.represent_scalar(key, value)
328 else:
329 node = dumper.represent_mapping(u'tag:yaml.org,2002:map', data)
330 return node
331
332 yaml.add_representer(dict, representer)
333 with self.__get_file("w") as file_obj:
334 yaml.dump_all(self.__documents, file_obj,
335 default_flow_style=self.default_flow_style,
336 default_style=self.default_style)
337
338 def __enter__(self):
339 self.__content = self.get_content()
340 self.__original_content = copy.deepcopy(self.content)
341 return self
342
343 def __exit__(self, x, y, z):
344 if self.content == self.__original_content:
345 return
346 self.write_content()
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200347
348
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200349def render_template(file_path, options=None, log_env_vars=True):
dis2b2d8632016-12-08 17:56:57 +0200350 required_env_vars = set()
351 optional_env_vars = dict()
Dina Belovae6fdffb2017-09-19 13:58:34 -0700352
dis2b2d8632016-12-08 17:56:57 +0200353 def os_env(var_name, default=None):
354 var = os.environ.get(var_name, default)
355
356 if var is None:
Dina Belovae6fdffb2017-09-19 13:58:34 -0700357 raise Exception("Environment variable '{0}' is undefined!"
358 .format(var_name))
dis2b2d8632016-12-08 17:56:57 +0200359
360 if default is None:
361 required_env_vars.add(var_name)
362 else:
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300363 optional_env_vars[var_name] = var
dis2b2d8632016-12-08 17:56:57 +0200364
365 return var
366
Dennis Dmitriev06979442018-10-24 18:53:49 +0300367 def basename(path):
368 return os.path.basename(path)
369
370 def dirname(path):
371 return os.path.dirname(path)
372
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300373 if options is None:
374 options = {}
Dina Belovae6fdffb2017-09-19 13:58:34 -0700375 options.update({'os_env': os_env, })
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300376
dis2b2d8632016-12-08 17:56:57 +0200377 LOG.info("Reading template {0}".format(file_path))
378
379 path, filename = os.path.split(file_path)
380 environment = jinja2.Environment(
Dina Belovae6fdffb2017-09-19 13:58:34 -0700381 loader=jinja2.FileSystemLoader([path, os.path.dirname(path)],
382 followlinks=True))
Dennis Dmitriev06979442018-10-24 18:53:49 +0300383 environment.filters['basename'] = basename
384 environment.filters['dirname'] = dirname
385
dis2b2d8632016-12-08 17:56:57 +0200386 template = environment.get_template(filename).render(options)
387
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200388 if required_env_vars and log_env_vars:
dis2b2d8632016-12-08 17:56:57 +0200389 LOG.info("Required environment variables:")
390 for var in required_env_vars:
391 LOG.info(" {0}".format(var))
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200392 if optional_env_vars and log_env_vars:
dis2b2d8632016-12-08 17:56:57 +0200393 LOG.info("Optional environment variables:")
394 for var, default in sorted(optional_env_vars.iteritems()):
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300395 LOG.info(" {0} , value = {1}".format(var, default))
dis2b2d8632016-12-08 17:56:57 +0200396 return template
397
398
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400399def extract_name_from_mark(mark, info='name'):
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200400 """Simple function to extract name from pytest mark
401
402 :param mark: pytest.mark.MarkInfo
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400403 :param info: Kwarg with information
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200404 :rtype: string or None
405 """
406 if mark:
407 if len(mark.args) > 0:
408 return mark.args[0]
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400409 elif info in mark.kwargs:
410 return mark.kwargs[info]
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200411 return None
412
413
414def get_top_fixtures_marks(request, mark_name):
415 """Order marks according to fixtures order
416
417 When a test use fixtures that depend on each other in some order,
418 that fixtures can have the same pytest mark.
419
420 This method extracts such marks from fixtures that are used in the
421 current test and return the content of the marks ordered by the
422 fixture dependences.
423 If the test case have the same mark, than the content of this mark
424 will be the first element in the resulting list.
425
426 :param request: pytest 'request' fixture
427 :param mark_name: name of the mark to search on the fixtures and the test
428
429 :rtype list: marks content, from last to first executed.
430 """
431
432 fixtureinfo = request.session._fixturemanager.getfixtureinfo(
433 request.node, request.function, request.cls)
434
435 top_fixtures_names = []
436 for _ in enumerate(fixtureinfo.name2fixturedefs):
437 parent_fixtures = set()
438 child_fixtures = set()
439 for name in sorted(fixtureinfo.name2fixturedefs):
440 if name in top_fixtures_names:
441 continue
442 parent_fixtures.add(name)
443 child_fixtures.update(
444 fixtureinfo.name2fixturedefs[name][0].argnames)
445 top_fixtures_names.extend(list(parent_fixtures - child_fixtures))
446
447 top_fixtures_marks = []
448
449 if mark_name in request.function.func_dict:
450 # The top priority is the 'revert_snapshot' mark on the test
451 top_fixtures_marks.append(
452 extract_name_from_mark(
453 request.function.func_dict[mark_name]))
454
455 for top_fixtures_name in top_fixtures_names:
456 fd = fixtureinfo.name2fixturedefs[top_fixtures_name][0]
457 if mark_name in fd.func.func_dict:
458 fixture_mark = extract_name_from_mark(
459 fd.func.func_dict[mark_name])
460 # Append the snapshot names in the order that fixtures are called
461 # starting from the last called fixture to the first one
462 top_fixtures_marks.append(fixture_mark)
463
464 LOG.debug("Fixtures ordered from last to first called: {0}"
465 .format(top_fixtures_names))
466 LOG.debug("Marks ordered from most to least preffered: {0}"
467 .format(top_fixtures_marks))
468
469 return top_fixtures_marks
Dmitry Tyzhnenkob610afd2018-02-19 15:43:45 +0200470
471
472class RunLimit(object):
473 def __init__(self, seconds=60, error_message='Timeout'):
474 self.seconds = seconds
475 self.error_message = error_message
476
477 def handle_timeout(self, signum, frame):
478 raise TimeoutException(self.error_message)
479
480 def __enter__(self):
481 signal.signal(signal.SIGALRM, self.handle_timeout)
482 signal.alarm(self.seconds)
483
484 def __exit__(self, exc_type, value, traceback):
485 signal.alarm(0)
486
487
488class TimeoutException(Exception):
489 pass
Anna Arhipovab8869ae2023-04-06 12:40:42 +0200490
491
492pool = list()
493LOG_FORMAT = '%(asctime)s - %(levelname)s %(filename)s:%(lineno)d ' \
494 '/%(processName)s/ -- %(message)s'
495
496
497class Worker:
498 def __init__(self, limit=4, timeout=None):
499 """
500 limit of parallel thread to execute
501 timeout of waiting threads in seconds
502 """
503 LOG.debug("Created multithreading Worker limited by {} "
504 "threads".format(limit))
505 self._sema = BoundedSemaphore(limit)
506 self.timeout = timeout
507 pass
508
509 @property
510 def pool(self):
511 global pool
512 return pool
513
514 def _worker(self, func, args):
515 try:
516 # FIXME: logging doesn't work
517 memory_handler = logging.handlers.MemoryHandler(
518 50,
519 target=logger.console)
520 formatter = logging.Formatter(fmt=LOG_FORMAT)
521
522 LOG = logging.getLogger("{}{}".format(func, args))
523 LOG.setLevel(logging.DEBUG)
524 memory_handler.setFormatter(formatter)
525 LOG.addHandler(memory_handler)
526 # #######
527 func(*args)
528 # #######
529 memory_handler.close()
530 finally:
531 # allow a new process to be started now that this one is exiting
532 self._sema.release()
533
534 def start(self, func, args, name=None):
535 self._sema.acquire() # wait to start until another process is finished
536 p = Process(target=self._worker,
537 args=(func, args),
538 name=name
539 )
540 self.pool.append(p)
541 p.start()
542
543 def are_completed(self):
544 for t in self.pool:
545 LOG.info("Joining {}....".format(t))
546 t.join(timeout=self.timeout)
547 return all([not (task.is_alive()) for task in self.pool])
548
549 def clean_pool(self):
550 for i in range(self.pool.__len__()):
551 del self.pool[0]
552
553 def all_tasks_successfully_completed(self):
554 return all([task.exitcode == 0 for task in self.pool])
555
556 def print_failed_tasks(self):
557 return "\n".join([str(task)
558 for task in self.pool
559 if task.exitcode != 0])