fuel-plugin: Initial version targeting Fuel8
[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-vnfs', action='store_true',
125                         help='list all system vnfs and exit')
126     parser.add_argument('--list-settings', action='store_true',
127                         help='list effective settings configuration and exit')
128     parser.add_argument('exact_test_name', nargs='*', help='Exact names of\
129             tests to run. E.g "vsperf phy2phy_tput phy2phy_cont"\
130             runs only the two tests with those exact names.\
131             To run all tests omit both positional args and --tests arg.')
132
133     group = parser.add_argument_group('test selection options')
134     group.add_argument('-f', '--test-spec', help='test specification file')
135     group.add_argument('-d', '--test-dir', help='directory containing tests')
136     group.add_argument('-t', '--tests', help='Comma-separated list of terms \
137             indicating tests to run. e.g. "RFC2544,!p2p" - run all tests whose\
138             name contains RFC2544 less those containing "p2p"')
139     group.add_argument('--verbosity', choices=list_logging_levels(),
140                        help='debug level')
141     group.add_argument('--trafficgen', help='traffic generator to use')
142     group.add_argument('--vswitch', help='vswitch implementation to use')
143     group.add_argument('--vnf', help='vnf to use')
144     group.add_argument('--duration', help='traffic transmit duration')
145     group.add_argument('--sysmetrics', help='system metrics logger to use')
146     group = parser.add_argument_group('test behavior options')
147     group.add_argument('--xunit', action='store_true',
148                        help='enable xUnit-formatted output')
149     group.add_argument('--xunit-dir', action=_ValidateDirAction,
150                        help='output directory of xUnit-formatted output')
151     group.add_argument('--load-env', action='store_true',
152                        help='enable loading of settings from the environment')
153     group.add_argument('--conf-file', action=_ValidateFileAction,
154                        help='settings file')
155     group.add_argument('--test-params', action=_SplitTestParamsAction,
156                        help='csv list of test parameters: key=val; e.g.'
157                        'including pkt_sizes=x,y; duration=x; '
158                        'rfc2544_trials=x ...')
159     group.add_argument('--opnfvpod', help='name of POD in opnfv')
160
161     args = vars(parser.parse_args())
162
163     return args
164
165
166 def configure_logging(level):
167     """Configure logging.
168     """
169     log_file_default = os.path.join(
170         settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_DEFAULT'))
171     log_file_host_cmds = os.path.join(
172         settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_HOST_CMDS'))
173     log_file_traffic_gen = os.path.join(
174         settings.getValue('LOG_DIR'),
175         settings.getValue('LOG_FILE_TRAFFIC_GEN'))
176
177     logger = logging.getLogger()
178     logger.setLevel(logging.DEBUG)
179
180     stream_logger = logging.StreamHandler(sys.stdout)
181     stream_logger.setLevel(VERBOSITY_LEVELS[level])
182     stream_logger.setFormatter(logging.Formatter(
183         '[%(levelname)s]  %(asctime)s : (%(name)s) - %(message)s'))
184     logger.addHandler(stream_logger)
185
186     file_logger = logging.FileHandler(filename=log_file_default)
187     file_logger.setLevel(logging.DEBUG)
188     logger.addHandler(file_logger)
189
190     class CommandFilter(logging.Filter):
191         """Filter out strings beginning with 'cmd :'"""
192         def filter(self, record):
193             return record.getMessage().startswith(tasks.CMD_PREFIX)
194
195     class TrafficGenCommandFilter(logging.Filter):
196         """Filter out strings beginning with 'gencmd :'"""
197         def filter(self, record):
198             return record.getMessage().startswith(trafficgen.CMD_PREFIX)
199
200     cmd_logger = logging.FileHandler(filename=log_file_host_cmds)
201     cmd_logger.setLevel(logging.DEBUG)
202     cmd_logger.addFilter(CommandFilter())
203     logger.addHandler(cmd_logger)
204
205     gen_logger = logging.FileHandler(filename=log_file_traffic_gen)
206     gen_logger.setLevel(logging.DEBUG)
207     gen_logger.addFilter(TrafficGenCommandFilter())
208     logger.addHandler(gen_logger)
209
210
211 def apply_filter(tests, tc_filter):
212     """Allow a subset of tests to be conveniently selected
213
214     :param tests: The list of Tests from which to select.
215     :param tc_filter: A case-insensitive string of comma-separated terms
216         indicating the Tests to select.
217         e.g. 'RFC' - select all tests whose name contains 'RFC'
218         e.g. 'RFC,burst' - select all tests whose name contains 'RFC' or
219             'burst'
220         e.g. 'RFC,burst,!p2p' - select all tests whose name contains 'RFC'
221             or 'burst' and from these remove any containing 'p2p'.
222         e.g. '' - empty string selects all tests.
223     :return: A list of the selected Tests.
224     """
225     result = []
226     if tc_filter is None:
227         tc_filter = ""
228
229     for term in [x.strip() for x in tc_filter.lower().split(",")]:
230         if not term or term[0] != '!':
231             # Add matching tests from 'tests' into results
232             result.extend([test for test in tests \
233                 if test.name.lower().find(term) >= 0])
234         else:
235             # Term begins with '!' so we remove matching tests
236             result = [test for test in result \
237                 if test.name.lower().find(term[1:]) < 0]
238
239     return result
240
241
242 def check_and_set_locale():
243     """ Function will check locale settings. In case, that it isn't configured
244     properly, then default values specified by DEFAULT_LOCALE will be used.
245     """
246
247     system_locale = locale.getdefaultlocale()
248     if None in system_locale:
249         os.environ['LC_ALL'] = settings.getValue('DEFAULT_LOCALE')
250         logging.warning("Locale was not properly configured. Default values were set. Old locale: %s, New locale: %s",
251                         system_locale, locale.getdefaultlocale())
252
253 class MockTestCase(unittest.TestCase):
254     """Allow use of xmlrunner to generate Jenkins compatible output without
255     using xmlrunner to actually run tests.
256
257     Usage:
258         suite = unittest.TestSuite()
259         suite.addTest(MockTestCase('Test1 passed ', True, 'Test1'))
260         suite.addTest(MockTestCase('Test2 failed because...', False, 'Test2'))
261         xmlrunner.XMLTestRunner(...).run(suite)
262     """
263
264     def __init__(self, msg, is_pass, test_name):
265         #remember the things
266         self.msg = msg
267         self.is_pass = is_pass
268
269         #dynamically create a test method with the right name
270         #but point the method at our generic test method
271         setattr(MockTestCase, test_name, self.generic_test)
272
273         super(MockTestCase, self).__init__(test_name)
274
275     def generic_test(self):
276         """Provide a generic function that raises or not based
277         on how self.is_pass was set in the constructor"""
278         self.assertTrue(self.is_pass, self.msg)
279
280
281 def main():
282     """Main function.
283     """
284     args = parse_arguments()
285
286     # configure settings
287
288     settings.load_from_dir('conf')
289
290     # load command line parameters first in case there are settings files
291     # to be used
292     settings.load_from_dict(args)
293
294     if args['conf_file']:
295         settings.load_from_file(args['conf_file'])
296
297     if args['load_env']:
298         settings.load_from_env()
299
300     # reload command line parameters since these should take higher priority
301     # than both a settings file and environment variables
302     settings.load_from_dict(args)
303
304     # set dpdk and ovs paths accorfing to VNF and VSWITCH
305     if settings.getValue('VSWITCH').endswith('Vanilla'):
306         # settings paths for Vanilla
307         settings.setValue('OVS_DIR', (settings.getValue('OVS_DIR_VANILLA')))
308     elif settings.getValue('VSWITCH').endswith('Vhost'):
309         if settings.getValue('VNF').endswith('Cuse'):
310             # settings paths for Cuse
311             settings.setValue('RTE_SDK', (settings.getValue('RTE_SDK_CUSE')))
312             settings.setValue('OVS_DIR', (settings.getValue('OVS_DIR_CUSE')))
313         else:
314             # settings paths for VhostUser
315             settings.setValue('RTE_SDK', (settings.getValue('RTE_SDK_USER')))
316             settings.setValue('OVS_DIR', (settings.getValue('OVS_DIR_USER')))
317     else:
318         # default - set to VHOST USER but can be changed during enhancement
319         settings.setValue('RTE_SDK', (settings.getValue('RTE_SDK_USER')))
320         settings.setValue('OVS_DIR', (settings.getValue('OVS_DIR_USER')))
321
322     configure_logging(settings.getValue('VERBOSITY'))
323     logger = logging.getLogger()
324
325     # check and fix locale
326     check_and_set_locale()
327
328     # configure trafficgens
329
330     if args['trafficgen']:
331         trafficgens = Loader().get_trafficgens()
332         if args['trafficgen'] not in trafficgens:
333             logging.error('There are no trafficgens matching \'%s\' found in'
334                           ' \'%s\'. Exiting...', args['trafficgen'],
335                           settings.getValue('TRAFFICGEN_DIR'))
336             sys.exit(1)
337
338     # configure vswitch
339     if args['vswitch']:
340         vswitches = Loader().get_vswitches()
341         if args['vswitch'] not in vswitches:
342             logging.error('There are no vswitches matching \'%s\' found in'
343                           ' \'%s\'. Exiting...', args['vswitch'],
344                           settings.getValue('VSWITCH_DIR'))
345             sys.exit(1)
346
347     if args['vnf']:
348         vnfs = Loader().get_vnfs()
349         if args['vnf'] not in vnfs:
350             logging.error('there are no vnfs matching \'%s\' found in'
351                           ' \'%s\'. exiting...', args['vnf'],
352                           settings.getValue('vnf_dir'))
353             sys.exit(1)
354
355     if args['duration']:
356         if args['duration'].isdigit() and int(args['duration']) > 0:
357             settings.setValue('duration', args['duration'])
358         else:
359             logging.error('The selected Duration is not a number')
360             sys.exit(1)
361
362     # update global settings
363     guest_loopback = get_test_param('guest_loopback', None)
364     if guest_loopback:
365         tmp_gl = []
366         for i in range(len(settings.getValue('GUEST_LOOPBACK'))):
367             tmp_gl.append(guest_loopback)
368         settings.setValue('GUEST_LOOPBACK', tmp_gl)
369
370     # generate results directory name
371     date = datetime.datetime.fromtimestamp(time.time())
372     results_dir = "results_" + date.strftime('%Y-%m-%d_%H-%M-%S')
373     results_path = os.path.join(settings.getValue('LOG_DIR'), results_dir)
374
375     # configure tests
376     testcases = settings.getValue('PERFORMANCE_TESTS')
377     all_tests = []
378     for cfg in testcases:
379         try:
380             all_tests.append(TestCase(cfg, results_path))
381         except (Exception) as _:
382             logger.exception("Failed to create test: %s",
383                              cfg.get('Name', '<Name not set>'))
384             raise
385
386     # if required, handle list-* operations
387
388     if args['list']:
389         print("Available Tests:")
390         print("======")
391         for test in all_tests:
392             print('* %-18s%s' % ('%s:' % test.name, test.desc))
393         exit()
394
395     if args['list_trafficgens']:
396         print(Loader().get_trafficgens_printable())
397         exit()
398
399     if args['list_collectors']:
400         print(Loader().get_collectors_printable())
401         exit()
402
403     if args['list_vswitches']:
404         print(Loader().get_vswitches_printable())
405         exit()
406
407     if args['list_vnfs']:
408         print(Loader().get_vnfs_printable())
409         exit()
410
411     if args['list_settings']:
412         print(str(settings))
413         exit()
414
415     # select requested tests
416     if args['exact_test_name'] and args['tests']:
417         logger.error("Cannot specify tests with both positional args and --test.")
418         sys.exit(1)
419
420     if args['exact_test_name']:
421         exact_names = args['exact_test_name']
422         # positional args => exact matches only
423         selected_tests = [test for test in all_tests if test.name in exact_names]
424     elif args['tests']:
425         # --tests => apply filter to select requested tests
426         selected_tests = apply_filter(all_tests, args['tests'])
427     else:
428         # Default - run all tests
429         selected_tests = all_tests
430
431     if not selected_tests:
432         logger.error("No tests matched --test option or positional args. Done.")
433         sys.exit(1)
434
435     # create results directory
436     if not os.path.exists(results_path):
437         logger.info("Creating result directory: "  + results_path)
438         os.makedirs(results_path)
439
440     # run tests
441     suite = unittest.TestSuite()
442     for test in selected_tests:
443         try:
444             test.run()
445             suite.addTest(MockTestCase('', True, test.name))
446         #pylint: disable=broad-except
447         except (Exception) as ex:
448             logger.exception("Failed to run test: %s", test.name)
449             suite.addTest(MockTestCase(str(ex), False, test.name))
450             logger.info("Continuing with next test...")
451
452     if settings.getValue('XUNIT'):
453         xmlrunner.XMLTestRunner(
454             output=settings.getValue('XUNIT_DIR'), outsuffix="",
455             verbosity=0).run(suite)
456
457     if args['opnfvpod']:
458         pod_name = args['opnfvpod']
459         installer_name = settings.getValue('OPNFV_INSTALLER')
460         opnfv_url = settings.getValue('OPNFV_URL')
461         pkg_list = settings.getValue('PACKAGE_LIST')
462
463         int_data = {'cuse': False,
464                     'vanilla': False,
465                     'pod': pod_name,
466                     'installer': installer_name,
467                     'pkg_list': pkg_list,
468                     'db_url': opnfv_url}
469         if settings.getValue('VSWITCH').endswith('Vanilla'):
470             int_data['vanilla'] = True
471         if settings.getValue('VNF').endswith('Cuse'):
472             int_data['cuse'] = True
473         opnfvdashboard.results2opnfv_dashboard(results_path, int_data)
474
475     #remove directory if no result files were created.
476     if os.path.exists(results_path):
477         files_list = os.listdir(results_path)
478         if files_list == []:
479             shutil.rmtree(results_path)
480
481 if __name__ == "__main__":
482     main()
483
484