blob: bf4eab4cf7ac3b0d8ebf61f0cb380bbce4de8cab [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
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 Dmitriev25bcbc32017-09-13 23:14:37 +0300172 os.chmod(file_path, 0644)
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 Dmitriev99b26fe2017-04-26 12:34:44 +0300336def render_template(file_path, options=None):
dis2b2d8632016-12-08 17:56:57 +0200337 required_env_vars = set()
338 optional_env_vars = dict()
339 def os_env(var_name, default=None):
340 var = os.environ.get(var_name, default)
341
342 if var is None:
343 raise Exception("Environment variable '{0}' is undefined!".format(var_name))
344
345 if default is None:
346 required_env_vars.add(var_name)
347 else:
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300348 optional_env_vars[var_name] = var
dis2b2d8632016-12-08 17:56:57 +0200349
350 return var
351
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300352 if options is None:
353 options = {}
354 options.update({'os_env': os_env,})
355
dis2b2d8632016-12-08 17:56:57 +0200356 LOG.info("Reading template {0}".format(file_path))
357
358 path, filename = os.path.split(file_path)
359 environment = jinja2.Environment(
360 loader=jinja2.FileSystemLoader([path, os.path.dirname(path)], followlinks=True))
361 template = environment.get_template(filename).render(options)
362
363 if required_env_vars:
364 LOG.info("Required environment variables:")
365 for var in required_env_vars:
366 LOG.info(" {0}".format(var))
367 if optional_env_vars:
368 LOG.info("Optional environment variables:")
369 for var, default in sorted(optional_env_vars.iteritems()):
Dennis Dmitriev3af1f8b2017-05-26 23:28:17 +0300370 LOG.info(" {0} , value = {1}".format(var, default))
dis2b2d8632016-12-08 17:56:57 +0200371 return template
372
373
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200374def extract_name_from_mark(mark):
375 """Simple function to extract name from pytest mark
376
377 :param mark: pytest.mark.MarkInfo
378 :rtype: string or None
379 """
380 if mark:
381 if len(mark.args) > 0:
382 return mark.args[0]
383 elif 'name' in mark.kwargs:
384 return mark.kwargs['name']
385 return None
386
387
388def get_top_fixtures_marks(request, mark_name):
389 """Order marks according to fixtures order
390
391 When a test use fixtures that depend on each other in some order,
392 that fixtures can have the same pytest mark.
393
394 This method extracts such marks from fixtures that are used in the
395 current test and return the content of the marks ordered by the
396 fixture dependences.
397 If the test case have the same mark, than the content of this mark
398 will be the first element in the resulting list.
399
400 :param request: pytest 'request' fixture
401 :param mark_name: name of the mark to search on the fixtures and the test
402
403 :rtype list: marks content, from last to first executed.
404 """
405
406 fixtureinfo = request.session._fixturemanager.getfixtureinfo(
407 request.node, request.function, request.cls)
408
409 top_fixtures_names = []
410 for _ in enumerate(fixtureinfo.name2fixturedefs):
411 parent_fixtures = set()
412 child_fixtures = set()
413 for name in sorted(fixtureinfo.name2fixturedefs):
414 if name in top_fixtures_names:
415 continue
416 parent_fixtures.add(name)
417 child_fixtures.update(
418 fixtureinfo.name2fixturedefs[name][0].argnames)
419 top_fixtures_names.extend(list(parent_fixtures - child_fixtures))
420
421 top_fixtures_marks = []
422
423 if mark_name in request.function.func_dict:
424 # The top priority is the 'revert_snapshot' mark on the test
425 top_fixtures_marks.append(
426 extract_name_from_mark(
427 request.function.func_dict[mark_name]))
428
429 for top_fixtures_name in top_fixtures_names:
430 fd = fixtureinfo.name2fixturedefs[top_fixtures_name][0]
431 if mark_name in fd.func.func_dict:
432 fixture_mark = extract_name_from_mark(
433 fd.func.func_dict[mark_name])
434 # Append the snapshot names in the order that fixtures are called
435 # starting from the last called fixture to the first one
436 top_fixtures_marks.append(fixture_mark)
437
438 LOG.debug("Fixtures ordered from last to first called: {0}"
439 .format(top_fixtures_names))
440 LOG.debug("Marks ordered from most to least preffered: {0}"
441 .format(top_fixtures_marks))
442
443 return top_fixtures_marks