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