vsperf: Performance Matrix functionality support
[vswitchperf.git] / vsperf
1 #!/usr/bin/env python3
2
3 # Copyright 2015-2017 Intel Corporation.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 #   http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 """VSPERF main script.
18 """
19
20 import logging
21 import os
22 import sys
23 import argparse
24 import re
25 import time
26 import csv
27 import datetime
28 import shutil
29 import unittest
30 import locale
31 import copy
32 import glob
33 import subprocess
34 import ast
35 import xmlrunner
36 from tabulate import tabulate
37 from conf import merge_spec
38 from conf import settings
39 import core.component_factory as component_factory
40 from core.loader import Loader
41 from testcases import PerformanceTestCase
42 from testcases import IntegrationTestCase
43 from tools import tasks
44 from tools import networkcard
45 from tools import functions
46 from tools.pkt_gen import trafficgen
47 from tools.opnfvdashboard import opnfvdashboard
48 sys.dont_write_bytecode = True
49
50 VERBOSITY_LEVELS = {
51     'debug': logging.DEBUG,
52     'info': logging.INFO,
53     'warning': logging.WARNING,
54     'error': logging.ERROR,
55     'critical': logging.CRITICAL
56 }
57
58 _CURR_DIR = os.path.dirname(os.path.realpath(__file__))
59
60 _TEMPLATE_RST = {'head'  : os.path.join(_CURR_DIR, 'tools/report/report_head.rst'),
61                  'foot'  : os.path.join(_CURR_DIR, 'tools/report/report_foot.rst'),
62                  'final' : 'test_report.rst',
63                  'tmp'   : os.path.join(_CURR_DIR, 'tools/report/report_tmp_caption.rst')
64                 }
65
66 _TEMPLATE_MATRIX = "Performance Matrix\n------------------\n\n"\
67                    "The following performance matrix was generated with the results of all the\n"\
68                    "currently run tests. The metric used for comparison is {}.\n\n{}\n\n"
69
70 _LOGGER = logging.getLogger()
71
72 def parse_param_string(values):
73     """
74     Parse and split a single '--test-params' argument.
75
76     This expects either 'x=y', 'x=y,z' or 'x' (implicit true)
77     values. For multiple overrides use a ; separated list for
78     e.g. --test-params 'x=z; y=(a,b)'
79     """
80     results = {}
81
82     if values == '':
83         return {}
84
85     for param, _, value in re.findall('([^;=]+)(=([^;]+))?', values):
86         param = param.strip()
87         value = value.strip()
88         if param:
89             if value:
90                 # values are passed inside string from CLI, so we must retype them accordingly
91                 try:
92                     results[param] = ast.literal_eval(value)
93                 except ValueError:
94                     # for backward compatibility, we have to accept strings without quotes
95                     _LOGGER.warning("Adding missing quotes around string value: %s = %s",
96                                     param, str(value))
97                     results[param] = str(value)
98             else:
99                 results[param] = True
100     return results
101
102
103 def parse_arguments():
104     """
105     Parse command line arguments.
106     """
107     class _SplitTestParamsAction(argparse.Action):
108         """
109         Parse and split '--test-params' arguments.
110
111         This expects either a single list of ; separated overrides
112         as 'x=y', 'x=y,z' or 'x' (implicit true) values.
113         e.g. --test-params 'x=z; y=(a,b)'
114         Or a list of these ; separated lists with overrides for
115         multiple tests.
116         e.g. --test-params "['x=z; y=(a,b)','x=z']"
117         """
118         def __call__(self, parser, namespace, values, option_string=None):
119
120             if values[0] == '[':
121                 input_list = ast.literal_eval(values)
122                 parameter_list = []
123                 for test_params in input_list:
124                     parameter_list.append(parse_param_string(test_params))
125             else:
126                 parameter_list = parse_param_string(values)
127             results = {'_PARAMS_LIST':parameter_list}
128             setattr(namespace, self.dest, results)
129
130     class _ValidateFileAction(argparse.Action):
131         """Validate a file can be read from before using it.
132         """
133         def __call__(self, parser, namespace, values, option_string=None):
134             if not os.path.isfile(values):
135                 raise argparse.ArgumentTypeError(
136                     'the path \'%s\' is not a valid path' % values)
137             elif not os.access(values, os.R_OK):
138                 raise argparse.ArgumentTypeError(
139                     'the path \'%s\' is not accessible' % values)
140
141             setattr(namespace, self.dest, values)
142
143     class _ValidateDirAction(argparse.Action):
144         """Validate a directory can be written to before using it.
145         """
146         def __call__(self, parser, namespace, values, option_string=None):
147             if not os.path.isdir(values):
148                 raise argparse.ArgumentTypeError(
149                     'the path \'%s\' is not a valid path' % values)
150             elif not os.access(values, os.W_OK):
151                 raise argparse.ArgumentTypeError(
152                     'the path \'%s\' is not accessible' % values)
153
154             setattr(namespace, self.dest, values)
155
156     def list_logging_levels():
157         """Give a summary of all available logging levels.
158
159         :return: List of verbosity level names in decreasing order of
160             verbosity
161         """
162         return sorted(VERBOSITY_LEVELS.keys(),
163                       key=lambda x: VERBOSITY_LEVELS[x])
164
165     parser = argparse.ArgumentParser(prog=__file__, formatter_class=
166                                      argparse.ArgumentDefaultsHelpFormatter)
167     parser.add_argument('--version', action='version', version='%(prog)s 0.2')
168     parser.add_argument('--list', '--list-tests', action='store_true',
169                         help='list all tests and exit')
170     parser.add_argument('--list-trafficgens', action='store_true',
171                         help='list all traffic generators and exit')
172     parser.add_argument('--list-collectors', action='store_true',
173                         help='list all system metrics loggers and exit')
174     parser.add_argument('--list-vswitches', action='store_true',
175                         help='list all system vswitches and exit')
176     parser.add_argument('--list-fwdapps', action='store_true',
177                         help='list all system forwarding applications and exit')
178     parser.add_argument('--list-vnfs', action='store_true',
179                         help='list all system vnfs and exit')
180     parser.add_argument('--list-loadgens', action='store_true',
181                         help='list all background load generators')
182     parser.add_argument('--list-settings', action='store_true',
183                         help='list effective settings configuration and exit')
184     parser.add_argument('exact_test_name', nargs='*', help='Exact names of\
185             tests to run. E.g "vsperf phy2phy_tput phy2phy_cont"\
186             runs only the two tests with those exact names.\
187             To run all tests omit both positional args and --tests arg.')
188
189     group = parser.add_argument_group('test selection options')
190     group.add_argument('-m', '--mode', help='vsperf mode of operation;\
191             Values: "normal" - execute vSwitch, VNF and traffic generator;\
192             "trafficgen" - execute only traffic generator; "trafficgen-off" \
193             - execute vSwitch and VNF; trafficgen-pause - execute vSwitch \
194             and VNF but pause before traffic transmission ', default='normal')
195
196     group.add_argument('-f', '--test-spec', help='test specification file')
197     group.add_argument('-d', '--test-dir', help='directory containing tests')
198     group.add_argument('-t', '--tests', help='Comma-separated list of terms \
199             indicating tests to run. e.g. "RFC2544,!p2p" - run all tests whose\
200             name contains RFC2544 less those containing "p2p"; "!back2back" - \
201             run all tests except those containing back2back')
202     group.add_argument('--verbosity', choices=list_logging_levels(),
203                        help='debug level')
204     group.add_argument('--integration', action='store_true', help='execute integration tests')
205     group.add_argument('--trafficgen', help='traffic generator to use')
206     group.add_argument('--vswitch', help='vswitch implementation to use')
207     group.add_argument('--fwdapp', help='packet forwarding application to use')
208     group.add_argument('--vnf', help='vnf to use')
209     group.add_argument('--loadgen', help='loadgen to use')
210     group.add_argument('--sysmetrics', help='system metrics logger to use')
211     group = parser.add_argument_group('test behavior options')
212     group.add_argument('--xunit', action='store_true',
213                        help='enable xUnit-formatted output')
214     group.add_argument('--xunit-dir', action=_ValidateDirAction,
215                        help='output directory of xUnit-formatted output')
216     group.add_argument('--load-env', action='store_true',
217                        help='enable loading of settings from the environment')
218     group.add_argument('--conf-file', action=_ValidateFileAction,
219                        help='settings file')
220     group.add_argument('--test-params', action=_SplitTestParamsAction,
221                        help='csv list of test parameters: key=val; e.g. '
222                        'TRAFFICGEN_PKT_SIZES=(64,128);TRAFFICGEN_DURATION=30; '
223                        'GUEST_LOOPBACK=["l2fwd"] ...'
224                        ' or a list of csv lists of test parameters: key=val; e.g. '
225                        '[\'TRAFFICGEN_DURATION=10;TRAFFICGEN_PKT_SIZES=(128,)\','
226                        '\'TRAFFICGEN_DURATION=10;TRAFFICGEN_PKT_SIZES=(64,)\']')
227     group.add_argument('--opnfvpod', help='name of POD in opnfv')
228     group.add_argument('--matrix', help='enable performance matrix analysis',
229                        action='store_true', default=False)
230
231     args = vars(parser.parse_args())
232
233     return args
234
235
236 def configure_logging(level):
237     """Configure logging.
238     """
239     log_file_default = os.path.join(
240         settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_DEFAULT'))
241     log_file_host_cmds = os.path.join(
242         settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_HOST_CMDS'))
243     log_file_traffic_gen = os.path.join(
244         settings.getValue('LOG_DIR'),
245         settings.getValue('LOG_FILE_TRAFFIC_GEN'))
246
247     _LOGGER.setLevel(logging.DEBUG)
248
249     stream_logger = logging.StreamHandler(sys.stdout)
250     stream_logger.setLevel(VERBOSITY_LEVELS[level])
251     stream_logger.setFormatter(logging.Formatter(
252         '[%(levelname)-5s]  %(asctime)s : (%(name)s) - %(message)s'))
253     _LOGGER.addHandler(stream_logger)
254
255     file_logger = logging.FileHandler(filename=log_file_default)
256     file_logger.setLevel(logging.DEBUG)
257     _LOGGER.addHandler(file_logger)
258
259     class CommandFilter(logging.Filter):
260         """Filter out strings beginning with 'cmd :'"""
261         def filter(self, record):
262             return record.getMessage().startswith(tasks.CMD_PREFIX)
263
264     class TrafficGenCommandFilter(logging.Filter):
265         """Filter out strings beginning with 'gencmd :'"""
266         def filter(self, record):
267             return record.getMessage().startswith(trafficgen.CMD_PREFIX)
268
269     cmd_logger = logging.FileHandler(filename=log_file_host_cmds)
270     cmd_logger.setLevel(logging.DEBUG)
271     cmd_logger.addFilter(CommandFilter())
272     _LOGGER.addHandler(cmd_logger)
273
274     gen_logger = logging.FileHandler(filename=log_file_traffic_gen)
275     gen_logger.setLevel(logging.DEBUG)
276     gen_logger.addFilter(TrafficGenCommandFilter())
277     _LOGGER.addHandler(gen_logger)
278
279
280 def apply_filter(tests, tc_filter):
281     """Allow a subset of tests to be conveniently selected
282
283     :param tests: The list of Tests from which to select.
284     :param tc_filter: A case-insensitive string of comma-separated terms
285         indicating the Tests to select.
286         e.g. 'RFC' - select all tests whose name contains 'RFC'
287         e.g. 'RFC,burst' - select all tests whose name contains 'RFC' or
288             'burst'
289         e.g. 'RFC,burst,!p2p' - select all tests whose name contains 'RFC'
290             or 'burst' and from these remove any containing 'p2p'.
291         e.g. '' - empty string selects all tests.
292     :return: A list of the selected Tests.
293     """
294     # if negative filter is first we have to start with full list of tests
295     if tc_filter.strip()[0] == '!':
296         result = tests
297     else:
298         result = []
299     if tc_filter is None:
300         tc_filter = ""
301
302     for term in [x.strip() for x in tc_filter.lower().split(",")]:
303         if not term or term[0] != '!':
304             # Add matching tests from 'tests' into results
305             result.extend([test for test in tests \
306                 if test['Name'].lower().find(term) >= 0])
307         else:
308             # Term begins with '!' so we remove matching tests
309             result = [test for test in result \
310                 if test['Name'].lower().find(term[1:]) < 0]
311
312     return result
313
314
315 def check_and_set_locale():
316     """ Function will check locale settings. In case, that it isn't configured
317     properly, then default values specified by DEFAULT_LOCALE will be used.
318     """
319
320     system_locale = locale.getdefaultlocale()
321     if None in system_locale:
322         os.environ['LC_ALL'] = settings.getValue('DEFAULT_LOCALE')
323         _LOGGER.warning("Locale was not properly configured. Default values were set. Old locale: %s, New locale: %s",
324                         system_locale, locale.getdefaultlocale())
325
326 def get_vswitch_names(rst_files):
327     """ Function will return a list of vSwitches detected in given ``rst_files``.
328     """
329     vswitch_names = set()
330     if len(rst_files):
331         try:
332             output = subprocess.check_output(['grep', '-h', '^* vSwitch'] + rst_files).decode().splitlines()
333             for line in output:
334                 match = re.search(r'^\* vSwitch: ([^,]+)', str(line))
335                 if match:
336                     vswitch_names.add(match.group(1))
337
338             if len(vswitch_names):
339                 return list(vswitch_names)
340
341         except subprocess.CalledProcessError:
342             _LOGGER.warning('Cannot detect vSwitches used during testing.')
343
344     # fallback to the default value
345     return ['vSwitch']
346
347 def get_build_tag():
348     """ Function will return a Jenkins job ID environment variable.
349     """
350
351     try:
352         build_tag = os.environ['BUILD_TAG']
353
354     except KeyError:
355         _LOGGER.warning('Cannot detect Jenkins job ID')
356         build_tag = "none"
357
358     return build_tag
359
360 def generate_final_report():
361     """ Function will check if partial test results are available
362     and generates final report in rst format.
363     """
364
365     path = settings.getValue('RESULTS_PATH')
366     # check if there are any results in rst format
367     rst_results = glob.glob(os.path.join(path, 'result*rst'))
368     pkt_processors = get_vswitch_names(rst_results)
369     if len(rst_results):
370         try:
371             test_report = os.path.join(path, '{}_{}'.format('_'.join(pkt_processors), _TEMPLATE_RST['final']))
372             # create report caption directly - it is not worth to execute jinja machinery
373             report_caption = '{}\n{} {}\n{}\n\n'.format(
374                 '============================================================',
375                 'Performance report for',
376                 ', '.join(pkt_processors),
377                 '============================================================')
378
379             with open(_TEMPLATE_RST['tmp'], 'w') as file_:
380                 file_.write(report_caption)
381
382             retval = subprocess.call('cat {} {} {} {} > {}'.format(_TEMPLATE_RST['tmp'], _TEMPLATE_RST['head'],
383                                                                    ' '.join(rst_results), _TEMPLATE_RST['foot'],
384                                                                    test_report), shell=True)
385             if retval == 0 and os.path.isfile(test_report):
386                 _LOGGER.info('Overall test report written to "%s"', test_report)
387             else:
388                 _LOGGER.error('Generation of overall test report has failed.')
389
390             # remove temporary file
391             os.remove(_TEMPLATE_RST['tmp'])
392
393         except subprocess.CalledProcessError:
394             _LOGGER.error('Generatrion of overall test report has failed.')
395
396
397 def generate_performance_matrix(selected_tests, results_path):
398     """
399     Loads the results of all the currently run tests, compares them
400     based on the MATRIX_METRIC, outputs and saves the generated table.
401     :selected_tests: list of currently run test
402     :results_path: directory path to the results of current tests
403     """
404     _LOGGER.info('Performance Matrix:')
405     test_list = []
406
407     for test in selected_tests:
408         test_name = test.get('Name', '<Name not set>')
409         test_deployment = test.get('Deployment', '<Deployment not set>')
410         test_list.append({'test_name':test_name, 'test_deployment':test_deployment, 'csv_data':False})
411
412     test_params = {}
413     output = []
414     all_params = settings.getValue('_PARAMS_LIST')
415     for i in range(len(selected_tests)):
416         test = test_list[i]
417         if isinstance(all_params, list):
418             list_index = i
419             if i >= len(all_params):
420                 list_index = len(all_params) - 1
421             if settings.getValue('CUMULATIVE_PARAMS') and (i > 0):
422                 test_params.update(all_params[list_index])
423             else:
424                 test_params = all_params[list_index]
425         else:
426             test_params = all_params
427         settings.setValue('TEST_PARAMS', test_params)
428         test['test_params'] = copy.deepcopy(test_params)
429         try:
430             with open("{}/result_{}_{}_{}.csv".format(results_path, str(i),
431                                                       test['test_name'], test['test_deployment'])) as csvfile:
432                 reader = list(csv.DictReader(csvfile))
433                 test['csv_data'] = reader[0]
434         # pylint: disable=broad-except
435         except (Exception) as ex:
436             _LOGGER.error("Result file not found: %s", ex)
437
438     metric = settings.getValue('MATRIX_METRIC')
439     change = {}
440     output_header = ("ID", "Name", metric, "Change [%]", "Parameters, "\
441                      "CUMULATIVE_PARAMS = {}".format(settings.getValue('CUMULATIVE_PARAMS')))
442     if not test_list[0]['csv_data'] or float(test_list[0]['csv_data'][metric]) == 0:
443         _LOGGER.error("Incorrect format of test results")
444         return
445     for i, test in enumerate(test_list):
446         if test['csv_data']:
447             change[i] = float(test['csv_data'][metric])/\
448                         (float(test_list[0]['csv_data'][metric]) / 100) - 100
449             output.append([i, test['test_name'], float(test['csv_data'][metric]),
450                            change[i], str(test['test_params'])[1:-1]])
451         else:
452             change[i] = 0
453             output.append([i, test['test_name'], "Test Failed", 0, test['test_params']])
454     print(tabulate(output, headers=output_header, tablefmt="grid", floatfmt="0.3f"))
455     with open(results_path + '/result_performance_matrix.rst', 'w+') as output_file:
456         output_file.write(_TEMPLATE_MATRIX.format(metric, tabulate(output, headers=output_header,
457                                                                    tablefmt="rst", floatfmt="0.3f")))
458         _LOGGER.info('Performance matrix written to: "%s/result_performance_matrix.rst"', results_path)
459
460 def enable_sriov(nic_list):
461     """ Enable SRIOV for given enhanced PCI IDs
462
463     :param nic_list: A list of enhanced PCI IDs
464     """
465     # detect if sriov is required
466     sriov_nic = {}
467     for nic in nic_list:
468         if networkcard.is_sriov_nic(nic):
469             tmp_nic = nic.split('|')
470             if tmp_nic[0] in sriov_nic:
471                 if int(tmp_nic[1][2:]) > sriov_nic[tmp_nic[0]]:
472                     sriov_nic[tmp_nic[0]] = int(tmp_nic[1][2:])
473             else:
474                 sriov_nic.update({tmp_nic[0] : int(tmp_nic[1][2:])})
475
476     # sriov is required for some NICs
477     if len(sriov_nic):
478         for nic in sriov_nic:
479             # check if SRIOV is supported and enough virt interfaces are available
480             if not networkcard.is_sriov_supported(nic) \
481                 or networkcard.get_sriov_numvfs(nic) <= sriov_nic[nic]:
482                 # if not, enable and set appropriate number of VFs
483                 if not networkcard.set_sriov_numvfs(nic, sriov_nic[nic] + 1):
484                     raise RuntimeError('SRIOV cannot be enabled for NIC {}'.format(nic))
485                 else:
486                     _LOGGER.debug("SRIOV enabled for NIC %s", nic)
487
488                 # ensure that path to the bind tool is valid
489                 functions.settings_update_paths()
490
491                 # WORKAROUND: it has been observed with IXGBE(VF) driver,
492                 # that NIC doesn't correclty dispatch traffic to VFs based
493                 # on their MAC address. Unbind and bind to the same driver
494                 # solves this issue.
495                 networkcard.reinit_vfs(nic)
496
497         # After SRIOV is enabled it takes some time until network drivers
498         # properly initialize all cards.
499         # Wait also in case, that SRIOV was already configured as it can be
500         # configured automatically just before vsperf execution.
501         time.sleep(2)
502
503         return True
504
505     return False
506
507
508 def disable_sriov(nic_list):
509     """ Disable SRIOV for given PCI IDs
510
511     :param nic_list: A list of enhanced PCI IDs
512     """
513     for nic in nic_list:
514         if networkcard.is_sriov_nic(nic):
515             if not networkcard.set_sriov_numvfs(nic.split('|')[0], 0):
516                 raise RuntimeError('SRIOV cannot be disabled for NIC {}'.format(nic))
517             else:
518                 _LOGGER.debug("SRIOV disabled for NIC %s", nic.split('|')[0])
519
520
521 def handle_list_options(args):
522     """ Process --list cli arguments if needed
523
524     :param args: A dictionary with all CLI arguments
525     """
526     if args['list_trafficgens']:
527         print(Loader().get_trafficgens_printable())
528         sys.exit(0)
529
530     if args['list_collectors']:
531         print(Loader().get_collectors_printable())
532         sys.exit(0)
533
534     if args['list_vswitches']:
535         print(Loader().get_vswitches_printable())
536         sys.exit(0)
537
538     if args['list_vnfs']:
539         print(Loader().get_vnfs_printable())
540         sys.exit(0)
541
542     if args['list_fwdapps']:
543         print(Loader().get_pktfwds_printable())
544         sys.exit(0)
545
546     if args['list_loadgens']:
547         print(Loader().get_loadgens_printable())
548         sys.exit(0)
549
550     if args['list_settings']:
551         print(str(settings))
552         sys.exit(0)
553
554     if args['list']:
555         list_testcases(args)
556         sys.exit(0)
557
558
559 def list_testcases(args):
560     """ Print list of testcases requested by --list CLI argument
561
562     :param args: A dictionary with all CLI arguments
563     """
564     # configure tests
565     if args['integration']:
566         testcases = settings.getValue('INTEGRATION_TESTS')
567     else:
568         testcases = settings.getValue('PERFORMANCE_TESTS')
569
570     print("Available Tests:")
571     print("================")
572
573     for test in testcases:
574         description = functions.format_description(test['Description'], 70)
575         if len(test['Name']) < 40:
576             print('* {:40} {}'.format('{}:'.format(test['Name']), description[0]))
577         else:
578             print('* {}'.format('{}:'.format(test['Name'])))
579             print('  {:40} {}'.format('', description[0]))
580         for i in range(1, len(description)):
581             print('  {:40} {}'.format('', description[i]))
582
583
584 def vsperf_finalize():
585     """ Clean up before exit
586     """
587     # remove directory if no result files were created
588     try:
589         results_path = settings.getValue('RESULTS_PATH')
590         if os.path.exists(results_path):
591             files_list = os.listdir(results_path)
592             if files_list == []:
593                 _LOGGER.info("Removing empty result directory: "  + results_path)
594                 shutil.rmtree(results_path)
595     except AttributeError:
596         # skip it if parameter doesn't exist
597         pass
598
599     # disable SRIOV if needed
600     try:
601         if settings.getValue('SRIOV_ENABLED'):
602             disable_sriov(settings.getValue('WHITELIST_NICS_ORIG'))
603     except AttributeError:
604         # skip it if parameter doesn't exist
605         pass
606
607
608 class MockTestCase(unittest.TestCase):
609     """Allow use of xmlrunner to generate Jenkins compatible output without
610     using xmlrunner to actually run tests.
611
612     Usage:
613         suite = unittest.TestSuite()
614         suite.addTest(MockTestCase('Test1 passed ', True, 'Test1'))
615         suite.addTest(MockTestCase('Test2 failed because...', False, 'Test2'))
616         xmlrunner.XMLTestRunner(...).run(suite)
617     """
618
619     def __init__(self, msg, is_pass, test_name):
620         #remember the things
621         self.msg = msg
622         self.is_pass = is_pass
623
624         #dynamically create a test method with the right name
625         #but point the method at our generic test method
626         setattr(MockTestCase, test_name, self.generic_test)
627
628         super(MockTestCase, self).__init__(test_name)
629
630     def generic_test(self):
631         """Provide a generic function that raises or not based
632         on how self.is_pass was set in the constructor"""
633         self.assertTrue(self.is_pass, self.msg)
634
635 # pylint: disable=too-many-locals, too-many-branches, too-many-statements
636 def main():
637     """Main function.
638     """
639     args = parse_arguments()
640
641     # configure settings
642
643     settings.load_from_dir(os.path.join(_CURR_DIR, 'conf'))
644
645     # Load non performance/integration tests
646     if args['integration']:
647         settings.load_from_dir(os.path.join(_CURR_DIR, 'conf/integration'))
648
649     # load command line parameters first in case there are settings files
650     # to be used
651     settings.load_from_dict(args)
652
653     if args['conf_file']:
654         settings.load_from_file(args['conf_file'])
655
656     if args['load_env']:
657         settings.load_from_env()
658
659     # reload command line parameters since these should take higher priority
660     # than both a settings file and environment variables
661     settings.load_from_dict(args)
662
663     settings.setValue('mode', args['mode'])
664
665     # update paths to trafficgens if required
666     if settings.getValue('mode') == 'trafficgen':
667         functions.settings_update_paths()
668
669     # if required, handle list-* operations
670     handle_list_options(args)
671
672     configure_logging(settings.getValue('VERBOSITY'))
673
674     # check and fix locale
675     check_and_set_locale()
676
677     # configure trafficgens
678     if args['trafficgen']:
679         trafficgens = Loader().get_trafficgens()
680         if args['trafficgen'] not in trafficgens:
681             _LOGGER.error('There are no trafficgens matching \'%s\' found in'
682                           ' \'%s\'. Exiting...', args['trafficgen'],
683                           settings.getValue('TRAFFICGEN_DIR'))
684             sys.exit(1)
685
686     # configuration validity checks
687     if args['vswitch']:
688         vswitch_none = args['vswitch'].strip().lower() == 'none'
689         if vswitch_none:
690             settings.setValue('VSWITCH', 'none')
691         else:
692             vswitches = Loader().get_vswitches()
693             if args['vswitch'] not in vswitches:
694                 _LOGGER.error('There are no vswitches matching \'%s\' found in'
695                               ' \'%s\'. Exiting...', args['vswitch'],
696                               settings.getValue('VSWITCH_DIR'))
697                 sys.exit(1)
698
699     if args['fwdapp']:
700         settings.setValue('PKTFWD', args['fwdapp'])
701         fwdapps = Loader().get_pktfwds()
702         if args['fwdapp'] not in fwdapps:
703             _LOGGER.error('There are no forwarding application'
704                           ' matching \'%s\' found in'
705                           ' \'%s\'. Exiting...', args['fwdapp'],
706                           settings.getValue('PKTFWD_DIR'))
707             sys.exit(1)
708
709     if args['vnf']:
710         vnfs = Loader().get_vnfs()
711         if args['vnf'] not in vnfs:
712             _LOGGER.error('there are no vnfs matching \'%s\' found in'
713                           ' \'%s\'. exiting...', args['vnf'],
714                           settings.getValue('VNF_DIR'))
715             sys.exit(1)
716
717     if args['loadgen']:
718         loadgens = Loader().get_loadgens()
719         if args['loadgen'] not in loadgens:
720             _LOGGER.error('There are no loadgens matching \'%s\' found in'
721                           ' \'%s\'. Exiting...', args['loadgen'],
722                           settings.getValue('LOADGEN_DIR'))
723             sys.exit(1)
724
725     if args['exact_test_name'] and args['tests']:
726         _LOGGER.error("Cannot specify tests with both positional args and --test.")
727         sys.exit(1)
728
729     # modify NIC configuration to decode enhanced PCI IDs
730     wl_nics_orig = list(networkcard.check_pci(pci) for pci in settings.getValue('WHITELIST_NICS'))
731     settings.setValue('WHITELIST_NICS_ORIG', wl_nics_orig)
732
733     # sriov handling is performed on checked/expanded PCI IDs
734     settings.setValue('SRIOV_ENABLED', enable_sriov(wl_nics_orig))
735
736     nic_list = []
737     for nic in wl_nics_orig:
738         tmp_nic = networkcard.get_nic_info(nic)
739         if tmp_nic:
740             nic_list.append({'pci' : tmp_nic,
741                              'type' : 'vf' if networkcard.get_sriov_pf(tmp_nic) else 'pf',
742                              'mac' : networkcard.get_mac(tmp_nic),
743                              'driver' : networkcard.get_driver(tmp_nic),
744                              'device' : networkcard.get_device_name(tmp_nic)})
745         else:
746             vsperf_finalize()
747             raise RuntimeError("Invalid network card PCI ID: '{}'".format(nic))
748
749     settings.setValue('NICS', nic_list)
750     # for backward compatibility
751     settings.setValue('WHITELIST_NICS', list(nic['pci'] for nic in nic_list))
752
753     # generate results directory name
754     date = datetime.datetime.fromtimestamp(time.time())
755     results_dir = "results_" + date.strftime('%Y-%m-%d_%H-%M-%S')
756     results_path = os.path.join(settings.getValue('LOG_DIR'), results_dir)
757     settings.setValue('RESULTS_PATH', results_path)
758
759     # create results directory
760     if not os.path.exists(results_path):
761         _LOGGER.info("Creating result directory: "  + results_path)
762         os.makedirs(results_path)
763     # pylint: disable=too-many-nested-blocks
764     if settings.getValue('mode') == 'trafficgen':
765         # execute only traffic generator
766         _LOGGER.debug("Executing traffic generator:")
767         loader = Loader()
768         # set traffic details, so they can be passed to traffic ctl
769         traffic = copy.deepcopy(settings.getValue('TRAFFIC'))
770         traffic = functions.check_traffic(traffic)
771
772         traffic_ctl = component_factory.create_traffic(
773             traffic['traffic_type'],
774             loader.get_trafficgen_class())
775         with traffic_ctl:
776             traffic_ctl.send_traffic(traffic)
777         _LOGGER.debug("Traffic Results:")
778         traffic_ctl.print_results()
779
780         # write results into CSV file
781         result_file = os.path.join(results_path, "result.csv")
782         PerformanceTestCase.write_result_to_file(traffic_ctl.get_results(), result_file)
783     else:
784         # configure tests
785         if args['integration']:
786             testcases = settings.getValue('INTEGRATION_TESTS')
787         else:
788             testcases = settings.getValue('PERFORMANCE_TESTS')
789
790         if args['exact_test_name']:
791             exact_names = args['exact_test_name']
792             # positional args => exact matches only
793             selected_tests = []
794             for test_name in exact_names:
795                 for test in testcases:
796                     if test['Name'] == test_name:
797                         selected_tests.append(test)
798         elif args['tests']:
799             # --tests => apply filter to select requested tests
800             selected_tests = apply_filter(testcases, args['tests'])
801         else:
802             # Default - run all tests
803             selected_tests = testcases
804
805         if not len(selected_tests):
806             _LOGGER.error("No tests matched --tests option or positional args. Done.")
807             vsperf_finalize()
808             sys.exit(1)
809
810         # run tests
811         # Add pylint exception: Redefinition of test type from
812         # testcases.integration.IntegrationTestCase to testcases.performance.PerformanceTestCase
813         # pylint: disable=redefined-variable-type
814         suite = unittest.TestSuite()
815         settings_snapshot = copy.deepcopy(settings.__dict__)
816
817         for i, cfg in enumerate(selected_tests):
818             settings.setValue('_TEST_INDEX', i)
819             test_name = cfg.get('Name', '<Name not set>')
820             try:
821                 test_params = settings.getValue('_PARAMS_LIST')
822                 if isinstance(test_params, list):
823                     list_index = i
824                     if i >= len(test_params):
825                         list_index = len(test_params) - 1
826                     test_params = test_params[list_index]
827                 if settings.getValue('CUMULATIVE_PARAMS'):
828                     test_params = merge_spec(settings.getValue('TEST_PARAMS'), test_params)
829                 settings.setValue('TEST_PARAMS', test_params)
830
831                 if args['integration']:
832                     test = IntegrationTestCase(cfg)
833                 else:
834                     test = PerformanceTestCase(cfg)
835
836                 test.run()
837                 suite.addTest(MockTestCase('', True, test.name))
838
839             # pylint: disable=broad-except
840             except (Exception) as ex:
841                 _LOGGER.exception("Failed to run test: %s", test_name)
842                 suite.addTest(MockTestCase(str(ex), False, test_name))
843                 _LOGGER.info("Continuing with next test...")
844             finally:
845                 if not settings.getValue('CUMULATIVE_PARAMS'):
846                     settings.restore_from_dict(settings_snapshot)
847
848         settings.restore_from_dict(settings_snapshot)
849
850
851         # Generate and printout Performance Matrix
852         if args['matrix']:
853             generate_performance_matrix(selected_tests, results_path)
854
855         # generate final rst report with results of all executed TCs
856         generate_final_report()
857
858
859
860         if settings.getValue('XUNIT'):
861             xmlrunner.XMLTestRunner(
862                 output=settings.getValue('XUNIT_DIR'), outsuffix="",
863                 verbosity=0).run(suite)
864
865         if args['opnfvpod']:
866             pod_name = args['opnfvpod']
867             installer_name = str(settings.getValue('OPNFV_INSTALLER')).lower()
868             opnfv_url = settings.getValue('OPNFV_URL')
869             pkg_list = settings.getValue('PACKAGE_LIST')
870
871             int_data = {'pod': pod_name,
872                         'build_tag': get_build_tag(),
873                         'installer': installer_name,
874                         'pkg_list': pkg_list,
875                         'db_url': opnfv_url,
876                         # pass vswitch name from configuration to be used for failed
877                         # TCs; In case of successful TCs it is safer to use vswitch
878                         # name from CSV as TC can override global configuration
879                         'vswitch': str(settings.getValue('VSWITCH')).lower()}
880             tc_names = [tc['Name'] for tc in selected_tests]
881             opnfvdashboard.results2opnfv_dashboard(tc_names, results_path, int_data)
882
883     # cleanup before exit
884     vsperf_finalize()
885
886 if __name__ == "__main__":
887     main()