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