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 import tasks
36 from tools.pkt_gen import trafficgen
39 'debug': logging.DEBUG,
41 'warning': logging.WARNING,
42 'error': logging.ERROR,
43 'critical': logging.CRITICAL
47 def parse_arguments():
49 Parse command line arguments.
51 class _SplitTestParamsAction(argparse.Action):
53 Parse and split the '--test-params' argument.
55 This expects either 'x=y', 'x=y,z' or 'x' (implicit true)
56 values. For multiple overrides use a ; separated list for
57 e.g. --test-params 'x=z; y=a,b'
59 def __call__(self, parser, namespace, values, option_string=None):
62 for value in values.split(';'):
63 result = [key.strip() for key in value.split('=')]
65 results[result[0]] = True
66 elif len(result) == 2:
67 results[result[0]] = result[1]
69 raise argparse.ArgumentTypeError(
70 'expected \'%s\' to be of format \'key=val\' or'
73 setattr(namespace, self.dest, results)
75 class _ValidateFileAction(argparse.Action):
76 """Validate a file can be read from before using it.
78 def __call__(self, parser, namespace, values, option_string=None):
79 if not os.path.isfile(values):
80 raise argparse.ArgumentTypeError(
81 'the path \'%s\' is not a valid path' % values)
82 elif not os.access(values, os.R_OK):
83 raise argparse.ArgumentTypeError(
84 'the path \'%s\' is not accessible' % values)
86 setattr(namespace, self.dest, values)
88 class _ValidateDirAction(argparse.Action):
89 """Validate a directory can be written to before using it.
91 def __call__(self, parser, namespace, values, option_string=None):
92 if not os.path.isdir(values):
93 raise argparse.ArgumentTypeError(
94 'the path \'%s\' is not a valid path' % values)
95 elif not os.access(values, os.W_OK):
96 raise argparse.ArgumentTypeError(
97 'the path \'%s\' is not accessible' % values)
99 setattr(namespace, self.dest, values)
101 def list_logging_levels():
102 """Give a summary of all available logging levels.
104 :return: List of verbosity level names in decreasing order of
107 return sorted(VERBOSITY_LEVELS.keys(),
108 key=lambda x: VERBOSITY_LEVELS[x])
110 parser = argparse.ArgumentParser(prog=__file__, formatter_class=
111 argparse.ArgumentDefaultsHelpFormatter)
112 parser.add_argument('--version', action='version', version='%(prog)s 0.2')
113 parser.add_argument('--list', '--list-tests', action='store_true',
114 help='list all tests and exit')
115 parser.add_argument('--list-trafficgens', action='store_true',
116 help='list all traffic generators and exit')
117 parser.add_argument('--list-collectors', action='store_true',
118 help='list all system metrics loggers and exit')
119 parser.add_argument('--list-vswitches', action='store_true',
120 help='list all system vswitches and exit')
121 parser.add_argument('--list-vnfs', action='store_true',
122 help='list all system vnfs and exit')
123 parser.add_argument('--list-settings', action='store_true',
124 help='list effective settings configuration and exit')
125 parser.add_argument('exact_test_name', nargs='*', help='Exact names of\
126 tests to run. E.g "vsperf phy2phy_tput phy2phy_cont"\
127 runs only the two tests with those exact names.\
128 To run all tests omit both positional args and --tests arg.')
130 group = parser.add_argument_group('test selection options')
131 group.add_argument('-f', '--test-spec', help='test specification file')
132 group.add_argument('-d', '--test-dir', help='directory containing tests')
133 group.add_argument('-t', '--tests', help='Comma-separated list of terms \
134 indicating tests to run. e.g. "RFC2544,!p2p" - run all tests whose\
135 name contains RFC2544 less those containing "p2p"')
136 group.add_argument('--verbosity', choices=list_logging_levels(),
138 group.add_argument('--trafficgen', help='traffic generator to use')
139 group.add_argument('--vswitch', help='vswitch implementation to use')
140 group.add_argument('--vnf', help='vnf to use')
141 group.add_argument('--duration', help='traffic transmit duration')
142 group.add_argument('--sysmetrics', help='system metrics logger to use')
143 group = parser.add_argument_group('test behavior options')
144 group.add_argument('--xunit', action='store_true',
145 help='enable xUnit-formatted output')
146 group.add_argument('--xunit-dir', action=_ValidateDirAction,
147 help='output directory of xUnit-formatted output')
148 group.add_argument('--load-env', action='store_true',
149 help='enable loading of settings from the environment')
150 group.add_argument('--conf-file', action=_ValidateFileAction,
151 help='settings file')
152 group.add_argument('--test-params', action=_SplitTestParamsAction,
153 help='csv list of test parameters: key=val; e.g.'
154 'including pkt_sizes=x,y; duration=x; '
155 'rfc2544_trials=x ...')
157 args = vars(parser.parse_args())
162 def configure_logging(level):
163 """Configure logging.
165 log_file_default = os.path.join(
166 settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_DEFAULT'))
167 log_file_host_cmds = os.path.join(
168 settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_HOST_CMDS'))
169 log_file_traffic_gen = os.path.join(
170 settings.getValue('LOG_DIR'),
171 settings.getValue('LOG_FILE_TRAFFIC_GEN'))
173 logger = logging.getLogger()
174 logger.setLevel(logging.DEBUG)
176 stream_logger = logging.StreamHandler(sys.stdout)
177 stream_logger.setLevel(VERBOSITY_LEVELS[level])
178 stream_logger.setFormatter(logging.Formatter(
179 '[%(levelname)s] %(asctime)s : (%(name)s) - %(message)s'))
180 logger.addHandler(stream_logger)
182 file_logger = logging.FileHandler(filename=log_file_default)
183 file_logger.setLevel(logging.DEBUG)
184 logger.addHandler(file_logger)
186 class CommandFilter(logging.Filter):
187 """Filter out strings beginning with 'cmd :'"""
188 def filter(self, record):
189 return record.getMessage().startswith(tasks.CMD_PREFIX)
191 class TrafficGenCommandFilter(logging.Filter):
192 """Filter out strings beginning with 'gencmd :'"""
193 def filter(self, record):
194 return record.getMessage().startswith(trafficgen.CMD_PREFIX)
196 cmd_logger = logging.FileHandler(filename=log_file_host_cmds)
197 cmd_logger.setLevel(logging.DEBUG)
198 cmd_logger.addFilter(CommandFilter())
199 logger.addHandler(cmd_logger)
201 gen_logger = logging.FileHandler(filename=log_file_traffic_gen)
202 gen_logger.setLevel(logging.DEBUG)
203 gen_logger.addFilter(TrafficGenCommandFilter())
204 logger.addHandler(gen_logger)
207 def apply_filter(tests, tc_filter):
208 """Allow a subset of tests to be conveniently selected
210 :param tests: The list of Tests from which to select.
211 :param tc_filter: A case-insensitive string of comma-separated terms
212 indicating the Tests to select.
213 e.g. 'RFC' - select all tests whose name contains 'RFC'
214 e.g. 'RFC,burst' - select all tests whose name contains 'RFC' or
216 e.g. 'RFC,burst,!p2p' - select all tests whose name contains 'RFC'
217 or 'burst' and from these remove any containing 'p2p'.
218 e.g. '' - empty string selects all tests.
219 :return: A list of the selected Tests.
222 if tc_filter is None:
225 for term in [x.strip() for x in tc_filter.lower().split(",")]:
226 if not term or term[0] != '!':
227 # Add matching tests from 'tests' into results
228 result.extend([test for test in tests \
229 if test.name.lower().find(term) >= 0])
231 # Term begins with '!' so we remove matching tests
232 result = [test for test in result \
233 if test.name.lower().find(term[1:]) < 0]
238 class MockTestCase(unittest.TestCase):
239 """Allow use of xmlrunner to generate Jenkins compatible output without
240 using xmlrunner to actually run tests.
243 suite = unittest.TestSuite()
244 suite.addTest(MockTestCase('Test1 passed ', True, 'Test1'))
245 suite.addTest(MockTestCase('Test2 failed because...', False, 'Test2'))
246 xmlrunner.XMLTestRunner(...).run(suite)
249 def __init__(self, msg, is_pass, test_name):
252 self.is_pass = is_pass
254 #dynamically create a test method with the right name
255 #but point the method at our generic test method
256 setattr(MockTestCase, test_name, self.generic_test)
258 super(MockTestCase, self).__init__(test_name)
260 def generic_test(self):
261 """Provide a generic function that raises or not based
262 on how self.is_pass was set in the constructor"""
263 self.assertTrue(self.is_pass, self.msg)
269 args = parse_arguments()
273 settings.load_from_dir('conf')
275 # load command line parameters first in case there are settings files
277 settings.load_from_dict(args)
279 if args['conf_file']:
280 settings.load_from_file(args['conf_file'])
283 settings.load_from_env()
285 # reload command line parameters since these should take higher priority
286 # than both a settings file and environment variables
287 settings.load_from_dict(args)
289 configure_logging(settings.getValue('VERBOSITY'))
290 logger = logging.getLogger()
292 # configure trafficgens
294 if args['trafficgen']:
295 trafficgens = Loader().get_trafficgens()
296 if args['trafficgen'] not in trafficgens:
297 logging.error('There are no trafficgens matching \'%s\' found in'
298 ' \'%s\'. Exiting...', args['trafficgen'],
299 settings.getValue('TRAFFICGEN_DIR'))
304 vswitches = Loader().get_vswitches()
305 if args['vswitch'] not in vswitches:
306 logging.error('There are no vswitches matching \'%s\' found in'
307 ' \'%s\'. Exiting...', args['vswitch'],
308 settings.getValue('VSWITCH_DIR'))
312 vnfs = Loader().get_vnfs()
313 if args['vnf'] not in vnfs:
314 logging.error('there are no vnfs matching \'%s\' found in'
315 ' \'%s\'. exiting...', args['vnf'],
316 settings.getValue('vnf_dir'))
320 if args['duration'].isdigit() and int(args['duration']) > 0:
321 settings.setValue('duration', args['duration'])
323 logging.error('The selected Duration is not a number')
326 # generate results directory name
327 date = datetime.datetime.fromtimestamp(time.time())
328 results_dir = "results_" + date.strftime('%Y-%m-%d_%H-%M-%S')
329 results_path = os.path.join(settings.getValue('LOG_DIR'), results_dir)
332 testcases = settings.getValue('PERFORMANCE_TESTS')
334 for cfg in testcases:
336 all_tests.append(TestCase(cfg, results_path))
337 except (Exception) as _:
338 logger.exception("Failed to create test: %s",
339 cfg.get('Name', '<Name not set>'))
342 # if required, handle list-* operations
345 print("Available Tests:")
347 for test in all_tests:
348 print('* %-18s%s' % ('%s:' % test.name, test.desc))
351 if args['list_trafficgens']:
352 print(Loader().get_trafficgens_printable())
355 if args['list_collectors']:
356 print(Loader().get_collectors_printable())
359 if args['list_vswitches']:
360 print(Loader().get_vswitches_printable())
363 if args['list_vnfs']:
364 print(Loader().get_vnfs_printable())
367 if args['list_settings']:
371 # select requested tests
372 if args['exact_test_name'] and args['tests']:
373 logger.error("Cannot specify tests with both positional args and --test.")
376 if args['exact_test_name']:
377 exact_names = args['exact_test_name']
378 # positional args => exact matches only
379 selected_tests = [test for test in all_tests if test.name in exact_names]
381 # --tests => apply filter to select requested tests
382 selected_tests = apply_filter(all_tests, args['tests'])
384 # Default - run all tests
385 selected_tests = all_tests
387 if not selected_tests:
388 logger.error("No tests matched --test option or positional args. Done.")
391 # create results directory
392 if not os.path.exists(results_path):
393 logger.info("Creating result directory: " + results_path)
394 os.makedirs(results_path)
397 suite = unittest.TestSuite()
398 for test in selected_tests:
401 suite.addTest(MockTestCase('', True, test.name))
402 #pylint: disable=broad-except
403 except (Exception) as ex:
404 logger.exception("Failed to run test: %s", test.name)
405 suite.addTest(MockTestCase(str(ex), False, test.name))
406 logger.info("Continuing with next test...")
408 if settings.getValue('XUNIT'):
409 xmlrunner.XMLTestRunner(
410 output=settings.getValue('XUNIT_DIR'), outsuffix="",
411 verbosity=0).run(suite)
413 #remove directory if no result files were created.
414 if os.path.exists(results_path):
415 files_list = os.listdir(results_path)
417 shutil.rmtree(results_path)
419 if __name__ == "__main__":