Enhance variable manipulation in rally
[functest.git] / functest / opnfv_tests / openstack / rally / rally.py
1 #!/usr/bin/env python
2 #
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
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10
11 """Rally testcases implementation."""
12
13 from __future__ import division
14
15 import json
16 import logging
17 import os
18 import re
19 import subprocess
20 import time
21 import uuid
22
23 import pkg_resources
24 import prettytable
25 import yaml
26
27 from functest.core import testcase
28 from functest.energy import energy
29 from functest.opnfv_tests.openstack.snaps import snaps_utils
30 from functest.opnfv_tests.openstack.tempest import conf_utils
31 from functest.utils.constants import CONST
32
33 from snaps.config.flavor import FlavorConfig
34 from snaps.config.image import ImageConfig
35 from snaps.config.network import NetworkConfig, SubnetConfig
36 from snaps.config.router import RouterConfig
37
38 from snaps.openstack.create_flavor import OpenStackFlavor
39 from snaps.openstack.utils import deploy_utils
40
41 LOGGER = logging.getLogger(__name__)
42
43
44 class RallyBase(testcase.TestCase):
45     """Base class form Rally testcases implementation."""
46
47     # pylint: disable=too-many-instance-attributes
48     TESTS = ['authenticate', 'glance', 'ceilometer', 'cinder', 'heat',
49              'keystone', 'neutron', 'nova', 'quotas', 'vm', 'all']
50     GLANCE_IMAGE_NAME = getattr(CONST, 'openstack_image_name')
51     GLANCE_IMAGE_FILENAME = getattr(CONST, 'openstack_image_file_name')
52     GLANCE_IMAGE_PATH = os.path.join(getattr(CONST, 'dir_functest_images'),
53                                      GLANCE_IMAGE_FILENAME)
54     GLANCE_IMAGE_FORMAT = getattr(CONST, 'openstack_image_disk_format')
55     GLANCE_IMAGE_USERNAME = getattr(CONST, 'openstack_image_username')
56     GLANCE_IMAGE_EXTRA_PROPERTIES = getattr(CONST,
57                                             'openstack_extra_properties', {})
58     FLAVOR_NAME = getattr(CONST, 'rally_flavor_name')
59     FLAVOR_ALT_NAME = getattr(CONST, 'rally_flavor_alt_name')
60     FLAVOR_RAM = 512
61     FLAVOR_RAM_ALT = 1024
62     FLAVOR_EXTRA_SPECS = getattr(CONST, 'flavor_extra_specs', None)
63     if FLAVOR_EXTRA_SPECS:
64         FLAVOR_RAM = 1024
65         FLAVOR_RAM_ALT = 2048
66
67     RALLY_DIR = pkg_resources.resource_filename(
68         'functest', 'opnfv_tests/openstack/rally')
69     RALLY_SCENARIO_DIR = pkg_resources.resource_filename(
70         'functest', 'opnfv_tests/openstack/rally/scenario')
71     TEMPLATE_DIR = pkg_resources.resource_filename(
72         'functest', 'opnfv_tests/openstack/rally/scenario/templates')
73     SUPPORT_DIR = pkg_resources.resource_filename(
74         'functest', 'opnfv_tests/openstack/rally/scenario/support')
75     USERS_AMOUNT = 2
76     TENANTS_AMOUNT = 3
77     ITERATIONS_AMOUNT = 10
78     CONCURRENCY = 4
79     RESULTS_DIR = os.path.join(getattr(CONST, 'dir_results'), 'rally')
80     BLACKLIST_FILE = os.path.join(RALLY_DIR, "blacklist.txt")
81     TEMP_DIR = os.path.join(RALLY_DIR, "var")
82
83     RALLY_PRIVATE_NET_NAME = getattr(CONST, 'rally_network_name')
84     RALLY_PRIVATE_SUBNET_NAME = getattr(CONST, 'rally_subnet_name')
85     RALLY_PRIVATE_SUBNET_CIDR = getattr(CONST, 'rally_subnet_cidr')
86     RALLY_ROUTER_NAME = getattr(CONST, 'rally_router_name')
87
88     def __init__(self, **kwargs):
89         """Initialize RallyBase object."""
90         super(RallyBase, self).__init__(**kwargs)
91         self.os_creds = kwargs.get('os_creds') or snaps_utils.get_credentials()
92         self.guid = '-' + str(uuid.uuid4())
93         self.creators = []
94         self.mode = ''
95         self.summary = []
96         self.scenario_dir = ''
97         self.image_name = None
98         self.ext_net_name = None
99         self.priv_net_id = None
100         self.flavor_name = None
101         self.flavor_alt_name = None
102         self.smoke = None
103         self.test_name = None
104         self.start_time = None
105         self.result = None
106         self.details = None
107         self.compute_cnt = 0
108
109     def _build_task_args(self, test_file_name):
110         """Build arguments for the Rally task."""
111         task_args = {'service_list': [test_file_name]}
112         task_args['image_name'] = self.image_name
113         task_args['flavor_name'] = self.flavor_name
114         task_args['flavor_alt_name'] = self.flavor_alt_name
115         task_args['glance_image_location'] = self.GLANCE_IMAGE_PATH
116         task_args['glance_image_format'] = self.GLANCE_IMAGE_FORMAT
117         task_args['tmpl_dir'] = self.TEMPLATE_DIR
118         task_args['sup_dir'] = self.SUPPORT_DIR
119         task_args['users_amount'] = self.USERS_AMOUNT
120         task_args['tenants_amount'] = self.TENANTS_AMOUNT
121         task_args['use_existing_users'] = False
122         task_args['iterations'] = self.ITERATIONS_AMOUNT
123         task_args['concurrency'] = self.CONCURRENCY
124         task_args['smoke'] = self.smoke
125
126         ext_net = self.ext_net_name
127         if ext_net:
128             task_args['floating_network'] = str(ext_net)
129         else:
130             task_args['floating_network'] = ''
131
132         net_id = self.priv_net_id
133         if net_id:
134             task_args['netid'] = str(net_id)
135         else:
136             task_args['netid'] = ''
137
138         return task_args
139
140     def _prepare_test_list(self, test_name):
141         """Build the list of test cases to be executed."""
142         test_yaml_file_name = 'opnfv-{}.yaml'.format(test_name)
143         scenario_file_name = os.path.join(self.RALLY_SCENARIO_DIR,
144                                           test_yaml_file_name)
145
146         if not os.path.exists(scenario_file_name):
147             scenario_file_name = os.path.join(self.scenario_dir,
148                                               test_yaml_file_name)
149
150             if not os.path.exists(scenario_file_name):
151                 raise Exception("The scenario '%s' does not exist."
152                                 % scenario_file_name)
153
154         LOGGER.debug('Scenario fetched from : %s', scenario_file_name)
155         test_file_name = os.path.join(self.TEMP_DIR, test_yaml_file_name)
156
157         if not os.path.exists(self.TEMP_DIR):
158             os.makedirs(self.TEMP_DIR)
159
160         self._apply_blacklist(scenario_file_name, test_file_name)
161         return test_file_name
162
163     @staticmethod
164     def get_task_id(cmd_raw):
165         """
166         Get task id from command rally result.
167
168         :param cmd_raw:
169         :return: task_id as string
170         """
171         taskid_re = re.compile('^Task +(.*): started$')
172         for line in cmd_raw.splitlines(True):
173             line = line.strip()
174             match = taskid_re.match(line)
175             if match:
176                 return match.group(1)
177         return None
178
179     @staticmethod
180     def task_succeed(json_raw):
181         """
182         Parse JSON from rally JSON results.
183
184         :param json_raw:
185         :return: Bool
186         """
187         rally_report = json.loads(json_raw)
188         for report in rally_report:
189             if report is None or report.get('result') is None:
190                 return False
191
192             for result in report.get('result'):
193                 if result is None or len(result.get('error')) > 0:
194                     return False
195
196         return True
197
198     def _migration_supported(self):
199         """Determine if migration is supported."""
200         if self.compute_cnt > 1:
201             return True
202
203         return False
204
205     @staticmethod
206     def get_cmd_output(proc):
207         """Get command stdout."""
208         result = ""
209         for line in proc.stdout:
210             result += line
211         return result
212
213     @staticmethod
214     def excl_scenario():
215         """Exclude scenario."""
216         black_tests = []
217         try:
218             with open(RallyBase.BLACKLIST_FILE, 'r') as black_list_file:
219                 black_list_yaml = yaml.safe_load(black_list_file)
220
221             installer_type = os.getenv('INSTALLER_TYPE', None)
222             deploy_scenario = os.getenv('DEPLOY_SCENARIO', None)
223             if (bool(installer_type) and bool(deploy_scenario) and
224                     'scenario' in black_list_yaml.keys()):
225                 for item in black_list_yaml['scenario']:
226                     scenarios = item['scenarios']
227                     installers = item['installers']
228                     in_it = RallyBase.in_iterable_re
229                     if (in_it(deploy_scenario, scenarios) and
230                             in_it(installer_type, installers)):
231                         tests = item['tests']
232                         black_tests.extend(tests)
233         except Exception:  # pylint: disable=broad-except
234             LOGGER.debug("Scenario exclusion not applied.")
235
236         return black_tests
237
238     @staticmethod
239     def in_iterable_re(needle, haystack):
240         """
241         Check if given needle is in the iterable haystack, using regex.
242
243         :param needle: string to be matched
244         :param haystack: iterable of strings (optionally regex patterns)
245         :return: True if needle is eqial to any of the elements in haystack,
246                  or if a nonempty regex pattern in haystack is found in needle.
247         """
248         # match without regex
249         if needle in haystack:
250             return True
251
252         for pattern in haystack:
253             # match if regex pattern is set and found in the needle
254             if pattern and re.search(pattern, needle) is not None:
255                 return True
256
257         return False
258
259     def excl_func(self):
260         """Exclude functionalities."""
261         black_tests = []
262         func_list = []
263
264         try:
265             with open(RallyBase.BLACKLIST_FILE, 'r') as black_list_file:
266                 black_list_yaml = yaml.safe_load(black_list_file)
267
268             if not self._migration_supported():
269                 func_list.append("no_migration")
270
271             if 'functionality' in black_list_yaml.keys():
272                 for item in black_list_yaml['functionality']:
273                     functions = item['functions']
274                     for func in func_list:
275                         if func in functions:
276                             tests = item['tests']
277                             black_tests.extend(tests)
278         except Exception:  # pylint: disable=broad-except
279             LOGGER.debug("Functionality exclusion not applied.")
280
281         return black_tests
282
283     def _apply_blacklist(self, case_file_name, result_file_name):
284         """Apply blacklist."""
285         LOGGER.debug("Applying blacklist...")
286         cases_file = open(case_file_name, 'r')
287         result_file = open(result_file_name, 'w')
288
289         black_tests = list(set(self.excl_func() +
290                                self.excl_scenario()))
291
292         if black_tests:
293             LOGGER.debug("Blacklisted tests: " + str(black_tests))
294
295         include = True
296         for cases_line in cases_file:
297             if include:
298                 for black_tests_line in black_tests:
299                     if re.search(black_tests_line,
300                                  cases_line.strip().rstrip(':')):
301                         include = False
302                         break
303                 else:
304                     result_file.write(str(cases_line))
305             else:
306                 if cases_line.isspace():
307                     include = True
308
309         cases_file.close()
310         result_file.close()
311
312     @staticmethod
313     def file_is_empty(file_name):
314         """Determine is a file is empty."""
315         try:
316             if os.stat(file_name).st_size > 0:
317                 return False
318         except Exception:  # pylint: disable=broad-except
319             pass
320
321         return True
322
323     def _run_task(self, test_name):
324         """Run a task."""
325         LOGGER.info('Starting test scenario "%s" ...', test_name)
326
327         task_file = os.path.join(self.RALLY_DIR, 'task.yaml')
328         if not os.path.exists(task_file):
329             LOGGER.error("Task file '%s' does not exist.", task_file)
330             raise Exception("Task file '%s' does not exist.", task_file)
331
332         file_name = self._prepare_test_list(test_name)
333         if self.file_is_empty(file_name):
334             LOGGER.info('No tests for scenario "%s"', test_name)
335             return
336
337         cmd = (["rally", "task", "start", "--abort-on-sla-failure", "--task",
338                 task_file, "--task-args",
339                 str(self._build_task_args(test_name))])
340         LOGGER.debug('running command: %s', cmd)
341
342         proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
343                                 stderr=subprocess.STDOUT)
344         output = self.get_cmd_output(proc)
345         task_id = self.get_task_id(output)
346
347         LOGGER.debug('task_id : %s', task_id)
348
349         if task_id is None:
350             LOGGER.error('Failed to retrieve task_id, validating task...')
351             cmd = (["rally", "task", "validate", "--task", task_file,
352                     "--task-args", str(self._build_task_args(test_name))])
353             LOGGER.debug('running command: %s', cmd)
354             proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
355                                     stderr=subprocess.STDOUT)
356             output = self.get_cmd_output(proc)
357             LOGGER.error("Task validation result:" + "\n" + output)
358             return
359
360         # check for result directory and create it otherwise
361         if not os.path.exists(self.RESULTS_DIR):
362             LOGGER.debug('%s does not exist, we create it.',
363                          self.RESULTS_DIR)
364             os.makedirs(self.RESULTS_DIR)
365
366         # get and save rally operation JSON result
367         cmd = (["rally", "task", "detailed", task_id])
368         LOGGER.debug('running command: %s', cmd)
369         proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
370                                 stderr=subprocess.STDOUT)
371         json_detailed = self.get_cmd_output(proc)
372         LOGGER.info('%s', json_detailed)
373
374         cmd = (["rally", "task", "results", task_id])
375         LOGGER.debug('running command: %s', cmd)
376         proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
377                                 stderr=subprocess.STDOUT)
378         json_results = self.get_cmd_output(proc)
379         self._append_summary(json_results, test_name)
380         report_json_name = 'opnfv-{}.json'.format(test_name)
381         report_json_dir = os.path.join(self.RESULTS_DIR, report_json_name)
382         with open(report_json_dir, 'w') as r_file:
383             LOGGER.debug('saving json file')
384             r_file.write(json_results)
385
386         # write html report file
387         report_html_name = 'opnfv-{}.html'.format(test_name)
388         report_html_dir = os.path.join(self.RESULTS_DIR, report_html_name)
389         cmd = (["rally", "task", "report", task_id, "--out", report_html_dir])
390         LOGGER.debug('running command: %s', cmd)
391         subprocess.Popen(cmd, stdout=subprocess.PIPE,
392                          stderr=subprocess.STDOUT)
393
394         # parse JSON operation result
395         if self.task_succeed(json_results):
396             LOGGER.info('Test scenario: "{}" OK.'.format(test_name) + "\n")
397         else:
398             LOGGER.info('Test scenario: "{}" Failed.'.format(test_name) + "\n")
399
400     def _append_summary(self, json_raw, test_name):
401         """Update statistics summary info."""
402         nb_tests = 0
403         nb_success = 0
404         overall_duration = 0.0
405
406         rally_report = json.loads(json_raw)
407         for report in rally_report:
408             if report.get('full_duration'):
409                 overall_duration += report.get('full_duration')
410
411             if report.get('result'):
412                 for result in report.get('result'):
413                     nb_tests += 1
414                     if not result.get('error'):
415                         nb_success += 1
416
417         scenario_summary = {'test_name': test_name,
418                             'overall_duration': overall_duration,
419                             'nb_tests': nb_tests,
420                             'nb_success': nb_success}
421         self.summary.append(scenario_summary)
422
423     def _prepare_env(self):
424         """Create resources needed by test scenarios."""
425         LOGGER.debug('Validating the test name...')
426         if self.test_name not in self.TESTS:
427             raise Exception("Test name '%s' is invalid" % self.test_name)
428
429         network_name = self.RALLY_PRIVATE_NET_NAME + self.guid
430         subnet_name = self.RALLY_PRIVATE_SUBNET_NAME + self.guid
431         router_name = self.RALLY_ROUTER_NAME + self.guid
432         self.image_name = self.GLANCE_IMAGE_NAME + self.guid
433         self.flavor_name = self.FLAVOR_NAME + self.guid
434         self.flavor_alt_name = self.FLAVOR_ALT_NAME + self.guid
435         self.ext_net_name = snaps_utils.get_ext_net_name(self.os_creds)
436         self.compute_cnt = snaps_utils.get_active_compute_cnt(self.os_creds)
437
438         LOGGER.debug("Creating image '%s'...", self.image_name)
439         image_creator = deploy_utils.create_image(
440             self.os_creds, ImageConfig(
441                 name=self.image_name,
442                 image_file=self.GLANCE_IMAGE_PATH,
443                 img_format=self.GLANCE_IMAGE_FORMAT,
444                 image_user=self.GLANCE_IMAGE_USERNAME,
445                 public=True,
446                 extra_properties=self.GLANCE_IMAGE_EXTRA_PROPERTIES))
447         if image_creator is None:
448             raise Exception("Failed to create image")
449         self.creators.append(image_creator)
450
451         LOGGER.debug("Creating network '%s'...", network_name)
452
453         rally_network_type = getattr(CONST, 'rally_network_type', None)
454         rally_physical_network = getattr(CONST, 'rally_physical_network', None)
455         rally_segmentation_id = getattr(CONST, 'rally_segmentation_id', None)
456
457         network_creator = deploy_utils.create_network(
458             self.os_creds, NetworkConfig(
459                 name=network_name,
460                 shared=True,
461                 network_type=rally_network_type,
462                 physical_network=rally_physical_network,
463                 segmentation_id=rally_segmentation_id,
464                 subnet_settings=[SubnetConfig(
465                     name=subnet_name,
466                     cidr=self.RALLY_PRIVATE_SUBNET_CIDR)]))
467         if network_creator is None:
468             raise Exception("Failed to create private network")
469         self.priv_net_id = network_creator.get_network().id
470         self.creators.append(network_creator)
471
472         LOGGER.debug("Creating router '%s'...", router_name)
473         router_creator = deploy_utils.create_router(
474             self.os_creds, RouterConfig(
475                 name=router_name,
476                 external_gateway=self.ext_net_name,
477                 internal_subnets=[subnet_name]))
478         if router_creator is None:
479             raise Exception("Failed to create router")
480         self.creators.append(router_creator)
481
482         LOGGER.debug("Creating flavor '%s'...", self.flavor_name)
483         flavor_creator = OpenStackFlavor(
484             self.os_creds, FlavorConfig(
485                 name=self.flavor_name, ram=self.FLAVOR_RAM, disk=1, vcpus=1,
486                 metadata=self.FLAVOR_EXTRA_SPECS))
487         if flavor_creator is None or flavor_creator.create() is None:
488             raise Exception("Failed to create flavor")
489         self.creators.append(flavor_creator)
490
491         LOGGER.debug("Creating flavor '%s'...", self.flavor_alt_name)
492         flavor_alt_creator = OpenStackFlavor(
493             self.os_creds, FlavorConfig(
494                 name=self.flavor_alt_name, ram=self.FLAVOR_RAM_ALT, disk=1,
495                 vcpus=1, metadata=self.FLAVOR_EXTRA_SPECS))
496         if flavor_alt_creator is None or flavor_alt_creator.create() is None:
497             raise Exception("Failed to create flavor")
498         self.creators.append(flavor_alt_creator)
499
500     def _run_tests(self):
501         """Execute tests."""
502         if self.test_name == 'all':
503             for test in self.TESTS:
504                 if test == 'all' or test == 'vm':
505                     continue
506                 self._run_task(test)
507         else:
508             self._run_task(self.test_name)
509
510     def _generate_report(self):
511         """Generate test execution summary report."""
512         total_duration = 0.0
513         total_nb_tests = 0
514         total_nb_success = 0
515         payload = []
516
517         res_table = prettytable.PrettyTable(
518             padding_width=2,
519             field_names=['Module', 'Duration', 'nb. Test Run', 'Success'])
520         res_table.align['Module'] = "l"
521         res_table.align['Duration'] = "r"
522         res_table.align['Success'] = "r"
523
524         # for each scenario we draw a row for the table
525         for item in self.summary:
526             total_duration += item['overall_duration']
527             total_nb_tests += item['nb_tests']
528             total_nb_success += item['nb_success']
529             try:
530                 success_avg = 100 * item['nb_success'] / item['nb_tests']
531             except ZeroDivisionError:
532                 success_avg = 0
533             success_str = str("{:0.2f}".format(success_avg)) + '%'
534             duration_str = time.strftime("%M:%S",
535                                          time.gmtime(item['overall_duration']))
536             res_table.add_row([item['test_name'], duration_str,
537                                item['nb_tests'], success_str])
538             payload.append({'module': item['test_name'],
539                             'details': {'duration': item['overall_duration'],
540                                         'nb tests': item['nb_tests'],
541                                         'success': success_str}})
542
543         total_duration_str = time.strftime("%H:%M:%S",
544                                            time.gmtime(total_duration))
545         try:
546             self.result = 100 * total_nb_success / total_nb_tests
547         except ZeroDivisionError:
548             self.result = 100
549         success_rate = "{:0.2f}".format(self.result)
550         success_rate_str = str(success_rate) + '%'
551         res_table.add_row(["", "", "", ""])
552         res_table.add_row(["TOTAL:", total_duration_str, total_nb_tests,
553                            success_rate_str])
554
555         LOGGER.info("Rally Summary Report:\n\n%s\n", res_table.get_string())
556         LOGGER.info("Rally '%s' success_rate is %s%%",
557                     self.case_name, success_rate)
558         payload.append({'summary': {'duration': total_duration,
559                                     'nb tests': total_nb_tests,
560                                     'nb success': success_rate}})
561         self.details = payload
562
563     def _clean_up(self):
564         """Cleanup all OpenStack objects. Should be called on completion."""
565         for creator in reversed(self.creators):
566             try:
567                 creator.clean()
568             except Exception as exc:  # pylint: disable=broad-except
569                 LOGGER.error('Unexpected error cleaning - %s', exc)
570
571     @energy.enable_recording
572     def run(self, **kwargs):
573         """Run testcase."""
574         self.start_time = time.time()
575         try:
576             conf_utils.create_rally_deployment()
577             self._prepare_env()
578             self._run_tests()
579             self._generate_report()
580             res = testcase.TestCase.EX_OK
581         except Exception as exc:   # pylint: disable=broad-except
582             LOGGER.error('Error with run: %s', exc)
583             res = testcase.TestCase.EX_RUN_ERROR
584         finally:
585             self._clean_up()
586
587         self.stop_time = time.time()
588         return res
589
590
591 class RallySanity(RallyBase):
592     """Rally sanity testcase implementation."""
593
594     def __init__(self, **kwargs):
595         """Initialize RallySanity object."""
596         if "case_name" not in kwargs:
597             kwargs["case_name"] = "rally_sanity"
598         super(RallySanity, self).__init__(**kwargs)
599         self.mode = 'sanity'
600         self.test_name = 'all'
601         self.smoke = True
602         self.scenario_dir = os.path.join(self.RALLY_SCENARIO_DIR, 'sanity')
603
604
605 class RallyFull(RallyBase):
606     """Rally full testcase implementation."""
607
608     def __init__(self, **kwargs):
609         """Initialize RallyFull object."""
610         if "case_name" not in kwargs:
611             kwargs["case_name"] = "rally_full"
612         super(RallyFull, self).__init__(**kwargs)
613         self.mode = 'full'
614         self.test_name = 'all'
615         self.smoke = False
616         self.scenario_dir = os.path.join(self.RALLY_SCENARIO_DIR, 'full')