info: Updated key project facts
[vswitchperf.git] / vsperf
1 #!/usr/bin/env python3
2
3 # Copyright 2015-2016 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 datetime
27 import shutil
28 import unittest
29 import locale
30 import copy
31 import glob
32 import subprocess
33 import ast
34 import xmlrunner
35 from conf import settings
36 import core.component_factory as component_factory
37 from core.loader import Loader
38 from testcases import PerformanceTestCase
39 from testcases import IntegrationTestCase
40 from tools import tasks
41 from tools import networkcard
42 from tools import functions
43 from tools.pkt_gen import trafficgen
44 from tools.opnfvdashboard import opnfvdashboard
45
46 sys.dont_write_bytecode = True
47
48 VERBOSITY_LEVELS = {
49     'debug': logging.DEBUG,
50     'info': logging.INFO,
51     'warning': logging.WARNING,
52     'error': logging.ERROR,
53     'critical': logging.CRITICAL
54 }
55
56 _CURR_DIR = os.path.dirname(os.path.realpath(__file__))
57
58 _TEMPLATE_RST = {'head'  : os.path.join(_CURR_DIR, 'tools/report/report_head.rst'),
59                  'foot'  : os.path.join(_CURR_DIR, 'tools/report/report_foot.rst'),
60                  'final' : 'test_report.rst',
61                  'tmp'   : os.path.join(_CURR_DIR, 'tools/report/report_tmp_caption.rst')
62                 }
63
64
65 _LOGGER = logging.getLogger()
66
67 def parse_arguments():
68     """
69     Parse command line arguments.
70     """
71     class _SplitTestParamsAction(argparse.Action):
72         """
73         Parse and split the '--test-params' argument.
74
75         This expects either 'x=y', 'x=y,z' or 'x' (implicit true)
76         values. For multiple overrides use a ; separated list for
77         e.g. --test-params 'x=z; y=(a,b)'
78         """
79         def __call__(self, parser, namespace, values, option_string=None):
80             results = {}
81
82             for param, _, value in re.findall('([^;=]+)(=([^;]+))?', values):
83                 param = param.strip()
84                 value = value.strip()
85                 if len(param):
86                     if len(value):
87                         # values are passed inside string from CLI, so we must retype them accordingly
88                         try:
89                             results[param] = ast.literal_eval(value)
90                         except ValueError:
91                             # for backward compatibility, we have to accept strings without quotes
92                             _LOGGER.warning("Adding missing quotes around string value: %s = %s",
93                                             param, str(value))
94                             results[param] = str(value)
95                     else:
96                         results[param] = True
97
98             setattr(namespace, self.dest, results)
99
100     class _ValidateFileAction(argparse.Action):
101         """Validate a file can be read from before using it.
102         """
103         def __call__(self, parser, namespace, values, option_string=None):
104             if not os.path.isfile(values):
105                 raise argparse.ArgumentTypeError(
106                     'the path \'%s\' is not a valid path' % values)
107             elif not os.access(values, os.R_OK):
108                 raise argparse.ArgumentTypeError(
109                     'the path \'%s\' is not accessible' % values)
110
111             setattr(namespace, self.dest, values)
112
113     class _ValidateDirAction(argparse.Action):
114         """Validate a directory can be written to before using it.
115         """
116         def __call__(self, parser, namespace, values, option_string=None):
117             if not os.path.isdir(values):
118                 raise argparse.ArgumentTypeError(
119                     'the path \'%s\' is not a valid path' % values)
120             elif not os.access(values, os.W_OK):
121                 raise argparse.ArgumentTypeError(
122                     'the path \'%s\' is not accessible' % values)
123
124             setattr(namespace, self.dest, values)
125
126     def list_logging_levels():
127         """Give a summary of all available logging levels.
128
129         :return: List of verbosity level names in decreasing order of
130             verbosity
131         """
132         return sorted(VERBOSITY_LEVELS.keys(),
133                       key=lambda x: VERBOSITY_LEVELS[x])
134
135     parser = argparse.ArgumentParser(prog=__file__, formatter_class=
136                                      argparse.ArgumentDefaultsHelpFormatter)
137     parser.add_argument('--version', action='version', version='%(prog)s 0.2')
138     parser.add_argument('--list', '--list-tests', action='store_true',
139                         help='list all tests and exit')
140     parser.add_argument('--list-trafficgens', action='store_true',
141                         help='list all traffic generators and exit')
142     parser.add_argument('--list-collectors', action='store_true',
143                         help='list all system metrics loggers and exit')
144     parser.add_argument('--list-vswitches', action='store_true',
145                         help='list all system vswitches and exit')
146     parser.add_argument('--list-fwdapps', action='store_true',
147                         help='list all system forwarding applications and exit')
148     parser.add_argument('--list-vnfs', action='store_true',
149                         help='list all system vnfs and exit')
150     parser.add_argument('--list-settings', action='store_true',
151                         help='list effective settings configuration and exit')
152     parser.add_argument('exact_test_name', nargs='*', help='Exact names of\
153             tests to run. E.g "vsperf phy2phy_tput phy2phy_cont"\
154             runs only the two tests with those exact names.\
155             To run all tests omit both positional args and --tests arg.')
156
157     group = parser.add_argument_group('test selection options')
158     group.add_argument('-m', '--mode', help='vsperf mode of operation;\
159             Values: "normal" - execute vSwitch, VNF and traffic generator;\
160             "trafficgen" - execute only traffic generator; "trafficgen-off" \
161             - execute vSwitch and VNF; trafficgen-pause - execute vSwitch \
162             and VNF but pause before traffic transmission ', default='normal')
163
164     group.add_argument('-f', '--test-spec', help='test specification file')
165     group.add_argument('-d', '--test-dir', help='directory containing tests')
166     group.add_argument('-t', '--tests', help='Comma-separated list of terms \
167             indicating tests to run. e.g. "RFC2544,!p2p" - run all tests whose\
168             name contains RFC2544 less those containing "p2p"; "!back2back" - \
169             run all tests except those containing back2back')
170     group.add_argument('--verbosity', choices=list_logging_levels(),
171                        help='debug level')
172     group.add_argument('--integration', action='store_true', help='execute integration tests')
173     group.add_argument('--trafficgen', help='traffic generator to use')
174     group.add_argument('--vswitch', help='vswitch implementation to use')
175     group.add_argument('--fwdapp', help='packet forwarding application to use')
176     group.add_argument('--vnf', help='vnf to use')
177     group.add_argument('--sysmetrics', help='system metrics logger to use')
178     group = parser.add_argument_group('test behavior options')
179     group.add_argument('--xunit', action='store_true',
180                        help='enable xUnit-formatted output')
181     group.add_argument('--xunit-dir', action=_ValidateDirAction,
182                        help='output directory of xUnit-formatted output')
183     group.add_argument('--load-env', action='store_true',
184                        help='enable loading of settings from the environment')
185     group.add_argument('--conf-file', action=_ValidateFileAction,
186                        help='settings file')
187     group.add_argument('--test-params', action=_SplitTestParamsAction,
188                        help='csv list of test parameters: key=val; e.g. '
189                        'TRAFFICGEN_PKT_SIZES=(64,128);TRAFICGEN_DURATION=30; '
190                        'GUEST_LOOPBACK=["l2fwd"] ...')
191     group.add_argument('--opnfvpod', help='name of POD in opnfv')
192
193     args = vars(parser.parse_args())
194
195     return args
196
197
198 def configure_logging(level):
199     """Configure logging.
200     """
201     log_file_default = os.path.join(
202         settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_DEFAULT'))
203     log_file_host_cmds = os.path.join(
204         settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_HOST_CMDS'))
205     log_file_traffic_gen = os.path.join(
206         settings.getValue('LOG_DIR'),
207         settings.getValue('LOG_FILE_TRAFFIC_GEN'))
208
209     _LOGGER.setLevel(logging.DEBUG)
210
211     stream_logger = logging.StreamHandler(sys.stdout)
212     stream_logger.setLevel(VERBOSITY_LEVELS[level])
213     stream_logger.setFormatter(logging.Formatter(
214         '[%(levelname)-5s]  %(asctime)s : (%(name)s) - %(message)s'))
215     _LOGGER.addHandler(stream_logger)
216
217     file_logger = logging.FileHandler(filename=log_file_default)
218     file_logger.setLevel(logging.DEBUG)
219     _LOGGER.addHandler(file_logger)
220
221     class CommandFilter(logging.Filter):
222         """Filter out strings beginning with 'cmd :'"""
223         def filter(self, record):
224             return record.getMessage().startswith(tasks.CMD_PREFIX)
225
226     class TrafficGenCommandFilter(logging.Filter):
227         """Filter out strings beginning with 'gencmd :'"""
228         def filter(self, record):
229             return record.getMessage().startswith(trafficgen.CMD_PREFIX)
230
231     cmd_logger = logging.FileHandler(filename=log_file_host_cmds)
232     cmd_logger.setLevel(logging.DEBUG)
233     cmd_logger.addFilter(CommandFilter())
234     _LOGGER.addHandler(cmd_logger)
235
236     gen_logger = logging.FileHandler(filename=log_file_traffic_gen)
237     gen_logger.setLevel(logging.DEBUG)
238     gen_logger.addFilter(TrafficGenCommandFilter())
239     _LOGGER.addHandler(gen_logger)
240
241
242 def apply_filter(tests, tc_filter):
243     """Allow a subset of tests to be conveniently selected
244
245     :param tests: The list of Tests from which to select.
246     :param tc_filter: A case-insensitive string of comma-separated terms
247         indicating the Tests to select.
248         e.g. 'RFC' - select all tests whose name contains 'RFC'
249         e.g. 'RFC,burst' - select all tests whose name contains 'RFC' or
250             'burst'
251         e.g. 'RFC,burst,!p2p' - select all tests whose name contains 'RFC'
252             or 'burst' and from these remove any containing 'p2p'.
253         e.g. '' - empty string selects all tests.
254     :return: A list of the selected Tests.
255     """
256     # if negative filter is first we have to start with full list of tests
257     if tc_filter.strip()[0] == '!':
258         result = tests
259     else:
260         result = []
261     if tc_filter is None:
262         tc_filter = ""
263
264     for term in [x.strip() for x in tc_filter.lower().split(",")]:
265         if not term or term[0] != '!':
266             # Add matching tests from 'tests' into results
267             result.extend([test for test in tests \
268                 if test['Name'].lower().find(term) >= 0])
269         else:
270             # Term begins with '!' so we remove matching tests
271             result = [test for test in result \
272                 if test['Name'].lower().find(term[1:]) < 0]
273
274     return result
275
276
277 def check_and_set_locale():
278     """ Function will check locale settings. In case, that it isn't configured
279     properly, then default values specified by DEFAULT_LOCALE will be used.
280     """
281
282     system_locale = locale.getdefaultlocale()
283     if None in system_locale:
284         os.environ['LC_ALL'] = settings.getValue('DEFAULT_LOCALE')
285         _LOGGER.warning("Locale was not properly configured. Default values were set. Old locale: %s, New locale: %s",
286                         system_locale, locale.getdefaultlocale())
287
288
289 def generate_final_report():
290     """ Function will check if partial test results are available
291     and generates final report in rst format.
292     """
293
294     path = settings.getValue('RESULTS_PATH')
295     # check if there are any results in rst format
296     rst_results = glob.glob(os.path.join(path, 'result*rst'))
297     if len(rst_results):
298         try:
299             test_report = os.path.join(path, '{}_{}'.format(settings.getValue('VSWITCH'), _TEMPLATE_RST['final']))
300             # create report caption directly - it is not worth to execute jinja machinery
301             if settings.getValue('VSWITCH').lower() != 'none':
302                 pkt_processor = Loader().get_vswitches()[settings.getValue('VSWITCH')].__doc__.strip().split('\n')[0]
303             else:
304                 pkt_processor = Loader().get_pktfwds()[settings.getValue('PKTFWD')].__doc__.strip().split('\n')[0]
305             report_caption = '{}\n{} {}\n{}\n\n'.format(
306                 '============================================================',
307                 'Performance report for',
308                 pkt_processor,
309                 '============================================================')
310
311             with open(_TEMPLATE_RST['tmp'], 'w') as file_:
312                 file_.write(report_caption)
313
314             retval = subprocess.call('cat {} {} {} {} > {}'.format(_TEMPLATE_RST['tmp'], _TEMPLATE_RST['head'],
315                                                                    ' '.join(rst_results), _TEMPLATE_RST['foot'],
316                                                                    test_report), shell=True)
317             if retval == 0 and os.path.isfile(test_report):
318                 _LOGGER.info('Overall test report written to "%s"', test_report)
319             else:
320                 _LOGGER.error('Generatrion of overall test report has failed.')
321
322             # remove temporary file
323             os.remove(_TEMPLATE_RST['tmp'])
324
325         except subprocess.CalledProcessError:
326             _LOGGER.error('Generatrion of overall test report has failed.')
327
328
329 def enable_sriov(nic_list):
330     """ Enable SRIOV for given enhanced PCI IDs
331
332     :param nic_list: A list of enhanced PCI IDs
333     """
334     # detect if sriov is required
335     sriov_nic = {}
336     for nic in nic_list:
337         if networkcard.is_sriov_nic(nic):
338             tmp_nic = nic.split('|')
339             if tmp_nic[0] in sriov_nic:
340                 if int(tmp_nic[1][2:]) > sriov_nic[tmp_nic[0]]:
341                     sriov_nic[tmp_nic[0]] = int(tmp_nic[1][2:])
342             else:
343                 sriov_nic.update({tmp_nic[0] : int(tmp_nic[1][2:])})
344
345     # sriov is required for some NICs
346     if len(sriov_nic):
347         for nic in sriov_nic:
348             # check if SRIOV is supported and enough virt interfaces are available
349             if not networkcard.is_sriov_supported(nic) \
350                 or networkcard.get_sriov_numvfs(nic) <= sriov_nic[nic]:
351                 # if not, enable and set appropriate number of VFs
352                 if not networkcard.set_sriov_numvfs(nic, sriov_nic[nic] + 1):
353                     raise RuntimeError('SRIOV cannot be enabled for NIC {}'.format(nic))
354                 else:
355                     _LOGGER.debug("SRIOV enabled for NIC %s", nic)
356
357                 # WORKAROUND: it has been observed with IXGBE(VF) driver,
358                 # that NIC doesn't correclty dispatch traffic to VFs based
359                 # on their MAC address. Unbind and bind to the same driver
360                 # solves this issue.
361                 networkcard.reinit_vfs(nic)
362
363         # After SRIOV is enabled it takes some time until network drivers
364         # properly initialize all cards.
365         # Wait also in case, that SRIOV was already configured as it can be
366         # configured automatically just before vsperf execution.
367         time.sleep(2)
368
369         return True
370
371     return False
372
373
374 def disable_sriov(nic_list):
375     """ Disable SRIOV for given PCI IDs
376
377     :param nic_list: A list of enhanced PCI IDs
378     """
379     for nic in nic_list:
380         if networkcard.is_sriov_nic(nic):
381             if not networkcard.set_sriov_numvfs(nic.split('|')[0], 0):
382                 raise RuntimeError('SRIOV cannot be disabled for NIC {}'.format(nic))
383             else:
384                 _LOGGER.debug("SRIOV disabled for NIC %s", nic.split('|')[0])
385
386
387 def handle_list_options(args):
388     """ Process --list cli arguments if needed
389
390     :param args: A dictionary with all CLI arguments
391     """
392     if args['list_trafficgens']:
393         print(Loader().get_trafficgens_printable())
394         sys.exit(0)
395
396     if args['list_collectors']:
397         print(Loader().get_collectors_printable())
398         sys.exit(0)
399
400     if args['list_vswitches']:
401         print(Loader().get_vswitches_printable())
402         sys.exit(0)
403
404     if args['list_vnfs']:
405         print(Loader().get_vnfs_printable())
406         sys.exit(0)
407
408     if args['list_fwdapps']:
409         print(Loader().get_pktfwds_printable())
410         sys.exit(0)
411
412     if args['list_settings']:
413         print(str(settings))
414         sys.exit(0)
415
416     if args['list']:
417         # configure tests
418         if args['integration']:
419             testcases = settings.getValue('INTEGRATION_TESTS')
420         else:
421             testcases = settings.getValue('PERFORMANCE_TESTS')
422
423         print("Available Tests:")
424         print("================")
425
426         for test in testcases:
427             print('* %-30s %s' % ('%s:' % test['Name'], test['Description']))
428         sys.exit(0)
429
430
431 def vsperf_finalize():
432     """ Clean up before exit
433     """
434     # remove directory if no result files were created
435     try:
436         results_path = settings.getValue('RESULTS_PATH')
437         if os.path.exists(results_path):
438             files_list = os.listdir(results_path)
439             if files_list == []:
440                 _LOGGER.info("Removing empty result directory: "  + results_path)
441                 shutil.rmtree(results_path)
442     except AttributeError:
443         # skip it if parameter doesn't exist
444         pass
445
446     # disable SRIOV if needed
447     try:
448         if settings.getValue('SRIOV_ENABLED'):
449             disable_sriov(settings.getValue('WHITELIST_NICS_ORIG'))
450     except AttributeError:
451         # skip it if parameter doesn't exist
452         pass
453
454
455 class MockTestCase(unittest.TestCase):
456     """Allow use of xmlrunner to generate Jenkins compatible output without
457     using xmlrunner to actually run tests.
458
459     Usage:
460         suite = unittest.TestSuite()
461         suite.addTest(MockTestCase('Test1 passed ', True, 'Test1'))
462         suite.addTest(MockTestCase('Test2 failed because...', False, 'Test2'))
463         xmlrunner.XMLTestRunner(...).run(suite)
464     """
465
466     def __init__(self, msg, is_pass, test_name):
467         #remember the things
468         self.msg = msg
469         self.is_pass = is_pass
470
471         #dynamically create a test method with the right name
472         #but point the method at our generic test method
473         setattr(MockTestCase, test_name, self.generic_test)
474
475         super(MockTestCase, self).__init__(test_name)
476
477     def generic_test(self):
478         """Provide a generic function that raises or not based
479         on how self.is_pass was set in the constructor"""
480         self.assertTrue(self.is_pass, self.msg)
481
482
483 def main():
484     """Main function.
485     """
486     args = parse_arguments()
487
488     # configure settings
489
490     settings.load_from_dir(os.path.join(_CURR_DIR, 'conf'))
491
492     # Load non performance/integration tests
493     if args['integration']:
494         settings.load_from_dir(os.path.join(_CURR_DIR, 'conf/integration'))
495
496     # load command line parameters first in case there are settings files
497     # to be used
498     settings.load_from_dict(args)
499
500     if args['conf_file']:
501         settings.load_from_file(args['conf_file'])
502
503     if args['load_env']:
504         settings.load_from_env()
505
506     # reload command line parameters since these should take higher priority
507     # than both a settings file and environment variables
508     settings.load_from_dict(args)
509
510     settings.setValue('mode', args['mode'])
511
512     # set dpdk and ovs paths according to VNF and VSWITCH
513     if settings.getValue('mode') != 'trafficgen':
514         functions.settings_update_paths()
515
516     # if required, handle list-* operations
517     handle_list_options(args)
518
519     configure_logging(settings.getValue('VERBOSITY'))
520
521     # check and fix locale
522     check_and_set_locale()
523
524     # configure trafficgens
525     if args['trafficgen']:
526         trafficgens = Loader().get_trafficgens()
527         if args['trafficgen'] not in trafficgens:
528             _LOGGER.error('There are no trafficgens matching \'%s\' found in'
529                           ' \'%s\'. Exiting...', args['trafficgen'],
530                           settings.getValue('TRAFFICGEN_DIR'))
531             sys.exit(1)
532
533     # configuration validity checks
534     if args['vswitch']:
535         vswitch_none = 'none' == args['vswitch'].strip().lower()
536         if vswitch_none:
537             settings.setValue('VSWITCH', 'none')
538         else:
539             vswitches = Loader().get_vswitches()
540             if args['vswitch'] not in vswitches:
541                 _LOGGER.error('There are no vswitches matching \'%s\' found in'
542                               ' \'%s\'. Exiting...', args['vswitch'],
543                               settings.getValue('VSWITCH_DIR'))
544                 sys.exit(1)
545
546     if args['fwdapp']:
547         settings.setValue('PKTFWD', args['fwdapp'])
548         fwdapps = Loader().get_pktfwds()
549         if args['fwdapp'] not in fwdapps:
550             _LOGGER.error('There are no forwarding application'
551                           ' matching \'%s\' found in'
552                           ' \'%s\'. Exiting...', args['fwdapp'],
553                           settings.getValue('PKTFWD_DIR'))
554             sys.exit(1)
555
556     if args['vnf']:
557         vnfs = Loader().get_vnfs()
558         if args['vnf'] not in vnfs:
559             _LOGGER.error('there are no vnfs matching \'%s\' found in'
560                           ' \'%s\'. exiting...', args['vnf'],
561                           settings.getValue('VNF_DIR'))
562             sys.exit(1)
563
564     if args['exact_test_name'] and args['tests']:
565         _LOGGER.error("Cannot specify tests with both positional args and --test.")
566         sys.exit(1)
567
568     # modify NIC configuration to decode enhanced PCI IDs
569     wl_nics_orig = list(networkcard.check_pci(pci) for pci in settings.getValue('WHITELIST_NICS'))
570     settings.setValue('WHITELIST_NICS_ORIG', wl_nics_orig)
571
572     # sriov handling is performed on checked/expanded PCI IDs
573     settings.setValue('SRIOV_ENABLED', enable_sriov(wl_nics_orig))
574
575     nic_list = []
576     for nic in wl_nics_orig:
577         tmp_nic = networkcard.get_nic_info(nic)
578         if tmp_nic:
579             nic_list.append({'pci' : tmp_nic,
580                              'type' : 'vf' if networkcard.get_sriov_pf(tmp_nic) else 'pf',
581                              'mac' : networkcard.get_mac(tmp_nic),
582                              'driver' : networkcard.get_driver(tmp_nic),
583                              'device' : networkcard.get_device_name(tmp_nic)})
584         else:
585             vsperf_finalize()
586             raise RuntimeError("Invalid network card PCI ID: '{}'".format(nic))
587
588     settings.setValue('NICS', nic_list)
589     # for backward compatibility
590     settings.setValue('WHITELIST_NICS', list(nic['pci'] for nic in nic_list))
591
592     # generate results directory name
593     date = datetime.datetime.fromtimestamp(time.time())
594     results_dir = "results_" + date.strftime('%Y-%m-%d_%H-%M-%S')
595     results_path = os.path.join(settings.getValue('LOG_DIR'), results_dir)
596     settings.setValue('RESULTS_PATH', results_path)
597
598     # create results directory
599     if not os.path.exists(results_path):
600         _LOGGER.info("Creating result directory: "  + results_path)
601         os.makedirs(results_path)
602
603     if settings.getValue('mode') == 'trafficgen':
604         # execute only traffic generator
605         _LOGGER.debug("Executing traffic generator:")
606         loader = Loader()
607         # set traffic details, so they can be passed to traffic ctl
608         traffic = copy.deepcopy(settings.getValue('TRAFFIC'))
609
610         traffic_ctl = component_factory.create_traffic(
611             traffic['traffic_type'],
612             loader.get_trafficgen_class())
613         with traffic_ctl:
614             traffic_ctl.send_traffic(traffic)
615         _LOGGER.debug("Traffic Results:")
616         traffic_ctl.print_results()
617
618         # write results into CSV file
619         result_file = os.path.join(results_path, "result.csv")
620         PerformanceTestCase.write_result_to_file(traffic_ctl.get_results(), result_file)
621     else:
622         # configure tests
623         if args['integration']:
624             testcases = settings.getValue('INTEGRATION_TESTS')
625         else:
626             testcases = settings.getValue('PERFORMANCE_TESTS')
627
628         if args['exact_test_name']:
629             exact_names = args['exact_test_name']
630             # positional args => exact matches only
631             selected_tests = [test for test in testcases if test['Name'] in exact_names]
632         elif args['tests']:
633             # --tests => apply filter to select requested tests
634             selected_tests = apply_filter(testcases, args['tests'])
635         else:
636             # Default - run all tests
637             selected_tests = testcases
638
639         if not len(selected_tests):
640             _LOGGER.error("No tests matched --tests option or positional args. Done.")
641             vsperf_finalize()
642             sys.exit(1)
643
644         # run tests
645         suite = unittest.TestSuite()
646         for cfg in selected_tests:
647             test_name = cfg.get('Name', '<Name not set>')
648             try:
649                 if args['integration']:
650                     test = IntegrationTestCase(cfg)
651                 else:
652                     test = PerformanceTestCase(cfg)
653                 test.run()
654                 suite.addTest(MockTestCase('', True, test.name))
655             #pylint: disable=broad-except
656             except (Exception) as ex:
657                 _LOGGER.exception("Failed to run test: %s", test_name)
658                 suite.addTest(MockTestCase(str(ex), False, test_name))
659                 _LOGGER.info("Continuing with next test...")
660
661         # generate final rst report with results of all executed TCs
662         generate_final_report()
663
664         if settings.getValue('XUNIT'):
665             xmlrunner.XMLTestRunner(
666                 output=settings.getValue('XUNIT_DIR'), outsuffix="",
667                 verbosity=0).run(suite)
668
669         if args['opnfvpod']:
670             pod_name = args['opnfvpod']
671             installer_name = settings.getValue('OPNFV_INSTALLER')
672             opnfv_url = settings.getValue('OPNFV_URL')
673             pkg_list = settings.getValue('PACKAGE_LIST')
674
675             int_data = {'vanilla': False,
676                         'pod': pod_name,
677                         'installer': installer_name,
678                         'pkg_list': pkg_list,
679                         'db_url': opnfv_url}
680             if settings.getValue('VSWITCH').endswith('Vanilla'):
681                 int_data['vanilla'] = True
682             opnfvdashboard.results2opnfv_dashboard(results_path, int_data)
683
684     # cleanup before exit
685     vsperf_finalize()
686
687 if __name__ == "__main__":
688     main()