NFVBENCH-42 Add multiple fluentd aggregators support
[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         summary = NFVBenchSummarizer(result, fluent_logger)
135         LOG.info(str(summary))
136
137     def save(self, result):
138         """Save results in json format file."""
139         utils.save_json_result(result,
140                                self.config.json_file,
141                                self.config.std_json_path,
142                                self.config.service_chain,
143                                self.config.service_chain_count,
144                                self.config.flow_count,
145                                self.config.frame_sizes)
146
147     def update_config(self, opts):
148         self.config = AttrDict(dict(self.base_config))
149         self.config.update(opts)
150
151         self.config.service_chain = self.config.service_chain.upper()
152         self.config.service_chain_count = int(self.config.service_chain_count)
153         self.config.flow_count = utils.parse_flow_count(self.config.flow_count)
154         required_flow_count = self.config.service_chain_count * 2
155         if self.config.flow_count < required_flow_count:
156             LOG.info("Flow count %d has been set to minimum value of '%d' "
157                      "for current configuration", self.config.flow_count,
158                      required_flow_count)
159             self.config.flow_count = required_flow_count
160
161         if self.config.flow_count % 2 != 0:
162             self.config.flow_count += 1
163
164         self.config.duration_sec = float(self.config.duration_sec)
165         self.config.interval_sec = float(self.config.interval_sec)
166
167         # Get traffic generator profile config
168         if not self.config.generator_profile:
169             self.config.generator_profile = self.config.traffic_generator.default_profile
170
171         generator_factory = TrafficGeneratorFactory(self.config)
172         self.config.generator_config = \
173             generator_factory.get_generator_config(self.config.generator_profile)
174
175         if not any(self.config.generator_config.pcis):
176             raise Exception("PCI addresses configuration for selected traffic generator profile "
177                             "({tg_profile}) are missing. Please specify them in configuration file."
178                             .format(tg_profile=self.config.generator_profile))
179
180         if self.config.traffic is None or not self.config.traffic:
181             raise Exception("No traffic profile found in traffic configuration, "
182                             "please fill 'traffic' section in configuration file.")
183
184         if isinstance(self.config.traffic, tuple):
185             self.config.traffic = self.config.traffic[0]
186
187         self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
188
189         self.config.ipv6_mode = False
190         self.config.no_dhcp = True
191         self.config.same_network_only = True
192         if self.config.openrc_file:
193             self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
194
195         self.config.ndr_run = (not self.config.no_traffic
196                                and 'ndr' in self.config.rate.strip().lower().split('_'))
197         self.config.pdr_run = (not self.config.no_traffic
198                                and 'pdr' in self.config.rate.strip().lower().split('_'))
199         self.config.single_run = (not self.config.no_traffic
200                                   and not (self.config.ndr_run or self.config.pdr_run))
201
202         if self.config.vlans and len(self.config.vlans) != 2:
203             raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
204
205         self.config.json_file = self.config.json if self.config.json else None
206         if self.config.json_file:
207             (path, _filename) = os.path.split(self.config.json)
208             if not os.path.exists(path):
209                 raise Exception('Please provide existing path for storing results in JSON file. '
210                                 'Path used: {path}'.format(path=path))
211
212         self.config.std_json_path = self.config.std_json if self.config.std_json else None
213         if self.config.std_json_path:
214             if not os.path.exists(self.config.std_json):
215                 raise Exception('Please provide existing path for storing results in JSON file. '
216                                 'Path used: {path}'.format(path=self.config.std_json_path))
217
218         self.config_plugin.validate_config(self.config, self.specs.openstack)
219
220
221 def parse_opts_from_cli():
222     parser = argparse.ArgumentParser()
223
224     parser.add_argument('-c', '--config', dest='config',
225                         action='store',
226                         help='Override default values with a config file or '
227                              'a yaml/json config string',
228                         metavar='<file_name_or_yaml>')
229
230     parser.add_argument('--server', dest='server',
231                         default=None,
232                         action='store',
233                         metavar='<http_root_pathname>',
234                         help='Run nfvbench in server mode and pass'
235                              ' the HTTP root folder full pathname')
236
237     parser.add_argument('--host', dest='host',
238                         action='store',
239                         default='0.0.0.0',
240                         help='Host IP address on which server will be listening (default 0.0.0.0)')
241
242     parser.add_argument('-p', '--port', dest='port',
243                         action='store',
244                         default=7555,
245                         help='Port on which server will be listening (default 7555)')
246
247     parser.add_argument('-sc', '--service-chain', dest='service_chain',
248                         choices=BasicFactory.chain_classes,
249                         action='store',
250                         help='Service chain to run')
251
252     parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
253                         action='store',
254                         help='Set number of service chains to run',
255                         metavar='<service_chain_count>')
256
257     parser.add_argument('-fc', '--flow-count', dest='flow_count',
258                         action='store',
259                         help='Set number of total flows for all chains and all directions',
260                         metavar='<flow_count>')
261
262     parser.add_argument('--rate', dest='rate',
263                         action='store',
264                         help='Specify rate in pps, bps or %% as total for all directions',
265                         metavar='<rate>')
266
267     parser.add_argument('--duration', dest='duration_sec',
268                         action='store',
269                         help='Set duration to run traffic generator (in seconds)',
270                         metavar='<duration_sec>')
271
272     parser.add_argument('--interval', dest='interval_sec',
273                         action='store',
274                         help='Set interval to record traffic generator stats (in seconds)',
275                         metavar='<interval_sec>')
276
277     parser.add_argument('--inter-node', dest='inter_node',
278                         default=None,
279                         action='store_true',
280                         help='run VMs in different compute nodes (PVVP only)')
281
282     parser.add_argument('--sriov', dest='sriov',
283                         default=None,
284                         action='store_true',
285                         help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
286
287     parser.add_argument('-d', '--debug', dest='debug',
288                         action='store_true',
289                         default=None,
290                         help='print debug messages (verbose)')
291
292     parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
293                         action='store',
294                         help='Traffic generator profile to use')
295
296     parser.add_argument('-0', '--no-traffic', dest='no_traffic',
297                         default=None,
298                         action='store_true',
299                         help='Check config and connectivity only - do not generate traffic')
300
301     parser.add_argument('--no-arp', dest='no_arp',
302                         default=None,
303                         action='store_true',
304                         help='Do not use ARP to find MAC addresses, '
305                              'instead use values in config file')
306
307     parser.add_argument('--no-reset', dest='no_reset',
308                         default=None,
309                         action='store_true',
310                         help='Do not reset counters prior to running')
311
312     parser.add_argument('--no-int-config', dest='no_int_config',
313                         default=None,
314                         action='store_true',
315                         help='Skip interfaces config on EXT service chain')
316
317     parser.add_argument('--no-tor-access', dest='no_tor_access',
318                         default=None,
319                         action='store_true',
320                         help='Skip TOR switch configuration and retrieving of stats')
321
322     parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
323                         default=None,
324                         action='store_true',
325                         help='Skip vswitch configuration and retrieving of stats')
326
327     parser.add_argument('--no-cleanup', dest='no_cleanup',
328                         default=None,
329                         action='store_true',
330                         help='no cleanup after run')
331
332     parser.add_argument('--json', dest='json',
333                         action='store',
334                         help='store results in json format file',
335                         metavar='<path>/<filename>')
336
337     parser.add_argument('--std-json', dest='std_json',
338                         action='store',
339                         help='store results in json format file with nfvbench standard filename: '
340                              '<service-chain-type>-<service-chain-count>-<flow-count>'
341                              '-<packet-sizes>.json',
342                         metavar='<path>')
343
344     parser.add_argument('--show-default-config', dest='show_default_config',
345                         default=None,
346                         action='store_true',
347                         help='print the default config in yaml format (unedited)')
348
349     parser.add_argument('--show-config', dest='show_config',
350                         default=None,
351                         action='store_true',
352                         help='print the running config in json format')
353
354     parser.add_argument('-ss', '--show-summary', dest='summary',
355                         action='store',
356                         help='Show summary from nfvbench json file',
357                         metavar='<json>')
358
359     parser.add_argument('-v', '--version', dest='version',
360                         default=None,
361                         action='store_true',
362                         help='Show version')
363
364     parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
365                         action='append',
366                         help='Override traffic profile frame sizes',
367                         metavar='<frame_size_bytes or IMIX>')
368
369     parser.add_argument('--unidir', dest='unidir',
370                         action='store_true',
371                         default=None,
372                         help='Override traffic profile direction (requires -fs)')
373
374     parser.add_argument('--log-file', '--logfile', dest='log_file',
375                         action='store',
376                         help='Filename for saving logs',
377                         metavar='<log_file>')
378
379     parser.add_argument('--user-label', '--userlabel', dest='user_label',
380                         action='store',
381                         help='Custom label for performance records')
382
383     opts, unknown_opts = parser.parse_known_args()
384     return opts, unknown_opts
385
386
387 def load_default_config():
388     default_cfg = resource_string(__name__, "cfg.default.yaml")
389     config = config_loads(default_cfg)
390     config.name = '(built-in default config)'
391     return config, default_cfg
392
393
394 def override_custom_traffic(config, frame_sizes, unidir):
395     """Override the traffic profiles with a custom one
396     """
397     if frame_sizes is not None:
398         traffic_profile_name = "custom_traffic_profile"
399         config.traffic_profile = [
400             {
401                 "l2frame_size": frame_sizes,
402                 "name": traffic_profile_name
403             }
404         ]
405     else:
406         traffic_profile_name = config.traffic["profile"]
407
408     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
409     config.traffic = {
410         "bidirectional": bidirectional,
411         "profile": traffic_profile_name
412     }
413
414
415 def check_physnet(name, netattrs):
416     if not netattrs.physical_network:
417         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
418                         .format(n=name))
419     if not netattrs.segmentation_id:
420         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
421                         .format(n=name))
422
423
424 def main():
425     global fluent_logger
426     run_summary_required = False
427     try:
428         log.setup()
429         # load default config file
430         config, default_cfg = load_default_config()
431         # create factory for platform specific classes
432         try:
433             factory_module = importlib.import_module(config['factory_module'])
434             factory = getattr(factory_module, config['factory_class'])()
435         except AttributeError:
436             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
437                             .format(m=config['factory_module'], c=config['factory_class']))
438         # create config plugin for this platform
439         config_plugin = factory.get_config_plugin_class()(config)
440         config = config_plugin.get_config()
441         openstack_spec = config_plugin.get_openstack_spec()
442
443         opts, unknown_opts = parse_opts_from_cli()
444         log.set_level(debug=opts.debug)
445
446         # setup the fluent logger as soon as possible right after the config plugin is called,
447         # if there is any logging or result tag is set then initialize the fluent logger
448         for fluentd in config.fluentd:
449             if fluentd.logging_tag or fluentd.result_tag:
450                 fluent_logger = FluentLogHandler(config.fluentd)
451                 LOG.addHandler(fluent_logger)
452                 break
453
454         if opts.version:
455             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
456             sys.exit(0)
457
458         if opts.summary:
459             with open(opts.summary) as json_data:
460                 result = json.load(json_data)
461                 if opts.user_label:
462                     result['config']['user_label'] = opts.user_label
463                 print NFVBenchSummarizer(result, fluent_logger)
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_instance = 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_instance, fluent_logger)
521                 nfvbench_instance.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_instance.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_instance.save(result['result'])
549                     nfvbench_instance.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()