50f0996a39c6cf14b73155c24f46e5db299f75d5
[vswitchperf.git] / vsperf
1 #!/usr/bin/env python3
2
3 # Copyright 2015-2016 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 time
25 import datetime
26 import shutil
27 import unittest
28 import xmlrunner
29 import locale
30
31 sys.dont_write_bytecode = True
32
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
40
41 VERBOSITY_LEVELS = {
42     'debug': logging.DEBUG,
43     'info': logging.INFO,
44     'warning': logging.WARNING,
45     'error': logging.ERROR,
46     'critical': logging.CRITICAL
47 }
48
49
50 def parse_arguments():
51     """
52     Parse command line arguments.
53     """
54     class _SplitTestParamsAction(argparse.Action):
55         """
56         Parse and split the '--test-params' argument.
57
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'
61         """
62         def __call__(self, parser, namespace, values, option_string=None):
63             results = {}
64
65             for value in values.split(';'):
66                 result = [key.strip() for key in value.split('=')]
67                 if len(result) == 1:
68                     results[result[0]] = True
69                 elif len(result) == 2:
70                     results[result[0]] = result[1]
71                 else:
72                     raise argparse.ArgumentTypeError(
73                         'expected \'%s\' to be of format \'key=val\' or'
74                         ' \'key\'' % result)
75
76             setattr(namespace, self.dest, results)
77
78     class _ValidateFileAction(argparse.Action):
79         """Validate a file can be read from before using it.
80         """
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)
88
89             setattr(namespace, self.dest, values)
90
91     class _ValidateDirAction(argparse.Action):
92         """Validate a directory can be written to before using it.
93         """
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)
101
102             setattr(namespace, self.dest, values)
103
104     def list_logging_levels():
105         """Give a summary of all available logging levels.
106
107         :return: List of verbosity level names in decreasing order of
108             verbosity
109         """
110         return sorted(VERBOSITY_LEVELS.keys(),
111                       key=lambda x: VERBOSITY_LEVELS[x])
112
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.')
134
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(),
142                        help='debug level')
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')
163
164     args = vars(parser.parse_args())
165
166     return args
167
168
169 def configure_logging(level):
170     """Configure logging.
171     """
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'))
179
180     logger = logging.getLogger()
181     logger.setLevel(logging.DEBUG)
182
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)
188
189     file_logger = logging.FileHandler(filename=log_file_default)
190     file_logger.setLevel(logging.DEBUG)
191     logger.addHandler(file_logger)
192
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)
197
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)
202
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)
207
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)
212
213
214 def apply_filter(tests, tc_filter):
215     """Allow a subset of tests to be conveniently selected
216
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
222             'burst'
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.
227     """
228     result = []
229     if tc_filter is None:
230         tc_filter = ""
231
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])
237         else:
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]
241
242     return result
243
244
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.
248     """
249
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())
255
256 class MockTestCase(unittest.TestCase):
257     """Allow use of xmlrunner to generate Jenkins compatible output without
258     using xmlrunner to actually run tests.
259
260     Usage:
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)
265     """
266
267     def __init__(self, msg, is_pass, test_name):
268         #remember the things
269         self.msg = msg
270         self.is_pass = is_pass
271
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)
275
276         super(MockTestCase, self).__init__(test_name)
277
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)
282
283
284 def main():
285     """Main function.
286     """
287     args = parse_arguments()
288
289     # configure settings
290
291     settings.load_from_dir('conf')
292
293     # load command line parameters first in case there are settings files
294     # to be used
295     settings.load_from_dict(args)
296
297     if args['conf_file']:
298         settings.load_from_file(args['conf_file'])
299
300     if args['load_env']:
301         settings.load_from_env()
302
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)
306
307     vswitch_none =  False
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')))
317         else:
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')))
321     else:
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():
326             vswitch_none = True
327
328     configure_logging(settings.getValue('VERBOSITY'))
329     logger = logging.getLogger()
330
331     # check and fix locale
332     check_and_set_locale()
333
334     # configure trafficgens
335
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'))
342             sys.exit(1)
343
344     # configure vswitch
345     if args['vswitch']:
346         vswitch_none =  'none' == args['vswitch'].strip().lower()
347         if vswitch_none:
348             settings.setValue('VSWITCH', 'none')
349         else:
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'))
355                 sys.exit(1)
356
357     if args['fwdapp']:
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'))
365             sys.exit(1)
366
367     if args['vnf']:
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'))
373             sys.exit(1)
374
375     if args['duration']:
376         if args['duration'].isdigit() and int(args['duration']) > 0:
377             settings.setValue('duration', args['duration'])
378         else:
379             logging.error('The selected Duration is not a number')
380             sys.exit(1)
381
382     # update global settings
383     guest_loopback = get_test_param('guest_loopback', None)
384     if guest_loopback:
385         tmp_gl = []
386         for i in range(len(settings.getValue('GUEST_LOOPBACK'))):
387             tmp_gl.append(guest_loopback)
388         settings.setValue('GUEST_LOOPBACK', tmp_gl)
389
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)
394
395     # configure tests
396     testcases = settings.getValue('PERFORMANCE_TESTS')
397     all_tests = []
398     for cfg in testcases:
399         try:
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>'))
404             raise
405
406     # if required, handle list-* operations
407
408     if args['list']:
409         print("Available Tests:")
410         print("======")
411         for test in all_tests:
412             print('* %-18s%s' % ('%s:' % test.name, test.desc))
413         exit()
414
415     if args['list_trafficgens']:
416         print(Loader().get_trafficgens_printable())
417         exit()
418
419     if args['list_collectors']:
420         print(Loader().get_collectors_printable())
421         exit()
422
423     if args['list_vswitches']:
424         print(Loader().get_vswitches_printable())
425         exit()
426
427     if args['list_fwdapps']:
428         print(Loader().get_pktfwds_printable())
429         exit()
430
431     if args['list_vnfs']:
432         print(Loader().get_vnfs_printable())
433         exit()
434
435     if args['list_settings']:
436         print(str(settings))
437         exit()
438
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.")
442         sys.exit(1)
443
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]
448     elif args['tests']:
449         # --tests => apply filter to select requested tests
450         selected_tests = apply_filter(all_tests, args['tests'])
451     else:
452         # Default - run all tests
453         selected_tests = all_tests
454
455     if not selected_tests:
456         logger.error("No tests matched --test option or positional args. Done.")
457         sys.exit(1)
458
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)
463
464     # run tests
465     suite = unittest.TestSuite()
466     for test in selected_tests:
467         try:
468             if vswitch_none:
469                 if test.deployment.lower() != 'p2p':
470                     logging.error('\'none\' vswitch option supported only'
471                                   ' for p2p deployment.')
472                     sys.exit(1)
473             test.run()
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...")
480
481     if settings.getValue('XUNIT'):
482         xmlrunner.XMLTestRunner(
483             output=settings.getValue('XUNIT_DIR'), outsuffix="",
484             verbosity=0).run(suite)
485
486     if args['opnfvpod']:
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')
491
492         int_data = {'cuse': False,
493                     'vanilla': False,
494                     'pod': pod_name,
495                     'installer': installer_name,
496                     'pkg_list': pkg_list,
497                     'db_url': opnfv_url}
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)
503
504     #remove directory if no result files were created.
505     if os.path.exists(results_path):
506         files_list = os.listdir(results_path)
507         if files_list == []:
508             shutil.rmtree(results_path)
509
510 if __name__ == "__main__":
511     main()
512
513