blob: 480a646fa0b93968f0a218e6ef709f702cc69c40 [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
Tatyana Leontovich81128412017-04-05 18:46:29 +0300179def retry(tries_number=2, exception=Exception):
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
198 iter_number += 1
199 return wrapper
200 return _retry
201
202
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300203class YamlEditor(object):
204 """Manipulations with local or remote .yaml files.
205
206 Usage:
207
208 with YamlEditor("tasks.yaml") as editor:
209 editor.content[key] = "value"
210
211 with YamlEditor("astute.yaml", ip=self.admin_ip) as editor:
212 editor.content[key] = "value"
213 """
214
215 def __init__(self, file_path, host=None, port=None,
216 username=None, password=None, private_keys=None,
217 document_id=0,
218 default_flow_style=False, default_style=None):
219 self.__file_path = file_path
220 self.host = host
221 self.port = port or 22
222 self.username = username
223 self.__password = password
224 self.__private_keys = private_keys or []
225 self.__content = None
226 self.__documents = [{}, ]
227 self.__document_id = document_id
228 self.__original_content = None
229 self.default_flow_style = default_flow_style
230 self.default_style = default_style
231
232 @property
233 def file_path(self):
234 """Open file path
235
236 :rtype: str
237 """
238 return self.__file_path
239
240 @property
241 def content(self):
242 if self.__content is None:
243 self.__content = self.get_content()
244 return self.__content
245
246 @content.setter
247 def content(self, new_content):
248 self.__content = new_content
249
250 def __get_file(self, mode="r"):
251 if self.host:
252 remote = ssh_client.SSHClient(
253 host=self.host,
254 port=self.port,
255 username=self.username,
256 password=self.__password,
257 private_keys=self.__private_keys)
258
259 return remote.open(self.__file_path, mode=mode)
260 else:
261 return open(self.__file_path, mode=mode)
262
263 def get_content(self):
264 """Return a single document from YAML"""
265 def multi_constructor(loader, tag_suffix, node):
266 """Stores all unknown tags content into a dict
267
268 Original yaml:
269 !unknown_tag
270 - some content
271
272 Python object:
273 {"!unknown_tag": ["some content", ]}
274 """
275 if type(node.value) is list:
276 if type(node.value[0]) is tuple:
277 return {node.tag: loader.construct_mapping(node)}
278 else:
279 return {node.tag: loader.construct_sequence(node)}
280 else:
281 return {node.tag: loader.construct_scalar(node)}
282
283 yaml.add_multi_constructor("!", multi_constructor)
Dmitry Tyzhnenkoc56b77e2018-05-21 11:01:43 +0300284 with self.__get_file(mode="a+") as file_obj:
285 file_obj.seek(0)
286 self.__documents = [x for x in yaml.load_all(file_obj)] or [{}, ]
287 # try:
288 # self.__documents = [x for x in yaml.load_all(file_obj)]
289 # except IOError:
290 # self.__documents[self.__document_id] = {}
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300291 return self.__documents[self.__document_id]
292
293 def write_content(self, content=None):
294 if content:
295 self.content = content
296 self.__documents[self.__document_id] = self.content
297
298 def representer(dumper, data):
299 """Represents a dict key started with '!' as a YAML tag
300
301 Assumes that there is only one !tag in the dict at the
302 current indent.
303
304 Python object:
305 {"!unknown_tag": ["some content", ]}
306
307 Resulting yaml:
308 !unknown_tag
309 - some content
310 """
311 key = data.keys()[0]
312 if key.startswith("!"):
313 value = data[key]
314 if type(value) is dict:
315 node = dumper.represent_mapping(key, value)
316 elif type(value) is list:
317 node = dumper.represent_sequence(key, value)
318 else:
319 node = dumper.represent_scalar(key, value)
320 else:
321 node = dumper.represent_mapping(u'tag:yaml.org,2002:map', data)
322 return node
323
324 yaml.add_representer(dict, representer)
325 with self.__get_file("w") as file_obj:
326 yaml.dump_all(self.__documents, file_obj,
327 default_flow_style=self.default_flow_style,
328 default_style=self.default_style)
329
330 def __enter__(self):
331 self.__content = self.get_content()
332 self.__original_content = copy.deepcopy(self.content)
333 return self
334
335 def __exit__(self, x, y, z):
336 if self.content == self.__original_content:
337 return
338 self.write_content()
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200339
340
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200341def render_template(file_path, options=None, log_env_vars=True):
dis2b2d8632016-12-08 17:56:57 +0200342 required_env_vars = set()
343 optional_env_vars = dict()
Dina Belovae6fdffb2017-09-19 13:58:34 -0700344
dis2b2d8632016-12-08 17:56:57 +0200345 def os_env(var_name, default=None):
346 var = os.environ.get(var_name, default)
347
348 if var is None:
Dina Belovae6fdffb2017-09-19 13:58:34 -0700349 raise Exception("Environment variable '{0}' is undefined!"
350 .format(var_name))
dis2b2d8632016-12-08 17:56:57 +0200351
352 if default is None:
353 required_env_vars.add(var_name)
354 else:
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300355 optional_env_vars[var_name] = var
dis2b2d8632016-12-08 17:56:57 +0200356
357 return var
358
Dennis Dmitriev06979442018-10-24 18:53:49 +0300359 def basename(path):
360 return os.path.basename(path)
361
362 def dirname(path):
363 return os.path.dirname(path)
364
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300365 if options is None:
366 options = {}
Dina Belovae6fdffb2017-09-19 13:58:34 -0700367 options.update({'os_env': os_env, })
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300368
dis2b2d8632016-12-08 17:56:57 +0200369 LOG.info("Reading template {0}".format(file_path))
370
371 path, filename = os.path.split(file_path)
372 environment = jinja2.Environment(
Dina Belovae6fdffb2017-09-19 13:58:34 -0700373 loader=jinja2.FileSystemLoader([path, os.path.dirname(path)],
374 followlinks=True))
Dennis Dmitriev06979442018-10-24 18:53:49 +0300375 environment.filters['basename'] = basename
376 environment.filters['dirname'] = dirname
377
dis2b2d8632016-12-08 17:56:57 +0200378 template = environment.get_template(filename).render(options)
379
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200380 if required_env_vars and log_env_vars:
dis2b2d8632016-12-08 17:56:57 +0200381 LOG.info("Required environment variables:")
382 for var in required_env_vars:
383 LOG.info(" {0}".format(var))
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200384 if optional_env_vars and log_env_vars:
dis2b2d8632016-12-08 17:56:57 +0200385 LOG.info("Optional environment variables:")
386 for var, default in sorted(optional_env_vars.iteritems()):
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300387 LOG.info(" {0} , value = {1}".format(var, default))
dis2b2d8632016-12-08 17:56:57 +0200388 return template
389
390
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400391def extract_name_from_mark(mark, info='name'):
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200392 """Simple function to extract name from pytest mark
393
394 :param mark: pytest.mark.MarkInfo
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400395 :param info: Kwarg with information
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200396 :rtype: string or None
397 """
398 if mark:
399 if len(mark.args) > 0:
400 return mark.args[0]
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400401 elif info in mark.kwargs:
402 return mark.kwargs[info]
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200403 return None
404
405
406def get_top_fixtures_marks(request, mark_name):
407 """Order marks according to fixtures order
408
409 When a test use fixtures that depend on each other in some order,
410 that fixtures can have the same pytest mark.
411
412 This method extracts such marks from fixtures that are used in the
413 current test and return the content of the marks ordered by the
414 fixture dependences.
415 If the test case have the same mark, than the content of this mark
416 will be the first element in the resulting list.
417
418 :param request: pytest 'request' fixture
419 :param mark_name: name of the mark to search on the fixtures and the test
420
421 :rtype list: marks content, from last to first executed.
422 """
423
424 fixtureinfo = request.session._fixturemanager.getfixtureinfo(
425 request.node, request.function, request.cls)
426
427 top_fixtures_names = []
428 for _ in enumerate(fixtureinfo.name2fixturedefs):
429 parent_fixtures = set()
430 child_fixtures = set()
431 for name in sorted(fixtureinfo.name2fixturedefs):
432 if name in top_fixtures_names:
433 continue
434 parent_fixtures.add(name)
435 child_fixtures.update(
436 fixtureinfo.name2fixturedefs[name][0].argnames)
437 top_fixtures_names.extend(list(parent_fixtures - child_fixtures))
438
439 top_fixtures_marks = []
440
441 if mark_name in request.function.func_dict:
442 # The top priority is the 'revert_snapshot' mark on the test
443 top_fixtures_marks.append(
444 extract_name_from_mark(
445 request.function.func_dict[mark_name]))
446
447 for top_fixtures_name in top_fixtures_names:
448 fd = fixtureinfo.name2fixturedefs[top_fixtures_name][0]
449 if mark_name in fd.func.func_dict:
450 fixture_mark = extract_name_from_mark(
451 fd.func.func_dict[mark_name])
452 # Append the snapshot names in the order that fixtures are called
453 # starting from the last called fixture to the first one
454 top_fixtures_marks.append(fixture_mark)
455
456 LOG.debug("Fixtures ordered from last to first called: {0}"
457 .format(top_fixtures_names))
458 LOG.debug("Marks ordered from most to least preffered: {0}"
459 .format(top_fixtures_marks))
460
461 return top_fixtures_marks
Dmitry Tyzhnenkob610afd2018-02-19 15:43:45 +0200462
463
464class RunLimit(object):
465 def __init__(self, seconds=60, error_message='Timeout'):
466 self.seconds = seconds
467 self.error_message = error_message
468
469 def handle_timeout(self, signum, frame):
470 raise TimeoutException(self.error_message)
471
472 def __enter__(self):
473 signal.signal(signal.SIGALRM, self.handle_timeout)
474 signal.alarm(self.seconds)
475
476 def __exit__(self, exc_type, value, traceback):
477 signal.alarm(0)
478
479
480class TimeoutException(Exception):
481 pass