[NFVBENCH-62] Add support for non-openstack environments
[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             if config.openrc_file else None
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                 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
99                 "benchmarks": {
100                     "network": {
101                         "service_chain": self.chain_runner.run(),
102                         "versions": self.chain_runner.get_version(),
103                     }
104                 }
105             }
106             if self.specs.openstack:
107                 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
108                                             "encaps": self.specs.openstack.encaps}
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
448         opts, unknown_opts = parse_opts_from_cli()
449         log.set_level(debug=opts.debug)
450
451         # setup the fluent logger as soon as possible right after the config plugin is called,
452         # if there is any logging or result tag is set then initialize the fluent logger
453         for fluentd in config.fluentd:
454             if fluentd.logging_tag or fluentd.result_tag:
455                 fluent_logger = FluentLogHandler(config.fluentd)
456                 LOG.addHandler(fluent_logger)
457                 break
458
459         if opts.version:
460             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
461             sys.exit(0)
462
463         if opts.summary:
464             with open(opts.summary) as json_data:
465                 result = json.load(json_data)
466                 if opts.user_label:
467                     result['config']['user_label'] = opts.user_label
468                 print NFVBenchSummarizer(result, fluent_logger)
469             sys.exit(0)
470
471         # show default config in text/yaml format
472         if opts.show_default_config:
473             print default_cfg
474             sys.exit(0)
475
476         config.name = ''
477         if opts.config:
478             # do not check extra_specs in flavor as it can contain any key/value pairs
479             whitelist_keys = ['extra_specs']
480             # override default config options with start config at path parsed from CLI
481             # check if it is an inline yaml/json config or a file name
482             if os.path.isfile(opts.config):
483                 LOG.info('Loading configuration file: %s', opts.config)
484                 config = config_load(opts.config, config, whitelist_keys)
485                 config.name = os.path.basename(opts.config)
486             else:
487                 LOG.info('Loading configuration string: %s', opts.config)
488                 config = config_loads(opts.config, config, whitelist_keys)
489
490         # traffic profile override options
491         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
492
493         # copy over cli options that are used in config
494         config.generator_profile = opts.generator_profile
495         if opts.sriov:
496             config.sriov = True
497         if opts.log_file:
498             config.log_file = opts.log_file
499         if opts.service_chain:
500             config.service_chain = opts.service_chain
501         if opts.service_chain_count:
502             config.service_chain_count = opts.service_chain_count
503
504         if opts.use_sriov_middle_net:
505             if (not config.sriov) or (not config.service_chain == ChainType.PVVP):
506                 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
507             config.use_sriov_middle_net = True
508
509         if config.sriov and config.service_chain != ChainType.EXT:
510             # if sriov is requested (does not apply to ext chains)
511             # make sure the physnet names are specified
512             check_physnet("left", config.internal_networks.left)
513             check_physnet("right", config.internal_networks.right)
514             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
515                 check_physnet("middle", config.internal_networks.middle)
516
517         # show running config in json format
518         if opts.show_config:
519             print json.dumps(config, sort_keys=True, indent=4)
520             sys.exit(0)
521
522         # check that an empty openrc file (no OpenStack) is only allowed
523         # with EXT chain
524         if not config.openrc_file:
525             if config.service_chain == ChainType.EXT:
526                 LOG.info('EXT chain with OpenStack mode disabled')
527             else:
528                 raise Exception("openrc_file is empty in the configuration and is required")
529
530         # update the config in the config plugin as it might have changed
531         # in a copy of the dict (config plugin still holds the original dict)
532         config_plugin.set_config(config)
533
534         # add file log if requested
535         if config.log_file:
536             log.add_file_logger(config.log_file)
537
538         openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
539             else None
540
541         nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
542
543         if opts.server:
544             if os.path.isdir(opts.server):
545                 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
546                 nfvbench_instance.set_notifier(server)
547                 try:
548                     port = int(opts.port)
549                 except ValueError:
550                     server.run(host=opts.host)
551                 else:
552                     server.run(host=opts.host, port=port)
553             else:
554                 print 'Invalid HTTP root directory: ' + opts.server
555                 sys.exit(1)
556         else:
557             with utils.RunLock():
558                 run_summary_required = True
559                 if unknown_opts:
560                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
561                     LOG.error(err_msg)
562                     raise Exception(err_msg)
563
564                 # remove unfilled values
565                 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
566                 # get CLI args
567                 params = ' '.join(str(e) for e in sys.argv[1:])
568                 result = nfvbench_instance.run(opts, params)
569                 if 'error_message' in result:
570                     raise Exception(result['error_message'])
571
572                 if 'result' in result and result['status']:
573                     nfvbench_instance.save(result['result'])
574                     nfvbench_instance.prepare_summary(result['result'])
575     except Exception as exc:
576         run_summary_required = True
577         LOG.error({
578             'status': NFVBench.STATUS_ERROR,
579             'error_message': traceback.format_exc()
580         })
581         print str(exc)
582     finally:
583         if fluent_logger:
584             # only send a summary record if there was an actual nfvbench run or
585             # if an error/exception was logged.
586             fluent_logger.send_run_summary(run_summary_required)
587
588
589 if __name__ == '__main__':
590     main()