NFVBENCH-75 fluent logger configure not override by -c my_config.yaml
[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             if config.openrc_file else None
63         self.chain_runner = None
64         self.specs = Specs()
65         self.specs.set_openstack_spec(openstack_spec)
66         self.clients = defaultdict(lambda: None)
67         self.vni_ports = []
68         sys.stdout.flush()
69
70     def setup(self):
71         self.specs.set_run_spec(self.config_plugin.get_run_spec(self.specs.openstack))
72         self.chain_runner = ChainRunner(self.config,
73                                         self.clients,
74                                         self.cred,
75                                         self.specs,
76                                         self.factory,
77                                         self.notifier)
78
79     def set_notifier(self, notifier):
80         self.notifier = notifier
81
82     def run(self, opts, args):
83         status = NFVBench.STATUS_OK
84         result = None
85         message = ''
86         if fluent_logger:
87             # take a snapshot of the current time for this new run
88             # so that all subsequent logs can relate to this run
89             fluent_logger.start_new_run()
90         LOG.info(args)
91         try:
92             self.update_config(opts)
93             self.setup()
94
95             result = {
96                 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
97                 "nfvbench_version": __version__,
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             if self.specs.openstack:
107                 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
108                                             "encaps": self.specs.openstack.encaps}
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         # Check length of mac_addrs_left/right for serivce_chain EXT with no_arp
176         if self.config.service_chain == ChainType.EXT and self.config.no_arp:
177             if not (self.config.generator_config.mac_addrs_left is None and
178                     self.config.generator_config.mac_addrs_right is None):
179                 if (self.config.generator_config.mac_addrs_left is None or
180                         self.config.generator_config.mac_addrs_right is None):
181                     raise Exception("mac_addrs_left and mac_addrs_right must either "
182                                     "both be None or have a number of entries matching "
183                                     "service_chain_count")
184                 if not (len(self.config.generator_config.mac_addrs_left) ==
185                         self.config.service_chain_count and
186                         len(self.config.generator_config.mac_addrs_right) ==
187                         self.config.service_chain_count):
188                     raise Exception("length of mac_addrs_left ({a}) and/or mac_addrs_right ({b}) "
189                                     "does not match service_chain_count ({c})"
190                                     .format(a=len(self.config.generator_config.mac_addrs_left),
191                                             b=len(self.config.generator_config.mac_addrs_right),
192                                             c=self.config.service_chain_count))
193
194         if not any(self.config.generator_config.pcis):
195             raise Exception("PCI addresses configuration for selected traffic generator profile "
196                             "({tg_profile}) are missing. Please specify them in configuration file."
197                             .format(tg_profile=self.config.generator_profile))
198
199         if self.config.traffic is None or not self.config.traffic:
200             raise Exception("No traffic profile found in traffic configuration, "
201                             "please fill 'traffic' section in configuration file.")
202
203         if isinstance(self.config.traffic, tuple):
204             self.config.traffic = self.config.traffic[0]
205
206         self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
207
208         self.config.ipv6_mode = False
209         self.config.no_dhcp = True
210         self.config.same_network_only = True
211         if self.config.openrc_file:
212             self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
213
214         self.config.ndr_run = (not self.config.no_traffic
215                                and 'ndr' in self.config.rate.strip().lower().split('_'))
216         self.config.pdr_run = (not self.config.no_traffic
217                                and 'pdr' in self.config.rate.strip().lower().split('_'))
218         self.config.single_run = (not self.config.no_traffic
219                                   and not (self.config.ndr_run or self.config.pdr_run))
220
221         if self.config.vlans and len(self.config.vlans) != 2:
222             raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
223
224         self.config.json_file = self.config.json if self.config.json else None
225         if self.config.json_file:
226             (path, _filename) = os.path.split(self.config.json)
227             if not os.path.exists(path):
228                 raise Exception('Please provide existing path for storing results in JSON file. '
229                                 'Path used: {path}'.format(path=path))
230
231         self.config.std_json_path = self.config.std_json if self.config.std_json else None
232         if self.config.std_json_path:
233             if not os.path.exists(self.config.std_json):
234                 raise Exception('Please provide existing path for storing results in JSON file. '
235                                 'Path used: {path}'.format(path=self.config.std_json_path))
236
237         self.config_plugin.validate_config(self.config, self.specs.openstack)
238
239
240 def parse_opts_from_cli():
241     parser = argparse.ArgumentParser()
242
243     parser.add_argument('-c', '--config', dest='config',
244                         action='store',
245                         help='Override default values with a config file or '
246                              'a yaml/json config string',
247                         metavar='<file_name_or_yaml>')
248
249     parser.add_argument('--server', dest='server',
250                         default=None,
251                         action='store',
252                         metavar='<http_root_pathname>',
253                         help='Run nfvbench in server mode and pass'
254                              ' the HTTP root folder full pathname')
255
256     parser.add_argument('--host', dest='host',
257                         action='store',
258                         default='0.0.0.0',
259                         help='Host IP address on which server will be listening (default 0.0.0.0)')
260
261     parser.add_argument('-p', '--port', dest='port',
262                         action='store',
263                         default=7555,
264                         help='Port on which server will be listening (default 7555)')
265
266     parser.add_argument('-sc', '--service-chain', dest='service_chain',
267                         choices=BasicFactory.chain_classes,
268                         action='store',
269                         help='Service chain to run')
270
271     parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
272                         action='store',
273                         help='Set number of service chains to run',
274                         metavar='<service_chain_count>')
275
276     parser.add_argument('-fc', '--flow-count', dest='flow_count',
277                         action='store',
278                         help='Set number of total flows for all chains and all directions',
279                         metavar='<flow_count>')
280
281     parser.add_argument('--rate', dest='rate',
282                         action='store',
283                         help='Specify rate in pps, bps or %% as total for all directions',
284                         metavar='<rate>')
285
286     parser.add_argument('--duration', dest='duration_sec',
287                         action='store',
288                         help='Set duration to run traffic generator (in seconds)',
289                         metavar='<duration_sec>')
290
291     parser.add_argument('--interval', dest='interval_sec',
292                         action='store',
293                         help='Set interval to record traffic generator stats (in seconds)',
294                         metavar='<interval_sec>')
295
296     parser.add_argument('--inter-node', dest='inter_node',
297                         default=None,
298                         action='store_true',
299                         help='run VMs in different compute nodes (PVVP only)')
300
301     parser.add_argument('--sriov', dest='sriov',
302                         default=None,
303                         action='store_true',
304                         help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
305
306     parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
307                         default=None,
308                         action='store_true',
309                         help='Use SRIOV to handle the middle network traffic '
310                              '(PVVP with SRIOV only)')
311
312     parser.add_argument('-d', '--debug', dest='debug',
313                         action='store_true',
314                         default=None,
315                         help='print debug messages (verbose)')
316
317     parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
318                         action='store',
319                         help='Traffic generator profile to use')
320
321     parser.add_argument('-0', '--no-traffic', dest='no_traffic',
322                         default=None,
323                         action='store_true',
324                         help='Check config and connectivity only - do not generate traffic')
325
326     parser.add_argument('--no-arp', dest='no_arp',
327                         default=None,
328                         action='store_true',
329                         help='Do not use ARP to find MAC addresses, '
330                              'instead use values in config file')
331
332     parser.add_argument('--no-reset', dest='no_reset',
333                         default=None,
334                         action='store_true',
335                         help='Do not reset counters prior to running')
336
337     parser.add_argument('--no-int-config', dest='no_int_config',
338                         default=None,
339                         action='store_true',
340                         help='Skip interfaces config on EXT service chain')
341
342     parser.add_argument('--no-tor-access', dest='no_tor_access',
343                         default=None,
344                         action='store_true',
345                         help='Skip TOR switch configuration and retrieving of stats')
346
347     parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
348                         default=None,
349                         action='store_true',
350                         help='Skip vswitch configuration and retrieving of stats')
351
352     parser.add_argument('--no-cleanup', dest='no_cleanup',
353                         default=None,
354                         action='store_true',
355                         help='no cleanup after run')
356
357     parser.add_argument('--json', dest='json',
358                         action='store',
359                         help='store results in json format file',
360                         metavar='<path>/<filename>')
361
362     parser.add_argument('--std-json', dest='std_json',
363                         action='store',
364                         help='store results in json format file with nfvbench standard filename: '
365                              '<service-chain-type>-<service-chain-count>-<flow-count>'
366                              '-<packet-sizes>.json',
367                         metavar='<path>')
368
369     parser.add_argument('--show-default-config', dest='show_default_config',
370                         default=None,
371                         action='store_true',
372                         help='print the default config in yaml format (unedited)')
373
374     parser.add_argument('--show-config', dest='show_config',
375                         default=None,
376                         action='store_true',
377                         help='print the running config in json format')
378
379     parser.add_argument('-ss', '--show-summary', dest='summary',
380                         action='store',
381                         help='Show summary from nfvbench json file',
382                         metavar='<json>')
383
384     parser.add_argument('-v', '--version', dest='version',
385                         default=None,
386                         action='store_true',
387                         help='Show version')
388
389     parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
390                         action='append',
391                         help='Override traffic profile frame sizes',
392                         metavar='<frame_size_bytes or IMIX>')
393
394     parser.add_argument('--unidir', dest='unidir',
395                         action='store_true',
396                         default=None,
397                         help='Override traffic profile direction (requires -fs)')
398
399     parser.add_argument('--log-file', '--logfile', dest='log_file',
400                         action='store',
401                         help='Filename for saving logs',
402                         metavar='<log_file>')
403
404     parser.add_argument('--user-label', '--userlabel', dest='user_label',
405                         action='store',
406                         help='Custom label for performance records')
407
408     opts, unknown_opts = parser.parse_known_args()
409     return opts, unknown_opts
410
411
412 def load_default_config():
413     default_cfg = resource_string(__name__, "cfg.default.yaml")
414     config = config_loads(default_cfg)
415     config.name = '(built-in default config)'
416     return config, default_cfg
417
418
419 def override_custom_traffic(config, frame_sizes, unidir):
420     """Override the traffic profiles with a custom one
421     """
422     if frame_sizes is not None:
423         traffic_profile_name = "custom_traffic_profile"
424         config.traffic_profile = [
425             {
426                 "l2frame_size": frame_sizes,
427                 "name": traffic_profile_name
428             }
429         ]
430     else:
431         traffic_profile_name = config.traffic["profile"]
432
433     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
434     config.traffic = {
435         "bidirectional": bidirectional,
436         "profile": traffic_profile_name
437     }
438
439
440 def check_physnet(name, netattrs):
441     if not netattrs.physical_network:
442         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
443                         .format(n=name))
444     if not netattrs.segmentation_id:
445         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
446                         .format(n=name))
447
448
449 def main():
450     global fluent_logger
451     run_summary_required = False
452     try:
453         log.setup()
454         # load default config file
455         config, default_cfg = load_default_config()
456         # create factory for platform specific classes
457         try:
458             factory_module = importlib.import_module(config['factory_module'])
459             factory = getattr(factory_module, config['factory_class'])()
460         except AttributeError:
461             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
462                             .format(m=config['factory_module'], c=config['factory_class']))
463         # create config plugin for this platform
464         config_plugin = factory.get_config_plugin_class()(config)
465         config = config_plugin.get_config()
466
467         opts, unknown_opts = parse_opts_from_cli()
468         log.set_level(debug=opts.debug)
469
470         if opts.version:
471             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
472             sys.exit(0)
473
474         if opts.summary:
475             with open(opts.summary) as json_data:
476                 result = json.load(json_data)
477                 if opts.user_label:
478                     result['config']['user_label'] = opts.user_label
479                 print NFVBenchSummarizer(result, fluent_logger)
480             sys.exit(0)
481
482         # show default config in text/yaml format
483         if opts.show_default_config:
484             print default_cfg
485             sys.exit(0)
486
487         config.name = ''
488         if opts.config:
489             # do not check extra_specs in flavor as it can contain any key/value pairs
490             whitelist_keys = ['extra_specs']
491             # override default config options with start config at path parsed from CLI
492             # check if it is an inline yaml/json config or a file name
493             if os.path.isfile(opts.config):
494                 LOG.info('Loading configuration file: %s', opts.config)
495                 config = config_load(opts.config, config, whitelist_keys)
496                 config.name = os.path.basename(opts.config)
497             else:
498                 LOG.info('Loading configuration string: %s', opts.config)
499                 config = config_loads(opts.config, config, whitelist_keys)
500
501         # setup the fluent logger as soon as possible right after the config plugin is called,
502         # if there is any logging or result tag is set then initialize the fluent logger
503         for fluentd in config.fluentd:
504             if fluentd.logging_tag or fluentd.result_tag:
505                 fluent_logger = FluentLogHandler(config.fluentd)
506                 LOG.addHandler(fluent_logger)
507                 break
508
509         # traffic profile override options
510         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
511
512         # copy over cli options that are used in config
513         config.generator_profile = opts.generator_profile
514         if opts.sriov:
515             config.sriov = True
516         if opts.log_file:
517             config.log_file = opts.log_file
518         if opts.service_chain:
519             config.service_chain = opts.service_chain
520         if opts.service_chain_count:
521             config.service_chain_count = opts.service_chain_count
522         if opts.no_vswitch_access:
523             config.no_vswitch_access = opts.no_vswitch_access
524         if opts.no_int_config:
525             config.no_int_config = opts.no_int_config
526
527         if opts.use_sriov_middle_net:
528             if (not config.sriov) or (not config.service_chain == ChainType.PVVP):
529                 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
530             config.use_sriov_middle_net = True
531
532         if config.sriov and config.service_chain != ChainType.EXT:
533             # if sriov is requested (does not apply to ext chains)
534             # make sure the physnet names are specified
535             check_physnet("left", config.internal_networks.left)
536             check_physnet("right", config.internal_networks.right)
537             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
538                 check_physnet("middle", config.internal_networks.middle)
539
540         # show running config in json format
541         if opts.show_config:
542             print json.dumps(config, sort_keys=True, indent=4)
543             sys.exit(0)
544
545         # check that an empty openrc file (no OpenStack) is only allowed
546         # with EXT chain
547         if not config.openrc_file:
548             if config.service_chain == ChainType.EXT:
549                 LOG.info('EXT chain with OpenStack mode disabled')
550             else:
551                 raise Exception("openrc_file is empty in the configuration and is required")
552
553         # update the config in the config plugin as it might have changed
554         # in a copy of the dict (config plugin still holds the original dict)
555         config_plugin.set_config(config)
556
557         # add file log if requested
558         if config.log_file:
559             log.add_file_logger(config.log_file)
560
561         openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
562             else None
563
564         nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
565
566         if opts.server:
567             if os.path.isdir(opts.server):
568                 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
569                 nfvbench_instance.set_notifier(server)
570                 try:
571                     port = int(opts.port)
572                 except ValueError:
573                     server.run(host=opts.host)
574                 else:
575                     server.run(host=opts.host, port=port)
576             else:
577                 print 'Invalid HTTP root directory: ' + opts.server
578                 sys.exit(1)
579         else:
580             with utils.RunLock():
581                 run_summary_required = True
582                 if unknown_opts:
583                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
584                     LOG.error(err_msg)
585                     raise Exception(err_msg)
586
587                 # remove unfilled values
588                 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
589                 # get CLI args
590                 params = ' '.join(str(e) for e in sys.argv[1:])
591                 result = nfvbench_instance.run(opts, params)
592                 if 'error_message' in result:
593                     raise Exception(result['error_message'])
594
595                 if 'result' in result and result['status']:
596                     nfvbench_instance.save(result['result'])
597                     nfvbench_instance.prepare_summary(result['result'])
598     except Exception as exc:
599         run_summary_required = True
600         LOG.error({
601             'status': NFVBench.STATUS_ERROR,
602             'error_message': traceback.format_exc()
603         })
604         print str(exc)
605     finally:
606         if fluent_logger:
607             # only send a summary record if there was an actual nfvbench run or
608             # if an error/exception was logged.
609             fluent_logger.send_run_summary(run_summary_required)
610
611
612 if __name__ == '__main__':
613     main()