3 # Copyright 2015 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.
30 sys.dont_write_bytecode = True
32 from conf import settings
33 from core.loader import Loader
34 from testcases import TestCase
35 from tools.report import report
36 from tools import tasks
37 from tools.collectors import collector
38 from tools.pkt_gen import trafficgen
39 from vswitches import vswitch
43 'debug': logging.DEBUG,
45 'warning': logging.WARNING,
46 'error': logging.ERROR,
47 'critical': logging.CRITICAL
51 def parse_arguments():
53 Parse command line arguments.
55 class _SplitTestParamsAction(argparse.Action):
57 Parse and split the '--test-params' argument.
59 This expects either 'x=y', 'x=y,z' or 'x' (implicit true)
60 values. For multiple overrides use a ; separated list for
61 e.g. --test-params 'x=z; y=a,b'
63 def __call__(self, parser, namespace, values, option_string=None):
66 for value in values.split(';'):
67 result = [key.strip() for key in value.split('=')]
69 results[result[0]] = True
70 elif len(result) == 2:
71 results[result[0]] = result[1]
73 raise argparse.ArgumentTypeError(
74 'expected \'%s\' to be of format \'key=val\' or'
77 setattr(namespace, self.dest, results)
79 class _ValidateFileAction(argparse.Action):
80 """Validate a file can be read from before using it.
82 def __call__(self, parser, namespace, values, option_string=None):
83 if not os.path.isfile(values):
84 raise argparse.ArgumentTypeError(
85 'the path \'%s\' is not a valid path' % values)
86 elif not os.access(values, os.R_OK):
87 raise argparse.ArgumentTypeError(
88 'the path \'%s\' is not accessible' % values)
90 setattr(namespace, self.dest, values)
92 class _ValidateDirAction(argparse.Action):
93 """Validate a directory can be written to before using it.
95 def __call__(self, parser, namespace, values, option_string=None):
96 if not os.path.isdir(values):
97 raise argparse.ArgumentTypeError(
98 'the path \'%s\' is not a valid path' % values)
99 elif not os.access(values, os.W_OK):
100 raise argparse.ArgumentTypeError(
101 'the path \'%s\' is not accessible' % values)
103 setattr(namespace, self.dest, values)
105 def list_logging_levels():
106 """Give a summary of all available logging levels.
108 :return: List of verbosity level names in decreasing order of
111 return sorted(VERBOSITY_LEVELS.keys(),
112 key=lambda x: VERBOSITY_LEVELS[x])
114 parser = argparse.ArgumentParser(prog=__file__, formatter_class=
115 argparse.ArgumentDefaultsHelpFormatter)
116 parser.add_argument('--version', action='version', version='%(prog)s 0.2')
117 parser.add_argument('--list', '--list-tests', action='store_true',
118 help='list all tests and exit')
119 parser.add_argument('--list-trafficgens', action='store_true',
120 help='list all traffic generators and exit')
121 parser.add_argument('--list-collectors', action='store_true',
122 help='list all system metrics loggers and exit')
123 parser.add_argument('--list-vswitches', action='store_true',
124 help='list all system vswitches and exit')
125 parser.add_argument('--list-vnfs', action='store_true',
126 help='list all system vnfs and exit')
127 parser.add_argument('--list-settings', action='store_true',
128 help='list effective settings configuration and exit')
129 parser.add_argument('exact_test_name', nargs='*', help='Exact names of\
130 tests to run. E.g "vsperf phy2phy_tput phy2phy_cont"\
131 runs only the two tests with those exact names.\
132 To run all tests omit both positional args and --tests arg.')
134 group = parser.add_argument_group('test selection options')
135 group.add_argument('-f', '--test-spec', help='test specification file')
136 group.add_argument('-d', '--test-dir', help='directory containing tests')
137 group.add_argument('-t', '--tests', help='Comma-separated list of terms \
138 indicating tests to run. e.g. "RFC2544,!p2p" - run all tests whose\
139 name contains RFC2544 less those containing "p2p"')
140 group.add_argument('--verbosity', choices=list_logging_levels(),
142 group.add_argument('--trafficgen', help='traffic generator to use')
143 group.add_argument('--vswitch', help='vswitch implementation to use')
144 group.add_argument('--vnf', help='vnf to use')
145 group.add_argument('--duration', help='traffic transmit duration')
146 group.add_argument('--sysmetrics', help='system metrics logger to use')
147 group = parser.add_argument_group('test behavior options')
148 group.add_argument('--xunit', action='store_true',
149 help='enable xUnit-formatted output')
150 group.add_argument('--xunit-dir', action=_ValidateDirAction,
151 help='output directory of xUnit-formatted output')
152 group.add_argument('--load-env', action='store_true',
153 help='enable loading of settings from the environment')
154 group.add_argument('--conf-file', action=_ValidateFileAction,
155 help='settings file')
156 group.add_argument('--test-params', action=_SplitTestParamsAction,
157 help='csv list of test parameters: key=val; e.g.'
158 'including pkt_sizes=x,y; duration=x; '
159 'rfc2544_trials=x ...')
161 args = vars(parser.parse_args())
166 def configure_logging(level):
167 """Configure logging.
169 log_file_default = os.path.join(
170 settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_DEFAULT'))
171 log_file_host_cmds = os.path.join(
172 settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_HOST_CMDS'))
173 log_file_traffic_gen = os.path.join(
174 settings.getValue('LOG_DIR'),
175 settings.getValue('LOG_FILE_TRAFFIC_GEN'))
176 log_file_sys_metrics = os.path.join(
177 settings.getValue('LOG_DIR'),
178 settings.getValue('LOG_FILE_SYS_METRICS'))
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 class SystemMetricsCommandFilter(logging.Filter):
204 """Filter out strings beginning with 'gencmd :'"""
205 def filter(self, record):
206 return record.getMessage().startswith(collector.CMD_PREFIX)
208 cmd_logger = logging.FileHandler(filename=log_file_host_cmds)
209 cmd_logger.setLevel(logging.DEBUG)
210 cmd_logger.addFilter(CommandFilter())
211 logger.addHandler(cmd_logger)
213 gen_logger = logging.FileHandler(filename=log_file_traffic_gen)
214 gen_logger.setLevel(logging.DEBUG)
215 gen_logger.addFilter(TrafficGenCommandFilter())
216 logger.addHandler(gen_logger)
218 metrics_logger = logging.FileHandler(filename=log_file_sys_metrics)
219 metrics_logger.setLevel(logging.DEBUG)
220 metrics_logger.addFilter(SystemMetricsCommandFilter())
221 logger.addHandler(metrics_logger)
224 def apply_filter(tests, tc_filter):
225 """Allow a subset of tests to be conveniently selected
227 :param tests: The list of Tests from which to select.
228 :param tc_filter: A case-insensitive string of comma-separated terms
229 indicating the Tests to select.
230 e.g. 'RFC' - select all tests whose name contains 'RFC'
231 e.g. 'RFC,burst' - select all tests whose name contains 'RFC' or
233 e.g. 'RFC,burst,!p2p' - select all tests whose name contains 'RFC'
234 or 'burst' and from these remove any containing 'p2p'.
235 e.g. '' - empty string selects all tests.
236 :return: A list of the selected Tests.
239 if tc_filter is None:
242 for term in [x.strip() for x in tc_filter.lower().split(",")]:
243 if not term or term[0] != '!':
244 # Add matching tests from 'tests' into results
245 result.extend([test for test in tests \
246 if test.name.lower().find(term) >= 0])
248 # Term begins with '!' so we remove matching tests
249 result = [test for test in result \
250 if test.name.lower().find(term[1:]) < 0]
255 class MockTestCase(unittest.TestCase):
256 """Allow use of xmlrunner to generate Jenkins compatible output without
257 using xmlrunner to actually run tests.
260 suite = unittest.TestSuite()
261 suite.addTest(MockTestCase('Test1 passed ', True, 'Test1'))
262 suite.addTest(MockTestCase('Test2 failed because...', False, 'Test2'))
263 xmlrunner.XMLTestRunner(...).run(suite)
266 def __init__(self, msg, is_pass, test_name):
269 self.is_pass = is_pass
271 #dynamically create a test method with the right name
272 #but point the method at our generic test method
273 setattr(MockTestCase, test_name, self.generic_test)
275 super(MockTestCase, self).__init__(test_name)
277 def generic_test(self):
278 """Provide a generic function that raises or not based
279 on how self.is_pass was set in the constructor"""
280 self.assertTrue(self.is_pass, self.msg)
286 args = parse_arguments()
290 settings.load_from_dir('conf')
292 # load command line parameters first in case there are settings files
294 settings.load_from_dict(args)
296 if args['conf_file']:
297 settings.load_from_file(args['conf_file'])
300 settings.load_from_env()
302 # reload command line parameters since these should take higher priority
303 # than both a settings file and environment variables
304 settings.load_from_dict(args)
306 configure_logging(settings.getValue('VERBOSITY'))
307 logger = logging.getLogger()
309 # configure trafficgens
311 if args['trafficgen']:
312 trafficgens = Loader().get_trafficgens()
313 if args['trafficgen'] not in trafficgens:
314 logging.error('There are no trafficgens matching \'%s\' found in'
315 ' \'%s\'. Exiting...', args['trafficgen'],
316 settings.getValue('TRAFFICGEN_DIR'))
321 vswitches = Loader().get_vswitches()
322 if args['vswitch'] not in vswitches:
323 logging.error('There are no vswitches matching \'%s\' found in'
324 ' \'%s\'. Exiting...', args['vswitch'],
325 settings.getValue('VSWITCH_DIR'))
329 vnfs = Loader().get_vnfs()
330 if args['vnf'] not in vnfs:
331 logging.error('there are no vnfs matching \'%s\' found in'
332 ' \'%s\'. exiting...', args['vnf'],
333 settings.getValue('vnf_dir'))
337 if args['duration'].isdigit() and int(args['duration']) > 0:
338 settings.setValue('duration', args['duration'])
340 logging.error('The selected Duration is not a number')
343 # generate results directory name
344 date = datetime.datetime.fromtimestamp(time.time())
345 results_dir = "results_" + date.strftime('%Y-%m-%d_%H-%M-%S')
346 results_path = os.path.join(settings.getValue('LOG_DIR'), results_dir)
349 testcases = settings.getValue('PERFORMANCE_TESTS')
351 for cfg in testcases:
353 all_tests.append(TestCase(cfg, results_path))
354 except (Exception) as _:
355 logger.exception("Failed to create test: %s",
356 cfg.get('Name', '<Name not set>'))
359 # if required, handle list-* operations
362 print("Available Tests:")
364 for test in all_tests:
365 print('* %-18s%s' % ('%s:' % test.name, test.desc))
368 if args['list_trafficgens']:
369 print(Loader().get_trafficgens_printable())
372 if args['list_collectors']:
373 print(Loader().get_collectors_printable())
376 if args['list_vswitches']:
377 print(Loader().get_vswitches_printable())
380 if args['list_vnfs']:
381 print(Loader().get_vnfs_printable())
384 if args['list_settings']:
388 # select requested tests
389 if args['exact_test_name'] and args['tests']:
390 logger.error("Cannot specify tests with both positional args and --test.")
393 if args['exact_test_name']:
394 exact_names = args['exact_test_name']
395 # positional args => exact matches only
396 selected_tests = [test for test in all_tests if test.name in exact_names]
398 # --tests => apply filter to select requested tests
399 selected_tests = apply_filter(all_tests, args['tests'])
401 # Default - run all tests
402 selected_tests = all_tests
404 if not selected_tests:
405 logger.error("No tests matched --test option or positional args. Done.")
408 # create results directory
409 if not os.path.exists(results_path):
410 logger.info("Creating result directory: " + results_path)
411 os.makedirs(results_path)
414 suite = unittest.TestSuite()
415 for test in selected_tests:
418 suite.addTest(MockTestCase('', True, test.name))
419 #pylint: disable=broad-except
420 except (Exception) as ex:
421 logger.exception("Failed to run test: %s", test.name)
422 suite.addTest(MockTestCase(str(ex), False, test.name))
423 logger.info("Continuing with next test...")
425 if settings.getValue('XUNIT'):
426 xmlrunner.XMLTestRunner(
427 output=settings.getValue('XUNIT_DIR'), outsuffix="",
428 verbosity=0).run(suite)
430 #remove directory if no result files were created.
431 if os.path.exists(results_path):
432 files_list = os.listdir(results_path)
434 shutil.rmtree(results_path)
436 for file in files_list:
437 # generate report from all csv files
438 if file[-3:] == 'csv':
439 results_csv = os.path.join(results_path, file)
440 if os.path.isfile(results_csv) and os.access(results_csv, os.R_OK):
441 report.generate(testcases, results_csv)
443 if __name__ == "__main__":