blob: 76641bd757a046f9fdd564e3b2736057eb69bc21 [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
21
dis2b2d8632016-12-08 17:56:57 +020022import jinja2
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030023import paramiko
24import yaml
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030025from devops.helpers import ssh_client
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030026
27from tcp_tests import logger
28from tcp_tests import settings
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030029
30LOG = logger.logger
31
32
33def get_test_method_name():
34 raise NotImplementedError
35
36
37def update_yaml(yaml_tree=None, yaml_value='', is_uniq=True,
38 yaml_file=settings.TIMESTAT_PATH_YAML, remote=None):
39 """Store/update a variable in YAML file.
40
41 yaml_tree - path to the variable in YAML file, will be created if absent,
42 yaml_value - value of the variable, will be overwritten if exists,
43 is_uniq - If false, add the unique two-digit suffix to the variable name.
44 """
45 def get_file(path, remote=None, mode="r"):
46 if remote:
47 return remote.open(path, mode)
48 else:
49 return open(path, mode)
50
51 if yaml_tree is None:
52 yaml_tree = []
53 with get_file(yaml_file, remote) as file_obj:
54 yaml_data = yaml.safe_load(file_obj)
55
56 # Walk through the 'yaml_data' dict, find or create a tree using
57 # sub-keys in order provided in 'yaml_tree' list
58 item = yaml_data
59 for n in yaml_tree[:-1]:
60 if n not in item:
61 item[n] = {}
62 item = item[n]
63
64 if is_uniq:
65 last = yaml_tree[-1]
66 else:
67 # Create an uniq suffix in range '_00' to '_99'
68 for n in range(100):
69 last = str(yaml_tree[-1]) + '_' + str(n).zfill(2)
70 if last not in item:
71 break
72
73 item[last] = yaml_value
74 with get_file(yaml_file, remote, mode='w') as file_obj:
75 yaml.dump(yaml_data, file_obj, default_flow_style=False)
76
77
78class TimeStat(object):
79 """Context manager for measuring the execution time of the code.
80
81 Usage:
82 with TimeStat([name],[is_uniq=True]):
83 """
84
85 def __init__(self, name=None, is_uniq=False):
86 if name:
87 self.name = name
88 else:
89 self.name = 'timestat'
90 self.is_uniq = is_uniq
91 self.begin_time = 0
92 self.end_time = 0
93 self.total_time = 0
94
95 def __enter__(self):
96 self.begin_time = time.time()
97 return self
98
99 def __exit__(self, exc_type, exc_value, exc_tb):
100 self.end_time = time.time()
101 self.total_time = self.end_time - self.begin_time
102
103 # Create a path where the 'self.total_time' will be stored.
104 yaml_path = []
105
106 # There will be a list of one or two yaml subkeys:
107 # - first key name is the method name of the test
108 method_name = get_test_method_name()
109 if method_name:
110 yaml_path.append(method_name)
111
112 # - second (subkey) name is provided from the decorator (the name of
113 # the just executed function), or manually.
114 yaml_path.append(self.name)
115
116 try:
117 update_yaml(yaml_path, '{:.2f}'.format(self.total_time),
118 self.is_uniq)
119 except Exception:
120 LOG.error("Error storing time statistic for {0}"
121 " {1}".format(yaml_path, traceback.format_exc()))
122 raise
123
124 @property
125 def spent_time(self):
126 return time.time() - self.begin_time
127
128
129def reduce_occurrences(items, text):
130 """ Return string without items(substrings)
131 Args:
132 items: iterable of strings
133 test: string
134 Returns:
135 string
136 Raise:
137 AssertionError if any substing not present in source text
138 """
139 for item in items:
140 LOG.debug(
141 "Verifying string {} is shown in "
142 "\"\"\"\n{}\n\"\"\"".format(item, text))
143 assert text.count(item) != 0
144 text = text.replace(item, "", 1)
145 return text
146
147
148def generate_keys():
Artem Panchenkodb0a97f2017-06-27 19:09:13 +0300149 file_obj = StringIO.StringIO()
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300150 key = paramiko.RSAKey.generate(1024)
Artem Panchenkodb0a97f2017-06-27 19:09:13 +0300151 key.write_private_key(file_obj)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300152 public = key.get_base64()
Artem Panchenkodb0a97f2017-06-27 19:09:13 +0300153 private = file_obj.getvalue()
154 file_obj.close()
155 return {'private': private,
156 'public': public}
157
158
159def load_keyfile(file_path):
160 with open(file_path, 'r') as private_key_file:
161 private = private_key_file.read()
162 key = paramiko.RSAKey(file_obj=StringIO.StringIO(private))
163 public = key.get_base64()
164 return {'private': private,
165 'public': public}
166
167
168def dump_keyfile(file_path, key):
169 key = paramiko.RSAKey(file_obj=StringIO.StringIO(key['private']))
170 key.write_private_key_file(file_path)
Dennis Dmitriev25bcbc32017-09-13 23:14:37 +0300171 os.chmod(file_path, 0644)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300172
173
174def clean_dir(dirpath):
175 shutil.rmtree(dirpath)
176
177
Tatyana Leontovich81128412017-04-05 18:46:29 +0300178def retry(tries_number=2, exception=Exception):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300179 def _retry(func):
180 assert tries_number >= 1, 'ERROR! @retry is called with no tries!'
181
182 def wrapper(*args, **kwargs):
183 iter_number = 1
184 while True:
185 try:
186 LOG.debug('Calling function "{0}" with args "{1}" and '
187 'kwargs "{2}". Try # {3}.'.format(func.__name__,
188 args,
189 kwargs,
190 iter_number))
191 return func(*args, **kwargs)
192 except exception as e:
193 if iter_number > tries_number:
194 LOG.debug('Failed to execute function "{0}" with {1} '
195 'tries!'.format(func.__name__, tries_number))
196 raise e
197 iter_number += 1
198 return wrapper
199 return _retry
200
201
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300202class YamlEditor(object):
203 """Manipulations with local or remote .yaml files.
204
205 Usage:
206
207 with YamlEditor("tasks.yaml") as editor:
208 editor.content[key] = "value"
209
210 with YamlEditor("astute.yaml", ip=self.admin_ip) as editor:
211 editor.content[key] = "value"
212 """
213
214 def __init__(self, file_path, host=None, port=None,
215 username=None, password=None, private_keys=None,
216 document_id=0,
217 default_flow_style=False, default_style=None):
218 self.__file_path = file_path
219 self.host = host
220 self.port = port or 22
221 self.username = username
222 self.__password = password
223 self.__private_keys = private_keys or []
224 self.__content = None
225 self.__documents = [{}, ]
226 self.__document_id = document_id
227 self.__original_content = None
228 self.default_flow_style = default_flow_style
229 self.default_style = default_style
230
231 @property
232 def file_path(self):
233 """Open file path
234
235 :rtype: str
236 """
237 return self.__file_path
238
239 @property
240 def content(self):
241 if self.__content is None:
242 self.__content = self.get_content()
243 return self.__content
244
245 @content.setter
246 def content(self, new_content):
247 self.__content = new_content
248
249 def __get_file(self, mode="r"):
250 if self.host:
251 remote = ssh_client.SSHClient(
252 host=self.host,
253 port=self.port,
254 username=self.username,
255 password=self.__password,
256 private_keys=self.__private_keys)
257
258 return remote.open(self.__file_path, mode=mode)
259 else:
260 return open(self.__file_path, mode=mode)
261
262 def get_content(self):
263 """Return a single document from YAML"""
264 def multi_constructor(loader, tag_suffix, node):
265 """Stores all unknown tags content into a dict
266
267 Original yaml:
268 !unknown_tag
269 - some content
270
271 Python object:
272 {"!unknown_tag": ["some content", ]}
273 """
274 if type(node.value) is list:
275 if type(node.value[0]) is tuple:
276 return {node.tag: loader.construct_mapping(node)}
277 else:
278 return {node.tag: loader.construct_sequence(node)}
279 else:
280 return {node.tag: loader.construct_scalar(node)}
281
282 yaml.add_multi_constructor("!", multi_constructor)
283 with self.__get_file() as file_obj:
284 self.__documents = [x for x in yaml.load_all(file_obj)]
285 return self.__documents[self.__document_id]
286
287 def write_content(self, content=None):
288 if content:
289 self.content = content
290 self.__documents[self.__document_id] = self.content
291
292 def representer(dumper, data):
293 """Represents a dict key started with '!' as a YAML tag
294
295 Assumes that there is only one !tag in the dict at the
296 current indent.
297
298 Python object:
299 {"!unknown_tag": ["some content", ]}
300
301 Resulting yaml:
302 !unknown_tag
303 - some content
304 """
305 key = data.keys()[0]
306 if key.startswith("!"):
307 value = data[key]
308 if type(value) is dict:
309 node = dumper.represent_mapping(key, value)
310 elif type(value) is list:
311 node = dumper.represent_sequence(key, value)
312 else:
313 node = dumper.represent_scalar(key, value)
314 else:
315 node = dumper.represent_mapping(u'tag:yaml.org,2002:map', data)
316 return node
317
318 yaml.add_representer(dict, representer)
319 with self.__get_file("w") as file_obj:
320 yaml.dump_all(self.__documents, file_obj,
321 default_flow_style=self.default_flow_style,
322 default_style=self.default_style)
323
324 def __enter__(self):
325 self.__content = self.get_content()
326 self.__original_content = copy.deepcopy(self.content)
327 return self
328
329 def __exit__(self, x, y, z):
330 if self.content == self.__original_content:
331 return
332 self.write_content()
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200333
334
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300335def render_template(file_path, options=None):
dis2b2d8632016-12-08 17:56:57 +0200336 required_env_vars = set()
337 optional_env_vars = dict()
Dina Belovae6fdffb2017-09-19 13:58:34 -0700338
dis2b2d8632016-12-08 17:56:57 +0200339 def os_env(var_name, default=None):
340 var = os.environ.get(var_name, default)
341
342 if var is None:
Dina Belovae6fdffb2017-09-19 13:58:34 -0700343 raise Exception("Environment variable '{0}' is undefined!"
344 .format(var_name))
dis2b2d8632016-12-08 17:56:57 +0200345
346 if default is None:
347 required_env_vars.add(var_name)
348 else:
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300349 optional_env_vars[var_name] = var
dis2b2d8632016-12-08 17:56:57 +0200350
351 return var
352
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300353 if options is None:
354 options = {}
Dina Belovae6fdffb2017-09-19 13:58:34 -0700355 options.update({'os_env': os_env, })
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300356
dis2b2d8632016-12-08 17:56:57 +0200357 LOG.info("Reading template {0}".format(file_path))
358
359 path, filename = os.path.split(file_path)
360 environment = jinja2.Environment(
Dina Belovae6fdffb2017-09-19 13:58:34 -0700361 loader=jinja2.FileSystemLoader([path, os.path.dirname(path)],
362 followlinks=True))
dis2b2d8632016-12-08 17:56:57 +0200363 template = environment.get_template(filename).render(options)
364
365 if required_env_vars:
366 LOG.info("Required environment variables:")
367 for var in required_env_vars:
368 LOG.info(" {0}".format(var))
369 if optional_env_vars:
370 LOG.info("Optional environment variables:")
371 for var, default in sorted(optional_env_vars.iteritems()):
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300372 LOG.info(" {0} , value = {1}".format(var, default))
dis2b2d8632016-12-08 17:56:57 +0200373 return template
374
375
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200376def extract_name_from_mark(mark):
377 """Simple function to extract name from pytest mark
378
379 :param mark: pytest.mark.MarkInfo
380 :rtype: string or None
381 """
382 if mark:
383 if len(mark.args) > 0:
384 return mark.args[0]
385 elif 'name' in mark.kwargs:
386 return mark.kwargs['name']
387 return None
388
389
390def get_top_fixtures_marks(request, mark_name):
391 """Order marks according to fixtures order
392
393 When a test use fixtures that depend on each other in some order,
394 that fixtures can have the same pytest mark.
395
396 This method extracts such marks from fixtures that are used in the
397 current test and return the content of the marks ordered by the
398 fixture dependences.
399 If the test case have the same mark, than the content of this mark
400 will be the first element in the resulting list.
401
402 :param request: pytest 'request' fixture
403 :param mark_name: name of the mark to search on the fixtures and the test
404
405 :rtype list: marks content, from last to first executed.
406 """
407
408 fixtureinfo = request.session._fixturemanager.getfixtureinfo(
409 request.node, request.function, request.cls)
410
411 top_fixtures_names = []
412 for _ in enumerate(fixtureinfo.name2fixturedefs):
413 parent_fixtures = set()
414 child_fixtures = set()
415 for name in sorted(fixtureinfo.name2fixturedefs):
416 if name in top_fixtures_names:
417 continue
418 parent_fixtures.add(name)
419 child_fixtures.update(
420 fixtureinfo.name2fixturedefs[name][0].argnames)
421 top_fixtures_names.extend(list(parent_fixtures - child_fixtures))
422
423 top_fixtures_marks = []
424
425 if mark_name in request.function.func_dict:
426 # The top priority is the 'revert_snapshot' mark on the test
427 top_fixtures_marks.append(
428 extract_name_from_mark(
429 request.function.func_dict[mark_name]))
430
431 for top_fixtures_name in top_fixtures_names:
432 fd = fixtureinfo.name2fixturedefs[top_fixtures_name][0]
433 if mark_name in fd.func.func_dict:
434 fixture_mark = extract_name_from_mark(
435 fd.func.func_dict[mark_name])
436 # Append the snapshot names in the order that fixtures are called
437 # starting from the last called fixture to the first one
438 top_fixtures_marks.append(fixture_mark)
439
440 LOG.debug("Fixtures ordered from last to first called: {0}"
441 .format(top_fixtures_names))
442 LOG.debug("Marks ordered from most to least preffered: {0}"
443 .format(top_fixtures_marks))
444
445 return top_fixtures_marks