blob: 46bf9c8d2080a67c398f5f52e270f1254279c41c [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)
284 with self.__get_file() as file_obj:
285 self.__documents = [x for x in yaml.load_all(file_obj)]
286 return self.__documents[self.__document_id]
287
288 def write_content(self, content=None):
289 if content:
290 self.content = content
291 self.__documents[self.__document_id] = self.content
292
293 def representer(dumper, data):
294 """Represents a dict key started with '!' as a YAML tag
295
296 Assumes that there is only one !tag in the dict at the
297 current indent.
298
299 Python object:
300 {"!unknown_tag": ["some content", ]}
301
302 Resulting yaml:
303 !unknown_tag
304 - some content
305 """
306 key = data.keys()[0]
307 if key.startswith("!"):
308 value = data[key]
309 if type(value) is dict:
310 node = dumper.represent_mapping(key, value)
311 elif type(value) is list:
312 node = dumper.represent_sequence(key, value)
313 else:
314 node = dumper.represent_scalar(key, value)
315 else:
316 node = dumper.represent_mapping(u'tag:yaml.org,2002:map', data)
317 return node
318
319 yaml.add_representer(dict, representer)
320 with self.__get_file("w") as file_obj:
321 yaml.dump_all(self.__documents, file_obj,
322 default_flow_style=self.default_flow_style,
323 default_style=self.default_style)
324
325 def __enter__(self):
326 self.__content = self.get_content()
327 self.__original_content = copy.deepcopy(self.content)
328 return self
329
330 def __exit__(self, x, y, z):
331 if self.content == self.__original_content:
332 return
333 self.write_content()
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200334
335
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200336def render_template(file_path, options=None, log_env_vars=True):
dis2b2d8632016-12-08 17:56:57 +0200337 required_env_vars = set()
338 optional_env_vars = dict()
Dina Belovae6fdffb2017-09-19 13:58:34 -0700339
dis2b2d8632016-12-08 17:56:57 +0200340 def os_env(var_name, default=None):
341 var = os.environ.get(var_name, default)
342
343 if var is None:
Dina Belovae6fdffb2017-09-19 13:58:34 -0700344 raise Exception("Environment variable '{0}' is undefined!"
345 .format(var_name))
dis2b2d8632016-12-08 17:56:57 +0200346
347 if default is None:
348 required_env_vars.add(var_name)
349 else:
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300350 optional_env_vars[var_name] = var
dis2b2d8632016-12-08 17:56:57 +0200351
352 return var
353
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300354 if options is None:
355 options = {}
Dina Belovae6fdffb2017-09-19 13:58:34 -0700356 options.update({'os_env': os_env, })
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300357
dis2b2d8632016-12-08 17:56:57 +0200358 LOG.info("Reading template {0}".format(file_path))
359
360 path, filename = os.path.split(file_path)
361 environment = jinja2.Environment(
Dina Belovae6fdffb2017-09-19 13:58:34 -0700362 loader=jinja2.FileSystemLoader([path, os.path.dirname(path)],
363 followlinks=True))
dis2b2d8632016-12-08 17:56:57 +0200364 template = environment.get_template(filename).render(options)
365
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200366 if required_env_vars and log_env_vars:
dis2b2d8632016-12-08 17:56:57 +0200367 LOG.info("Required environment variables:")
368 for var in required_env_vars:
369 LOG.info(" {0}".format(var))
Dennis Dmitrievc9b677d2017-11-21 16:42:35 +0200370 if optional_env_vars and log_env_vars:
dis2b2d8632016-12-08 17:56:57 +0200371 LOG.info("Optional environment variables:")
372 for var, default in sorted(optional_env_vars.iteritems()):
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300373 LOG.info(" {0} , value = {1}".format(var, default))
dis2b2d8632016-12-08 17:56:57 +0200374 return template
375
376
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400377def extract_name_from_mark(mark, info='name'):
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200378 """Simple function to extract name from pytest mark
379
380 :param mark: pytest.mark.MarkInfo
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400381 :param info: Kwarg with information
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200382 :rtype: string or None
383 """
384 if mark:
385 if len(mark.args) > 0:
386 return mark.args[0]
Victor Ryzhenkincf26c932018-03-29 20:08:21 +0400387 elif info in mark.kwargs:
388 return mark.kwargs[info]
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200389 return None
390
391
392def get_top_fixtures_marks(request, mark_name):
393 """Order marks according to fixtures order
394
395 When a test use fixtures that depend on each other in some order,
396 that fixtures can have the same pytest mark.
397
398 This method extracts such marks from fixtures that are used in the
399 current test and return the content of the marks ordered by the
400 fixture dependences.
401 If the test case have the same mark, than the content of this mark
402 will be the first element in the resulting list.
403
404 :param request: pytest 'request' fixture
405 :param mark_name: name of the mark to search on the fixtures and the test
406
407 :rtype list: marks content, from last to first executed.
408 """
409
410 fixtureinfo = request.session._fixturemanager.getfixtureinfo(
411 request.node, request.function, request.cls)
412
413 top_fixtures_names = []
414 for _ in enumerate(fixtureinfo.name2fixturedefs):
415 parent_fixtures = set()
416 child_fixtures = set()
417 for name in sorted(fixtureinfo.name2fixturedefs):
418 if name in top_fixtures_names:
419 continue
420 parent_fixtures.add(name)
421 child_fixtures.update(
422 fixtureinfo.name2fixturedefs[name][0].argnames)
423 top_fixtures_names.extend(list(parent_fixtures - child_fixtures))
424
425 top_fixtures_marks = []
426
427 if mark_name in request.function.func_dict:
428 # The top priority is the 'revert_snapshot' mark on the test
429 top_fixtures_marks.append(
430 extract_name_from_mark(
431 request.function.func_dict[mark_name]))
432
433 for top_fixtures_name in top_fixtures_names:
434 fd = fixtureinfo.name2fixturedefs[top_fixtures_name][0]
435 if mark_name in fd.func.func_dict:
436 fixture_mark = extract_name_from_mark(
437 fd.func.func_dict[mark_name])
438 # Append the snapshot names in the order that fixtures are called
439 # starting from the last called fixture to the first one
440 top_fixtures_marks.append(fixture_mark)
441
442 LOG.debug("Fixtures ordered from last to first called: {0}"
443 .format(top_fixtures_names))
444 LOG.debug("Marks ordered from most to least preffered: {0}"
445 .format(top_fixtures_marks))
446
447 return top_fixtures_marks
Dmitry Tyzhnenkob610afd2018-02-19 15:43:45 +0200448
449
450class RunLimit(object):
451 def __init__(self, seconds=60, error_message='Timeout'):
452 self.seconds = seconds
453 self.error_message = error_message
454
455 def handle_timeout(self, signum, frame):
456 raise TimeoutException(self.error_message)
457
458 def __enter__(self):
459 signal.signal(signal.SIGALRM, self.handle_timeout)
460 signal.alarm(self.seconds)
461
462 def __exit__(self, exc_type, value, traceback):
463 signal.alarm(0)
464
465
466class TimeoutException(Exception):
467 pass