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