pkt_gen: support of standalone execution of traffic generator
[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 import copy
31
32 sys.dont_write_bytecode = True
33
34 from conf import settings
35 from conf import get_test_param
36 from core.loader import Loader
37 from testcases import TestCase
38 from tools import tasks
39 from tools.pkt_gen import trafficgen
40 from tools.opnfvdashboard import opnfvdashboard
41 from tools.pkt_gen.trafficgen.trafficgenhelper import TRAFFIC_DEFAULTS
42 from conf import get_test_param
43 import core.component_factory as component_factory
44
45 VERBOSITY_LEVELS = {
46     'debug': logging.DEBUG,
47     'info': logging.INFO,
48     'warning': logging.WARNING,
49     'error': logging.ERROR,
50     'critical': logging.CRITICAL
51 }
52
53
54 def parse_arguments():
55     """
56     Parse command line arguments.
57     """
58     class _SplitTestParamsAction(argparse.Action):
59         """
60         Parse and split the '--test-params' argument.
61
62         This expects either 'x=y', 'x=y,z' or 'x' (implicit true)
63         values. For multiple overrides use a ; separated list for
64         e.g. --test-params 'x=z; y=a,b'
65         """
66         def __call__(self, parser, namespace, values, option_string=None):
67             results = {}
68
69             for value in values.split(';'):
70                 result = [key.strip() for key in value.split('=')]
71                 if len(result) == 1:
72                     results[result[0]] = True
73                 elif len(result) == 2:
74                     results[result[0]] = result[1]
75                 else:
76                     raise argparse.ArgumentTypeError(
77                         'expected \'%s\' to be of format \'key=val\' or'
78                         ' \'key\'' % result)
79
80             setattr(namespace, self.dest, results)
81
82     class _ValidateFileAction(argparse.Action):
83         """Validate a file can be read from before using it.
84         """
85         def __call__(self, parser, namespace, values, option_string=None):
86             if not os.path.isfile(values):
87                 raise argparse.ArgumentTypeError(
88                     'the path \'%s\' is not a valid path' % values)
89             elif not os.access(values, os.R_OK):
90                 raise argparse.ArgumentTypeError(
91                     'the path \'%s\' is not accessible' % values)
92
93             setattr(namespace, self.dest, values)
94
95     class _ValidateDirAction(argparse.Action):
96         """Validate a directory can be written to before using it.
97         """
98         def __call__(self, parser, namespace, values, option_string=None):
99             if not os.path.isdir(values):
100                 raise argparse.ArgumentTypeError(
101                     'the path \'%s\' is not a valid path' % values)
102             elif not os.access(values, os.W_OK):
103                 raise argparse.ArgumentTypeError(
104                     'the path \'%s\' is not accessible' % values)
105
106             setattr(namespace, self.dest, values)
107
108     def list_logging_levels():
109         """Give a summary of all available logging levels.
110
111         :return: List of verbosity level names in decreasing order of
112             verbosity
113         """
114         return sorted(VERBOSITY_LEVELS.keys(),
115                       key=lambda x: VERBOSITY_LEVELS[x])
116
117     parser = argparse.ArgumentParser(prog=__file__, formatter_class=
118                                      argparse.ArgumentDefaultsHelpFormatter)
119     parser.add_argument('--version', action='version', version='%(prog)s 0.2')
120     parser.add_argument('--list', '--list-tests', action='store_true',
121                         help='list all tests and exit')
122     parser.add_argument('--list-trafficgens', action='store_true',
123                         help='list all traffic generators and exit')
124     parser.add_argument('--list-collectors', action='store_true',
125                         help='list all system metrics loggers and exit')
126     parser.add_argument('--list-vswitches', action='store_true',
127                         help='list all system vswitches and exit')
128     parser.add_argument('--list-fwdapps', action='store_true',
129                         help='list all system forwarding applications and exit')
130     parser.add_argument('--list-vnfs', action='store_true',
131                         help='list all system vnfs and exit')
132     parser.add_argument('--list-settings', action='store_true',
133                         help='list effective settings configuration and exit')
134     parser.add_argument('exact_test_name', nargs='*', help='Exact names of\
135             tests to run. E.g "vsperf phy2phy_tput phy2phy_cont"\
136             runs only the two tests with those exact names.\
137             To run all tests omit both positional args and --tests arg.')
138
139     group = parser.add_argument_group('test selection options')
140     group.add_argument('-m', '--mode', help='vsperf mode of operation;\
141             Values: "normal" - execute vSwitch, VNF and traffic generator;\
142             "trafficgen" - execute only traffic generator; "trafficgen-off" \
143             - execute vSwitch and VNF', default='normal')
144
145     group.add_argument('-f', '--test-spec', help='test specification file')
146     group.add_argument('-d', '--test-dir', help='directory containing tests')
147     group.add_argument('-t', '--tests', help='Comma-separated list of terms \
148             indicating tests to run. e.g. "RFC2544,!p2p" - run all tests whose\
149             name contains RFC2544 less those containing "p2p"')
150     group.add_argument('--verbosity', choices=list_logging_levels(),
151                        help='debug level')
152     group.add_argument('--trafficgen', help='traffic generator to use')
153     group.add_argument('--vswitch', help='vswitch implementation to use')
154     group.add_argument('--fwdapp', help='packet forwarding application to use')
155     group.add_argument('--vnf', help='vnf to use')
156     group.add_argument('--duration', help='traffic transmit duration')
157     group.add_argument('--sysmetrics', help='system metrics logger to use')
158     group = parser.add_argument_group('test behavior options')
159     group.add_argument('--xunit', action='store_true',
160                        help='enable xUnit-formatted output')
161     group.add_argument('--xunit-dir', action=_ValidateDirAction,
162                        help='output directory of xUnit-formatted output')
163     group.add_argument('--load-env', action='store_true',
164                        help='enable loading of settings from the environment')
165     group.add_argument('--conf-file', action=_ValidateFileAction,
166                        help='settings file')
167     group.add_argument('--test-params', action=_SplitTestParamsAction,
168                        help='csv list of test parameters: key=val; e.g.'
169                        'including pkt_sizes=x,y; duration=x; '
170                        'rfc2544_trials=x ...')
171     group.add_argument('--opnfvpod', help='name of POD in opnfv')
172
173     args = vars(parser.parse_args())
174
175     return args
176
177
178 def configure_logging(level):
179     """Configure logging.
180     """
181     log_file_default = os.path.join(
182         settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_DEFAULT'))
183     log_file_host_cmds = os.path.join(
184         settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_HOST_CMDS'))
185     log_file_traffic_gen = os.path.join(
186         settings.getValue('LOG_DIR'),
187         settings.getValue('LOG_FILE_TRAFFIC_GEN'))
188
189     logger = logging.getLogger()
190     logger.setLevel(logging.DEBUG)
191
192     stream_logger = logging.StreamHandler(sys.stdout)
193     stream_logger.setLevel(VERBOSITY_LEVELS[level])
194     stream_logger.setFormatter(logging.Formatter(
195         '[%(levelname)s]  %(asctime)s : (%(name)s) - %(message)s'))
196     logger.addHandler(stream_logger)
197
198     file_logger = logging.FileHandler(filename=log_file_default)
199     file_logger.setLevel(logging.DEBUG)
200     logger.addHandler(file_logger)
201
202     class CommandFilter(logging.Filter):
203         """Filter out strings beginning with 'cmd :'"""
204         def filter(self, record):
205             return record.getMessage().startswith(tasks.CMD_PREFIX)
206
207     class TrafficGenCommandFilter(logging.Filter):
208         """Filter out strings beginning with 'gencmd :'"""
209         def filter(self, record):
210             return record.getMessage().startswith(trafficgen.CMD_PREFIX)
211
212     cmd_logger = logging.FileHandler(filename=log_file_host_cmds)
213     cmd_logger.setLevel(logging.DEBUG)
214     cmd_logger.addFilter(CommandFilter())
215     logger.addHandler(cmd_logger)
216
217     gen_logger = logging.FileHandler(filename=log_file_traffic_gen)
218     gen_logger.setLevel(logging.DEBUG)
219     gen_logger.addFilter(TrafficGenCommandFilter())
220     logger.addHandler(gen_logger)
221
222
223 def apply_filter(tests, tc_filter):
224     """Allow a subset of tests to be conveniently selected
225
226     :param tests: The list of Tests from which to select.
227     :param tc_filter: A case-insensitive string of comma-separated terms
228         indicating the Tests to select.
229         e.g. 'RFC' - select all tests whose name contains 'RFC'
230         e.g. 'RFC,burst' - select all tests whose name contains 'RFC' or
231             'burst'
232         e.g. 'RFC,burst,!p2p' - select all tests whose name contains 'RFC'
233             or 'burst' and from these remove any containing 'p2p'.
234         e.g. '' - empty string selects all tests.
235     :return: A list of the selected Tests.
236     """
237     result = []
238     if tc_filter is None:
239         tc_filter = ""
240
241     for term in [x.strip() for x in tc_filter.lower().split(",")]:
242         if not term or term[0] != '!':
243             # Add matching tests from 'tests' into results
244             result.extend([test for test in tests \
245                 if test.name.lower().find(term) >= 0])
246         else:
247             # Term begins with '!' so we remove matching tests
248             result = [test for test in result \
249                 if test.name.lower().find(term[1:]) < 0]
250
251     return result
252
253
254 def check_and_set_locale():
255     """ Function will check locale settings. In case, that it isn't configured
256     properly, then default values specified by DEFAULT_LOCALE will be used.
257     """
258
259     system_locale = locale.getdefaultlocale()
260     if None in system_locale:
261         os.environ['LC_ALL'] = settings.getValue('DEFAULT_LOCALE')
262         logging.warning("Locale was not properly configured. Default values were set. Old locale: %s, New locale: %s",
263                         system_locale, locale.getdefaultlocale())
264
265 class MockTestCase(unittest.TestCase):
266     """Allow use of xmlrunner to generate Jenkins compatible output without
267     using xmlrunner to actually run tests.
268
269     Usage:
270         suite = unittest.TestSuite()
271         suite.addTest(MockTestCase('Test1 passed ', True, 'Test1'))
272         suite.addTest(MockTestCase('Test2 failed because...', False, 'Test2'))
273         xmlrunner.XMLTestRunner(...).run(suite)
274     """
275
276     def __init__(self, msg, is_pass, test_name):
277         #remember the things
278         self.msg = msg
279         self.is_pass = is_pass
280
281         #dynamically create a test method with the right name
282         #but point the method at our generic test method
283         setattr(MockTestCase, test_name, self.generic_test)
284
285         super(MockTestCase, self).__init__(test_name)
286
287     def generic_test(self):
288         """Provide a generic function that raises or not based
289         on how self.is_pass was set in the constructor"""
290         self.assertTrue(self.is_pass, self.msg)
291
292
293 def main():
294     """Main function.
295     """
296     args = parse_arguments()
297
298     # configure settings
299
300     settings.load_from_dir('conf')
301
302     # load command line parameters first in case there are settings files
303     # to be used
304     settings.load_from_dict(args)
305
306     if args['conf_file']:
307         settings.load_from_file(args['conf_file'])
308
309     if args['load_env']:
310         settings.load_from_env()
311
312     # reload command line parameters since these should take higher priority
313     # than both a settings file and environment variables
314     settings.load_from_dict(args)
315
316     vswitch_none =  False
317     # set dpdk and ovs paths accorfing to VNF and VSWITCH
318     if settings.getValue('VSWITCH').endswith('Vanilla'):
319         # settings paths for Vanilla
320         settings.setValue('OVS_DIR', (settings.getValue('OVS_DIR_VANILLA')))
321     elif settings.getValue('VSWITCH').endswith('Vhost'):
322         if settings.getValue('VNF').endswith('Cuse'):
323             # settings paths for Cuse
324             settings.setValue('RTE_SDK', (settings.getValue('RTE_SDK_CUSE')))
325             settings.setValue('OVS_DIR', (settings.getValue('OVS_DIR_CUSE')))
326         else:
327             # settings paths for VhostUser
328             settings.setValue('RTE_SDK', (settings.getValue('RTE_SDK_USER')))
329             settings.setValue('OVS_DIR', (settings.getValue('OVS_DIR_USER')))
330     else:
331         # default - set to VHOST USER but can be changed during enhancement
332         settings.setValue('RTE_SDK', (settings.getValue('RTE_SDK_USER')))
333         settings.setValue('OVS_DIR', (settings.getValue('OVS_DIR_USER')))
334         if 'none' == settings.getValue('VSWITCH').strip().lower():
335             vswitch_none = True
336
337     configure_logging(settings.getValue('VERBOSITY'))
338     logger = logging.getLogger()
339
340     # check and fix locale
341     check_and_set_locale()
342
343     # configure trafficgens
344     if args['trafficgen']:
345         trafficgens = Loader().get_trafficgens()
346         if args['trafficgen'] not in trafficgens:
347             logging.error('There are no trafficgens matching \'%s\' found in'
348                           ' \'%s\'. Exiting...', args['trafficgen'],
349                           settings.getValue('TRAFFICGEN_DIR'))
350             sys.exit(1)
351
352     # configure vswitch
353     if args['vswitch']:
354         vswitch_none =  'none' == args['vswitch'].strip().lower()
355         if vswitch_none:
356             settings.setValue('VSWITCH', 'none')
357         else:
358             vswitches = Loader().get_vswitches()
359             if args['vswitch'] not in vswitches:
360                 logging.error('There are no vswitches matching \'%s\' found in'
361                               ' \'%s\'. Exiting...', args['vswitch'],
362                               settings.getValue('VSWITCH_DIR'))
363                 sys.exit(1)
364
365     if args['fwdapp']:
366         settings.setValue('PKTFWD', args['fwdapp'])
367         fwdapps = Loader().get_pktfwds()
368         if args['fwdapp'] not in fwdapps:
369             logging.error('There are no forwarding application'
370                           ' matching \'%s\' found in'
371                           ' \'%s\'. Exiting...', args['fwdapp'],
372                           settings.getValue('PKTFWD_DIR'))
373             sys.exit(1)
374
375     if args['vnf']:
376         vnfs = Loader().get_vnfs()
377         if args['vnf'] not in vnfs:
378             logging.error('there are no vnfs matching \'%s\' found in'
379                           ' \'%s\'. exiting...', args['vnf'],
380                           settings.getValue('vnf_dir'))
381             sys.exit(1)
382
383     if args['duration']:
384         if args['duration'].isdigit() and int(args['duration']) > 0:
385             settings.setValue('duration', args['duration'])
386         else:
387             logging.error('The selected Duration is not a number')
388             sys.exit(1)
389
390     # update global settings
391     guest_loopback = get_test_param('guest_loopback', None)
392     if guest_loopback:
393         tmp_gl = []
394         for i in range(len(settings.getValue('GUEST_LOOPBACK'))):
395             tmp_gl.append(guest_loopback)
396         settings.setValue('GUEST_LOOPBACK', tmp_gl)
397
398     settings.setValue('mode', args['mode'])
399
400     # generate results directory name
401     date = datetime.datetime.fromtimestamp(time.time())
402     results_dir = "results_" + date.strftime('%Y-%m-%d_%H-%M-%S')
403     results_path = os.path.join(settings.getValue('LOG_DIR'), results_dir)
404
405     # create results directory
406     if not os.path.exists(results_path):
407         logger.info("Creating result directory: "  + results_path)
408         os.makedirs(results_path)
409
410     if settings.getValue('mode') == 'trafficgen':
411         # execute only traffic generator
412         logging.debug("Executing traffic generator:")
413         loader = Loader()
414         # set traffic details, so they can be passed to traffic ctl
415         traffic = copy.deepcopy(TRAFFIC_DEFAULTS)
416         traffic.update({'traffic_type': get_test_param('traffic_type', 'rfc2544'),
417                         'bidir': get_test_param('bidirectional', False),
418                         'multistream': int(get_test_param('multistream', 0)),
419                         'stream_type': get_test_param('stream_type', 'L4'),
420                         'frame_rate': int(get_test_param('iload', 100))})
421
422         traffic_ctl = component_factory.create_traffic(
423             traffic['traffic_type'],
424             loader.get_trafficgen_class())
425         with traffic_ctl:
426             traffic_ctl.send_traffic(traffic)
427         logging.debug("Traffic Results:")
428         traffic_ctl.print_results()
429     else:
430         # configure tests
431         testcases = settings.getValue('PERFORMANCE_TESTS')
432         all_tests = []
433         for cfg in testcases:
434             try:
435                 all_tests.append(TestCase(cfg, results_path))
436             except (Exception) as _:
437                 logger.exception("Failed to create test: %s",
438                                  cfg.get('Name', '<Name not set>'))
439                 raise
440
441         # if required, handle list-* operations
442
443         if args['list']:
444             print("Available Tests:")
445             print("======")
446             for test in all_tests:
447                 print('* %-18s%s' % ('%s:' % test.name, test.desc))
448             exit()
449
450         if args['list_trafficgens']:
451             print(Loader().get_trafficgens_printable())
452             exit()
453
454         if args['list_collectors']:
455             print(Loader().get_collectors_printable())
456             exit()
457
458         if args['list_vswitches']:
459             print(Loader().get_vswitches_printable())
460             exit()
461
462         if args['list_vnfs']:
463             print(Loader().get_vnfs_printable())
464             exit()
465
466         if args['list_settings']:
467             print(str(settings))
468             exit()
469
470         # select requested tests
471         if args['exact_test_name'] and args['tests']:
472             logger.error("Cannot specify tests with both positional args and --test.")
473             sys.exit(1)
474
475         if args['exact_test_name']:
476             exact_names = args['exact_test_name']
477             # positional args => exact matches only
478             selected_tests = [test for test in all_tests if test.name in exact_names]
479         elif args['tests']:
480             # --tests => apply filter to select requested tests
481             selected_tests = apply_filter(all_tests, args['tests'])
482         else:
483             # Default - run all tests
484             selected_tests = all_tests
485
486         if not selected_tests:
487             logger.error("No tests matched --test option or positional args. Done.")
488             sys.exit(1)
489
490         # run tests
491         suite = unittest.TestSuite()
492         for test in selected_tests:
493             try:
494                 test.run()
495                 suite.addTest(MockTestCase('', True, test.name))
496             #pylint: disable=broad-except
497             except (Exception) as ex:
498                 logger.exception("Failed to run test: %s", test.name)
499                 suite.addTest(MockTestCase(str(ex), False, test.name))
500                 logger.info("Continuing with next test...")
501
502         if settings.getValue('XUNIT'):
503             xmlrunner.XMLTestRunner(
504                 output=settings.getValue('XUNIT_DIR'), outsuffix="",
505                 verbosity=0).run(suite)
506
507         if args['opnfvpod']:
508             pod_name = args['opnfvpod']
509             installer_name = settings.getValue('OPNFV_INSTALLER')
510             opnfv_url = settings.getValue('OPNFV_URL')
511             pkg_list = settings.getValue('PACKAGE_LIST')
512
513             int_data = {'cuse': False,
514                         'vanilla': False,
515                         'pod': pod_name,
516                         'installer': installer_name,
517                         'pkg_list': pkg_list,
518                         'db_url': opnfv_url}
519             if settings.getValue('VSWITCH').endswith('Vanilla'):
520                 int_data['vanilla'] = True
521             if settings.getValue('VNF').endswith('Cuse'):
522                 int_data['cuse'] = True
523             opnfvdashboard.results2opnfv_dashboard(results_path, int_data)
524
525     #remove directory if no result files were created.
526     if os.path.exists(results_path):
527         files_list = os.listdir(results_path)
528         if files_list == []:
529             shutil.rmtree(results_path)
530
531 if __name__ == "__main__":
532     main()
533
534