3 # Copyright 2015-2017 Intel Corporation.
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 """VSPERF main script.
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
46 sys.dont_write_bytecode = True
49 'debug': logging.DEBUG,
51 'warning': logging.WARNING,
52 'error': logging.ERROR,
53 'critical': logging.CRITICAL
56 _CURR_DIR = os.path.dirname(os.path.realpath(__file__))
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')
65 _LOGGER = logging.getLogger()
67 def parse_arguments():
69 Parse command line arguments.
71 class _SplitTestParamsAction(argparse.Action):
73 Parse and split the '--test-params' argument.
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)'
79 def __call__(self, parser, namespace, values, option_string=None):
82 for param, _, value in re.findall('([^;=]+)(=([^;]+))?', values):
87 # values are passed inside string from CLI, so we must retype them accordingly
89 results[param] = ast.literal_eval(value)
91 # for backward compatibility, we have to accept strings without quotes
92 _LOGGER.warning("Adding missing quotes around string value: %s = %s",
94 results[param] = str(value)
98 setattr(namespace, self.dest, results)
100 class _ValidateFileAction(argparse.Action):
101 """Validate a file can be read from before using it.
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)
111 setattr(namespace, self.dest, values)
113 class _ValidateDirAction(argparse.Action):
114 """Validate a directory can be written to before using it.
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)
124 setattr(namespace, self.dest, values)
126 def list_logging_levels():
127 """Give a summary of all available logging levels.
129 :return: List of verbosity level names in decreasing order of
132 return sorted(VERBOSITY_LEVELS.keys(),
133 key=lambda x: VERBOSITY_LEVELS[x])
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.')
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')
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(),
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')
196 args = vars(parser.parse_args())
201 def configure_logging(level):
202 """Configure logging.
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'))
212 _LOGGER.setLevel(logging.DEBUG)
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)
220 file_logger = logging.FileHandler(filename=log_file_default)
221 file_logger.setLevel(logging.DEBUG)
222 _LOGGER.addHandler(file_logger)
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)
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)
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)
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)
245 def apply_filter(tests, tc_filter):
246 """Allow a subset of tests to be conveniently selected
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
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.
259 # if negative filter is first we have to start with full list of tests
260 if tc_filter.strip()[0] == '!':
264 if tc_filter is None:
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])
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]
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.
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())
291 def get_vswitch_names(rst_files):
292 """ Function will return a list of vSwitches detected in given ``rst_files``.
294 vswitch_names = set()
297 output = subprocess.check_output(['grep', '-h', '^* vSwitch'] + rst_files).decode().splitlines()
299 match = re.search(r'^\* vSwitch: ([^,]+)', str(line))
301 vswitch_names.add(match.group(1))
303 if len(vswitch_names):
304 return list(vswitch_names)
306 except subprocess.CalledProcessError:
307 _LOGGER.warning('Cannot detect vSwitches used during testing.')
309 # fallback to the default value
313 """ Function will return a Jenkins job ID environment variable.
317 build_tag = os.environ['BUILD_TAG']
320 _LOGGER.warning('Cannot detect Jenkins job ID')
325 def generate_final_report():
326 """ Function will check if partial test results are available
327 and generates final report in rst format.
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)
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 '============================================================')
344 with open(_TEMPLATE_RST['tmp'], 'w') as file_:
345 file_.write(report_caption)
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)
353 _LOGGER.error('Generation of overall test report has failed.')
355 # remove temporary file
356 os.remove(_TEMPLATE_RST['tmp'])
358 except subprocess.CalledProcessError:
359 _LOGGER.error('Generatrion of overall test report has failed.')
362 def enable_sriov(nic_list):
363 """ Enable SRIOV for given enhanced PCI IDs
365 :param nic_list: A list of enhanced PCI IDs
367 # detect if sriov is required
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:])
376 sriov_nic.update({tmp_nic[0] : int(tmp_nic[1][2:])})
378 # sriov is required for some NICs
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))
388 _LOGGER.debug("SRIOV enabled for NIC %s", nic)
390 # ensure that path to the bind tool is valid
391 functions.settings_update_paths()
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
397 networkcard.reinit_vfs(nic)
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.
410 def disable_sriov(nic_list):
411 """ Disable SRIOV for given PCI IDs
413 :param nic_list: A list of enhanced PCI IDs
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))
420 _LOGGER.debug("SRIOV disabled for NIC %s", nic.split('|')[0])
423 def handle_list_options(args):
424 """ Process --list cli arguments if needed
426 :param args: A dictionary with all CLI arguments
428 if args['list_trafficgens']:
429 print(Loader().get_trafficgens_printable())
432 if args['list_collectors']:
433 print(Loader().get_collectors_printable())
436 if args['list_vswitches']:
437 print(Loader().get_vswitches_printable())
440 if args['list_vnfs']:
441 print(Loader().get_vnfs_printable())
444 if args['list_fwdapps']:
445 print(Loader().get_pktfwds_printable())
448 if args['list_loadgens']:
449 print(Loader().get_loadgens_printable())
452 if args['list_settings']:
461 def list_testcases(args):
462 """ Print list of testcases requested by --list CLI argument
464 :param args: A dictionary with all CLI arguments
467 if args['integration']:
468 testcases = settings.getValue('INTEGRATION_TESTS')
470 testcases = settings.getValue('PERFORMANCE_TESTS')
472 print("Available Tests:")
473 print("================")
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]))
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]))
489 def vsperf_finalize():
490 """ Clean up before exit
492 # remove directory if no result files were created
494 results_path = settings.getValue('RESULTS_PATH')
495 if os.path.exists(results_path):
496 files_list = os.listdir(results_path)
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
504 # disable SRIOV if needed
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
513 class MockTestCase(unittest.TestCase):
514 """Allow use of xmlrunner to generate Jenkins compatible output without
515 using xmlrunner to actually run tests.
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)
524 def __init__(self, msg, is_pass, test_name):
527 self.is_pass = is_pass
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)
533 super(MockTestCase, self).__init__(test_name)
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)
540 # pylint: disable=too-many-locals, too-many-branches, too-many-statements
544 args = parse_arguments()
548 settings.load_from_dir(os.path.join(_CURR_DIR, 'conf'))
550 # Load non performance/integration tests
551 if args['integration']:
552 settings.load_from_dir(os.path.join(_CURR_DIR, 'conf/integration'))
554 # load command line parameters first in case there are settings files
556 settings.load_from_dict(args)
558 if args['conf_file']:
559 settings.load_from_file(args['conf_file'])
562 settings.load_from_env()
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)
568 settings.setValue('mode', args['mode'])
570 # update paths to trafficgens if required
571 if settings.getValue('mode') == 'trafficgen':
572 functions.settings_update_paths()
574 # if required, handle list-* operations
575 handle_list_options(args)
577 configure_logging(settings.getValue('VERBOSITY'))
579 # check and fix locale
580 check_and_set_locale()
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'))
591 # configuration validity checks
593 vswitch_none = args['vswitch'].strip().lower() == 'none'
595 settings.setValue('VSWITCH', 'none')
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'))
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'))
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'))
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'))
630 if args['exact_test_name'] and args['tests']:
631 _LOGGER.error("Cannot specify tests with both positional args and --test.")
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)
638 # sriov handling is performed on checked/expanded PCI IDs
639 settings.setValue('SRIOV_ENABLED', enable_sriov(wl_nics_orig))
642 for nic in wl_nics_orig:
643 tmp_nic = networkcard.get_nic_info(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)})
652 raise RuntimeError("Invalid network card PCI ID: '{}'".format(nic))
654 settings.setValue('NICS', nic_list)
655 # for backward compatibility
656 settings.setValue('WHITELIST_NICS', list(nic['pci'] for nic in nic_list))
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)
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)
669 if settings.getValue('mode') == 'trafficgen':
670 # execute only traffic generator
671 _LOGGER.debug("Executing traffic generator:")
673 # set traffic details, so they can be passed to traffic ctl
674 traffic = copy.deepcopy(settings.getValue('TRAFFIC'))
676 traffic = functions.check_traffic(traffic)
678 traffic_ctl = component_factory.create_traffic(
679 traffic['traffic_type'],
680 loader.get_trafficgen_class())
682 traffic_ctl.send_traffic(traffic)
683 _LOGGER.debug("Traffic Results:")
684 traffic_ctl.print_results()
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)
691 if args['integration']:
692 testcases = settings.getValue('INTEGRATION_TESTS')
694 testcases = settings.getValue('PERFORMANCE_TESTS')
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]
701 # --tests => apply filter to select requested tests
702 selected_tests = apply_filter(testcases, args['tests'])
704 # Default - run all tests
705 selected_tests = testcases
707 if not len(selected_tests):
708 _LOGGER.error("No tests matched --tests option or positional args. Done.")
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>')
721 if args['integration']:
722 test = IntegrationTestCase(cfg)
724 test = PerformanceTestCase(cfg)
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...")
733 settings.restore_from_dict(settings_snapshot)
735 # generate final rst report with results of all executed TCs
736 generate_final_report()
738 if settings.getValue('XUNIT'):
739 xmlrunner.XMLTestRunner(
740 output=settings.getValue('XUNIT_DIR'), outsuffix="",
741 verbosity=0).run(suite)
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')
749 int_data = {'pod': pod_name,
750 'build_tag': get_build_tag(),
751 'installer': installer_name,
752 'pkg_list': pkg_list,
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)
761 # cleanup before exit
764 if __name__ == "__main__":