blob: a16a3a3f70274569ad6db16d9acb6246e7390154 [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:
255 remote = ssh_client.SSHClient(
256 host=self.host,
257 port=self.port,
258 username=self.username,
259 password=self.__password,
260 private_keys=self.__private_keys)
261
262 return remote.open(self.__file_path, mode=mode)
263 else:
264 return open(self.__file_path, mode=mode)
265
266 def get_content(self):
267 """Return a single document from YAML"""
268 def multi_constructor(loader, tag_suffix, node):
269 """Stores all unknown tags content into a dict
270
271 Original yaml:
272 !unknown_tag
273 - some content
274
275 Python object:
276 {"!unknown_tag": ["some content", ]}
277 """
278 if type(node.value) is list:
279 if type(node.value[0]) is tuple:
280 return {node.tag: loader.construct_mapping(node)}
281 else:
282 return {node.tag: loader.construct_sequence(node)}
283 else:
284 return {node.tag: loader.construct_scalar(node)}
285
286 yaml.add_multi_constructor("!", multi_constructor)
Dmitry Tyzhnenkoc56b77e2018-05-21 11:01:43 +0300287 with self.__get_file(mode="a+") as file_obj:
288 file_obj.seek(0)
289 self.__documents = [x for x in yaml.load_all(file_obj)] or [{}, ]
290 # try:
291 # self.__documents = [x for x in yaml.load_all(file_obj)]
292 # except IOError:
293 # self.__documents[self.__document_id] = {}
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300294 return self.__documents[self.__document_id]
295
296 def write_content(self, content=None):
297 if content:
298 self.content = content
299 self.__documents[self.__document_id] = self.content
300
301 def representer(dumper, data):
302 """Represents a dict key started with '!' as a YAML tag
303
304 Assumes that there is only one !tag in the dict at the
305 current indent.
306
307 Python object:
308 {"!unknown_tag": ["some content", ]}
309
310 Resulting yaml:
311 !unknown_tag
312 - some content
313 """
314 key = data.keys()[0]
315 if key.startswith("!"):
316 value = data[key]
317 if type(value) is dict:
318 node = dumper.represent_mapping(key, value)
319 elif type(value) is list:
320 node = dumper.represent_sequence(key, value)
321 else:
322 node = dumper.represent_scalar(key, value)
323 else:
324 node = dumper.represent_mapping(u'tag:yaml.org,2002:map', data)
325 return node
326
327 yaml.add_representer(dict, representer)
328 with self.__get_file("w") as file_obj:
329 yaml.dump_all(self.__documents, file_obj,
330 default_flow_style=self.default_flow_style,
331 default_style=self.default_style)
332
333 def __enter__(self):
334 self.__content = self.get_content()
335 self.__original_content = copy.deepcopy(self.content)
336 return self
337
338 def __exit__(self, x, y, z):
339 if self.content == self.__original_content:
340 return
341 self.write_content()
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200342
343
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200344def render_template(file_path, options=None, log_env_vars=True):
dis2b2d8632016-12-08 17:56:57 +0200345 required_env_vars = set()
346 optional_env_vars = dict()
Dina Belovae6fdffb2017-09-19 13:58:34 -0700347
dis2b2d8632016-12-08 17:56:57 +0200348 def os_env(var_name, default=None):
349 var = os.environ.get(var_name, default)
350
351 if var is None:
Dina Belovae6fdffb2017-09-19 13:58:34 -0700352 raise Exception("Environment variable '{0}' is undefined!"
353 .format(var_name))
dis2b2d8632016-12-08 17:56:57 +0200354
355 if default is None:
356 required_env_vars.add(var_name)
357 else:
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300358 optional_env_vars[var_name] = var
dis2b2d8632016-12-08 17:56:57 +0200359
360 return var
361
Dennis Dmitriev06979442018-10-24 18:53:49 +0300362 def basename(path):
363 return os.path.basename(path)
364
365 def dirname(path):
366 return os.path.dirname(path)
367
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300368 if options is None:
369 options = {}
Dina Belovae6fdffb2017-09-19 13:58:34 -0700370 options.update({'os_env': os_env, })
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300371
dis2b2d8632016-12-08 17:56:57 +0200372 LOG.info("Reading template {0}".format(file_path))
373
374 path, filename = os.path.split(file_path)
375 environment = jinja2.Environment(
Dina Belovae6fdffb2017-09-19 13:58:34 -0700376 loader=jinja2.FileSystemLoader([path, os.path.dirname(path)],
377 followlinks=True))
Dennis Dmitriev06979442018-10-24 18:53:49 +0300378 environment.filters['basename'] = basename
379 environment.filters['dirname'] = dirname
380
dis2b2d8632016-12-08 17:56:57 +0200381 template = environment.get_template(filename).render(options)
382
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200383 if required_env_vars and log_env_vars:
dis2b2d8632016-12-08 17:56:57 +0200384 LOG.info("Required environment variables:")
385 for var in required_env_vars:
386 LOG.info(" {0}".format(var))
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200387 if optional_env_vars and log_env_vars:
dis2b2d8632016-12-08 17:56:57 +0200388 LOG.info("Optional environment variables:")
389 for var, default in sorted(optional_env_vars.iteritems()):
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300390 LOG.info(" {0} , value = {1}".format(var, default))
dis2b2d8632016-12-08 17:56:57 +0200391 return template
392
393
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400394def extract_name_from_mark(mark, info='name'):
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200395 """Simple function to extract name from pytest mark
396
397 :param mark: pytest.mark.MarkInfo
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400398 :param info: Kwarg with information
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200399 :rtype: string or None
400 """
401 if mark:
402 if len(mark.args) > 0:
403 return mark.args[0]
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400404 elif info in mark.kwargs:
405 return mark.kwargs[info]
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200406 return None
407
408
409def get_top_fixtures_marks(request, mark_name):
410 """Order marks according to fixtures order
411
412 When a test use fixtures that depend on each other in some order,
413 that fixtures can have the same pytest mark.
414
415 This method extracts such marks from fixtures that are used in the
416 current test and return the content of the marks ordered by the
417 fixture dependences.
418 If the test case have the same mark, than the content of this mark
419 will be the first element in the resulting list.
420
421 :param request: pytest 'request' fixture
422 :param mark_name: name of the mark to search on the fixtures and the test
423
424 :rtype list: marks content, from last to first executed.
425 """
426
427 fixtureinfo = request.session._fixturemanager.getfixtureinfo(
428 request.node, request.function, request.cls)
429
430 top_fixtures_names = []
431 for _ in enumerate(fixtureinfo.name2fixturedefs):
432 parent_fixtures = set()
433 child_fixtures = set()
434 for name in sorted(fixtureinfo.name2fixturedefs):
435 if name in top_fixtures_names:
436 continue
437 parent_fixtures.add(name)
438 child_fixtures.update(
439 fixtureinfo.name2fixturedefs[name][0].argnames)
440 top_fixtures_names.extend(list(parent_fixtures - child_fixtures))
441
442 top_fixtures_marks = []
443
444 if mark_name in request.function.func_dict:
445 # The top priority is the 'revert_snapshot' mark on the test
446 top_fixtures_marks.append(
447 extract_name_from_mark(
448 request.function.func_dict[mark_name]))
449
450 for top_fixtures_name in top_fixtures_names:
451 fd = fixtureinfo.name2fixturedefs[top_fixtures_name][0]
452 if mark_name in fd.func.func_dict:
453 fixture_mark = extract_name_from_mark(
454 fd.func.func_dict[mark_name])
455 # Append the snapshot names in the order that fixtures are called
456 # starting from the last called fixture to the first one
457 top_fixtures_marks.append(fixture_mark)
458
459 LOG.debug("Fixtures ordered from last to first called: {0}"
460 .format(top_fixtures_names))
461 LOG.debug("Marks ordered from most to least preffered: {0}"
462 .format(top_fixtures_marks))
463
464 return top_fixtures_marks
Dmitry Tyzhnenkob610afd2018-02-19 15:43:45 +0200465
466
467class RunLimit(object):
468 def __init__(self, seconds=60, error_message='Timeout'):
469 self.seconds = seconds
470 self.error_message = error_message
471
472 def handle_timeout(self, signum, frame):
473 raise TimeoutException(self.error_message)
474
475 def __enter__(self):
476 signal.signal(signal.SIGALRM, self.handle_timeout)
477 signal.alarm(self.seconds)
478
479 def __exit__(self, exc_type, value, traceback):
480 signal.alarm(0)
481
482
483class TimeoutException(Exception):
484 pass