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