Merge "framework: Add reworked framework to repo"
[vswitchperf.git] / vsperf
1 #!/usr/bin/env python3
2
3 # Copyright 2015 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
28 sys.dont_write_bytecode = True
29
30 from conf import settings
31 from core.loader import Loader
32 from testcases import TestCase
33 from tools import tasks
34 from tools.collectors import collector
35 from tools.pkt_gen import trafficgen
36
37 VERBOSITY_LEVELS = {
38     'debug': logging.DEBUG,
39     'info': logging.INFO,
40     'warning': logging.WARNING,
41     'error': logging.ERROR,
42     'critical': logging.CRITICAL
43 }
44
45
46 def parse_arguments():
47     """
48     Parse command line arguments.
49     """
50     class _SplitTestParamsAction(argparse.Action):
51         """
52         Parse and split the '--test-params' argument.
53
54         This expects either 'x=y' or 'x' (implicit true) values.
55         """
56         def __call__(self, parser, namespace, values, option_string=None):
57             results = {}
58
59             for value in values.split(';'):
60                 result = [key.strip() for key in value.split('=')]
61                 if len(result) == 1:
62                     results[result[0]] = True
63                 elif len(result) == 2:
64                     results[result[0]] = result[1]
65                 else:
66                     raise argparse.ArgumentTypeError(
67                         'expected \'%s\' to be of format \'key=val\' or'
68                         ' \'key\'' % result)
69
70             setattr(namespace, self.dest, results)
71
72     class _ValidateFileAction(argparse.Action):
73         """Validate a file can be read from before using it.
74         """
75         def __call__(self, parser, namespace, values, option_string=None):
76             if not os.path.isfile(values):
77                 raise argparse.ArgumentTypeError(
78                     'the path \'%s\' is not a valid path' % values)
79             elif not os.access(values, os.R_OK):
80                 raise argparse.ArgumentTypeError(
81                     'the path \'%s\' is not accessible' % values)
82
83             setattr(namespace, self.dest, values)
84
85     class _ValidateDirAction(argparse.Action):
86         """Validate a directory can be written to before using it.
87         """
88         def __call__(self, parser, namespace, values, option_string=None):
89             if not os.path.isdir(values):
90                 raise argparse.ArgumentTypeError(
91                     'the path \'%s\' is not a valid path' % values)
92             elif not os.access(values, os.W_OK):
93                 raise argparse.ArgumentTypeError(
94                     'the path \'%s\' is not accessible' % values)
95
96             setattr(namespace, self.dest, values)
97
98     def list_logging_levels():
99         """Give a summary of all available logging levels.
100
101         :return: List of verbosity level names in decreasing order of
102             verbosity
103         """
104         return sorted(VERBOSITY_LEVELS.keys(),
105                       key=lambda x: VERBOSITY_LEVELS[x])
106
107     parser = argparse.ArgumentParser(prog=__file__, formatter_class=
108                                      argparse.ArgumentDefaultsHelpFormatter)
109     parser.add_argument('--version', action='version', version='%(prog)s 0.2')
110     parser.add_argument('--list', '--list-tests', action='store_true',
111                         help='list all tests and exit')
112     parser.add_argument('--list-trafficgens', action='store_true',
113                         help='list all traffic generators and exit')
114     parser.add_argument('--list-collectors', action='store_true',
115                         help='list all system metrics loggers and exit')
116     parser.add_argument('--list-vswitches', action='store_true',
117                         help='list all system vswitches and exit')
118     parser.add_argument('--list-settings', action='store_true',
119                         help='list effective settings configuration and exit')
120     parser.add_argument('test', nargs='*', help='test specification(s)')
121
122     group = parser.add_argument_group('test selection options')
123     group.add_argument('-f', '--test-spec', help='test specification file')
124     group.add_argument('-d', '--test-dir', help='directory containing tests')
125     group.add_argument('-t', '--tests', help='Comma-separated list of terms \
126             indicating tests to run. e.g. "RFC2544,!p2p" - run all tests whose\
127             name contains RFC2544 less those containing "p2p"')
128     group.add_argument('--verbosity', choices=list_logging_levels(),
129                        help='debug level')
130     group.add_argument('--trafficgen', help='traffic generator to use')
131     group.add_argument('--sysmetrics', help='system metrics logger to use')
132     group = parser.add_argument_group('test behavior options')
133     group.add_argument('--load-env', action='store_true',
134                        help='enable loading of settings from the environment')
135     group.add_argument('--conf-file', action=_ValidateFileAction,
136                        help='settings file')
137     group.add_argument('--test-params', action=_SplitTestParamsAction,
138                        help='csv list of test parameters: key=val;...')
139
140     args = vars(parser.parse_args())
141
142     return args
143
144
145 def configure_logging(level):
146     """Configure logging.
147     """
148     log_file_default = os.path.join(
149         settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_DEFAULT'))
150     log_file_host_cmds = os.path.join(
151         settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_HOST_CMDS'))
152     log_file_traffic_gen = os.path.join(
153         settings.getValue('LOG_DIR'),
154         settings.getValue('LOG_FILE_TRAFFIC_GEN'))
155     log_file_sys_metrics = os.path.join(
156         settings.getValue('LOG_DIR'),
157         settings.getValue('LOG_FILE_SYS_METRICS'))
158
159     logger = logging.getLogger()
160     logger.setLevel(logging.DEBUG)
161
162     stream_logger = logging.StreamHandler(sys.stdout)
163     stream_logger.setLevel(VERBOSITY_LEVELS[level])
164     stream_logger.setFormatter(logging.Formatter(
165         '[%(levelname)s]  %(asctime)s : (%(name)s) - %(message)s'))
166     logger.addHandler(stream_logger)
167
168     file_logger = logging.FileHandler(filename=log_file_default)
169     file_logger.setLevel(logging.DEBUG)
170     logger.addHandler(file_logger)
171
172     class CommandFilter(logging.Filter):
173         """Filter out strings beginning with 'cmd :'"""
174         def filter(self, record):
175             return record.getMessage().startswith(tasks.CMD_PREFIX)
176
177     class TrafficGenCommandFilter(logging.Filter):
178         """Filter out strings beginning with 'gencmd :'"""
179         def filter(self, record):
180             return record.getMessage().startswith(trafficgen.CMD_PREFIX)
181
182     class SystemMetricsCommandFilter(logging.Filter):
183         """Filter out strings beginning with 'gencmd :'"""
184         def filter(self, record):
185             return record.getMessage().startswith(collector.CMD_PREFIX)
186
187     cmd_logger = logging.FileHandler(filename=log_file_host_cmds)
188     cmd_logger.setLevel(logging.DEBUG)
189     cmd_logger.addFilter(CommandFilter())
190     logger.addHandler(cmd_logger)
191
192     gen_logger = logging.FileHandler(filename=log_file_traffic_gen)
193     gen_logger.setLevel(logging.DEBUG)
194     gen_logger.addFilter(TrafficGenCommandFilter())
195     logger.addHandler(gen_logger)
196
197     metrics_logger = logging.FileHandler(filename=log_file_sys_metrics)
198     metrics_logger.setLevel(logging.DEBUG)
199     metrics_logger.addFilter(SystemMetricsCommandFilter())
200     logger.addHandler(metrics_logger)
201
202
203 def apply_filter(tests, tc_filter):
204     """Allow a subset of tests to be conveniently selected
205
206     :param tests: The list of Tests from which to select.
207     :param tc_filter: A case-insensitive string of comma-separated terms
208         indicating the Tests to select.
209         e.g. 'RFC' - select all tests whose name contains 'RFC'
210         e.g. 'RFC,burst' - select all tests whose name contains 'RFC' or
211             'burst'
212         e.g. 'RFC,burst,!p2p' - select all tests whose name contains 'RFC'
213             or 'burst' and from these remove any containing 'p2p'.
214         e.g. '' - empty string selects all tests.
215     :return: A list of the selected Tests.
216     """
217     result = []
218     if tc_filter is None:
219         tc_filter = ""
220
221     for term in [x.strip() for x in tc_filter.lower().split(",")]:
222         if not term or term[0] != '!':
223             # Add matching tests from 'tests' into results
224             result.extend([test for test in tests \
225                 if test.name.lower().find(term) >= 0])
226         else:
227             # Term begins with '!' so we remove matching tests
228             result = [test for test in result \
229                 if test.name.lower().find(term[1:]) < 0]
230
231     return result
232
233
234 def main():
235     """Main function.
236     """
237     args = parse_arguments()
238
239     # configure settings
240
241     settings.load_from_dir('conf')
242
243     # load command line parameters first in case there are settings files
244     # to be used
245     settings.load_from_dict(args)
246
247     if args['conf_file']:
248         settings.load_from_file(args['conf_file'])
249
250     if args['load_env']:
251         settings.load_from_env()
252
253     # reload command line parameters since these should take higher priority
254     # than both a settings file and environment variables
255     settings.load_from_dict(args)
256
257     configure_logging(settings.getValue('VERBOSITY'))
258     logger = logging.getLogger()
259
260     # configure trafficgens
261
262     if args['trafficgen']:
263         trafficgens = Loader().get_trafficgens()
264         if args['trafficgen'] not in trafficgens:
265             logging.error('There are no trafficgens matching \'%s\' found in'
266                           ' \'%s\'. Exiting...', args['trafficgen'],
267                           settings.getValue('TRAFFICGEN_DIR'))
268             sys.exit(1)
269
270
271     # generate results directory name
272     date = datetime.datetime.fromtimestamp(time.time())
273     results_dir = "results_" + date.strftime('%Y-%m-%d_%H-%M-%S')
274     results_path = os.path.join(settings.getValue('LOG_DIR'), results_dir)
275
276     # configure tests
277     testcases = settings.getValue('PERFORMANCE_TESTS')
278     all_tests = []
279     for cfg in testcases:
280         try:
281             all_tests.append(TestCase(cfg, results_path))
282         except (Exception) as _:
283             logger.exception("Failed to create test: %s",
284                              cfg.get('Name', '<Name not set>'))
285             raise
286
287     # TODO(BOM) Apply filter to select requested tests
288     all_tests = apply_filter(all_tests, args['tests'])
289
290     # if required, handle list-* operations
291
292     if args['list']:
293         print("Available Tests:")
294         print("======")
295         for test in all_tests:
296             print('* %-18s%s' % ('%s:' % test.name, test.desc))
297         exit()
298
299     if args['list_trafficgens']:
300         print(Loader().get_trafficgens_printable())
301         exit()
302
303     if args['list_collectors']:
304         print(Loader().get_collectors_printable())
305         exit()
306
307     if args['list_vswitches']:
308         print(Loader().get_vswitches_printable())
309         exit()
310
311     if args['list_settings']:
312         print(str(settings))
313         exit()
314
315     # create results directory
316     if not os.path.exists(results_dir):
317         logger.info("Creating result directory: "  + results_path)
318         os.makedirs(results_path)
319
320     # run tests
321     for test in all_tests:
322         try:
323             test.run()
324         #pylint: disable=broad-except
325         except (Exception) as _:
326             logger.exception("Failed to run test: %s", test.name)
327             logger.info("Continuing with next test...")
328
329     #remove directory if no result files were created.
330     if os.path.exists(results_path):
331         if os.listdir(results_path) == []:
332             shutil.rmtree(results_path)
333
334 if __name__ == "__main__":
335     main()
336
337