3 # Copyright (c) 2015 All rights reserved
4 # This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
8 # http://www.apache.org/licenses/LICENSE-2.0
11 """Rally testcases implementation."""
13 from __future__ import division
25 from ruamel.yaml import YAML
26 from six.moves import configparser
27 from xtesting.core import testcase
28 from xtesting.energy import energy
31 from functest.core import singlevm
32 from functest.opnfv_tests.openstack.tempest import conf_utils
33 from functest.utils import config
34 from functest.utils import env
36 LOGGER = logging.getLogger(__name__)
39 class RallyBase(singlevm.VmReady2):
40 """Base class form Rally testcases implementation."""
42 # pylint: disable=too-many-instance-attributes
43 TESTS = ['authenticate', 'glance', 'cinder', 'gnocchi', 'heat',
44 'keystone', 'neutron', 'nova', 'quotas']
46 RALLY_DIR = pkg_resources.resource_filename(
47 'functest', 'opnfv_tests/openstack/rally')
48 RALLY_SCENARIO_DIR = pkg_resources.resource_filename(
49 'functest', 'opnfv_tests/openstack/rally/scenario')
50 TEMPLATE_DIR = pkg_resources.resource_filename(
51 'functest', 'opnfv_tests/openstack/rally/scenario/templates')
52 SUPPORT_DIR = pkg_resources.resource_filename(
53 'functest', 'opnfv_tests/openstack/rally/scenario/support')
56 ITERATIONS_AMOUNT = 10
58 BLACKLIST_FILE = os.path.join(RALLY_DIR, "blacklist.txt")
59 TEMP_DIR = os.path.join(RALLY_DIR, "var")
64 def __init__(self, **kwargs):
65 """Initialize RallyBase object."""
66 super(RallyBase, self).__init__(**kwargs)
67 assert self.orig_cloud
69 if self.orig_cloud.get_role("admin"):
71 elif self.orig_cloud.get_role("Admin"):
74 raise Exception("Cannot detect neither admin nor Admin")
75 self.orig_cloud.grant_role(
76 role_name, user=self.project.user.id,
77 project=self.project.project.id,
78 domain=self.project.domain.id)
79 self.results_dir = os.path.join(
80 getattr(config.CONF, 'dir_results'), self.case_name)
81 self.task_file = os.path.join(self.RALLY_DIR, 'task.yaml')
84 self.scenario_dir = ''
87 self.start_time = None
91 self.flavor_alt = None
94 self.network_extensions = []
96 def _build_task_args(self, test_file_name):
97 """Build arguments for the Rally task."""
98 task_args = {'service_list': [test_file_name]}
99 task_args['image_name'] = str(self.image.name)
100 task_args['flavor_name'] = str(self.flavor.name)
101 task_args['flavor_alt_name'] = str(self.flavor_alt.name)
102 task_args['glance_image_location'] = str(self.filename)
103 task_args['glance_image_format'] = str(self.image_format)
104 task_args['tmpl_dir'] = str(self.TEMPLATE_DIR)
105 task_args['sup_dir'] = str(self.SUPPORT_DIR)
106 task_args['users_amount'] = self.USERS_AMOUNT
107 task_args['tenants_amount'] = self.TENANTS_AMOUNT
108 task_args['use_existing_users'] = False
109 task_args['iterations'] = self.ITERATIONS_AMOUNT
110 task_args['concurrency'] = self.CONCURRENCY
111 task_args['smoke'] = self.smoke
114 task_args['floating_network'] = str(self.ext_net.name)
116 task_args['floating_network'] = ''
119 task_args['netid'] = str(self.network.id)
121 task_args['netid'] = ''
125 def _prepare_test_list(self, test_name):
126 """Build the list of test cases to be executed."""
127 test_yaml_file_name = 'opnfv-{}.yaml'.format(test_name)
128 scenario_file_name = os.path.join(self.RALLY_SCENARIO_DIR,
131 if not os.path.exists(scenario_file_name):
132 scenario_file_name = os.path.join(self.scenario_dir,
135 if not os.path.exists(scenario_file_name):
136 raise Exception("The scenario '%s' does not exist."
137 % scenario_file_name)
139 LOGGER.debug('Scenario fetched from : %s', scenario_file_name)
140 test_file_name = os.path.join(self.TEMP_DIR, test_yaml_file_name)
142 if not os.path.exists(self.TEMP_DIR):
143 os.makedirs(self.TEMP_DIR)
145 self.apply_blacklist(scenario_file_name, test_file_name)
146 return test_file_name
149 def update_keystone_default_role(rally_conf='/etc/rally/rally.conf'):
150 """Set keystone_default_role in rally.conf"""
151 if env.get("NEW_USER_ROLE").lower() != "member":
152 rconfig = configparser.RawConfigParser()
153 rconfig.read(rally_conf)
154 if not rconfig.has_section('openstack'):
155 rconfig.add_section('openstack')
157 'openstack', 'keystone_default_role', env.get("NEW_USER_ROLE"))
158 with open(rally_conf, 'wb') as config_file:
159 rconfig.write(config_file)
162 def clean_rally_conf(rally_conf='/etc/rally/rally.conf'):
163 """Clean Rally config"""
164 if env.get("NEW_USER_ROLE").lower() != "member":
165 rconfig = configparser.RawConfigParser()
166 rconfig.read(rally_conf)
167 if rconfig.has_option('openstack', 'keystone_default_role'):
168 rconfig.remove_option('openstack', 'keystone_default_role')
169 with open(rally_conf, 'wb') as config_file:
170 rconfig.write(config_file)
173 def get_task_id(cmd_raw):
175 Get task id from command rally result.
178 :return: task_id as string
180 taskid_re = re.compile('^Task +(.*): started$')
181 for line in cmd_raw.splitlines(True):
183 match = taskid_re.match(line)
185 return match.group(1)
189 def task_succeed(json_raw):
191 Parse JSON from rally JSON results.
196 rally_report = json.loads(json_raw)
197 tasks = rally_report.get('tasks')
200 if task.get('status') != 'finished' or \
201 task.get('pass_sla') is not True:
207 def _migration_supported(self):
208 """Determine if migration is supported."""
209 if self.compute_cnt > 1:
213 def _network_trunk_supported(self):
214 """Determine if network trunk service is available"""
215 if 'trunk' in self.network_extensions:
221 """Exclude scenario."""
224 with open(RallyBase.BLACKLIST_FILE, 'r') as black_list_file:
225 black_list_yaml = yaml.safe_load(black_list_file)
227 deploy_scenario = env.get('DEPLOY_SCENARIO')
228 if (bool(deploy_scenario) and
229 'scenario' in black_list_yaml.keys()):
230 for item in black_list_yaml['scenario']:
231 scenarios = item['scenarios']
232 in_it = RallyBase.in_iterable_re
233 if in_it(deploy_scenario, scenarios):
234 tests = item['tests']
235 black_tests.extend(tests)
236 except Exception: # pylint: disable=broad-except
237 LOGGER.debug("Scenario exclusion not applied.")
242 def in_iterable_re(needle, haystack):
244 Check if given needle is in the iterable haystack, using regex.
246 :param needle: string to be matched
247 :param haystack: iterable of strings (optionally regex patterns)
248 :return: True if needle is eqial to any of the elements in haystack,
249 or if a nonempty regex pattern in haystack is found in needle.
251 # match without regex
252 if needle in haystack:
255 for pattern in haystack:
256 # match if regex pattern is set and found in the needle
257 if pattern and re.search(pattern, needle) is not None:
263 """Exclude functionalities."""
268 with open(RallyBase.BLACKLIST_FILE, 'r') as black_list_file:
269 black_list_yaml = yaml.safe_load(black_list_file)
271 if not self._migration_supported():
272 func_list.append("no_migration")
273 if not self._network_trunk_supported():
274 func_list.append("no_net_trunk_service")
276 if 'functionality' in black_list_yaml.keys():
277 for item in black_list_yaml['functionality']:
278 functions = item['functions']
279 for func in func_list:
280 if func in functions:
281 tests = item['tests']
282 black_tests.extend(tests)
283 except Exception: # pylint: disable=broad-except
284 LOGGER.debug("Functionality exclusion not applied.")
288 def apply_blacklist(self, case_file_name, result_file_name):
289 """Apply blacklist."""
290 LOGGER.debug("Applying blacklist...")
291 cases_file = open(case_file_name, 'r')
292 result_file = open(result_file_name, 'w')
294 black_tests = list(set(self.excl_func() +
295 self.excl_scenario()))
298 LOGGER.debug("Blacklisted tests: %s", str(black_tests))
301 for cases_line in cases_file:
303 for black_tests_line in black_tests:
304 if re.search(black_tests_line,
305 cases_line.strip().rstrip(':')):
309 result_file.write(str(cases_line))
311 if cases_line.isspace():
318 def file_is_empty(file_name):
319 """Determine is a file is empty."""
321 if os.stat(file_name).st_size > 0:
323 except Exception: # pylint: disable=broad-except
328 def _save_results(self, test_name, task_id):
329 """ Generate and save task execution results"""
330 # check for result directory and create it otherwise
331 if not os.path.exists(self.results_dir):
332 LOGGER.debug('%s does not exist, we create it.',
334 os.makedirs(self.results_dir)
336 # put detailed result to log
337 cmd = (["rally", "task", "detailed", "--uuid", task_id])
338 LOGGER.debug('running command: %s', cmd)
339 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
340 LOGGER.info("%s\n%s", " ".join(cmd), output)
342 # save report as JSON
343 report_json_name = '{}.json'.format(test_name)
344 report_json_dir = os.path.join(self.results_dir, report_json_name)
345 cmd = (["rally", "task", "report", "--json", "--uuid", task_id,
346 "--out", report_json_dir])
347 LOGGER.debug('running command: %s', cmd)
348 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
349 LOGGER.info("%s\n%s", " ".join(cmd), output)
351 # save report as HTML
352 report_html_name = '{}.html'.format(test_name)
353 report_html_dir = os.path.join(self.results_dir, report_html_name)
354 cmd = (["rally", "task", "report", "--html", "--uuid", task_id,
355 "--out", report_html_dir])
356 LOGGER.debug('running command: %s', cmd)
357 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
358 LOGGER.info("%s\n%s", " ".join(cmd), output)
360 json_results = open(report_json_dir).read()
361 self._append_summary(json_results, test_name)
363 # parse JSON operation result
364 if self.task_succeed(json_results):
365 LOGGER.info('Test scenario: "%s" OK.', test_name)
367 LOGGER.info('Test scenario: "%s" Failed.', test_name)
369 def run_task(self, test_name):
371 LOGGER.info('Starting test scenario "%s" ...', test_name)
372 LOGGER.debug('running command: %s', self.run_cmd)
373 proc = subprocess.Popen(self.run_cmd, stdout=subprocess.PIPE,
374 stderr=subprocess.STDOUT)
375 output = proc.communicate()[0]
377 task_id = self.get_task_id(output)
378 LOGGER.debug('task_id : %s', task_id)
380 LOGGER.error("Failed to retrieve task_id")
381 LOGGER.error("Result:\n%s", output)
382 raise Exception("Failed to retrieve task id")
383 self._save_results(test_name, task_id)
385 def _append_summary(self, json_raw, test_name):
386 """Update statistics summary info."""
389 overall_duration = 0.0
391 rally_report = json.loads(json_raw)
392 for task in rally_report.get('tasks'):
393 for subtask in task.get('subtasks'):
394 for workload in subtask.get('workloads'):
395 if workload.get('full_duration'):
396 overall_duration += workload.get('full_duration')
398 if workload.get('data'):
399 nb_tests += len(workload.get('data'))
401 for result in workload.get('data'):
402 if not result.get('error'):
405 scenario_summary = {'test_name': test_name,
406 'overall_duration': overall_duration,
407 'nb_tests': nb_tests,
408 'nb_success': nb_success,
409 'task_status': self.task_succeed(json_raw)}
410 self.summary.append(scenario_summary)
412 def prepare_run(self):
413 """Prepare resources needed by test scenarios."""
415 LOGGER.debug('Validating the test name...')
416 if self.test_name == 'all':
417 self.tests = self.TESTS
418 elif self.test_name in self.TESTS:
419 self.tests = [self.test_name]
421 raise Exception("Test name '%s' is invalid" % self.test_name)
423 if not os.path.exists(self.task_file):
424 LOGGER.error("Task file '%s' does not exist.", self.task_file)
425 raise Exception("Task file '{}' does not exist.".
426 format(self.task_file))
428 self.update_keystone_default_role()
429 self.compute_cnt = len(self.cloud.list_hypervisors())
430 self.network_extensions = self.cloud.get_network_extensions()
431 self.flavor_alt = self.create_flavor_alt()
432 LOGGER.debug("flavor: %s", self.flavor_alt)
434 def prepare_task(self, test_name):
435 """Prepare resources for test run."""
436 file_name = self._prepare_test_list(test_name)
437 if self.file_is_empty(file_name):
438 LOGGER.info('No tests for scenario "%s"', test_name)
440 self.run_cmd = (["rally", "task", "start", "--abort-on-sla-failure",
441 "--task", self.task_file, "--task-args",
442 str(self._build_task_args(test_name))])
447 for test in self.tests:
448 if self.prepare_task(test):
451 def _generate_report(self):
452 """Generate test execution summary report."""
459 res_table = prettytable.PrettyTable(
461 field_names=['Module', 'Duration', 'nb. Test Run', 'Success'])
462 res_table.align['Module'] = "l"
463 res_table.align['Duration'] = "r"
464 res_table.align['Success'] = "r"
466 # for each scenario we draw a row for the table
467 for item in self.summary:
468 if item['task_status'] is True:
470 total_duration += item['overall_duration']
471 total_nb_tests += item['nb_tests']
472 total_nb_success += item['nb_success']
474 success_avg = 100 * item['nb_success'] / item['nb_tests']
475 except ZeroDivisionError:
477 success_str = str("{:0.2f}".format(success_avg)) + '%'
478 duration_str = time.strftime("%H:%M:%S",
479 time.gmtime(item['overall_duration']))
480 res_table.add_row([item['test_name'], duration_str,
481 item['nb_tests'], success_str])
482 payload.append({'module': item['test_name'],
483 'details': {'duration': item['overall_duration'],
484 'nb tests': item['nb_tests'],
485 'success': success_str}})
487 total_duration_str = time.strftime("%H:%M:%S",
488 time.gmtime(total_duration))
490 self.result = 100 * total_nb_success / total_nb_tests
491 except ZeroDivisionError:
493 success_rate = "{:0.2f}".format(self.result)
494 success_rate_str = str(success_rate) + '%'
495 res_table.add_row(["", "", "", ""])
496 res_table.add_row(["TOTAL:", total_duration_str, total_nb_tests,
499 LOGGER.info("Rally Summary Report:\n\n%s\n", res_table.get_string())
500 LOGGER.info("Rally '%s' success_rate is %s%% in %s/%s modules",
501 self.case_name, success_rate, nb_modules,
503 payload.append({'summary': {'duration': total_duration,
504 'nb tests': total_nb_tests,
505 'nb success': success_rate}})
506 self.details = payload
509 """Cleanup of OpenStack resources. Should be called on completion."""
510 self.clean_rally_conf()
512 self.orig_cloud.delete_flavor(self.flavor_alt.id)
513 super(RallyBase, self).clean()
515 def is_successful(self):
516 """The overall result of the test."""
517 for item in self.summary:
518 if item['task_status'] is False:
519 return testcase.TestCase.EX_TESTCASE_FAILED
521 return super(RallyBase, self).is_successful()
523 @energy.enable_recording
524 def run(self, **kwargs):
526 self.start_time = time.time()
528 assert super(RallyBase, self).run(
529 **kwargs) == testcase.TestCase.EX_OK
532 OS_USERNAME=self.project.user.name,
533 OS_PROJECT_NAME=self.project.project.name,
534 OS_PROJECT_ID=self.project.project.id,
535 OS_PASSWORD=self.project.password)
536 conf_utils.create_rally_deployment(environ=environ)
539 self._generate_report()
540 res = testcase.TestCase.EX_OK
541 except Exception as exc: # pylint: disable=broad-except
542 LOGGER.error('Error with run: %s', exc)
544 res = testcase.TestCase.EX_RUN_ERROR
545 self.stop_time = time.time()
549 class RallySanity(RallyBase):
550 """Rally sanity testcase implementation."""
552 def __init__(self, **kwargs):
553 """Initialize RallySanity object."""
554 if "case_name" not in kwargs:
555 kwargs["case_name"] = "rally_sanity"
556 super(RallySanity, self).__init__(**kwargs)
557 self.test_name = 'all'
559 self.scenario_dir = os.path.join(self.RALLY_SCENARIO_DIR, 'sanity')
562 class RallyFull(RallyBase):
563 """Rally full testcase implementation."""
565 def __init__(self, **kwargs):
566 """Initialize RallyFull object."""
567 if "case_name" not in kwargs:
568 kwargs["case_name"] = "rally_full"
569 super(RallyFull, self).__init__(**kwargs)
570 self.test_name = 'all'
572 self.scenario_dir = os.path.join(self.RALLY_SCENARIO_DIR, 'full')
575 class RallyJobs(RallyBase):
576 """Rally OpenStack CI testcase implementation."""
580 def __init__(self, **kwargs):
581 """Initialize RallyJobs object."""
582 if "case_name" not in kwargs:
583 kwargs["case_name"] = "rally_jobs"
584 super(RallyJobs, self).__init__(**kwargs)
585 self.test_name = 'all'
586 self.task_file = os.path.join(self.RALLY_DIR, 'rally_jobs.yaml')
587 self.task_yaml = None
589 def prepare_run(self):
590 """Create resources needed by test scenarios."""
591 super(RallyJobs, self).prepare_run()
592 with open(os.path.join(self.RALLY_DIR,
593 'rally_jobs.yaml'), 'r') as task_file:
594 self.task_yaml = yaml.safe_load(task_file)
596 if not all(task in self.task_yaml for task in self.tests):
597 raise Exception("Test '%s' not in '%s'" %
598 (self.test_name, self.tests))
600 def apply_blacklist(self, case_file_name, result_file_name):
601 # pylint: disable=too-many-branches
602 """Apply blacklist."""
603 LOGGER.debug("Applying blacklist...")
604 black_tests = list(set(self.excl_func() +
605 self.excl_scenario()))
607 LOGGER.debug("Blacklisted tests: %s", str(black_tests))
609 template = YAML(typ='jinja2')
610 with open(case_file_name, 'r') as fname:
611 cases = template.load(fname)
612 if cases.get("version", 1) == 1:
613 # scenarios in dictionary
614 for name in cases.keys():
615 if self.in_iterable_re(name, black_tests):
618 # workloads in subtasks
619 for sind, subtask in enumerate(cases.get('subtasks', [])):
621 for wind, workload in enumerate(subtask.get('workloads', [])):
622 scenario = workload.get('scenario', {})
623 for name in scenario.keys():
624 if self.in_iterable_re(name, black_tests):
627 for wind in reversed(idx):
628 cases['subtasks'][sind]['workloads'].pop(wind)
629 # scenarios in subtasks
631 for sind, subtask in enumerate(cases.get('subtasks', [])):
632 scenario = subtask.get('scenario', {})
633 for name in scenario.keys():
634 if self.in_iterable_re(name, black_tests):
637 for sind in reversed(idx):
638 cases['subtasks'].pop(sind)
640 with open(result_file_name, 'w') as fname:
641 template.dump(cases, fname)
644 def _remove_plugins_extra():
645 inst_dir = getattr(config.CONF, 'dir_rally_inst')
647 shutil.rmtree(os.path.join(inst_dir, 'plugins'))
648 shutil.rmtree(os.path.join(inst_dir, 'extra'))
649 except Exception: # pylint: disable=broad-except
652 def prepare_task(self, test_name):
653 """Prepare resources for test run."""
654 self._remove_plugins_extra()
655 jobs_dir = os.path.join(
656 getattr(config.CONF, 'dir_rally_data'), test_name, 'rally-jobs')
657 inst_dir = getattr(config.CONF, 'dir_rally_inst')
658 shutil.copytree(os.path.join(jobs_dir, 'plugins'),
659 os.path.join(inst_dir, 'plugins'))
660 shutil.copytree(os.path.join(jobs_dir, 'extra'),
661 os.path.join(inst_dir, 'extra'))
663 task_name = self.task_yaml.get(test_name).get("task")
664 task = os.path.join(jobs_dir, task_name)
665 if not os.path.exists(task):
666 raise Exception("The scenario '%s' does not exist." % task)
667 LOGGER.debug('Scenario fetched from : %s', task)
669 if not os.path.exists(self.TEMP_DIR):
670 os.makedirs(self.TEMP_DIR)
671 task_file_name = os.path.join(self.TEMP_DIR, task_name)
672 self.apply_blacklist(task, task_file_name)
673 self.run_cmd = (["rally", "task", "start", "--task", task_file_name])
677 self._remove_plugins_extra()
678 super(RallyJobs, self).clean()