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