NFVBENCH-41 Add fluentd result tag to nfvbench server
[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 import argparse
18 from collections import defaultdict
19 import copy
20 import datetime
21 import importlib
22 import json
23 import os
24 import sys
25 import traceback
26
27 from attrdict import AttrDict
28 import pbr.version
29 from pkg_resources import resource_string
30
31 from __init__ import __version__
32 from chain_runner import ChainRunner
33 from config import config_load
34 from config import config_loads
35 import credentials as credentials
36 from factory import BasicFactory
37 from fluentd import FluentLogHandler
38 import log
39 from log import LOG
40 from nfvbenchd import WebSocketIoServer
41 from specs import ChainType
42 from specs import Specs
43 from summarizer import NFVBenchSummarizer
44 from traffic_client import TrafficGeneratorFactory
45 import utils
46
47 fluent_logger = None
48
49
50 class NFVBench(object):
51     """Main class of NFV benchmarking tool."""
52     STATUS_OK = 'OK'
53     STATUS_ERROR = 'ERROR'
54
55     def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
56         self.base_config = config
57         self.config = None
58         self.config_plugin = config_plugin
59         self.factory = factory
60         self.notifier = notifier
61         self.cred = credentials.Credentials(config.openrc_file, None, False)
62         self.chain_runner = None
63         self.specs = Specs()
64         self.specs.set_openstack_spec(openstack_spec)
65         self.clients = defaultdict(lambda: None)
66         self.vni_ports = []
67         sys.stdout.flush()
68
69     def setup(self):
70         self.specs.set_run_spec(self.config_plugin.get_run_spec(self.specs.openstack))
71         self.chain_runner = ChainRunner(self.config,
72                                         self.clients,
73                                         self.cred,
74                                         self.specs,
75                                         self.factory,
76                                         self.notifier)
77
78     def set_notifier(self, notifier):
79         self.notifier = notifier
80
81     def run(self, opts, args):
82         status = NFVBench.STATUS_OK
83         result = None
84         message = ''
85         if fluent_logger:
86             # take a snapshot of the current time for this new run
87             # so that all subsequent logs can relate to this run
88             fluent_logger.start_new_run()
89         LOG.info(args)
90         try:
91             self.update_config(opts)
92             self.setup()
93
94             result = {
95                 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
96                 "nfvbench_version": __version__,
97                 "openstack_spec": {
98                     "vswitch": self.specs.openstack.vswitch,
99                     "encaps": self.specs.openstack.encaps
100                 },
101                 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
102                 "benchmarks": {
103                     "network": {
104                         "service_chain": self.chain_runner.run(),
105                         "versions": self.chain_runner.get_version(),
106                     }
107                 }
108             }
109             result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
110         except Exception:
111             status = NFVBench.STATUS_ERROR
112             message = traceback.format_exc()
113         except KeyboardInterrupt:
114             status = NFVBench.STATUS_ERROR
115             message = traceback.format_exc()
116         finally:
117             if self.chain_runner:
118                 self.chain_runner.close()
119
120         if status == NFVBench.STATUS_OK:
121             result = utils.dict_to_json_dict(result)
122             return {
123                 'status': status,
124                 'result': result
125             }
126         return {
127             'status': status,
128             'error_message': message
129         }
130
131     def prepare_summary(self, result):
132         """Prepares summary of the result to print and send it to logger (eg: fluentd)"""
133         global fluent_logger
134         sender = None
135         if self.config.fluentd.result_tag:
136             sender = FluentLogHandler(self.config.fluentd.result_tag,
137                                       fluentd_ip=self.config.fluentd.ip,
138                                       fluentd_port=self.config.fluentd.port)
139             sender.runlogdate = fluent_logger.runlogdate
140         summary = NFVBenchSummarizer(result, sender)
141         LOG.info(str(summary))
142
143     def save(self, result):
144         """Save results in json format file."""
145         utils.save_json_result(result,
146                                self.config.json_file,
147                                self.config.std_json_path,
148                                self.config.service_chain,
149                                self.config.service_chain_count,
150                                self.config.flow_count,
151                                self.config.frame_sizes)
152
153     def update_config(self, opts):
154         self.config = AttrDict(dict(self.base_config))
155         self.config.update(opts)
156
157         self.config.service_chain = self.config.service_chain.upper()
158         self.config.service_chain_count = int(self.config.service_chain_count)
159         self.config.flow_count = utils.parse_flow_count(self.config.flow_count)
160         required_flow_count = self.config.service_chain_count * 2
161         if self.config.flow_count < required_flow_count:
162             LOG.info("Flow count %d has been set to minimum value of '%d' "
163                      "for current configuration", self.config.flow_count,
164                      required_flow_count)
165             self.config.flow_count = required_flow_count
166
167         if self.config.flow_count % 2 != 0:
168             self.config.flow_count += 1
169
170         self.config.duration_sec = float(self.config.duration_sec)
171         self.config.interval_sec = float(self.config.interval_sec)
172
173         # Get traffic generator profile config
174         if not self.config.generator_profile:
175             self.config.generator_profile = self.config.traffic_generator.default_profile
176
177         generator_factory = TrafficGeneratorFactory(self.config)
178         self.config.generator_config = \
179             generator_factory.get_generator_config(self.config.generator_profile)
180
181         if not any(self.config.generator_config.pcis):
182             raise Exception("PCI addresses configuration for selected traffic generator profile "
183                             "({tg_profile}) are missing. Please specify them in configuration file."
184                             .format(tg_profile=self.config.generator_profile))
185
186         if self.config.traffic is None or not self.config.traffic:
187             raise Exception("No traffic profile found in traffic configuration, "
188                             "please fill 'traffic' section in configuration file.")
189
190         if isinstance(self.config.traffic, tuple):
191             self.config.traffic = self.config.traffic[0]
192
193         self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
194
195         self.config.ipv6_mode = False
196         self.config.no_dhcp = True
197         self.config.same_network_only = True
198         if self.config.openrc_file:
199             self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
200
201         self.config.ndr_run = (not self.config.no_traffic
202                                and 'ndr' in self.config.rate.strip().lower().split('_'))
203         self.config.pdr_run = (not self.config.no_traffic
204                                and 'pdr' in self.config.rate.strip().lower().split('_'))
205         self.config.single_run = (not self.config.no_traffic
206                                   and not (self.config.ndr_run or self.config.pdr_run))
207
208         if self.config.vlans and len(self.config.vlans) != 2:
209             raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
210
211         self.config.json_file = self.config.json if self.config.json else None
212         if self.config.json_file:
213             (path, _filename) = os.path.split(self.config.json)
214             if not os.path.exists(path):
215                 raise Exception('Please provide existing path for storing results in JSON file. '
216                                 'Path used: {path}'.format(path=path))
217
218         self.config.std_json_path = self.config.std_json if self.config.std_json else None
219         if self.config.std_json_path:
220             if not os.path.exists(self.config.std_json):
221                 raise Exception('Please provide existing path for storing results in JSON file. '
222                                 'Path used: {path}'.format(path=self.config.std_json_path))
223
224         self.config_plugin.validate_config(self.config, self.specs.openstack)
225
226
227 def parse_opts_from_cli():
228     parser = argparse.ArgumentParser()
229
230     parser.add_argument('-c', '--config', dest='config',
231                         action='store',
232                         help='Override default values with a config file or '
233                              'a yaml/json config string',
234                         metavar='<file_name_or_yaml>')
235
236     parser.add_argument('--server', dest='server',
237                         default=None,
238                         action='store',
239                         metavar='<http_root_pathname>',
240                         help='Run nfvbench in server mode and pass'
241                              ' the HTTP root folder full pathname')
242
243     parser.add_argument('--host', dest='host',
244                         action='store',
245                         default='0.0.0.0',
246                         help='Host IP address on which server will be listening (default 0.0.0.0)')
247
248     parser.add_argument('-p', '--port', dest='port',
249                         action='store',
250                         default=7555,
251                         help='Port on which server will be listening (default 7555)')
252
253     parser.add_argument('-sc', '--service-chain', dest='service_chain',
254                         choices=BasicFactory.chain_classes,
255                         action='store',
256                         help='Service chain to run')
257
258     parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
259                         action='store',
260                         help='Set number of service chains to run',
261                         metavar='<service_chain_count>')
262
263     parser.add_argument('-fc', '--flow-count', dest='flow_count',
264                         action='store',
265                         help='Set number of total flows for all chains and all directions',
266                         metavar='<flow_count>')
267
268     parser.add_argument('--rate', dest='rate',
269                         action='store',
270                         help='Specify rate in pps, bps or %% as total for all directions',
271                         metavar='<rate>')
272
273     parser.add_argument('--duration', dest='duration_sec',
274                         action='store',
275                         help='Set duration to run traffic generator (in seconds)',
276                         metavar='<duration_sec>')
277
278     parser.add_argument('--interval', dest='interval_sec',
279                         action='store',
280                         help='Set interval to record traffic generator stats (in seconds)',
281                         metavar='<interval_sec>')
282
283     parser.add_argument('--inter-node', dest='inter_node',
284                         default=None,
285                         action='store_true',
286                         help='run VMs in different compute nodes (PVVP only)')
287
288     parser.add_argument('--sriov', dest='sriov',
289                         default=None,
290                         action='store_true',
291                         help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
292
293     parser.add_argument('-d', '--debug', dest='debug',
294                         action='store_true',
295                         default=None,
296                         help='print debug messages (verbose)')
297
298     parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
299                         action='store',
300                         help='Traffic generator profile 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     parser.add_argument('--user-label', '--userlabel', dest='user_label',
386                         action='store',
387                         help='Custom label for performance records')
388
389     opts, unknown_opts = parser.parse_known_args()
390     return opts, unknown_opts
391
392
393 def load_default_config():
394     default_cfg = resource_string(__name__, "cfg.default.yaml")
395     config = config_loads(default_cfg)
396     config.name = '(built-in default config)'
397     return config, default_cfg
398
399
400 def override_custom_traffic(config, frame_sizes, unidir):
401     """Override the traffic profiles with a custom one
402     """
403     if frame_sizes is not None:
404         traffic_profile_name = "custom_traffic_profile"
405         config.traffic_profile = [
406             {
407                 "l2frame_size": frame_sizes,
408                 "name": traffic_profile_name
409             }
410         ]
411     else:
412         traffic_profile_name = config.traffic["profile"]
413
414     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
415     config.traffic = {
416         "bidirectional": bidirectional,
417         "profile": traffic_profile_name
418     }
419
420
421 def check_physnet(name, netattrs):
422     if not netattrs.physical_network:
423         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
424                         .format(n=name))
425     if not netattrs.segmentation_id:
426         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
427                         .format(n=name))
428
429
430 def main():
431     global fluent_logger
432     run_summary_required = False
433     try:
434         log.setup()
435         # load default config file
436         config, default_cfg = load_default_config()
437         # create factory for platform specific classes
438         try:
439             factory_module = importlib.import_module(config['factory_module'])
440             factory = getattr(factory_module, config['factory_class'])()
441         except AttributeError:
442             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
443                             .format(m=config['factory_module'], c=config['factory_class']))
444         # create config plugin for this platform
445         config_plugin = factory.get_config_plugin_class()(config)
446         config = config_plugin.get_config()
447         openstack_spec = config_plugin.get_openstack_spec()
448
449         # setup the fluent logger as soon as possible right after the config plugin is called
450         if config.fluentd.logging_tag:
451             fluent_logger = FluentLogHandler(config.fluentd.logging_tag,
452                                              fluentd_ip=config.fluentd.ip,
453                                              fluentd_port=config.fluentd.port)
454             LOG.addHandler(fluent_logger)
455         else:
456             fluent_logger = None
457
458         opts, unknown_opts = parse_opts_from_cli()
459         log.set_level(debug=opts.debug)
460
461         if opts.version:
462             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
463             sys.exit(0)
464
465         if opts.summary:
466             with open(opts.summary) as json_data:
467                 result = json.load(json_data)
468                 if opts.user_label:
469                     result['config']['user_label'] = opts.user_label
470                 if config.fluentd.result_tag:
471                     sender = FluentLogHandler(config.fluentd.result_tag,
472                                               fluentd_ip=config.fluentd.ip,
473                                               fluentd_port=config.fluentd.port)
474                     sender.runlogdate = fluent_logger.runlogdate
475                     print NFVBenchSummarizer(result, sender)
476                 else:
477                     print NFVBenchSummarizer(result, None)
478             sys.exit(0)
479
480         # show default config in text/yaml format
481         if opts.show_default_config:
482             print default_cfg
483             sys.exit(0)
484
485         config.name = ''
486         if opts.config:
487             # do not check extra_specs in flavor as it can contain any key/value pairs
488             whitelist_keys = ['extra_specs']
489             # override default config options with start config at path parsed from CLI
490             # check if it is an inline yaml/json config or a file name
491             if os.path.isfile(opts.config):
492                 LOG.info('Loading configuration file: ' + opts.config)
493                 config = config_load(opts.config, config, whitelist_keys)
494                 config.name = os.path.basename(opts.config)
495             else:
496                 LOG.info('Loading configuration string: ' + opts.config)
497                 config = config_loads(opts.config, config, whitelist_keys)
498
499         # traffic profile override options
500         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
501
502         # copy over cli options that are used in config
503         config.generator_profile = opts.generator_profile
504         if opts.sriov:
505             config.sriov = True
506         if opts.log_file:
507             config.log_file = opts.log_file
508
509         # show running config in json format
510         if opts.show_config:
511             print json.dumps(config, sort_keys=True, indent=4)
512             sys.exit(0)
513
514         if config.sriov and config.service_chain != ChainType.EXT:
515             # if sriov is requested (does not apply to ext chains)
516             # make sure the physnet names are specified
517             check_physnet("left", config.internal_networks.left)
518             check_physnet("right", config.internal_networks.right)
519             if config.service_chain == ChainType.PVVP:
520                 check_physnet("middle", config.internal_networks.middle)
521
522         # update the config in the config plugin as it might have changed
523         # in a copy of the dict (config plugin still holds the original dict)
524         config_plugin.set_config(config)
525
526         # add file log if requested
527         if config.log_file:
528             log.add_file_logger(config.log_file)
529
530         nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
531
532         if opts.server:
533             if os.path.isdir(opts.server):
534                 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger,
535                                            config.fluentd.result_tag)
536                 nfvbench_instance.set_notifier(server)
537                 try:
538                     port = int(opts.port)
539                 except ValueError:
540                     server.run(host=opts.host)
541                 else:
542                     server.run(host=opts.host, port=port)
543             else:
544                 print 'Invalid HTTP root directory: ' + opts.server
545                 sys.exit(1)
546         else:
547             with utils.RunLock():
548                 run_summary_required = True
549                 if unknown_opts:
550                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
551                     LOG.error(err_msg)
552                     raise Exception(err_msg)
553
554                 # remove unfilled values
555                 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
556                 # get CLI args
557                 params = ' '.join(str(e) for e in sys.argv[1:])
558                 result = nfvbench_instance.run(opts, params)
559                 if 'error_message' in result:
560                     raise Exception(result['error_message'])
561
562                 if 'result' in result and result['status']:
563                     nfvbench_instance.save(result['result'])
564                     nfvbench_instance.prepare_summary(result['result'])
565     except Exception as exc:
566         run_summary_required = True
567         LOG.error({
568             'status': NFVBench.STATUS_ERROR,
569             'error_message': traceback.format_exc()
570         })
571         print str(exc)
572     finally:
573         if fluent_logger:
574             # only send a summary record if there was an actual nfvbench run or
575             # if an error/exception was logged.
576             fluent_logger.send_run_summary(run_summary_required)
577
578
579 if __name__ == '__main__':
580     main()