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