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