3 # Copyright 2015-2016 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.
31 sys.dont_write_bytecode = True
33 from conf import settings
34 from conf import get_test_param
35 from core.loader import Loader
36 from testcases import TestCase
37 from tools import tasks
38 from tools.pkt_gen import trafficgen
39 from tools.opnfvdashboard import opnfvdashboard
42 'debug': logging.DEBUG,
44 'warning': logging.WARNING,
45 'error': logging.ERROR,
46 'critical': logging.CRITICAL
50 def parse_arguments():
52 Parse command line arguments.
54 class _SplitTestParamsAction(argparse.Action):
56 Parse and split the '--test-params' argument.
58 This expects either 'x=y', 'x=y,z' or 'x' (implicit true)
59 values. For multiple overrides use a ; separated list for
60 e.g. --test-params 'x=z; y=a,b'
62 def __call__(self, parser, namespace, values, option_string=None):
65 for value in values.split(';'):
66 result = [key.strip() for key in value.split('=')]
68 results[result[0]] = True
69 elif len(result) == 2:
70 results[result[0]] = result[1]
72 raise argparse.ArgumentTypeError(
73 'expected \'%s\' to be of format \'key=val\' or'
76 setattr(namespace, self.dest, results)
78 class _ValidateFileAction(argparse.Action):
79 """Validate a file can be read from before using it.
81 def __call__(self, parser, namespace, values, option_string=None):
82 if not os.path.isfile(values):
83 raise argparse.ArgumentTypeError(
84 'the path \'%s\' is not a valid path' % values)
85 elif not os.access(values, os.R_OK):
86 raise argparse.ArgumentTypeError(
87 'the path \'%s\' is not accessible' % values)
89 setattr(namespace, self.dest, values)
91 class _ValidateDirAction(argparse.Action):
92 """Validate a directory can be written to before using it.
94 def __call__(self, parser, namespace, values, option_string=None):
95 if not os.path.isdir(values):
96 raise argparse.ArgumentTypeError(
97 'the path \'%s\' is not a valid path' % values)
98 elif not os.access(values, os.W_OK):
99 raise argparse.ArgumentTypeError(
100 'the path \'%s\' is not accessible' % values)
102 setattr(namespace, self.dest, values)
104 def list_logging_levels():
105 """Give a summary of all available logging levels.
107 :return: List of verbosity level names in decreasing order of
110 return sorted(VERBOSITY_LEVELS.keys(),
111 key=lambda x: VERBOSITY_LEVELS[x])
113 parser = argparse.ArgumentParser(prog=__file__, formatter_class=
114 argparse.ArgumentDefaultsHelpFormatter)
115 parser.add_argument('--version', action='version', version='%(prog)s 0.2')
116 parser.add_argument('--list', '--list-tests', action='store_true',
117 help='list all tests and exit')
118 parser.add_argument('--list-trafficgens', action='store_true',
119 help='list all traffic generators and exit')
120 parser.add_argument('--list-collectors', action='store_true',
121 help='list all system metrics loggers and exit')
122 parser.add_argument('--list-vswitches', action='store_true',
123 help='list all system vswitches and exit')
124 parser.add_argument('--list-fwdapps', action='store_true',
125 help='list all system forwarding applications and exit')
126 parser.add_argument('--list-vnfs', action='store_true',
127 help='list all system vnfs and exit')
128 parser.add_argument('--list-settings', action='store_true',
129 help='list effective settings configuration and exit')
130 parser.add_argument('exact_test_name', nargs='*', help='Exact names of\
131 tests to run. E.g "vsperf phy2phy_tput phy2phy_cont"\
132 runs only the two tests with those exact names.\
133 To run all tests omit both positional args and --tests arg.')
135 group = parser.add_argument_group('test selection options')
136 group.add_argument('-f', '--test-spec', help='test specification file')
137 group.add_argument('-d', '--test-dir', help='directory containing tests')
138 group.add_argument('-t', '--tests', help='Comma-separated list of terms \
139 indicating tests to run. e.g. "RFC2544,!p2p" - run all tests whose\
140 name contains RFC2544 less those containing "p2p"')
141 group.add_argument('--verbosity', choices=list_logging_levels(),
143 group.add_argument('--trafficgen', help='traffic generator to use')
144 group.add_argument('--vswitch', help='vswitch implementation to use')
145 group.add_argument('--fwdapp', help='packet forwarding application to use')
146 group.add_argument('--vnf', help='vnf to use')
147 group.add_argument('--duration', help='traffic transmit duration')
148 group.add_argument('--sysmetrics', help='system metrics logger to use')
149 group = parser.add_argument_group('test behavior options')
150 group.add_argument('--xunit', action='store_true',
151 help='enable xUnit-formatted output')
152 group.add_argument('--xunit-dir', action=_ValidateDirAction,
153 help='output directory of xUnit-formatted output')
154 group.add_argument('--load-env', action='store_true',
155 help='enable loading of settings from the environment')
156 group.add_argument('--conf-file', action=_ValidateFileAction,
157 help='settings file')
158 group.add_argument('--test-params', action=_SplitTestParamsAction,
159 help='csv list of test parameters: key=val; e.g.'
160 'including pkt_sizes=x,y; duration=x; '
161 'rfc2544_trials=x ...')
162 group.add_argument('--opnfvpod', help='name of POD in opnfv')
164 args = vars(parser.parse_args())
169 def configure_logging(level):
170 """Configure logging.
172 log_file_default = os.path.join(
173 settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_DEFAULT'))
174 log_file_host_cmds = os.path.join(
175 settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_HOST_CMDS'))
176 log_file_traffic_gen = os.path.join(
177 settings.getValue('LOG_DIR'),
178 settings.getValue('LOG_FILE_TRAFFIC_GEN'))
180 logger = logging.getLogger()
181 logger.setLevel(logging.DEBUG)
183 stream_logger = logging.StreamHandler(sys.stdout)
184 stream_logger.setLevel(VERBOSITY_LEVELS[level])
185 stream_logger.setFormatter(logging.Formatter(
186 '[%(levelname)s] %(asctime)s : (%(name)s) - %(message)s'))
187 logger.addHandler(stream_logger)
189 file_logger = logging.FileHandler(filename=log_file_default)
190 file_logger.setLevel(logging.DEBUG)
191 logger.addHandler(file_logger)
193 class CommandFilter(logging.Filter):
194 """Filter out strings beginning with 'cmd :'"""
195 def filter(self, record):
196 return record.getMessage().startswith(tasks.CMD_PREFIX)
198 class TrafficGenCommandFilter(logging.Filter):
199 """Filter out strings beginning with 'gencmd :'"""
200 def filter(self, record):
201 return record.getMessage().startswith(trafficgen.CMD_PREFIX)
203 cmd_logger = logging.FileHandler(filename=log_file_host_cmds)
204 cmd_logger.setLevel(logging.DEBUG)
205 cmd_logger.addFilter(CommandFilter())
206 logger.addHandler(cmd_logger)
208 gen_logger = logging.FileHandler(filename=log_file_traffic_gen)
209 gen_logger.setLevel(logging.DEBUG)
210 gen_logger.addFilter(TrafficGenCommandFilter())
211 logger.addHandler(gen_logger)
214 def apply_filter(tests, tc_filter):
215 """Allow a subset of tests to be conveniently selected
217 :param tests: The list of Tests from which to select.
218 :param tc_filter: A case-insensitive string of comma-separated terms
219 indicating the Tests to select.
220 e.g. 'RFC' - select all tests whose name contains 'RFC'
221 e.g. 'RFC,burst' - select all tests whose name contains 'RFC' or
223 e.g. 'RFC,burst,!p2p' - select all tests whose name contains 'RFC'
224 or 'burst' and from these remove any containing 'p2p'.
225 e.g. '' - empty string selects all tests.
226 :return: A list of the selected Tests.
229 if tc_filter is None:
232 for term in [x.strip() for x in tc_filter.lower().split(",")]:
233 if not term or term[0] != '!':
234 # Add matching tests from 'tests' into results
235 result.extend([test for test in tests \
236 if test.name.lower().find(term) >= 0])
238 # Term begins with '!' so we remove matching tests
239 result = [test for test in result \
240 if test.name.lower().find(term[1:]) < 0]
245 def check_and_set_locale():
246 """ Function will check locale settings. In case, that it isn't configured
247 properly, then default values specified by DEFAULT_LOCALE will be used.
250 system_locale = locale.getdefaultlocale()
251 if None in system_locale:
252 os.environ['LC_ALL'] = settings.getValue('DEFAULT_LOCALE')
253 logging.warning("Locale was not properly configured. Default values were set. Old locale: %s, New locale: %s",
254 system_locale, locale.getdefaultlocale())
256 class MockTestCase(unittest.TestCase):
257 """Allow use of xmlrunner to generate Jenkins compatible output without
258 using xmlrunner to actually run tests.
261 suite = unittest.TestSuite()
262 suite.addTest(MockTestCase('Test1 passed ', True, 'Test1'))
263 suite.addTest(MockTestCase('Test2 failed because...', False, 'Test2'))
264 xmlrunner.XMLTestRunner(...).run(suite)
267 def __init__(self, msg, is_pass, test_name):
270 self.is_pass = is_pass
272 #dynamically create a test method with the right name
273 #but point the method at our generic test method
274 setattr(MockTestCase, test_name, self.generic_test)
276 super(MockTestCase, self).__init__(test_name)
278 def generic_test(self):
279 """Provide a generic function that raises or not based
280 on how self.is_pass was set in the constructor"""
281 self.assertTrue(self.is_pass, self.msg)
287 args = parse_arguments()
291 settings.load_from_dir('conf')
293 # load command line parameters first in case there are settings files
295 settings.load_from_dict(args)
297 if args['conf_file']:
298 settings.load_from_file(args['conf_file'])
301 settings.load_from_env()
303 # reload command line parameters since these should take higher priority
304 # than both a settings file and environment variables
305 settings.load_from_dict(args)
308 # set dpdk and ovs paths accorfing to VNF and VSWITCH
309 if settings.getValue('VSWITCH').endswith('Vanilla'):
310 # settings paths for Vanilla
311 settings.setValue('OVS_DIR', (settings.getValue('OVS_DIR_VANILLA')))
312 elif settings.getValue('VSWITCH').endswith('Vhost'):
313 if settings.getValue('VNF').endswith('Cuse'):
314 # settings paths for Cuse
315 settings.setValue('RTE_SDK', (settings.getValue('RTE_SDK_CUSE')))
316 settings.setValue('OVS_DIR', (settings.getValue('OVS_DIR_CUSE')))
318 # settings paths for VhostUser
319 settings.setValue('RTE_SDK', (settings.getValue('RTE_SDK_USER')))
320 settings.setValue('OVS_DIR', (settings.getValue('OVS_DIR_USER')))
322 # default - set to VHOST USER but can be changed during enhancement
323 settings.setValue('RTE_SDK', (settings.getValue('RTE_SDK_USER')))
324 settings.setValue('OVS_DIR', (settings.getValue('OVS_DIR_USER')))
325 if 'none' == settings.getValue('VSWITCH').strip().lower():
328 configure_logging(settings.getValue('VERBOSITY'))
329 logger = logging.getLogger()
331 # check and fix locale
332 check_and_set_locale()
334 # configure trafficgens
336 if args['trafficgen']:
337 trafficgens = Loader().get_trafficgens()
338 if args['trafficgen'] not in trafficgens:
339 logging.error('There are no trafficgens matching \'%s\' found in'
340 ' \'%s\'. Exiting...', args['trafficgen'],
341 settings.getValue('TRAFFICGEN_DIR'))
346 vswitch_none = 'none' == args['vswitch'].strip().lower()
348 settings.setValue('VSWITCH', 'none')
350 vswitches = Loader().get_vswitches()
351 if args['vswitch'] not in vswitches:
352 logging.error('There are no vswitches matching \'%s\' found in'
353 ' \'%s\'. Exiting...', args['vswitch'],
354 settings.getValue('VSWITCH_DIR'))
358 settings.setValue('PKTFWD', args['fwdapp'])
359 fwdapps = Loader().get_pktfwds()
360 if args['fwdapp'] not in fwdapps:
361 logging.error('There are no forwarding application'
362 ' matching \'%s\' found in'
363 ' \'%s\'. Exiting...', args['fwdapp'],
364 settings.getValue('PKTFWD_DIR'))
368 vnfs = Loader().get_vnfs()
369 if args['vnf'] not in vnfs:
370 logging.error('there are no vnfs matching \'%s\' found in'
371 ' \'%s\'. exiting...', args['vnf'],
372 settings.getValue('vnf_dir'))
376 if args['duration'].isdigit() and int(args['duration']) > 0:
377 settings.setValue('duration', args['duration'])
379 logging.error('The selected Duration is not a number')
382 # update global settings
383 guest_loopback = get_test_param('guest_loopback', None)
386 for i in range(len(settings.getValue('GUEST_LOOPBACK'))):
387 tmp_gl.append(guest_loopback)
388 settings.setValue('GUEST_LOOPBACK', tmp_gl)
390 # generate results directory name
391 date = datetime.datetime.fromtimestamp(time.time())
392 results_dir = "results_" + date.strftime('%Y-%m-%d_%H-%M-%S')
393 results_path = os.path.join(settings.getValue('LOG_DIR'), results_dir)
396 testcases = settings.getValue('PERFORMANCE_TESTS')
398 for cfg in testcases:
400 all_tests.append(TestCase(cfg, results_path))
401 except (Exception) as _:
402 logger.exception("Failed to create test: %s",
403 cfg.get('Name', '<Name not set>'))
406 # if required, handle list-* operations
409 print("Available Tests:")
411 for test in all_tests:
412 print('* %-18s%s' % ('%s:' % test.name, test.desc))
415 if args['list_trafficgens']:
416 print(Loader().get_trafficgens_printable())
419 if args['list_collectors']:
420 print(Loader().get_collectors_printable())
423 if args['list_vswitches']:
424 print(Loader().get_vswitches_printable())
427 if args['list_fwdapps']:
428 print(Loader().get_pktfwds_printable())
431 if args['list_vnfs']:
432 print(Loader().get_vnfs_printable())
435 if args['list_settings']:
439 # select requested tests
440 if args['exact_test_name'] and args['tests']:
441 logger.error("Cannot specify tests with both positional args and --test.")
444 if args['exact_test_name']:
445 exact_names = args['exact_test_name']
446 # positional args => exact matches only
447 selected_tests = [test for test in all_tests if test.name in exact_names]
449 # --tests => apply filter to select requested tests
450 selected_tests = apply_filter(all_tests, args['tests'])
452 # Default - run all tests
453 selected_tests = all_tests
455 if not selected_tests:
456 logger.error("No tests matched --test option or positional args. Done.")
459 # create results directory
460 if not os.path.exists(results_path):
461 logger.info("Creating result directory: " + results_path)
462 os.makedirs(results_path)
465 suite = unittest.TestSuite()
466 for test in selected_tests:
469 if test.deployment.lower() != 'p2p':
470 logging.error('\'none\' vswitch option supported only'
471 ' for p2p deployment.')
474 suite.addTest(MockTestCase('', True, test.name))
475 #pylint: disable=broad-except
476 except (Exception) as ex:
477 logger.exception("Failed to run test: %s", test.name)
478 suite.addTest(MockTestCase(str(ex), False, test.name))
479 logger.info("Continuing with next test...")
481 if settings.getValue('XUNIT'):
482 xmlrunner.XMLTestRunner(
483 output=settings.getValue('XUNIT_DIR'), outsuffix="",
484 verbosity=0).run(suite)
487 pod_name = args['opnfvpod']
488 installer_name = settings.getValue('OPNFV_INSTALLER')
489 opnfv_url = settings.getValue('OPNFV_URL')
490 pkg_list = settings.getValue('PACKAGE_LIST')
492 int_data = {'cuse': False,
495 'installer': installer_name,
496 'pkg_list': pkg_list,
498 if settings.getValue('VSWITCH').endswith('Vanilla'):
499 int_data['vanilla'] = True
500 if settings.getValue('VNF').endswith('Cuse'):
501 int_data['cuse'] = True
502 opnfvdashboard.results2opnfv_dashboard(results_path, int_data)
504 #remove directory if no result files were created.
505 if os.path.exists(results_path):
506 files_list = os.listdir(results_path)
508 shutil.rmtree(results_path)
510 if __name__ == "__main__":