blob: 46fc90dfa04d274e5a0ec69a1018cf02044a5721 [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
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030026from devops.helpers import ssh_client
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030027
28from tcp_tests import logger
29from tcp_tests import settings
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030030
31LOG = logger.logger
32
33
34def get_test_method_name():
35 raise NotImplementedError
36
37
38def update_yaml(yaml_tree=None, yaml_value='', is_uniq=True,
39 yaml_file=settings.TIMESTAT_PATH_YAML, remote=None):
40 """Store/update a variable in YAML file.
41
42 yaml_tree - path to the variable in YAML file, will be created if absent,
43 yaml_value - value of the variable, will be overwritten if exists,
44 is_uniq - If false, add the unique two-digit suffix to the variable name.
45 """
46 def get_file(path, remote=None, mode="r"):
47 if remote:
48 return remote.open(path, mode)
49 else:
50 return open(path, mode)
51
52 if yaml_tree is None:
53 yaml_tree = []
54 with get_file(yaml_file, remote) as file_obj:
55 yaml_data = yaml.safe_load(file_obj)
56
57 # Walk through the 'yaml_data' dict, find or create a tree using
58 # sub-keys in order provided in 'yaml_tree' list
59 item = yaml_data
60 for n in yaml_tree[:-1]:
61 if n not in item:
62 item[n] = {}
63 item = item[n]
64
65 if is_uniq:
66 last = yaml_tree[-1]
67 else:
68 # Create an uniq suffix in range '_00' to '_99'
69 for n in range(100):
70 last = str(yaml_tree[-1]) + '_' + str(n).zfill(2)
71 if last not in item:
72 break
73
74 item[last] = yaml_value
75 with get_file(yaml_file, remote, mode='w') as file_obj:
76 yaml.dump(yaml_data, file_obj, default_flow_style=False)
77
78
79class TimeStat(object):
80 """Context manager for measuring the execution time of the code.
81
82 Usage:
83 with TimeStat([name],[is_uniq=True]):
84 """
85
86 def __init__(self, name=None, is_uniq=False):
87 if name:
88 self.name = name
89 else:
90 self.name = 'timestat'
91 self.is_uniq = is_uniq
92 self.begin_time = 0
93 self.end_time = 0
94 self.total_time = 0
95
96 def __enter__(self):
97 self.begin_time = time.time()
98 return self
99
100 def __exit__(self, exc_type, exc_value, exc_tb):
101 self.end_time = time.time()
102 self.total_time = self.end_time - self.begin_time
103
104 # Create a path where the 'self.total_time' will be stored.
105 yaml_path = []
106
107 # There will be a list of one or two yaml subkeys:
108 # - first key name is the method name of the test
109 method_name = get_test_method_name()
110 if method_name:
111 yaml_path.append(method_name)
112
113 # - second (subkey) name is provided from the decorator (the name of
114 # the just executed function), or manually.
115 yaml_path.append(self.name)
116
117 try:
118 update_yaml(yaml_path, '{:.2f}'.format(self.total_time),
119 self.is_uniq)
120 except Exception:
121 LOG.error("Error storing time statistic for {0}"
122 " {1}".format(yaml_path, traceback.format_exc()))
123 raise
124
125 @property
126 def spent_time(self):
127 return time.time() - self.begin_time
128
129
130def reduce_occurrences(items, text):
131 """ Return string without items(substrings)
132 Args:
133 items: iterable of strings
134 test: string
135 Returns:
136 string
137 Raise:
138 AssertionError if any substing not present in source text
139 """
140 for item in items:
141 LOG.debug(
142 "Verifying string {} is shown in "
143 "\"\"\"\n{}\n\"\"\"".format(item, text))
144 assert text.count(item) != 0
145 text = text.replace(item, "", 1)
146 return text
147
148
149def generate_keys():
Artem Panchenkodb0a97f2017-06-27 19:09:13 +0300150 file_obj = StringIO.StringIO()
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300151 key = paramiko.RSAKey.generate(1024)
Artem Panchenkodb0a97f2017-06-27 19:09:13 +0300152 key.write_private_key(file_obj)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300153 public = key.get_base64()
Artem Panchenkodb0a97f2017-06-27 19:09:13 +0300154 private = file_obj.getvalue()
155 file_obj.close()
156 return {'private': private,
157 'public': public}
158
159
160def load_keyfile(file_path):
161 with open(file_path, 'r') as private_key_file:
162 private = private_key_file.read()
163 key = paramiko.RSAKey(file_obj=StringIO.StringIO(private))
164 public = key.get_base64()
165 return {'private': private,
166 'public': public}
167
168
169def dump_keyfile(file_path, key):
170 key = paramiko.RSAKey(file_obj=StringIO.StringIO(key['private']))
171 key.write_private_key_file(file_path)
Dennis Dmitriev9b02c8b2017-11-13 15:31:35 +0200172 os.chmod(file_path, 0o644)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300173
174
175def clean_dir(dirpath):
176 shutil.rmtree(dirpath)
177
178
Vladimir Jigulin174aab12019-01-28 22:17:46 +0400179def retry(tries_number=2, exception=Exception, interval=0):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300180 def _retry(func):
Hanna Arhipova168fc022020-09-04 14:36:17 +0300181 if tries_number < 1:
182 LOG.warning('ERROR! @retry is called with no tries!')
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300183
184 def wrapper(*args, **kwargs):
185 iter_number = 1
186 while True:
187 try:
188 LOG.debug('Calling function "{0}" with args "{1}" and '
189 'kwargs "{2}". Try # {3}.'.format(func.__name__,
190 args,
191 kwargs,
192 iter_number))
193 return func(*args, **kwargs)
194 except exception as e:
195 if iter_number > tries_number:
196 LOG.debug('Failed to execute function "{0}" with {1} '
197 'tries!'.format(func.__name__, tries_number))
198 raise e
Vladimir Jigulin174aab12019-01-28 22:17:46 +0400199 else:
200 if interval > 0:
201 time.sleep(interval)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300202 iter_number += 1
203 return wrapper
204 return _retry
205
206
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300207class YamlEditor(object):
208 """Manipulations with local or remote .yaml files.
209
210 Usage:
211
212 with YamlEditor("tasks.yaml") as editor:
213 editor.content[key] = "value"
214
215 with YamlEditor("astute.yaml", ip=self.admin_ip) as editor:
216 editor.content[key] = "value"
217 """
218
219 def __init__(self, file_path, host=None, port=None,
220 username=None, password=None, private_keys=None,
221 document_id=0,
222 default_flow_style=False, default_style=None):
223 self.__file_path = file_path
224 self.host = host
225 self.port = port or 22
226 self.username = username
227 self.__password = password
228 self.__private_keys = private_keys or []
229 self.__content = None
230 self.__documents = [{}, ]
231 self.__document_id = document_id
232 self.__original_content = None
233 self.default_flow_style = default_flow_style
234 self.default_style = default_style
235
236 @property
237 def file_path(self):
238 """Open file path
239
240 :rtype: str
241 """
242 return self.__file_path
243
244 @property
245 def content(self):
246 if self.__content is None:
247 self.__content = self.get_content()
248 return self.__content
249
250 @content.setter
251 def content(self, new_content):
252 self.__content = new_content
253
254 def __get_file(self, mode="r"):
255 if self.host:
Dmitry Tyzhnenko80ce0202019-02-07 13:27:19 +0200256 keys = map(paramiko.RSAKey.from_private_key,
257 map(StringIO.StringIO, self.__private_keys))
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300258 remote = ssh_client.SSHClient(
259 host=self.host,
260 port=self.port,
261 username=self.username,
262 password=self.__password,
Dmitry Tyzhnenko80ce0202019-02-07 13:27:19 +0200263 private_keys=keys)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300264
265 return remote.open(self.__file_path, mode=mode)
266 else:
267 return open(self.__file_path, mode=mode)
268
269 def get_content(self):
270 """Return a single document from YAML"""
271 def multi_constructor(loader, tag_suffix, node):
272 """Stores all unknown tags content into a dict
273
274 Original yaml:
275 !unknown_tag
276 - some content
277
278 Python object:
279 {"!unknown_tag": ["some content", ]}
280 """
281 if type(node.value) is list:
282 if type(node.value[0]) is tuple:
283 return {node.tag: loader.construct_mapping(node)}
284 else:
285 return {node.tag: loader.construct_sequence(node)}
286 else:
287 return {node.tag: loader.construct_scalar(node)}
288
289 yaml.add_multi_constructor("!", multi_constructor)
Dmitry Tyzhnenkoc56b77e2018-05-21 11:01:43 +0300290 with self.__get_file(mode="a+") as file_obj:
291 file_obj.seek(0)
292 self.__documents = [x for x in yaml.load_all(file_obj)] or [{}, ]
293 # try:
294 # self.__documents = [x for x in yaml.load_all(file_obj)]
295 # except IOError:
296 # self.__documents[self.__document_id] = {}
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300297 return self.__documents[self.__document_id]
298
299 def write_content(self, content=None):
300 if content:
301 self.content = content
302 self.__documents[self.__document_id] = self.content
303
304 def representer(dumper, data):
305 """Represents a dict key started with '!' as a YAML tag
306
307 Assumes that there is only one !tag in the dict at the
308 current indent.
309
310 Python object:
311 {"!unknown_tag": ["some content", ]}
312
313 Resulting yaml:
314 !unknown_tag
315 - some content
316 """
317 key = data.keys()[0]
318 if key.startswith("!"):
319 value = data[key]
320 if type(value) is dict:
321 node = dumper.represent_mapping(key, value)
322 elif type(value) is list:
323 node = dumper.represent_sequence(key, value)
324 else:
325 node = dumper.represent_scalar(key, value)
326 else:
327 node = dumper.represent_mapping(u'tag:yaml.org,2002:map', data)
328 return node
329
330 yaml.add_representer(dict, representer)
331 with self.__get_file("w") as file_obj:
332 yaml.dump_all(self.__documents, file_obj,
333 default_flow_style=self.default_flow_style,
334 default_style=self.default_style)
335
336 def __enter__(self):
337 self.__content = self.get_content()
338 self.__original_content = copy.deepcopy(self.content)
339 return self
340
341 def __exit__(self, x, y, z):
342 if self.content == self.__original_content:
343 return
344 self.write_content()
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200345
346
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200347def render_template(file_path, options=None, log_env_vars=True):
dis2b2d8632016-12-08 17:56:57 +0200348 required_env_vars = set()
349 optional_env_vars = dict()
Dina Belovae6fdffb2017-09-19 13:58:34 -0700350
dis2b2d8632016-12-08 17:56:57 +0200351 def os_env(var_name, default=None):
352 var = os.environ.get(var_name, default)
353
354 if var is None:
Dina Belovae6fdffb2017-09-19 13:58:34 -0700355 raise Exception("Environment variable '{0}' is undefined!"
356 .format(var_name))
dis2b2d8632016-12-08 17:56:57 +0200357
358 if default is None:
359 required_env_vars.add(var_name)
360 else:
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300361 optional_env_vars[var_name] = var
dis2b2d8632016-12-08 17:56:57 +0200362
363 return var
364
Dennis Dmitriev06979442018-10-24 18:53:49 +0300365 def basename(path):
366 return os.path.basename(path)
367
368 def dirname(path):
369 return os.path.dirname(path)
370
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300371 if options is None:
372 options = {}
Dina Belovae6fdffb2017-09-19 13:58:34 -0700373 options.update({'os_env': os_env, })
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300374
dis2b2d8632016-12-08 17:56:57 +0200375 LOG.info("Reading template {0}".format(file_path))
376
377 path, filename = os.path.split(file_path)
378 environment = jinja2.Environment(
Dina Belovae6fdffb2017-09-19 13:58:34 -0700379 loader=jinja2.FileSystemLoader([path, os.path.dirname(path)],
380 followlinks=True))
Dennis Dmitriev06979442018-10-24 18:53:49 +0300381 environment.filters['basename'] = basename
382 environment.filters['dirname'] = dirname
383
dis2b2d8632016-12-08 17:56:57 +0200384 template = environment.get_template(filename).render(options)
385
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200386 if required_env_vars and log_env_vars:
dis2b2d8632016-12-08 17:56:57 +0200387 LOG.info("Required environment variables:")
388 for var in required_env_vars:
389 LOG.info(" {0}".format(var))
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200390 if optional_env_vars and log_env_vars:
dis2b2d8632016-12-08 17:56:57 +0200391 LOG.info("Optional environment variables:")
392 for var, default in sorted(optional_env_vars.iteritems()):
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300393 LOG.info(" {0} , value = {1}".format(var, default))
dis2b2d8632016-12-08 17:56:57 +0200394 return template
395
396
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400397def extract_name_from_mark(mark, info='name'):
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200398 """Simple function to extract name from pytest mark
399
400 :param mark: pytest.mark.MarkInfo
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400401 :param info: Kwarg with information
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200402 :rtype: string or None
403 """
404 if mark:
405 if len(mark.args) > 0:
406 return mark.args[0]
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400407 elif info in mark.kwargs:
408 return mark.kwargs[info]
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200409 return None
410
411
412def get_top_fixtures_marks(request, mark_name):
413 """Order marks according to fixtures order
414
415 When a test use fixtures that depend on each other in some order,
416 that fixtures can have the same pytest mark.
417
418 This method extracts such marks from fixtures that are used in the
419 current test and return the content of the marks ordered by the
420 fixture dependences.
421 If the test case have the same mark, than the content of this mark
422 will be the first element in the resulting list.
423
424 :param request: pytest 'request' fixture
425 :param mark_name: name of the mark to search on the fixtures and the test
426
427 :rtype list: marks content, from last to first executed.
428 """
429
430 fixtureinfo = request.session._fixturemanager.getfixtureinfo(
431 request.node, request.function, request.cls)
432
433 top_fixtures_names = []
434 for _ in enumerate(fixtureinfo.name2fixturedefs):
435 parent_fixtures = set()
436 child_fixtures = set()
437 for name in sorted(fixtureinfo.name2fixturedefs):
438 if name in top_fixtures_names:
439 continue
440 parent_fixtures.add(name)
441 child_fixtures.update(
442 fixtureinfo.name2fixturedefs[name][0].argnames)
443 top_fixtures_names.extend(list(parent_fixtures - child_fixtures))
444
445 top_fixtures_marks = []
446
447 if mark_name in request.function.func_dict:
448 # The top priority is the 'revert_snapshot' mark on the test
449 top_fixtures_marks.append(
450 extract_name_from_mark(
451 request.function.func_dict[mark_name]))
452
453 for top_fixtures_name in top_fixtures_names:
454 fd = fixtureinfo.name2fixturedefs[top_fixtures_name][0]
455 if mark_name in fd.func.func_dict:
456 fixture_mark = extract_name_from_mark(
457 fd.func.func_dict[mark_name])
458 # Append the snapshot names in the order that fixtures are called
459 # starting from the last called fixture to the first one
460 top_fixtures_marks.append(fixture_mark)
461
462 LOG.debug("Fixtures ordered from last to first called: {0}"
463 .format(top_fixtures_names))
464 LOG.debug("Marks ordered from most to least preffered: {0}"
465 .format(top_fixtures_marks))
466
467 return top_fixtures_marks
Dmitry Tyzhnenkob610afd2018-02-19 15:43:45 +0200468
469
470class RunLimit(object):
471 def __init__(self, seconds=60, error_message='Timeout'):
472 self.seconds = seconds
473 self.error_message = error_message
474
475 def handle_timeout(self, signum, frame):
476 raise TimeoutException(self.error_message)
477
478 def __enter__(self):
479 signal.signal(signal.SIGALRM, self.handle_timeout)
480 signal.alarm(self.seconds)
481
482 def __exit__(self, exc_type, value, traceback):
483 signal.alarm(0)
484
485
486class TimeoutException(Exception):
487 pass