Merge "regression: OVS/DPDK regression tests"
[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                 # WORKAROUND: it has been observed with IXGBE(VF) driver,
391                 # that NIC doesn't correclty dispatch traffic to VFs based
392                 # on their MAC address. Unbind and bind to the same driver
393                 # solves this issue.
394                 networkcard.reinit_vfs(nic)
395
396         # After SRIOV is enabled it takes some time until network drivers
397         # properly initialize all cards.
398         # Wait also in case, that SRIOV was already configured as it can be
399         # configured automatically just before vsperf execution.
400         time.sleep(2)
401
402         return True
403
404     return False
405
406
407 def disable_sriov(nic_list):
408     """ Disable SRIOV for given PCI IDs
409
410     :param nic_list: A list of enhanced PCI IDs
411     """
412     for nic in nic_list:
413         if networkcard.is_sriov_nic(nic):
414             if not networkcard.set_sriov_numvfs(nic.split('|')[0], 0):
415                 raise RuntimeError('SRIOV cannot be disabled for NIC {}'.format(nic))
416             else:
417                 _LOGGER.debug("SRIOV disabled for NIC %s", nic.split('|')[0])
418
419
420 def handle_list_options(args):
421     """ Process --list cli arguments if needed
422
423     :param args: A dictionary with all CLI arguments
424     """
425     if args['list_trafficgens']:
426         print(Loader().get_trafficgens_printable())
427         sys.exit(0)
428
429     if args['list_collectors']:
430         print(Loader().get_collectors_printable())
431         sys.exit(0)
432
433     if args['list_vswitches']:
434         print(Loader().get_vswitches_printable())
435         sys.exit(0)
436
437     if args['list_vnfs']:
438         print(Loader().get_vnfs_printable())
439         sys.exit(0)
440
441     if args['list_fwdapps']:
442         print(Loader().get_pktfwds_printable())
443         sys.exit(0)
444
445     if args['list_loadgens']:
446         print(Loader().get_loadgens_printable())
447         sys.exit(0)
448
449     if args['list_settings']:
450         print(str(settings))
451         sys.exit(0)
452
453     if args['list']:
454         list_testcases(args)
455         sys.exit(0)
456
457
458 def list_testcases(args):
459     """ Print list of testcases requested by --list CLI argument
460
461     :param args: A dictionary with all CLI arguments
462     """
463     # configure tests
464     if args['integration']:
465         testcases = settings.getValue('INTEGRATION_TESTS')
466     else:
467         testcases = settings.getValue('PERFORMANCE_TESTS')
468
469     print("Available Tests:")
470     print("================")
471
472     for test in testcases:
473         description = functions.format_description(test['Description'], 70)
474         if len(test['Name']) < 40:
475             print('* {:40} {}'.format('{}:'.format(test['Name']), description[0]))
476         else:
477             print('* {}'.format('{}:'.format(test['Name'])))
478             print('  {:40} {}'.format('', description[0]))
479         for i in range(1, len(description)):
480             print('  {:40} {}'.format('', description[i]))
481
482
483
484
485
486 def vsperf_finalize():
487     """ Clean up before exit
488     """
489     # remove directory if no result files were created
490     try:
491         results_path = settings.getValue('RESULTS_PATH')
492         if os.path.exists(results_path):
493             files_list = os.listdir(results_path)
494             if files_list == []:
495                 _LOGGER.info("Removing empty result directory: "  + results_path)
496                 shutil.rmtree(results_path)
497     except AttributeError:
498         # skip it if parameter doesn't exist
499         pass
500
501     # disable SRIOV if needed
502     try:
503         if settings.getValue('SRIOV_ENABLED'):
504             disable_sriov(settings.getValue('WHITELIST_NICS_ORIG'))
505     except AttributeError:
506         # skip it if parameter doesn't exist
507         pass
508
509
510 class MockTestCase(unittest.TestCase):
511     """Allow use of xmlrunner to generate Jenkins compatible output without
512     using xmlrunner to actually run tests.
513
514     Usage:
515         suite = unittest.TestSuite()
516         suite.addTest(MockTestCase('Test1 passed ', True, 'Test1'))
517         suite.addTest(MockTestCase('Test2 failed because...', False, 'Test2'))
518         xmlrunner.XMLTestRunner(...).run(suite)
519     """
520
521     def __init__(self, msg, is_pass, test_name):
522         #remember the things
523         self.msg = msg
524         self.is_pass = is_pass
525
526         #dynamically create a test method with the right name
527         #but point the method at our generic test method
528         setattr(MockTestCase, test_name, self.generic_test)
529
530         super(MockTestCase, self).__init__(test_name)
531
532     def generic_test(self):
533         """Provide a generic function that raises or not based
534         on how self.is_pass was set in the constructor"""
535         self.assertTrue(self.is_pass, self.msg)
536
537 # pylint: disable=too-many-locals, too-many-branches, too-many-statements
538 def main():
539     """Main function.
540     """
541     args = parse_arguments()
542
543     # configure settings
544
545     settings.load_from_dir(os.path.join(_CURR_DIR, 'conf'))
546
547     # Load non performance/integration tests
548     if args['integration']:
549         settings.load_from_dir(os.path.join(_CURR_DIR, 'conf/integration'))
550
551     # load command line parameters first in case there are settings files
552     # to be used
553     settings.load_from_dict(args)
554
555     if args['conf_file']:
556         settings.load_from_file(args['conf_file'])
557
558     if args['load_env']:
559         settings.load_from_env()
560
561     # reload command line parameters since these should take higher priority
562     # than both a settings file and environment variables
563     settings.load_from_dict(args)
564
565     settings.setValue('mode', args['mode'])
566
567     # update paths to trafficgens if required
568     if settings.getValue('mode') == 'trafficgen':
569         functions.settings_update_paths()
570
571     # if required, handle list-* operations
572     handle_list_options(args)
573
574     configure_logging(settings.getValue('VERBOSITY'))
575
576     # check and fix locale
577     check_and_set_locale()
578
579     # configure trafficgens
580     if args['trafficgen']:
581         trafficgens = Loader().get_trafficgens()
582         if args['trafficgen'] not in trafficgens:
583             _LOGGER.error('There are no trafficgens matching \'%s\' found in'
584                           ' \'%s\'. Exiting...', args['trafficgen'],
585                           settings.getValue('TRAFFICGEN_DIR'))
586             sys.exit(1)
587
588     # configuration validity checks
589     if args['vswitch']:
590         vswitch_none = args['vswitch'].strip().lower() == 'none'
591         if vswitch_none:
592             settings.setValue('VSWITCH', 'none')
593         else:
594             vswitches = Loader().get_vswitches()
595             if args['vswitch'] not in vswitches:
596                 _LOGGER.error('There are no vswitches matching \'%s\' found in'
597                               ' \'%s\'. Exiting...', args['vswitch'],
598                               settings.getValue('VSWITCH_DIR'))
599                 sys.exit(1)
600
601     if args['fwdapp']:
602         settings.setValue('PKTFWD', args['fwdapp'])
603         fwdapps = Loader().get_pktfwds()
604         if args['fwdapp'] not in fwdapps:
605             _LOGGER.error('There are no forwarding application'
606                           ' matching \'%s\' found in'
607                           ' \'%s\'. Exiting...', args['fwdapp'],
608                           settings.getValue('PKTFWD_DIR'))
609             sys.exit(1)
610
611     if args['vnf']:
612         vnfs = Loader().get_vnfs()
613         if args['vnf'] not in vnfs:
614             _LOGGER.error('there are no vnfs matching \'%s\' found in'
615                           ' \'%s\'. exiting...', args['vnf'],
616                           settings.getValue('VNF_DIR'))
617             sys.exit(1)
618
619     if args['loadgen']:
620         loadgens = Loader().get_loadgens()
621         if args['loadgen'] not in loadgens:
622             _LOGGER.error('There are no loadgens matching \'%s\' found in'
623                           ' \'%s\'. Exiting...', args['loadgen'],
624                           settings.getValue('LOADGEN_DIR'))
625             sys.exit(1)
626
627     if args['exact_test_name'] and args['tests']:
628         _LOGGER.error("Cannot specify tests with both positional args and --test.")
629         sys.exit(1)
630
631     # modify NIC configuration to decode enhanced PCI IDs
632     wl_nics_orig = list(networkcard.check_pci(pci) for pci in settings.getValue('WHITELIST_NICS'))
633     settings.setValue('WHITELIST_NICS_ORIG', wl_nics_orig)
634
635     # sriov handling is performed on checked/expanded PCI IDs
636     settings.setValue('SRIOV_ENABLED', enable_sriov(wl_nics_orig))
637
638     nic_list = []
639     for nic in wl_nics_orig:
640         tmp_nic = networkcard.get_nic_info(nic)
641         if tmp_nic:
642             nic_list.append({'pci' : tmp_nic,
643                              'type' : 'vf' if networkcard.get_sriov_pf(tmp_nic) else 'pf',
644                              'mac' : networkcard.get_mac(tmp_nic),
645                              'driver' : networkcard.get_driver(tmp_nic),
646                              'device' : networkcard.get_device_name(tmp_nic)})
647         else:
648             vsperf_finalize()
649             raise RuntimeError("Invalid network card PCI ID: '{}'".format(nic))
650
651     settings.setValue('NICS', nic_list)
652     # for backward compatibility
653     settings.setValue('WHITELIST_NICS', list(nic['pci'] for nic in nic_list))
654
655     # generate results directory name
656     date = datetime.datetime.fromtimestamp(time.time())
657     results_dir = "results_" + date.strftime('%Y-%m-%d_%H-%M-%S')
658     results_path = os.path.join(settings.getValue('LOG_DIR'), results_dir)
659     settings.setValue('RESULTS_PATH', results_path)
660
661     # create results directory
662     if not os.path.exists(results_path):
663         _LOGGER.info("Creating result directory: "  + results_path)
664         os.makedirs(results_path)
665
666     if settings.getValue('mode') == 'trafficgen':
667         # execute only traffic generator
668         _LOGGER.debug("Executing traffic generator:")
669         loader = Loader()
670         # set traffic details, so they can be passed to traffic ctl
671         traffic = copy.deepcopy(settings.getValue('TRAFFIC'))
672
673         traffic = functions.check_traffic(traffic)
674
675         traffic_ctl = component_factory.create_traffic(
676             traffic['traffic_type'],
677             loader.get_trafficgen_class())
678         with traffic_ctl:
679             traffic_ctl.send_traffic(traffic)
680         _LOGGER.debug("Traffic Results:")
681         traffic_ctl.print_results()
682
683         # write results into CSV file
684         result_file = os.path.join(results_path, "result.csv")
685         PerformanceTestCase.write_result_to_file(traffic_ctl.get_results(), result_file)
686     else:
687         # configure tests
688         if args['integration']:
689             testcases = settings.getValue('INTEGRATION_TESTS')
690         else:
691             testcases = settings.getValue('PERFORMANCE_TESTS')
692
693         if args['exact_test_name']:
694             exact_names = args['exact_test_name']
695             # positional args => exact matches only
696             selected_tests = [test for test in testcases if test['Name'] in exact_names]
697         elif args['tests']:
698             # --tests => apply filter to select requested tests
699             selected_tests = apply_filter(testcases, args['tests'])
700         else:
701             # Default - run all tests
702             selected_tests = testcases
703
704         if not len(selected_tests):
705             _LOGGER.error("No tests matched --tests option or positional args. Done.")
706             vsperf_finalize()
707             sys.exit(1)
708
709         # run tests
710         # Add pylint exception: Redefinition of test type from
711         # testcases.integration.IntegrationTestCase to testcases.performance.PerformanceTestCase
712         # pylint: disable=redefined-variable-type
713         suite = unittest.TestSuite()
714         settings_snapshot = copy.deepcopy(settings.__dict__)
715         for cfg in selected_tests:
716             test_name = cfg.get('Name', '<Name not set>')
717             try:
718                 if args['integration']:
719                     test = IntegrationTestCase(cfg)
720                 else:
721                     test = PerformanceTestCase(cfg)
722                 test.run()
723                 suite.addTest(MockTestCase('', True, test.name))
724             # pylint: disable=broad-except
725             except (Exception) as ex:
726                 _LOGGER.exception("Failed to run test: %s", test_name)
727                 suite.addTest(MockTestCase(str(ex), False, test_name))
728                 _LOGGER.info("Continuing with next test...")
729             finally:
730                 settings.restore_from_dict(settings_snapshot)
731
732         # generate final rst report with results of all executed TCs
733         generate_final_report()
734
735         if settings.getValue('XUNIT'):
736             xmlrunner.XMLTestRunner(
737                 output=settings.getValue('XUNIT_DIR'), outsuffix="",
738                 verbosity=0).run(suite)
739
740         if args['opnfvpod']:
741             pod_name = args['opnfvpod']
742             installer_name = str(settings.getValue('OPNFV_INSTALLER')).lower()
743             opnfv_url = settings.getValue('OPNFV_URL')
744             pkg_list = settings.getValue('PACKAGE_LIST')
745
746             int_data = {'pod': pod_name,
747                         'build_tag': get_build_tag(),
748                         'installer': installer_name,
749                         'pkg_list': pkg_list,
750                         'db_url': opnfv_url,
751                         # pass vswitch name from configuration to be used for failed
752                         # TCs; In case of successful TCs it is safer to use vswitch
753                         # name from CSV as TC can override global configuration
754                         'vswitch': str(settings.getValue('VSWITCH')).lower()}
755             tc_names = [tc['Name'] for tc in selected_tests]
756             opnfvdashboard.results2opnfv_dashboard(tc_names, results_path, int_data)
757
758     # cleanup before exit
759     vsperf_finalize()
760
761 if __name__ == "__main__":
762     main()