4c9f56c5ff59d0c3ed664455b7ffcefb56750329
[nfvbench.git] / nfvbench / nfvbench.py
1 #!/usr/bin/env python
2 # Copyright 2016 Cisco Systems, Inc.  All rights reserved.
3 #
4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
5 #    not use this file except in compliance with the License. You may obtain
6 #    a copy of the License at
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
10 #    Unless required by applicable law or agreed to in writing, software
11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 #    License for the specific language governing permissions and limitations
14 #    under the License.
15 #
16
17 import argparse
18 from collections import defaultdict
19 import copy
20 import datetime
21 import importlib
22 import json
23 import os
24 import sys
25 import traceback
26
27 from attrdict import AttrDict
28 import pbr.version
29 from pkg_resources import resource_string
30
31 from __init__ import __version__
32 from chain_runner import ChainRunner
33 from config import config_load
34 from config import config_loads
35 import credentials as credentials
36 from factory import BasicFactory
37 from fluentd import FluentLogHandler
38 import log
39 from log import LOG
40 from nfvbenchd import WebSocketIoServer
41 from specs import ChainType
42 from specs import Specs
43 from summarizer import NFVBenchSummarizer
44 from traffic_client import TrafficGeneratorFactory
45 import utils
46
47 fluent_logger = None
48
49
50 class NFVBench(object):
51     """Main class of NFV benchmarking tool."""
52     STATUS_OK = 'OK'
53     STATUS_ERROR = 'ERROR'
54
55     def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
56         self.base_config = config
57         self.config = None
58         self.config_plugin = config_plugin
59         self.factory = factory
60         self.notifier = notifier
61         self.cred = credentials.Credentials(config.openrc_file, None, False)
62         self.chain_runner = None
63         self.specs = Specs()
64         self.specs.set_openstack_spec(openstack_spec)
65         self.clients = defaultdict(lambda: None)
66         self.vni_ports = []
67         sys.stdout.flush()
68
69     def setup(self):
70         self.specs.set_run_spec(self.config_plugin.get_run_spec(self.specs.openstack))
71         self.chain_runner = ChainRunner(self.config,
72                                         self.clients,
73                                         self.cred,
74                                         self.specs,
75                                         self.factory,
76                                         self.notifier)
77
78     def set_notifier(self, notifier):
79         self.notifier = notifier
80
81     def run(self, opts, args):
82         status = NFVBench.STATUS_OK
83         result = None
84         message = ''
85         if fluent_logger:
86             # take a snapshot of the current time for this new run
87             # so that all subsequent logs can relate to this run
88             fluent_logger.start_new_run()
89         LOG.info(args)
90         try:
91             self.update_config(opts)
92             self.setup()
93
94             result = {
95                 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
96                 "nfvbench_version": __version__,
97                 "openstack_spec": {
98                     "vswitch": self.specs.openstack.vswitch,
99                     "encaps": self.specs.openstack.encaps
100                 },
101                 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
102                 "benchmarks": {
103                     "network": {
104                         "service_chain": self.chain_runner.run(),
105                         "versions": self.chain_runner.get_version(),
106                     }
107                 }
108             }
109             result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
110         except Exception:
111             status = NFVBench.STATUS_ERROR
112             message = traceback.format_exc()
113         except KeyboardInterrupt:
114             status = NFVBench.STATUS_ERROR
115             message = traceback.format_exc()
116         finally:
117             if self.chain_runner:
118                 self.chain_runner.close()
119
120         if status == NFVBench.STATUS_OK:
121             result = utils.dict_to_json_dict(result)
122             return {
123                 'status': status,
124                 'result': result
125             }
126         return {
127             'status': status,
128             'error_message': message
129         }
130
131     def prepare_summary(self, result):
132         """Prepares summary of the result to print and send it to logger (eg: fluentd)"""
133         global fluent_logger
134         summary = NFVBenchSummarizer(result, fluent_logger)
135         LOG.info(str(summary))
136
137     def save(self, result):
138         """Save results in json format file."""
139         utils.save_json_result(result,
140                                self.config.json_file,
141                                self.config.std_json_path,
142                                self.config.service_chain,
143                                self.config.service_chain_count,
144                                self.config.flow_count,
145                                self.config.frame_sizes)
146
147     def update_config(self, opts):
148         self.config = AttrDict(dict(self.base_config))
149         self.config.update(opts)
150
151         self.config.service_chain = self.config.service_chain.upper()
152         self.config.service_chain_count = int(self.config.service_chain_count)
153         self.config.flow_count = utils.parse_flow_count(self.config.flow_count)
154         required_flow_count = self.config.service_chain_count * 2
155         if self.config.flow_count < required_flow_count:
156             LOG.info("Flow count %d has been set to minimum value of '%d' "
157                      "for current configuration", self.config.flow_count,
158                      required_flow_count)
159             self.config.flow_count = required_flow_count
160
161         if self.config.flow_count % 2 != 0:
162             self.config.flow_count += 1
163
164         self.config.duration_sec = float(self.config.duration_sec)
165         self.config.interval_sec = float(self.config.interval_sec)
166
167         # Get traffic generator profile config
168         if not self.config.generator_profile:
169             self.config.generator_profile = self.config.traffic_generator.default_profile
170
171         generator_factory = TrafficGeneratorFactory(self.config)
172         self.config.generator_config = \
173             generator_factory.get_generator_config(self.config.generator_profile)
174
175         if not any(self.config.generator_config.pcis):
176             raise Exception("PCI addresses configuration for selected traffic generator profile "
177                             "({tg_profile}) are missing. Please specify them in configuration file."
178                             .format(tg_profile=self.config.generator_profile))
179
180         if self.config.traffic is None or not self.config.traffic:
181             raise Exception("No traffic profile found in traffic configuration, "
182                             "please fill 'traffic' section in configuration file.")
183
184         if isinstance(self.config.traffic, tuple):
185             self.config.traffic = self.config.traffic[0]
186
187         self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
188
189         self.config.ipv6_mode = False
190         self.config.no_dhcp = True
191         self.config.same_network_only = True
192         if self.config.openrc_file:
193             self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
194
195         self.config.ndr_run = (not self.config.no_traffic
196                                and 'ndr' in self.config.rate.strip().lower().split('_'))
197         self.config.pdr_run = (not self.config.no_traffic
198                                and 'pdr' in self.config.rate.strip().lower().split('_'))
199         self.config.single_run = (not self.config.no_traffic
200                                   and not (self.config.ndr_run or self.config.pdr_run))
201
202         if self.config.vlans and len(self.config.vlans) != 2:
203             raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
204
205         self.config.json_file = self.config.json if self.config.json else None
206         if self.config.json_file:
207             (path, _filename) = os.path.split(self.config.json)
208             if not os.path.exists(path):
209                 raise Exception('Please provide existing path for storing results in JSON file. '
210                                 'Path used: {path}'.format(path=path))
211
212         self.config.std_json_path = self.config.std_json if self.config.std_json else None
213         if self.config.std_json_path:
214             if not os.path.exists(self.config.std_json):
215                 raise Exception('Please provide existing path for storing results in JSON file. '
216                                 'Path used: {path}'.format(path=self.config.std_json_path))
217
218         self.config_plugin.validate_config(self.config, self.specs.openstack)
219
220
221 def parse_opts_from_cli():
222     parser = argparse.ArgumentParser()
223
224     parser.add_argument('-c', '--config', dest='config',
225                         action='store',
226                         help='Override default values with a config file or '
227                              'a yaml/json config string',
228                         metavar='<file_name_or_yaml>')
229
230     parser.add_argument('--server', dest='server',
231                         default=None,
232                         action='store',
233                         metavar='<http_root_pathname>',
234                         help='Run nfvbench in server mode and pass'
235                              ' the HTTP root folder full pathname')
236
237     parser.add_argument('--host', dest='host',
238                         action='store',
239                         default='0.0.0.0',
240                         help='Host IP address on which server will be listening (default 0.0.0.0)')
241
242     parser.add_argument('-p', '--port', dest='port',
243                         action='store',
244                         default=7555,
245                         help='Port on which server will be listening (default 7555)')
246
247     parser.add_argument('-sc', '--service-chain', dest='service_chain',
248                         choices=BasicFactory.chain_classes,
249                         action='store',
250                         help='Service chain to run')
251
252     parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
253                         action='store',
254                         help='Set number of service chains to run',
255                         metavar='<service_chain_count>')
256
257     parser.add_argument('-fc', '--flow-count', dest='flow_count',
258                         action='store',
259                         help='Set number of total flows for all chains and all directions',
260                         metavar='<flow_count>')
261
262     parser.add_argument('--rate', dest='rate',
263                         action='store',
264                         help='Specify rate in pps, bps or %% as total for all directions',
265                         metavar='<rate>')
266
267     parser.add_argument('--duration', dest='duration_sec',
268                         action='store',
269                         help='Set duration to run traffic generator (in seconds)',
270                         metavar='<duration_sec>')
271
272     parser.add_argument('--interval', dest='interval_sec',
273                         action='store',
274                         help='Set interval to record traffic generator stats (in seconds)',
275                         metavar='<interval_sec>')
276
277     parser.add_argument('--inter-node', dest='inter_node',
278                         default=None,
279                         action='store_true',
280                         help='run VMs in different compute nodes (PVVP only)')
281
282     parser.add_argument('--sriov', dest='sriov',
283                         default=None,
284                         action='store_true',
285                         help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
286
287     parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
288                         default=None,
289                         action='store_true',
290                         help='Use SRIOV to handle the middle network traffic '
291                              '(PVVP with SRIOV only)')
292
293     parser.add_argument('-d', '--debug', dest='debug',
294                         action='store_true',
295                         default=None,
296                         help='print debug messages (verbose)')
297
298     parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
299                         action='store',
300                         help='Traffic generator profile to use')
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         opts, unknown_opts = parse_opts_from_cli()
450         log.set_level(debug=opts.debug)
451
452         # setup the fluent logger as soon as possible right after the config plugin is called,
453         # if there is any logging or result tag is set then initialize the fluent logger
454         for fluentd in config.fluentd:
455             if fluentd.logging_tag or fluentd.result_tag:
456                 fluent_logger = FluentLogHandler(config.fluentd)
457                 LOG.addHandler(fluent_logger)
458                 break
459
460         if opts.version:
461             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
462             sys.exit(0)
463
464         if opts.summary:
465             with open(opts.summary) as json_data:
466                 result = json.load(json_data)
467                 if opts.user_label:
468                     result['config']['user_label'] = opts.user_label
469                 print NFVBenchSummarizer(result, fluent_logger)
470             sys.exit(0)
471
472         # show default config in text/yaml format
473         if opts.show_default_config:
474             print default_cfg
475             sys.exit(0)
476
477         config.name = ''
478         if opts.config:
479             # do not check extra_specs in flavor as it can contain any key/value pairs
480             whitelist_keys = ['extra_specs']
481             # override default config options with start config at path parsed from CLI
482             # check if it is an inline yaml/json config or a file name
483             if os.path.isfile(opts.config):
484                 LOG.info('Loading configuration file: ' + opts.config)
485                 config = config_load(opts.config, config, whitelist_keys)
486                 config.name = os.path.basename(opts.config)
487             else:
488                 LOG.info('Loading configuration string: ' + opts.config)
489                 config = config_loads(opts.config, config, whitelist_keys)
490
491         # traffic profile override options
492         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
493
494         # copy over cli options that are used in config
495         config.generator_profile = opts.generator_profile
496         if opts.sriov:
497             config.sriov = True
498         if opts.log_file:
499             config.log_file = opts.log_file
500         if opts.service_chain:
501             config.service_chain = opts.service_chain
502         if opts.service_chain_count:
503             config.service_chain_count = opts.service_chain_count
504
505         if opts.use_sriov_middle_net:
506             if (not config.sriov) or (not config.service_chain == ChainType.PVVP):
507                 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
508             config.use_sriov_middle_net = True
509
510         if config.sriov and config.service_chain != ChainType.EXT:
511             # if sriov is requested (does not apply to ext chains)
512             # make sure the physnet names are specified
513             check_physnet("left", config.internal_networks.left)
514             check_physnet("right", config.internal_networks.right)
515             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
516                 check_physnet("middle", config.internal_networks.middle)
517
518         # show running config in json format
519         if opts.show_config:
520             print json.dumps(config, sort_keys=True, indent=4)
521             sys.exit(0)
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()