NFVBENCH-25 Send run results to fluentd
[nfvbench.git] / nfvbench / nfvbench.py
1 #!/usr/bin/env python
2 # Copyright 2016 Cisco Systems, Inc.  All rights reserved.
3 #
4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
5 #    not use this file except in compliance with the License. You may obtain
6 #    a copy of the License at
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
10 #    Unless required by applicable law or agreed to in writing, software
11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 #    License for the specific language governing permissions and limitations
14 #    under the License.
15 #
16
17 from __init__ import __version__
18 import argparse
19 from attrdict import AttrDict
20 from chain_runner import ChainRunner
21 from collections import defaultdict
22 from config import config_load
23 from config import config_loads
24 import copy
25 import credentials
26 import datetime
27 from factory import BasicFactory
28 from fluentd import FluentLogHandler
29 import importlib
30 import json
31 import log
32 from log import LOG
33 from nfvbenchd import WebSocketIoServer
34 import os
35 import pbr.version
36 from pkg_resources import resource_string
37 from specs import ChainType
38 from specs import Specs
39 from summarizer import NFVBenchSummarizer
40 import sys
41 import traceback
42 from traffic_client import TrafficGeneratorFactory
43 import utils
44
45 fluent_logger = None
46
47
48 class NFVBench(object):
49     """Main class of NFV benchmarking tool."""
50     STATUS_OK = 'OK'
51     STATUS_ERROR = 'ERROR'
52
53     def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
54         self.base_config = config
55         self.config = None
56         self.config_plugin = config_plugin
57         self.factory = factory
58         self.notifier = notifier
59         self.cred = credentials.Credentials(config.openrc_file, None, False)
60         self.chain_runner = None
61         self.specs = Specs()
62         self.specs.set_openstack_spec(openstack_spec)
63         self.clients = defaultdict(lambda: None)
64         self.vni_ports = []
65         sys.stdout.flush()
66
67     def setup(self):
68         self.specs.set_run_spec(self.config_plugin.get_run_spec(self.specs.openstack))
69         self.chain_runner = ChainRunner(self.config,
70                                         self.clients,
71                                         self.cred,
72                                         self.specs,
73                                         self.factory,
74                                         self.notifier)
75
76     def set_notifier(self, notifier):
77         self.notifier = notifier
78
79     def run(self, opts, args):
80         status = NFVBench.STATUS_OK
81         result = None
82         message = ''
83         if fluent_logger:
84             # take a snapshot of the current time for this new run
85             # so that all subsequent logs can relate to this run
86             fluent_logger.start_new_run()
87         LOG.info(args)
88         try:
89             self.update_config(opts)
90             self.setup()
91
92             result = {
93                 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
94                 "nfvbench_version": __version__,
95                 "openstack_spec": {
96                     "vswitch": self.specs.openstack.vswitch,
97                     "encaps": self.specs.openstack.encaps
98                 },
99                 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
100                 "benchmarks": {
101                     "network": {
102                         "service_chain": self.chain_runner.run(),
103                         "versions": self.chain_runner.get_version(),
104                     }
105                 }
106             }
107             result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
108         except Exception:
109             status = NFVBench.STATUS_ERROR
110             message = traceback.format_exc()
111         except KeyboardInterrupt:
112             status = NFVBench.STATUS_ERROR
113             message = traceback.format_exc()
114         finally:
115             if self.chain_runner:
116                 self.chain_runner.close()
117
118             if status == NFVBench.STATUS_OK:
119                 result = utils.dict_to_json_dict(result)
120                 return {
121                     'status': status,
122                     'result': result
123                 }
124             else:
125                 return {
126                     'status': status,
127                     'error_message': message
128                 }
129
130     def prepare_summary(self, result):
131         """Prepares summary of the result to print and send it to logger (eg: fluentd)"""
132         sender = FluentLogHandler("resultnfvbench",
133                                   fluentd_ip=self.config.fluentd.ip,
134                                   fluentd_port=self.config.fluentd.port) \
135             if self.config.fluentd.logging_tag else None
136         summary = NFVBenchSummarizer(result, sender)
137         LOG.info(str(summary))
138
139     def save(self, result):
140         """Save results in json format file."""
141         utils.save_json_result(result,
142                                self.config.json_file,
143                                self.config.std_json_path,
144                                self.config.service_chain,
145                                self.config.service_chain_count,
146                                self.config.flow_count,
147                                self.config.frame_sizes)
148
149     def update_config(self, opts):
150         self.config = AttrDict(dict(self.base_config))
151         self.config.update(opts)
152
153         self.config.service_chain = self.config.service_chain.upper()
154         self.config.service_chain_count = int(self.config.service_chain_count)
155         self.config.flow_count = utils.parse_flow_count(self.config.flow_count)
156         required_flow_count = self.config.service_chain_count * 2
157         if self.config.flow_count < required_flow_count:
158             LOG.info("Flow count '{}' has been set to minimum value of '{}' "
159                      "for current configuration".format(self.config.flow_count,
160                                                         required_flow_count))
161             self.config.flow_count = required_flow_count
162
163         if self.config.flow_count % 2 != 0:
164             self.config.flow_count += 1
165
166         self.config.duration_sec = float(self.config.duration_sec)
167         self.config.interval_sec = float(self.config.interval_sec)
168
169         # Get traffic generator profile config
170         if not self.config.generator_profile:
171             self.config.generator_profile = self.config.traffic_generator.default_profile
172
173         generator_factory = TrafficGeneratorFactory(self.config)
174         self.config.generator_config = \
175             generator_factory.get_generator_config(self.config.generator_profile)
176
177         if not any(self.config.generator_config.pcis):
178             raise Exception("PCI addresses configuration for selected traffic generator profile "
179                             "({tg_profile}) are missing. Please specify them in configuration file."
180                             .format(tg_profile=self.config.generator_profile))
181
182         if self.config.traffic is None or len(self.config.traffic) == 0:
183             raise Exception("No traffic profile found in traffic configuration, "
184                             "please fill 'traffic' section in configuration file.")
185
186         if isinstance(self.config.traffic, tuple):
187             self.config.traffic = self.config.traffic[0]
188
189         self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
190
191         self.config.ipv6_mode = False
192         self.config.no_dhcp = True
193         self.config.same_network_only = True
194         if self.config.openrc_file:
195             self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
196
197         self.config.ndr_run = (not self.config.no_traffic
198                                and 'ndr' in self.config.rate.strip().lower().split('_'))
199         self.config.pdr_run = (not self.config.no_traffic
200                                and 'pdr' in self.config.rate.strip().lower().split('_'))
201         self.config.single_run = (not self.config.no_traffic
202                                   and not (self.config.ndr_run or self.config.pdr_run))
203
204         if self.config.vlans and len(self.config.vlans) != 2:
205             raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
206
207         self.config.json_file = self.config.json if self.config.json else None
208         if self.config.json_file:
209             (path, filename) = os.path.split(self.config.json)
210             if not os.path.exists(path):
211                 raise Exception('Please provide existing path for storing results in JSON file. '
212                                 'Path used: {path}'.format(path=path))
213
214         self.config.std_json_path = self.config.std_json if self.config.std_json else None
215         if self.config.std_json_path:
216             if not os.path.exists(self.config.std_json):
217                 raise Exception('Please provide existing path for storing results in JSON file. '
218                                 'Path used: {path}'.format(path=self.config.std_json_path))
219
220         self.config_plugin.validate_config(self.config, self.specs.openstack)
221
222
223 def parse_opts_from_cli():
224     parser = argparse.ArgumentParser()
225
226     parser.add_argument('-c', '--config', dest='config',
227                         action='store',
228                         help='Override default values with a config file or '
229                              'a yaml/json config string',
230                         metavar='<file_name_or_yaml>')
231
232     parser.add_argument('--server', dest='server',
233                         default=None,
234                         action='store',
235                         metavar='<http_root_pathname>',
236                         help='Run nfvbench in server mode and pass'
237                              ' the HTTP root folder full pathname')
238
239     parser.add_argument('--host', dest='host',
240                         action='store',
241                         default='0.0.0.0',
242                         help='Host IP address on which server will be listening (default 0.0.0.0)')
243
244     parser.add_argument('-p', '--port', dest='port',
245                         action='store',
246                         default=7555,
247                         help='Port on which server will be listening (default 7555)')
248
249     parser.add_argument('-sc', '--service-chain', dest='service_chain',
250                         choices=BasicFactory.chain_classes,
251                         action='store',
252                         help='Service chain to run')
253
254     parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
255                         action='store',
256                         help='Set number of service chains to run',
257                         metavar='<service_chain_count>')
258
259     parser.add_argument('-fc', '--flow-count', dest='flow_count',
260                         action='store',
261                         help='Set number of total flows for all chains and all directions',
262                         metavar='<flow_count>')
263
264     parser.add_argument('--rate', dest='rate',
265                         action='store',
266                         help='Specify rate in pps, bps or %% as total for all directions',
267                         metavar='<rate>')
268
269     parser.add_argument('--duration', dest='duration_sec',
270                         action='store',
271                         help='Set duration to run traffic generator (in seconds)',
272                         metavar='<duration_sec>')
273
274     parser.add_argument('--interval', dest='interval_sec',
275                         action='store',
276                         help='Set interval to record traffic generator stats (in seconds)',
277                         metavar='<interval_sec>')
278
279     parser.add_argument('--inter-node', dest='inter_node',
280                         default=None,
281                         action='store_true',
282                         help='run VMs in different compute nodes (PVVP only)')
283
284     parser.add_argument('--sriov', dest='sriov',
285                         default=None,
286                         action='store_true',
287                         help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
288
289     parser.add_argument('-d', '--debug', dest='debug',
290                         action='store_true',
291                         default=None,
292                         help='print debug messages (verbose)')
293
294     parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
295                         action='store',
296                         help='Traffic generator profile to use')
297
298     parser.add_argument('-i', '--image', dest='image_name',
299                         action='store',
300                         help='VM image name to use')
301
302     parser.add_argument('-0', '--no-traffic', dest='no_traffic',
303                         default=None,
304                         action='store_true',
305                         help='Check config and connectivity only - do not generate traffic')
306
307     parser.add_argument('--no-arp', dest='no_arp',
308                         default=None,
309                         action='store_true',
310                         help='Do not use ARP to find MAC addresses, '
311                              'instead use values in config file')
312
313     parser.add_argument('--no-reset', dest='no_reset',
314                         default=None,
315                         action='store_true',
316                         help='Do not reset counters prior to running')
317
318     parser.add_argument('--no-int-config', dest='no_int_config',
319                         default=None,
320                         action='store_true',
321                         help='Skip interfaces config on EXT service chain')
322
323     parser.add_argument('--no-tor-access', dest='no_tor_access',
324                         default=None,
325                         action='store_true',
326                         help='Skip TOR switch configuration and retrieving of stats')
327
328     parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
329                         default=None,
330                         action='store_true',
331                         help='Skip vswitch configuration and retrieving of stats')
332
333     parser.add_argument('--no-cleanup', dest='no_cleanup',
334                         default=None,
335                         action='store_true',
336                         help='no cleanup after run')
337
338     parser.add_argument('--json', dest='json',
339                         action='store',
340                         help='store results in json format file',
341                         metavar='<path>/<filename>')
342
343     parser.add_argument('--std-json', dest='std_json',
344                         action='store',
345                         help='store results in json format file with nfvbench standard filename: '
346                              '<service-chain-type>-<service-chain-count>-<flow-count>'
347                              '-<packet-sizes>.json',
348                         metavar='<path>')
349
350     parser.add_argument('--show-default-config', dest='show_default_config',
351                         default=None,
352                         action='store_true',
353                         help='print the default config in yaml format (unedited)')
354
355     parser.add_argument('--show-config', dest='show_config',
356                         default=None,
357                         action='store_true',
358                         help='print the running config in json format')
359
360     parser.add_argument('-ss', '--show-summary', dest='summary',
361                         action='store',
362                         help='Show summary from nfvbench json file',
363                         metavar='<json>')
364
365     parser.add_argument('-v', '--version', dest='version',
366                         default=None,
367                         action='store_true',
368                         help='Show version')
369
370     parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
371                         action='append',
372                         help='Override traffic profile frame sizes',
373                         metavar='<frame_size_bytes or IMIX>')
374
375     parser.add_argument('--unidir', dest='unidir',
376                         action='store_true',
377                         default=None,
378                         help='Override traffic profile direction (requires -fs)')
379
380     parser.add_argument('--log-file', '--logfile', dest='log_file',
381                         action='store',
382                         help='Filename for saving logs',
383                         metavar='<log_file>')
384
385     opts, unknown_opts = parser.parse_known_args()
386     return opts, unknown_opts
387
388
389 def load_default_config():
390     default_cfg = resource_string(__name__, "cfg.default.yaml")
391     config = config_loads(default_cfg)
392     config.name = '(built-in default config)'
393     return config, default_cfg
394
395
396 def override_custom_traffic(config, frame_sizes, unidir):
397     """Override the traffic profiles with a custom one
398     """
399     if frame_sizes is not None:
400         traffic_profile_name = "custom_traffic_profile"
401         config.traffic_profile = [
402             {
403                 "l2frame_size": frame_sizes,
404                 "name": traffic_profile_name
405             }
406         ]
407     else:
408         traffic_profile_name = config.traffic["profile"]
409
410     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
411     config.traffic = {
412         "bidirectional": bidirectional,
413         "profile": traffic_profile_name
414     }
415
416
417 def check_physnet(name, netattrs):
418     if not netattrs.physical_network:
419         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
420                         .format(n=name))
421     if not netattrs.segmentation_id:
422         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
423                         .format(n=name))
424
425
426 def main():
427     global fluent_logger
428     run_summary_required = False
429     try:
430         log.setup()
431         # load default config file
432         config, default_cfg = load_default_config()
433         # create factory for platform specific classes
434         try:
435             factory_module = importlib.import_module(config['factory_module'])
436             factory = getattr(factory_module, config['factory_class'])()
437         except AttributeError:
438             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
439                             .format(m=config['factory_module'], c=config['factory_class']))
440         # create config plugin for this platform
441         config_plugin = factory.get_config_plugin_class()(config)
442         config = config_plugin.get_config()
443         openstack_spec = config_plugin.get_openstack_spec()
444
445         # setup the fluent logger as soon as possible right after the config plugin is called
446         if config.fluentd.logging_tag:
447             fluent_logger = FluentLogHandler(config.fluentd.logging_tag,
448                                              fluentd_ip=config.fluentd.ip,
449                                              fluentd_port=config.fluentd.port)
450             LOG.addHandler(fluent_logger)
451         else:
452             fluent_logger = None
453
454         opts, unknown_opts = parse_opts_from_cli()
455         log.set_level(debug=opts.debug)
456
457         if opts.version:
458             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
459             sys.exit(0)
460
461         if opts.summary:
462             with open(opts.summary) as json_data:
463                 print NFVBenchSummarizer(json.load(json_data), None)
464             sys.exit(0)
465
466         # show default config in text/yaml format
467         if opts.show_default_config:
468             print default_cfg
469             sys.exit(0)
470
471         config.name = ''
472         if opts.config:
473             # do not check extra_specs in flavor as it can contain any key/value pairs
474             whitelist_keys = ['extra_specs']
475             # override default config options with start config at path parsed from CLI
476             # check if it is an inline yaml/json config or a file name
477             if os.path.isfile(opts.config):
478                 LOG.info('Loading configuration file: ' + opts.config)
479                 config = config_load(opts.config, config, whitelist_keys)
480                 config.name = os.path.basename(opts.config)
481             else:
482                 LOG.info('Loading configuration string: ' + opts.config)
483                 config = config_loads(opts.config, config, whitelist_keys)
484
485         # traffic profile override options
486         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
487
488         # copy over cli options that are used in config
489         config.generator_profile = opts.generator_profile
490         if opts.sriov:
491             config.sriov = True
492         if opts.log_file:
493             config.log_file = opts.log_file
494
495         # show running config in json format
496         if opts.show_config:
497             print json.dumps(config, sort_keys=True, indent=4)
498             sys.exit(0)
499
500         if config.sriov and config.service_chain != ChainType.EXT:
501             # if sriov is requested (does not apply to ext chains)
502             # make sure the physnet names are specified
503             check_physnet("left", config.internal_networks.left)
504             check_physnet("right", config.internal_networks.right)
505             if config.service_chain == ChainType.PVVP:
506                 check_physnet("middle", config.internal_networks.middle)
507
508         # update the config in the config plugin as it might have changed
509         # in a copy of the dict (config plugin still holds the original dict)
510         config_plugin.set_config(config)
511
512         # add file log if requested
513         if config.log_file:
514             log.add_file_logger(config.log_file)
515
516         nfvbench = NFVBench(config, openstack_spec, config_plugin, factory)
517
518         if opts.server:
519             if os.path.isdir(opts.server):
520                 server = WebSocketIoServer(opts.server, nfvbench, fluent_logger)
521                 nfvbench.set_notifier(server)
522                 try:
523                     port = int(opts.port)
524                 except ValueError:
525                     server.run(host=opts.host)
526                 else:
527                     server.run(host=opts.host, port=port)
528             else:
529                 print 'Invalid HTTP root directory: ' + opts.server
530                 sys.exit(1)
531         else:
532             with utils.RunLock():
533                 run_summary_required = True
534                 if unknown_opts:
535                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
536                     LOG.error(err_msg)
537                     raise Exception(err_msg)
538
539                 # remove unfilled values
540                 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
541                 # get CLI args
542                 params = ' '.join(str(e) for e in sys.argv[1:])
543                 result = nfvbench.run(opts, params)
544                 if 'error_message' in result:
545                     raise Exception(result['error_message'])
546
547                 if 'result' in result and result['status']:
548                     nfvbench.save(result['result'])
549                     nfvbench.prepare_summary(result['result'])
550     except Exception as exc:
551         run_summary_required = True
552         LOG.error({
553             'status': NFVBench.STATUS_ERROR,
554             'error_message': traceback.format_exc()
555         })
556         print str(exc)
557     finally:
558         if fluent_logger:
559             # only send a summary record if there was an actual nfvbench run or
560             # if an error/exception was logged.
561             fluent_logger.send_run_summary(run_summary_required)
562
563
564 if __name__ == '__main__':
565     main()