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