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