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