920838abf7ea53a2d90172d2c576ec56accec567
[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     opts, unknown_opts = parser.parse_known_args()
383     return opts, unknown_opts
384
385
386 def load_default_config():
387     default_cfg = resource_string(__name__, "cfg.default.yaml")
388     config = config_loads(default_cfg)
389     config.name = '(built-in default config)'
390     return config, default_cfg
391
392
393 def override_custom_traffic(config, frame_sizes, unidir):
394     """Override the traffic profiles with a custom one
395     """
396     if frame_sizes is not None:
397         traffic_profile_name = "custom_traffic_profile"
398         config.traffic_profile = [
399             {
400                 "l2frame_size": frame_sizes,
401                 "name": traffic_profile_name
402             }
403         ]
404     else:
405         traffic_profile_name = config.traffic["profile"]
406
407     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
408     config.traffic = {
409         "bidirectional": bidirectional,
410         "profile": traffic_profile_name
411     }
412
413
414 def check_physnet(name, netattrs):
415     if not netattrs.physical_network:
416         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
417                         .format(n=name))
418     if not netattrs.segmentation_id:
419         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
420                         .format(n=name))
421
422
423 def main():
424     global fluent_logger
425     run_summary_required = False
426     try:
427         log.setup()
428         # load default config file
429         config, default_cfg = load_default_config()
430         # create factory for platform specific classes
431         try:
432             factory_module = importlib.import_module(config['factory_module'])
433             factory = getattr(factory_module, config['factory_class'])()
434         except AttributeError:
435             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
436                             .format(m=config['factory_module'], c=config['factory_class']))
437         # create config plugin for this platform
438         config_plugin = factory.get_config_plugin_class()(config)
439         config = config_plugin.get_config()
440         openstack_spec = config_plugin.get_openstack_spec()
441
442         # setup the fluent logger as soon as possible right after the config plugin is called
443         if config.fluentd.logging_tag:
444             fluent_logger = FluentLogHandler(config.fluentd.logging_tag,
445                                              fluentd_ip=config.fluentd.ip,
446                                              fluentd_port=config.fluentd.port)
447             LOG.addHandler(fluent_logger)
448         else:
449             fluent_logger = None
450
451         opts, unknown_opts = parse_opts_from_cli()
452         log.set_level(debug=opts.debug)
453
454         if opts.version:
455             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
456             sys.exit(0)
457
458         if opts.summary:
459             with open(opts.summary) as json_data:
460                 print NFVBenchSummarizer(json.load(json_data), None)
461             sys.exit(0)
462
463         # show default config in text/yaml format
464         if opts.show_default_config:
465             print default_cfg
466             sys.exit(0)
467
468         config.name = ''
469         if opts.config:
470             # do not check extra_specs in flavor as it can contain any key/value pairs
471             whitelist_keys = ['extra_specs']
472             # override default config options with start config at path parsed from CLI
473             # check if it is an inline yaml/json config or a file name
474             if os.path.isfile(opts.config):
475                 LOG.info('Loading configuration file: ' + opts.config)
476                 config = config_load(opts.config, config, whitelist_keys)
477                 config.name = os.path.basename(opts.config)
478             else:
479                 LOG.info('Loading configuration string: ' + opts.config)
480                 config = config_loads(opts.config, config, whitelist_keys)
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, fluent_logger)
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                 run_summary_required = True
531                 if unknown_opts:
532                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
533                     LOG.error(err_msg)
534                     raise Exception(err_msg)
535
536                 # remove unfilled values
537                 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
538                 # get CLI args
539                 params = ' '.join(str(e) for e in sys.argv[1:])
540                 result = nfvbench.run(opts, params)
541                 if 'error_message' in result:
542                     raise Exception(result['error_message'])
543
544                 if 'result' in result and result['status']:
545                     nfvbench.save(result['result'])
546                     nfvbench.prepare_summary(result['result'])
547     except Exception as exc:
548         run_summary_required = True
549         LOG.error({
550             'status': NFVBench.STATUS_ERROR,
551             'error_message': traceback.format_exc()
552         })
553         print str(exc)
554     finally:
555         if fluent_logger:
556             # only send a summary record if there was an actual nfvbench run or
557             # if an error/exception was logged.
558             fluent_logger.send_run_summary(run_summary_required)
559
560
561 if __name__ == '__main__':
562     main()