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