documentation: Installation prerequisites
[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 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-loadgens', action='store_true',
151                         help='list all background load generators')
152     parser.add_argument('--list-settings', action='store_true',
153                         help='list effective settings configuration and exit')
154     parser.add_argument('exact_test_name', nargs='*', help='Exact names of\
155             tests to run. E.g "vsperf phy2phy_tput phy2phy_cont"\
156             runs only the two tests with those exact names.\
157             To run all tests omit both positional args and --tests arg.')
158
159     group = parser.add_argument_group('test selection options')
160     group.add_argument('-m', '--mode', help='vsperf mode of operation;\
161             Values: "normal" - execute vSwitch, VNF and traffic generator;\
162             "trafficgen" - execute only traffic generator; "trafficgen-off" \
163             - execute vSwitch and VNF; trafficgen-pause - execute vSwitch \
164             and VNF but pause before traffic transmission ', default='normal')
165
166     group.add_argument('-f', '--test-spec', help='test specification file')
167     group.add_argument('-d', '--test-dir', help='directory containing tests')
168     group.add_argument('-t', '--tests', help='Comma-separated list of terms \
169             indicating tests to run. e.g. "RFC2544,!p2p" - run all tests whose\
170             name contains RFC2544 less those containing "p2p"; "!back2back" - \
171             run all tests except those containing back2back')
172     group.add_argument('--verbosity', choices=list_logging_levels(),
173                        help='debug level')
174     group.add_argument('--integration', action='store_true', help='execute integration tests')
175     group.add_argument('--trafficgen', help='traffic generator to use')
176     group.add_argument('--vswitch', help='vswitch implementation to use')
177     group.add_argument('--fwdapp', help='packet forwarding application to use')
178     group.add_argument('--vnf', help='vnf to use')
179     group.add_argument('--loadgen', help='loadgen to use')
180     group.add_argument('--sysmetrics', help='system metrics logger to use')
181     group = parser.add_argument_group('test behavior options')
182     group.add_argument('--xunit', action='store_true',
183                        help='enable xUnit-formatted output')
184     group.add_argument('--xunit-dir', action=_ValidateDirAction,
185                        help='output directory of xUnit-formatted output')
186     group.add_argument('--load-env', action='store_true',
187                        help='enable loading of settings from the environment')
188     group.add_argument('--conf-file', action=_ValidateFileAction,
189                        help='settings file')
190     group.add_argument('--test-params', action=_SplitTestParamsAction,
191                        help='csv list of test parameters: key=val; e.g. '
192                        'TRAFFICGEN_PKT_SIZES=(64,128);TRAFICGEN_DURATION=30; '
193                        'GUEST_LOOPBACK=["l2fwd"] ...')
194     group.add_argument('--opnfvpod', help='name of POD in opnfv')
195
196     args = vars(parser.parse_args())
197
198     return args
199
200
201 def configure_logging(level):
202     """Configure logging.
203     """
204     log_file_default = os.path.join(
205         settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_DEFAULT'))
206     log_file_host_cmds = os.path.join(
207         settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_HOST_CMDS'))
208     log_file_traffic_gen = os.path.join(
209         settings.getValue('LOG_DIR'),
210         settings.getValue('LOG_FILE_TRAFFIC_GEN'))
211
212     _LOGGER.setLevel(logging.DEBUG)
213
214     stream_logger = logging.StreamHandler(sys.stdout)
215     stream_logger.setLevel(VERBOSITY_LEVELS[level])
216     stream_logger.setFormatter(logging.Formatter(
217         '[%(levelname)-5s]  %(asctime)s : (%(name)s) - %(message)s'))
218     _LOGGER.addHandler(stream_logger)
219
220     file_logger = logging.FileHandler(filename=log_file_default)
221     file_logger.setLevel(logging.DEBUG)
222     _LOGGER.addHandler(file_logger)
223
224     class CommandFilter(logging.Filter):
225         """Filter out strings beginning with 'cmd :'"""
226         def filter(self, record):
227             return record.getMessage().startswith(tasks.CMD_PREFIX)
228
229     class TrafficGenCommandFilter(logging.Filter):
230         """Filter out strings beginning with 'gencmd :'"""
231         def filter(self, record):
232             return record.getMessage().startswith(trafficgen.CMD_PREFIX)
233
234     cmd_logger = logging.FileHandler(filename=log_file_host_cmds)
235     cmd_logger.setLevel(logging.DEBUG)
236     cmd_logger.addFilter(CommandFilter())
237     _LOGGER.addHandler(cmd_logger)
238
239     gen_logger = logging.FileHandler(filename=log_file_traffic_gen)
240     gen_logger.setLevel(logging.DEBUG)
241     gen_logger.addFilter(TrafficGenCommandFilter())
242     _LOGGER.addHandler(gen_logger)
243
244
245 def apply_filter(tests, tc_filter):
246     """Allow a subset of tests to be conveniently selected
247
248     :param tests: The list of Tests from which to select.
249     :param tc_filter: A case-insensitive string of comma-separated terms
250         indicating the Tests to select.
251         e.g. 'RFC' - select all tests whose name contains 'RFC'
252         e.g. 'RFC,burst' - select all tests whose name contains 'RFC' or
253             'burst'
254         e.g. 'RFC,burst,!p2p' - select all tests whose name contains 'RFC'
255             or 'burst' and from these remove any containing 'p2p'.
256         e.g. '' - empty string selects all tests.
257     :return: A list of the selected Tests.
258     """
259     # if negative filter is first we have to start with full list of tests
260     if tc_filter.strip()[0] == '!':
261         result = tests
262     else:
263         result = []
264     if tc_filter is None:
265         tc_filter = ""
266
267     for term in [x.strip() for x in tc_filter.lower().split(",")]:
268         if not term or term[0] != '!':
269             # Add matching tests from 'tests' into results
270             result.extend([test for test in tests \
271                 if test['Name'].lower().find(term) >= 0])
272         else:
273             # Term begins with '!' so we remove matching tests
274             result = [test for test in result \
275                 if test['Name'].lower().find(term[1:]) < 0]
276
277     return result
278
279
280 def check_and_set_locale():
281     """ Function will check locale settings. In case, that it isn't configured
282     properly, then default values specified by DEFAULT_LOCALE will be used.
283     """
284
285     system_locale = locale.getdefaultlocale()
286     if None in system_locale:
287         os.environ['LC_ALL'] = settings.getValue('DEFAULT_LOCALE')
288         _LOGGER.warning("Locale was not properly configured. Default values were set. Old locale: %s, New locale: %s",
289                         system_locale, locale.getdefaultlocale())
290
291 def get_vswitch_names(rst_files):
292     """ Function will return a list of vSwitches detected in given ``rst_files``.
293     """
294     vswitch_names = set()
295     if len(rst_files):
296         try:
297             output = subprocess.check_output(['grep', '-h', '^* vSwitch'] + rst_files).decode().splitlines()
298             for line in output:
299                 match = re.search(r'^\* vSwitch: ([^,]+)', str(line))
300                 if match:
301                     vswitch_names.add(match.group(1))
302
303             if len(vswitch_names):
304                 return list(vswitch_names)
305
306         except subprocess.CalledProcessError:
307             _LOGGER.warning('Cannot detect vSwitches used during testing.')
308
309     # fallback to the default value
310     return ['vSwitch']
311
312 def get_build_tag():
313     """ Function will return a Jenkins job ID environment variable.
314     """
315
316     try:
317         build_tag = os.environ['BUILD_TAG']
318
319     except KeyError:
320         _LOGGER.warning('Cannot detect Jenkins job ID')
321         build_tag = "none"
322
323     return build_tag
324
325 def generate_final_report():
326     """ Function will check if partial test results are available
327     and generates final report in rst format.
328     """
329
330     path = settings.getValue('RESULTS_PATH')
331     # check if there are any results in rst format
332     rst_results = glob.glob(os.path.join(path, 'result*rst'))
333     pkt_processors = get_vswitch_names(rst_results)
334     if len(rst_results):
335         try:
336             test_report = os.path.join(path, '{}_{}'.format('_'.join(pkt_processors), _TEMPLATE_RST['final']))
337             # create report caption directly - it is not worth to execute jinja machinery
338             report_caption = '{}\n{} {}\n{}\n\n'.format(
339                 '============================================================',
340                 'Performance report for',
341                 ', '.join(pkt_processors),
342                 '============================================================')
343
344             with open(_TEMPLATE_RST['tmp'], 'w') as file_:
345                 file_.write(report_caption)
346
347             retval = subprocess.call('cat {} {} {} {} > {}'.format(_TEMPLATE_RST['tmp'], _TEMPLATE_RST['head'],
348                                                                    ' '.join(rst_results), _TEMPLATE_RST['foot'],
349                                                                    test_report), shell=True)
350             if retval == 0 and os.path.isfile(test_report):
351                 _LOGGER.info('Overall test report written to "%s"', test_report)
352             else:
353                 _LOGGER.error('Generation of overall test report has failed.')
354
355             # remove temporary file
356             os.remove(_TEMPLATE_RST['tmp'])
357
358         except subprocess.CalledProcessError:
359             _LOGGER.error('Generatrion of overall test report has failed.')
360
361
362 def enable_sriov(nic_list):
363     """ Enable SRIOV for given enhanced PCI IDs
364
365     :param nic_list: A list of enhanced PCI IDs
366     """
367     # detect if sriov is required
368     sriov_nic = {}
369     for nic in nic_list:
370         if networkcard.is_sriov_nic(nic):
371             tmp_nic = nic.split('|')
372             if tmp_nic[0] in sriov_nic:
373                 if int(tmp_nic[1][2:]) > sriov_nic[tmp_nic[0]]:
374                     sriov_nic[tmp_nic[0]] = int(tmp_nic[1][2:])
375             else:
376                 sriov_nic.update({tmp_nic[0] : int(tmp_nic[1][2:])})
377
378     # sriov is required for some NICs
379     if len(sriov_nic):
380         for nic in sriov_nic:
381             # check if SRIOV is supported and enough virt interfaces are available
382             if not networkcard.is_sriov_supported(nic) \
383                 or networkcard.get_sriov_numvfs(nic) <= sriov_nic[nic]:
384                 # if not, enable and set appropriate number of VFs
385                 if not networkcard.set_sriov_numvfs(nic, sriov_nic[nic] + 1):
386                     raise RuntimeError('SRIOV cannot be enabled for NIC {}'.format(nic))
387                 else:
388                     _LOGGER.debug("SRIOV enabled for NIC %s", nic)
389
390                 # ensure that path to the bind tool is valid
391                 functions.settings_update_paths()
392
393                 # WORKAROUND: it has been observed with IXGBE(VF) driver,
394                 # that NIC doesn't correclty dispatch traffic to VFs based
395                 # on their MAC address. Unbind and bind to the same driver
396                 # solves this issue.
397                 networkcard.reinit_vfs(nic)
398
399         # After SRIOV is enabled it takes some time until network drivers
400         # properly initialize all cards.
401         # Wait also in case, that SRIOV was already configured as it can be
402         # configured automatically just before vsperf execution.
403         time.sleep(2)
404
405         return True
406
407     return False
408
409
410 def disable_sriov(nic_list):
411     """ Disable SRIOV for given PCI IDs
412
413     :param nic_list: A list of enhanced PCI IDs
414     """
415     for nic in nic_list:
416         if networkcard.is_sriov_nic(nic):
417             if not networkcard.set_sriov_numvfs(nic.split('|')[0], 0):
418                 raise RuntimeError('SRIOV cannot be disabled for NIC {}'.format(nic))
419             else:
420                 _LOGGER.debug("SRIOV disabled for NIC %s", nic.split('|')[0])
421
422
423 def handle_list_options(args):
424     """ Process --list cli arguments if needed
425
426     :param args: A dictionary with all CLI arguments
427     """
428     if args['list_trafficgens']:
429         print(Loader().get_trafficgens_printable())
430         sys.exit(0)
431
432     if args['list_collectors']:
433         print(Loader().get_collectors_printable())
434         sys.exit(0)
435
436     if args['list_vswitches']:
437         print(Loader().get_vswitches_printable())
438         sys.exit(0)
439
440     if args['list_vnfs']:
441         print(Loader().get_vnfs_printable())
442         sys.exit(0)
443
444     if args['list_fwdapps']:
445         print(Loader().get_pktfwds_printable())
446         sys.exit(0)
447
448     if args['list_loadgens']:
449         print(Loader().get_loadgens_printable())
450         sys.exit(0)
451
452     if args['list_settings']:
453         print(str(settings))
454         sys.exit(0)
455
456     if args['list']:
457         list_testcases(args)
458         sys.exit(0)
459
460
461 def list_testcases(args):
462     """ Print list of testcases requested by --list CLI argument
463
464     :param args: A dictionary with all CLI arguments
465     """
466     # configure tests
467     if args['integration']:
468         testcases = settings.getValue('INTEGRATION_TESTS')
469     else:
470         testcases = settings.getValue('PERFORMANCE_TESTS')
471
472     print("Available Tests:")
473     print("================")
474
475     for test in testcases:
476         description = functions.format_description(test['Description'], 70)
477         if len(test['Name']) < 40:
478             print('* {:40} {}'.format('{}:'.format(test['Name']), description[0]))
479         else:
480             print('* {}'.format('{}:'.format(test['Name'])))
481             print('  {:40} {}'.format('', description[0]))
482         for i in range(1, len(description)):
483             print('  {:40} {}'.format('', description[i]))
484
485
486
487
488
489 def vsperf_finalize():
490     """ Clean up before exit
491     """
492     # remove directory if no result files were created
493     try:
494         results_path = settings.getValue('RESULTS_PATH')
495         if os.path.exists(results_path):
496             files_list = os.listdir(results_path)
497             if files_list == []:
498                 _LOGGER.info("Removing empty result directory: "  + results_path)
499                 shutil.rmtree(results_path)
500     except AttributeError:
501         # skip it if parameter doesn't exist
502         pass
503
504     # disable SRIOV if needed
505     try:
506         if settings.getValue('SRIOV_ENABLED'):
507             disable_sriov(settings.getValue('WHITELIST_NICS_ORIG'))
508     except AttributeError:
509         # skip it if parameter doesn't exist
510         pass
511
512
513 class MockTestCase(unittest.TestCase):
514     """Allow use of xmlrunner to generate Jenkins compatible output without
515     using xmlrunner to actually run tests.
516
517     Usage:
518         suite = unittest.TestSuite()
519         suite.addTest(MockTestCase('Test1 passed ', True, 'Test1'))
520         suite.addTest(MockTestCase('Test2 failed because...', False, 'Test2'))
521         xmlrunner.XMLTestRunner(...).run(suite)
522     """
523
524     def __init__(self, msg, is_pass, test_name):
525         #remember the things
526         self.msg = msg
527         self.is_pass = is_pass
528
529         #dynamically create a test method with the right name
530         #but point the method at our generic test method
531         setattr(MockTestCase, test_name, self.generic_test)
532
533         super(MockTestCase, self).__init__(test_name)
534
535     def generic_test(self):
536         """Provide a generic function that raises or not based
537         on how self.is_pass was set in the constructor"""
538         self.assertTrue(self.is_pass, self.msg)
539
540 # pylint: disable=too-many-locals, too-many-branches, too-many-statements
541 def main():
542     """Main function.
543     """
544     args = parse_arguments()
545
546     # configure settings
547
548     settings.load_from_dir(os.path.join(_CURR_DIR, 'conf'))
549
550     # Load non performance/integration tests
551     if args['integration']:
552         settings.load_from_dir(os.path.join(_CURR_DIR, 'conf/integration'))
553
554     # load command line parameters first in case there are settings files
555     # to be used
556     settings.load_from_dict(args)
557
558     if args['conf_file']:
559         settings.load_from_file(args['conf_file'])
560
561     if args['load_env']:
562         settings.load_from_env()
563
564     # reload command line parameters since these should take higher priority
565     # than both a settings file and environment variables
566     settings.load_from_dict(args)
567
568     settings.setValue('mode', args['mode'])
569
570     # update paths to trafficgens if required
571     if settings.getValue('mode') == 'trafficgen':
572         functions.settings_update_paths()
573
574     # if required, handle list-* operations
575     handle_list_options(args)
576
577     configure_logging(settings.getValue('VERBOSITY'))
578
579     # check and fix locale
580     check_and_set_locale()
581
582     # configure trafficgens
583     if args['trafficgen']:
584         trafficgens = Loader().get_trafficgens()
585         if args['trafficgen'] not in trafficgens:
586             _LOGGER.error('There are no trafficgens matching \'%s\' found in'
587                           ' \'%s\'. Exiting...', args['trafficgen'],
588                           settings.getValue('TRAFFICGEN_DIR'))
589             sys.exit(1)
590
591     # configuration validity checks
592     if args['vswitch']:
593         vswitch_none = args['vswitch'].strip().lower() == 'none'
594         if vswitch_none:
595             settings.setValue('VSWITCH', 'none')
596         else:
597             vswitches = Loader().get_vswitches()
598             if args['vswitch'] not in vswitches:
599                 _LOGGER.error('There are no vswitches matching \'%s\' found in'
600                               ' \'%s\'. Exiting...', args['vswitch'],
601                               settings.getValue('VSWITCH_DIR'))
602                 sys.exit(1)
603
604     if args['fwdapp']:
605         settings.setValue('PKTFWD', args['fwdapp'])
606         fwdapps = Loader().get_pktfwds()
607         if args['fwdapp'] not in fwdapps:
608             _LOGGER.error('There are no forwarding application'
609                           ' matching \'%s\' found in'
610                           ' \'%s\'. Exiting...', args['fwdapp'],
611                           settings.getValue('PKTFWD_DIR'))
612             sys.exit(1)
613
614     if args['vnf']:
615         vnfs = Loader().get_vnfs()
616         if args['vnf'] not in vnfs:
617             _LOGGER.error('there are no vnfs matching \'%s\' found in'
618                           ' \'%s\'. exiting...', args['vnf'],
619                           settings.getValue('VNF_DIR'))
620             sys.exit(1)
621
622     if args['loadgen']:
623         loadgens = Loader().get_loadgens()
624         if args['loadgen'] not in loadgens:
625             _LOGGER.error('There are no loadgens matching \'%s\' found in'
626                           ' \'%s\'. Exiting...', args['loadgen'],
627                           settings.getValue('LOADGEN_DIR'))
628             sys.exit(1)
629
630     if args['exact_test_name'] and args['tests']:
631         _LOGGER.error("Cannot specify tests with both positional args and --test.")
632         sys.exit(1)
633
634     # modify NIC configuration to decode enhanced PCI IDs
635     wl_nics_orig = list(networkcard.check_pci(pci) for pci in settings.getValue('WHITELIST_NICS'))
636     settings.setValue('WHITELIST_NICS_ORIG', wl_nics_orig)
637
638     # sriov handling is performed on checked/expanded PCI IDs
639     settings.setValue('SRIOV_ENABLED', enable_sriov(wl_nics_orig))
640
641     nic_list = []
642     for nic in wl_nics_orig:
643         tmp_nic = networkcard.get_nic_info(nic)
644         if tmp_nic:
645             nic_list.append({'pci' : tmp_nic,
646                              'type' : 'vf' if networkcard.get_sriov_pf(tmp_nic) else 'pf',
647                              'mac' : networkcard.get_mac(tmp_nic),
648                              'driver' : networkcard.get_driver(tmp_nic),
649                              'device' : networkcard.get_device_name(tmp_nic)})
650         else:
651             vsperf_finalize()
652             raise RuntimeError("Invalid network card PCI ID: '{}'".format(nic))
653
654     settings.setValue('NICS', nic_list)
655     # for backward compatibility
656     settings.setValue('WHITELIST_NICS', list(nic['pci'] for nic in nic_list))
657
658     # generate results directory name
659     date = datetime.datetime.fromtimestamp(time.time())
660     results_dir = "results_" + date.strftime('%Y-%m-%d_%H-%M-%S')
661     results_path = os.path.join(settings.getValue('LOG_DIR'), results_dir)
662     settings.setValue('RESULTS_PATH', results_path)
663
664     # create results directory
665     if not os.path.exists(results_path):
666         _LOGGER.info("Creating result directory: "  + results_path)
667         os.makedirs(results_path)
668
669     if settings.getValue('mode') == 'trafficgen':
670         # execute only traffic generator
671         _LOGGER.debug("Executing traffic generator:")
672         loader = Loader()
673         # set traffic details, so they can be passed to traffic ctl
674         traffic = copy.deepcopy(settings.getValue('TRAFFIC'))
675
676         traffic = functions.check_traffic(traffic)
677
678         traffic_ctl = component_factory.create_traffic(
679             traffic['traffic_type'],
680             loader.get_trafficgen_class())
681         with traffic_ctl:
682             traffic_ctl.send_traffic(traffic)
683         _LOGGER.debug("Traffic Results:")
684         traffic_ctl.print_results()
685
686         # write results into CSV file
687         result_file = os.path.join(results_path, "result.csv")
688         PerformanceTestCase.write_result_to_file(traffic_ctl.get_results(), result_file)
689     else:
690         # configure tests
691         if args['integration']:
692             testcases = settings.getValue('INTEGRATION_TESTS')
693         else:
694             testcases = settings.getValue('PERFORMANCE_TESTS')
695
696         if args['exact_test_name']:
697             exact_names = args['exact_test_name']
698             # positional args => exact matches only
699             selected_tests = [test for test in testcases if test['Name'] in exact_names]
700         elif args['tests']:
701             # --tests => apply filter to select requested tests
702             selected_tests = apply_filter(testcases, args['tests'])
703         else:
704             # Default - run all tests
705             selected_tests = testcases
706
707         if not len(selected_tests):
708             _LOGGER.error("No tests matched --tests option or positional args. Done.")
709             vsperf_finalize()
710             sys.exit(1)
711
712         # run tests
713         # Add pylint exception: Redefinition of test type from
714         # testcases.integration.IntegrationTestCase to testcases.performance.PerformanceTestCase
715         # pylint: disable=redefined-variable-type
716         suite = unittest.TestSuite()
717         settings_snapshot = copy.deepcopy(settings.__dict__)
718         for cfg in selected_tests:
719             test_name = cfg.get('Name', '<Name not set>')
720             try:
721                 if args['integration']:
722                     test = IntegrationTestCase(cfg)
723                 else:
724                     test = PerformanceTestCase(cfg)
725                 test.run()
726                 suite.addTest(MockTestCase('', True, test.name))
727             # pylint: disable=broad-except
728             except (Exception) as ex:
729                 _LOGGER.exception("Failed to run test: %s", test_name)
730                 suite.addTest(MockTestCase(str(ex), False, test_name))
731                 _LOGGER.info("Continuing with next test...")
732             finally:
733                 settings.restore_from_dict(settings_snapshot)
734
735         # generate final rst report with results of all executed TCs
736         generate_final_report()
737
738         if settings.getValue('XUNIT'):
739             xmlrunner.XMLTestRunner(
740                 output=settings.getValue('XUNIT_DIR'), outsuffix="",
741                 verbosity=0).run(suite)
742
743         if args['opnfvpod']:
744             pod_name = args['opnfvpod']
745             installer_name = str(settings.getValue('OPNFV_INSTALLER')).lower()
746             opnfv_url = settings.getValue('OPNFV_URL')
747             pkg_list = settings.getValue('PACKAGE_LIST')
748
749             int_data = {'pod': pod_name,
750                         'build_tag': get_build_tag(),
751                         'installer': installer_name,
752                         'pkg_list': pkg_list,
753                         'db_url': opnfv_url,
754                         # pass vswitch name from configuration to be used for failed
755                         # TCs; In case of successful TCs it is safer to use vswitch
756                         # name from CSV as TC can override global configuration
757                         'vswitch': str(settings.getValue('VSWITCH')).lower()}
758             tc_names = [tc['Name'] for tc in selected_tests]
759             opnfvdashboard.results2opnfv_dashboard(tc_names, results_path, int_data)
760
761     # cleanup before exit
762     vsperf_finalize()
763
764 if __name__ == "__main__":
765     main()