blob: f0424e631694b156319d45e376216345e96b4c4f [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):
181 assert tries_number >= 1, 'ERROR! @retry is called with no tries!'
182
183 def wrapper(*args, **kwargs):
184 iter_number = 1
185 while True:
186 try:
187 LOG.debug('Calling function "{0}" with args "{1}" and '
188 'kwargs "{2}". Try # {3}.'.format(func.__name__,
189 args,
190 kwargs,
191 iter_number))
192 return func(*args, **kwargs)
193 except exception as e:
194 if iter_number > tries_number:
195 LOG.debug('Failed to execute function "{0}" with {1} '
196 'tries!'.format(func.__name__, tries_number))
197 raise e
Vladimir Jigulin174aab12019-01-28 22:17:46 +0400198 else:
199 if interval > 0:
200 time.sleep(interval)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300201 iter_number += 1
202 return wrapper
203 return _retry
204
205
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300206class YamlEditor(object):
207 """Manipulations with local or remote .yaml files.
208
209 Usage:
210
211 with YamlEditor("tasks.yaml") as editor:
212 editor.content[key] = "value"
213
214 with YamlEditor("astute.yaml", ip=self.admin_ip) as editor:
215 editor.content[key] = "value"
216 """
217
218 def __init__(self, file_path, host=None, port=None,
219 username=None, password=None, private_keys=None,
220 document_id=0,
221 default_flow_style=False, default_style=None):
222 self.__file_path = file_path
223 self.host = host
224 self.port = port or 22
225 self.username = username
226 self.__password = password
227 self.__private_keys = private_keys or []
228 self.__content = None
229 self.__documents = [{}, ]
230 self.__document_id = document_id
231 self.__original_content = None
232 self.default_flow_style = default_flow_style
233 self.default_style = default_style
234
235 @property
236 def file_path(self):
237 """Open file path
238
239 :rtype: str
240 """
241 return self.__file_path
242
243 @property
244 def content(self):
245 if self.__content is None:
246 self.__content = self.get_content()
247 return self.__content
248
249 @content.setter
250 def content(self, new_content):
251 self.__content = new_content
252
253 def __get_file(self, mode="r"):
254 if self.host:
Dmitry Tyzhnenko80ce0202019-02-07 13:27:19 +0200255 keys = map(paramiko.RSAKey.from_private_key,
256 map(StringIO.StringIO, self.__private_keys))
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300257 remote = ssh_client.SSHClient(
258 host=self.host,
259 port=self.port,
260 username=self.username,
261 password=self.__password,
Dmitry Tyzhnenko80ce0202019-02-07 13:27:19 +0200262 private_keys=keys)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300263
264 return remote.open(self.__file_path, mode=mode)
265 else:
266 return open(self.__file_path, mode=mode)
267
268 def get_content(self):
269 """Return a single document from YAML"""
270 def multi_constructor(loader, tag_suffix, node):
271 """Stores all unknown tags content into a dict
272
273 Original yaml:
274 !unknown_tag
275 - some content
276
277 Python object:
278 {"!unknown_tag": ["some content", ]}
279 """
280 if type(node.value) is list:
281 if type(node.value[0]) is tuple:
282 return {node.tag: loader.construct_mapping(node)}
283 else:
284 return {node.tag: loader.construct_sequence(node)}
285 else:
286 return {node.tag: loader.construct_scalar(node)}
287
288 yaml.add_multi_constructor("!", multi_constructor)
Dmitry Tyzhnenkoc56b77e2018-05-21 11:01:43 +0300289 with self.__get_file(mode="a+") as file_obj:
290 file_obj.seek(0)
291 self.__documents = [x for x in yaml.load_all(file_obj)] or [{}, ]
292 # try:
293 # self.__documents = [x for x in yaml.load_all(file_obj)]
294 # except IOError:
295 # self.__documents[self.__document_id] = {}
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300296 return self.__documents[self.__document_id]
297
298 def write_content(self, content=None):
299 if content:
300 self.content = content
301 self.__documents[self.__document_id] = self.content
302
303 def representer(dumper, data):
304 """Represents a dict key started with '!' as a YAML tag
305
306 Assumes that there is only one !tag in the dict at the
307 current indent.
308
309 Python object:
310 {"!unknown_tag": ["some content", ]}
311
312 Resulting yaml:
313 !unknown_tag
314 - some content
315 """
316 key = data.keys()[0]
317 if key.startswith("!"):
318 value = data[key]
319 if type(value) is dict:
320 node = dumper.represent_mapping(key, value)
321 elif type(value) is list:
322 node = dumper.represent_sequence(key, value)
323 else:
324 node = dumper.represent_scalar(key, value)
325 else:
326 node = dumper.represent_mapping(u'tag:yaml.org,2002:map', data)
327 return node
328
329 yaml.add_representer(dict, representer)
330 with self.__get_file("w") as file_obj:
331 yaml.dump_all(self.__documents, file_obj,
332 default_flow_style=self.default_flow_style,
333 default_style=self.default_style)
334
335 def __enter__(self):
336 self.__content = self.get_content()
337 self.__original_content = copy.deepcopy(self.content)
338 return self
339
340 def __exit__(self, x, y, z):
341 if self.content == self.__original_content:
342 return
343 self.write_content()
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200344
345
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200346def render_template(file_path, options=None, log_env_vars=True):
dis2b2d8632016-12-08 17:56:57 +0200347 required_env_vars = set()
348 optional_env_vars = dict()
Dina Belovae6fdffb2017-09-19 13:58:34 -0700349
dis2b2d8632016-12-08 17:56:57 +0200350 def os_env(var_name, default=None):
351 var = os.environ.get(var_name, default)
352
353 if var is None:
Dina Belovae6fdffb2017-09-19 13:58:34 -0700354 raise Exception("Environment variable '{0}' is undefined!"
355 .format(var_name))
dis2b2d8632016-12-08 17:56:57 +0200356
357 if default is None:
358 required_env_vars.add(var_name)
359 else:
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300360 optional_env_vars[var_name] = var
dis2b2d8632016-12-08 17:56:57 +0200361
362 return var
363
Dennis Dmitriev06979442018-10-24 18:53:49 +0300364 def basename(path):
365 return os.path.basename(path)
366
367 def dirname(path):
368 return os.path.dirname(path)
369
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300370 if options is None:
371 options = {}
Dina Belovae6fdffb2017-09-19 13:58:34 -0700372 options.update({'os_env': os_env, })
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300373
dis2b2d8632016-12-08 17:56:57 +0200374 LOG.info("Reading template {0}".format(file_path))
375
376 path, filename = os.path.split(file_path)
377 environment = jinja2.Environment(
Dina Belovae6fdffb2017-09-19 13:58:34 -0700378 loader=jinja2.FileSystemLoader([path, os.path.dirname(path)],
379 followlinks=True))
Dennis Dmitriev06979442018-10-24 18:53:49 +0300380 environment.filters['basename'] = basename
381 environment.filters['dirname'] = dirname
382
dis2b2d8632016-12-08 17:56:57 +0200383 template = environment.get_template(filename).render(options)
384
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200385 if required_env_vars and log_env_vars:
dis2b2d8632016-12-08 17:56:57 +0200386 LOG.info("Required environment variables:")
387 for var in required_env_vars:
388 LOG.info(" {0}".format(var))
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200389 if optional_env_vars and log_env_vars:
dis2b2d8632016-12-08 17:56:57 +0200390 LOG.info("Optional environment variables:")
391 for var, default in sorted(optional_env_vars.iteritems()):
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300392 LOG.info(" {0} , value = {1}".format(var, default))
dis2b2d8632016-12-08 17:56:57 +0200393 return template
394
395
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400396def extract_name_from_mark(mark, info='name'):
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200397 """Simple function to extract name from pytest mark
398
399 :param mark: pytest.mark.MarkInfo
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400400 :param info: Kwarg with information
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200401 :rtype: string or None
402 """
403 if mark:
404 if len(mark.args) > 0:
405 return mark.args[0]
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400406 elif info in mark.kwargs:
407 return mark.kwargs[info]
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200408 return None
409
410
411def get_top_fixtures_marks(request, mark_name):
412 """Order marks according to fixtures order
413
414 When a test use fixtures that depend on each other in some order,
415 that fixtures can have the same pytest mark.
416
417 This method extracts such marks from fixtures that are used in the
418 current test and return the content of the marks ordered by the
419 fixture dependences.
420 If the test case have the same mark, than the content of this mark
421 will be the first element in the resulting list.
422
423 :param request: pytest 'request' fixture
424 :param mark_name: name of the mark to search on the fixtures and the test
425
426 :rtype list: marks content, from last to first executed.
427 """
428
429 fixtureinfo = request.session._fixturemanager.getfixtureinfo(
430 request.node, request.function, request.cls)
431
432 top_fixtures_names = []
433 for _ in enumerate(fixtureinfo.name2fixturedefs):
434 parent_fixtures = set()
435 child_fixtures = set()
436 for name in sorted(fixtureinfo.name2fixturedefs):
437 if name in top_fixtures_names:
438 continue
439 parent_fixtures.add(name)
440 child_fixtures.update(
441 fixtureinfo.name2fixturedefs[name][0].argnames)
442 top_fixtures_names.extend(list(parent_fixtures - child_fixtures))
443
444 top_fixtures_marks = []
445
446 if mark_name in request.function.func_dict:
447 # The top priority is the 'revert_snapshot' mark on the test
448 top_fixtures_marks.append(
449 extract_name_from_mark(
450 request.function.func_dict[mark_name]))
451
452 for top_fixtures_name in top_fixtures_names:
453 fd = fixtureinfo.name2fixturedefs[top_fixtures_name][0]
454 if mark_name in fd.func.func_dict:
455 fixture_mark = extract_name_from_mark(
456 fd.func.func_dict[mark_name])
457 # Append the snapshot names in the order that fixtures are called
458 # starting from the last called fixture to the first one
459 top_fixtures_marks.append(fixture_mark)
460
461 LOG.debug("Fixtures ordered from last to first called: {0}"
462 .format(top_fixtures_names))
463 LOG.debug("Marks ordered from most to least preffered: {0}"
464 .format(top_fixtures_marks))
465
466 return top_fixtures_marks
Dmitry Tyzhnenkob610afd2018-02-19 15:43:45 +0200467
468
469class RunLimit(object):
470 def __init__(self, seconds=60, error_message='Timeout'):
471 self.seconds = seconds
472 self.error_message = error_message
473
474 def handle_timeout(self, signum, frame):
475 raise TimeoutException(self.error_message)
476
477 def __enter__(self):
478 signal.signal(signal.SIGALRM, self.handle_timeout)
479 signal.alarm(self.seconds)
480
481 def __exit__(self, exc_type, value, traceback):
482 signal.alarm(0)
483
484
485class TimeoutException(Exception):
486 pass